diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 70071f01c..a3221f703 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -26,7 +26,7 @@ jobs: - name: Install a software Vulkan driver run: | sudo apt-get update -y - sudo apt-get install -y xvfb mesa-vulkan-drivers + sudo apt-get install -y xvfb mesa-vulkan-drivers libgl1-mesa-dri - uses: actions/checkout@v5 - uses: actions/setup-java@v5 with: @@ -36,7 +36,7 @@ jobs: 25 - uses: gradle/actions/setup-gradle@v5 - name: Run SDL-GPU tests - run: xvfb-run -a --server-args="-screen 0 1366x768x24" ./gradlew --build-cache --stacktrace --continue :sdl-gpu:glsmSdlTest :sdl-gpu:sdlGpuDeviceTest + run: xvfb-run -a --server-args="-screen 0 1366x768x24" ./gradlew --build-cache --stacktrace --continue :sdl-gpu:glsmSdlTest :sdl-gpu:sdlGpuDeviceTest :lwjgl3-backend:glesTest - name: Check LWJGL did not extract natives to the shared temp dir run: test ! -e "/tmp/lwjgl_$USER" - name: Attach SDL-GPU test results @@ -48,3 +48,6 @@ jobs: sdl-gpu/build/test-results/ sdl-gpu/build/reports/tests/ sdl-gpu/hs_err_pid*.log + lwjgl3-backend/build/test-results/ + lwjgl3-backend/build/reports/tests/ + lwjgl3-backend/hs_err_pid*.log diff --git a/glsm/build.gradle.kts b/glsm/build.gradle.kts index 2fb758b02..3c1754d65 100644 --- a/glsm/build.gradle.kts +++ b/glsm/build.gradle.kts @@ -67,6 +67,22 @@ artifacts { } } +// LWJGL2-API stubs for test JVMs that strip the lwjgl-2 jars (glsmSdlTest, glesTest). +val lwjgl2StubsSourceSet = sourceSets.create("lwjgl2Stubs") { + java.srcDir("src/lwjgl2Stubs/java") +} + +val lwjgl2Stubs by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false +} + +artifacts { + add(lwjgl2Stubs.name, lwjgl2StubsSourceSet.java.classesDirectory) { + builtBy(tasks.named("lwjgl2StubsClasses")) + } +} + repositories { maven { name = "Mojang" @@ -111,6 +127,7 @@ val spirvTestRuntime by configurations.creating { dependencies { // MC compile stubs compileOnly(sourceSets["stubs"].output) + "lwjgl2StubsCompileOnly"(libs.lwjgl3.opengl) // LWJGL compileOnly(libs.lwjgl2) diff --git a/sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/Sys.java b/glsm/src/lwjgl2Stubs/java/org/lwjgl/Sys.java similarity index 100% rename from sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/Sys.java rename to glsm/src/lwjgl2Stubs/java/org/lwjgl/Sys.java diff --git a/sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/opengl/ContextCapabilities.java b/glsm/src/lwjgl2Stubs/java/org/lwjgl/opengl/ContextCapabilities.java similarity index 59% rename from sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/opengl/ContextCapabilities.java rename to glsm/src/lwjgl2Stubs/java/org/lwjgl/opengl/ContextCapabilities.java index ade3af2db..c2b7d5ca6 100644 --- a/sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/opengl/ContextCapabilities.java +++ b/glsm/src/lwjgl2Stubs/java/org/lwjgl/opengl/ContextCapabilities.java @@ -1,5 +1,7 @@ package org.lwjgl.opengl; +import java.lang.reflect.Field; + public class ContextCapabilities { public boolean OpenGL30 = true; @@ -21,11 +23,28 @@ public class ContextCapabilities { public boolean GL_ARB_sampler_objects = true; public boolean GL_ARB_shader_image_load_store = true; public boolean GL_ARB_shader_storage_buffer_object = true; + public boolean GL_ARB_stencil_texturing = false; public boolean GL_ARB_tessellation_shader = false; + public boolean GL_ARB_texture_storage = false; public boolean GL_EXT_direct_state_access = false; public boolean GL_EXT_gpu_shader4 = true; public boolean GL_EXT_shader_image_load_store = true; public boolean GL_NVX_gpu_memory_info = false; + + public ContextCapabilities() { + if ("lwjgl3".equals(System.getProperty("glsm.stubs.capabilities"))) copyFromCurrentContext(); + } + + private void copyFromCurrentContext() { + final GLCapabilities caps = GL.getCapabilities(); + for (Field field : ContextCapabilities.class.getFields()) { + try { + field.setBoolean(this, GLCapabilities.class.getField(field.getName()).getBoolean(caps)); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("LWJGL3 GLCapabilities has no " + field.getName(), e); + } + } + } } diff --git a/sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/opengl/Drawable.java b/glsm/src/lwjgl2Stubs/java/org/lwjgl/opengl/Drawable.java similarity index 100% rename from sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/opengl/Drawable.java rename to glsm/src/lwjgl2Stubs/java/org/lwjgl/opengl/Drawable.java diff --git a/glsm/src/lwjgl2Stubs/java/org/lwjgl/opengl/GLContext.java b/glsm/src/lwjgl2Stubs/java/org/lwjgl/opengl/GLContext.java new file mode 100644 index 000000000..da7f4a8b8 --- /dev/null +++ b/glsm/src/lwjgl2Stubs/java/org/lwjgl/opengl/GLContext.java @@ -0,0 +1,13 @@ +package org.lwjgl.opengl; + +public final class GLContext { + + private static ContextCapabilities capabilities; + + private GLContext() {} + + public static ContextCapabilities getCapabilities() { + if (capabilities == null) capabilities = new ContextCapabilities(); + return capabilities; + } +} diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMRasterizer.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMRasterizer.java index face3d271..b6ffe8038 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMRasterizer.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLSMRasterizer.java @@ -88,8 +88,11 @@ public final class GLSMRasterizer { static void probePointSizeRange(GLContextState glCtx) { final FloatBuffer range = glCtx.queryScratch; range.clear(); - RENDER_BACKEND.getFloat(GL11.GL_POINT_SIZE_RANGE, range); - final float smooth = range.get(1); + float smooth = 0.0f; + if (!RenderSystem.isGLES()) { + RENDER_BACKEND.getFloat(GL11.GL_POINT_SIZE_RANGE, range); + smooth = range.get(1); + } RENDER_BACKEND.getFloat(GL12.GL_ALIASED_POINT_SIZE_RANGE, range); maxPointSize = Math.max(smooth, range.get(1)); } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/shader/GlslVulkanPreprocess.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/shader/GlslVulkanPreprocess.java index 36373e7c1..d63f227a3 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/shader/GlslVulkanPreprocess.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/shader/GlslVulkanPreprocess.java @@ -15,8 +15,10 @@ import org.taumc.glsl.grammar.GLSLParser; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; public final class GlslVulkanPreprocess { @@ -56,7 +58,9 @@ public final class GlslVulkanPreprocess { private GlslVulkanPreprocess() {} private record CacheKey(String source, int glShaderType, boolean separateReadOnlyImages) {} - public record Result(String rewrittenSource, Set boolUniforms, Set explicitVsInputs) {} + public record Result(String rewrittenSource, Set boolUniforms, Set explicitVsInputs, Map uniformBlocks) {} + + public record UniformBlock(String qualifierText, @Nullable String instanceName) {} /** Replace {@code [startIdx, stopIdx]} with {@code replacement}. */ public record Edit(int startIdx, int stopIdx, String replacement) {} @@ -109,7 +113,7 @@ public final class GlslVulkanPreprocess { final List edits = new ArrayList<>(); final Metadata meta = collectEdits(new ShaderIndex(root, source), glShaderType, debugName, separateReadOnlyImages, edits); - final Result out = new Result(applyEdits(source, edits), meta.boolUniforms(), meta.explicitVsInputs()); + final Result out = new Result(applyEdits(source, edits), meta.boolUniforms(), meta.explicitVsInputs(), meta.uniformBlocks()); synchronized (CACHE) { CACHE.putAndMoveToFirst(key, out); while (CACHE.size() > CACHE_MAX) CACHE.removeLast(); @@ -117,11 +121,12 @@ public final class GlslVulkanPreprocess { return out; } - public record Metadata(Set boolUniforms, Set explicitVsInputs, boolean needsSamplerless) {} + public record Metadata(Set boolUniforms, Set explicitVsInputs, boolean needsSamplerless, Map uniformBlocks) {} public static Metadata collectEdits(ShaderIndex index, int glShaderType, String debugName, boolean separateReadOnlyImages, List edits) { final boolean isVertex = glShaderType == GL20.GL_VERTEX_SHADER; final Set bools = new HashSet<>(); + Map uniformBlocks = Map.of(); final Set explicitInputs = new HashSet<>(); final Set readOnlyImages = new HashSet<>(); boolean needsSamplerless = false; @@ -130,6 +135,14 @@ public final class GlslVulkanPreprocess { final List unlocatedVsInputStarts = new ArrayList<>(); for (GLSLParser.DeclarationContext declaration : index.globalDeclarations()) { + if (declaration.LEFT_BRACE() != null && ShaderIndex.hasStorageQualifier(declaration.type_qualifier(), "uniform")) { + final GLSLParser.Type_qualifierContext blockQualifier = declaration.type_qualifier(); + final String qualifierText = index.source().substring(startIdx(blockQualifier), stopIdx(blockQualifier) + 1); + final String instanceName = declaration.IDENTIFIER().size() > 1 ? declaration.IDENTIFIER(1).getText() : null; + if (uniformBlocks.isEmpty()) uniformBlocks = new HashMap<>(); + uniformBlocks.put(declaration.IDENTIFIER(0).getText(), new UniformBlock(qualifierText, instanceName)); + continue; + } final GLSLParser.Init_declarator_listContext list = declaration.init_declarator_list(); if (list == null) continue; final GLSLParser.Single_declarationContext single = list.single_declaration(); @@ -223,7 +236,7 @@ public final class GlslVulkanPreprocess { } } - return new Metadata(bools, explicitInputs, needsSamplerless); + return new Metadata(bools, explicitInputs, needsSamplerless, uniformBlocks); } public static boolean renamesIdentifier(String name) { diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/shader/SpirvShaderTranslator.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/shader/SpirvShaderTranslator.java index c0c6e48f9..5281ab658 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/shader/SpirvShaderTranslator.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/shader/SpirvShaderTranslator.java @@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.glsm.shader; import com.gtnewhorizons.angelica.glsm.GlslTransformUtils; import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.Edit; +import com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.UniformBlock; import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; @@ -26,8 +27,10 @@ import org.taumc.glsl.grammar.GLSLParserBaseListener; import java.nio.ByteBuffer; import java.nio.IntBuffer; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import static com.gtnewhorizons.angelica.glsm.shader.GlslVulkanPreprocess.startIdx; @@ -65,13 +68,13 @@ public final class SpirvShaderTranslator { try { final String emitted = emitGlslEs(cr.spirv(), debugName, glShaderType, pre.explicitVsInputs()); if (emitted == null) return null; - return postProcessEsOutput(emitted, pre.boolUniforms()); + return postProcessEsOutput(emitted, pre.boolUniforms(), pre.uniformBlocks()); } finally { memFree(cr.spirv()); } } - private static @Nullable String postProcessEsOutput(String glslEs, Set boolUniformNames) { + private static @Nullable String postProcessEsOutput(String glslEs, Set boolUniformNames, Map uniformBlocks) { final GLSLParser.Translation_unitContext root; try { root = GlslTransformUtils.parseFullQuiet(glslEs); @@ -80,21 +83,42 @@ public final class SpirvShaderTranslator { return null; } - final DefaultBlockInfo block = findDefaultBlock(root); - final List edits = new ArrayList<>(); + final BlockFinder finder = new BlockFinder(glslEs, uniformBlocks); + ParseTreeWalker.DEFAULT.walk(finder, root); - if (block != null) { - edits.add(new Edit(startIdx(block.structExternalDecl), stopIdx(block.structExternalDecl), buildLooseUniforms(block.members, boolUniformNames))); + final List edits = new ArrayList<>(); + BlockInfo defaultBlock = null; + final Map> bareMembersByVar = new HashMap<>(); + for (BlockInfo block : finder.blocks) { + final String structReplacement; + if (block.source == null) { + defaultBlock = block; + structReplacement = buildLooseUniforms(block.members, boolUniformNames); + bareMembersByVar.put(block.varName, block.memberNames()); + } else if (block.source.instanceName() == null) { + structReplacement = block.source.qualifierText() + " " + block.structName + " " + block.body + ";"; + bareMembersByVar.put(block.varName, block.memberNames()); + } else { + structReplacement = block.source.qualifierText() + " " + block.structName + " " + block.body + " " + block.declaratorText + ";"; + } + edits.add(new Edit(startIdx(block.structExternalDecl), stopIdx(block.structExternalDecl), structReplacement)); edits.add(new Edit(startIdx(block.uniformExternalDecl), stopIdx(block.uniformExternalDecl), "")); } - final UseSiteRewriter rewriter = new UseSiteRewriter(block, boolUniformNames, edits); + final Set defaultMembers = defaultBlock == null ? Set.of() : bareMembersByVar.get(defaultBlock.varName); + final UseSiteRewriter rewriter = new UseSiteRewriter(defaultBlock, defaultMembers, boolUniformNames, bareMembersByVar, edits); ParseTreeWalker.DEFAULT.walk(rewriter, root); return GlslVulkanPreprocess.applyEdits(glslEs, edits); } - private record DefaultBlockInfo(ParserRuleContext structExternalDecl, ParserRuleContext uniformExternalDecl, String blockVarName, List members) {} + private record BlockInfo(String structName, @Nullable UniformBlock source, ParserRuleContext structExternalDecl, String body, List members, ParserRuleContext uniformExternalDecl, String varName, String declaratorText) { + Set memberNames() { + final Set names = new HashSet<>(); + for (MemberInfo m : members) names.add(m.name); + return names; + } + } private record MemberInfo(String typePrefix, String name) {} @@ -111,31 +135,38 @@ public final class SpirvShaderTranslator { return sb.toString(); } - private static @Nullable DefaultBlockInfo findDefaultBlock(GLSLParser.Translation_unitContext root) { - final DefaultBlockFinder finder = new DefaultBlockFinder(); - ParseTreeWalker.DEFAULT.walk(finder, root); - if (finder.structExternalDecl == null || finder.uniformExternalDecl == null) return null; - return new DefaultBlockInfo(finder.structExternalDecl, finder.uniformExternalDecl, finder.blockVarName, finder.members); + private static boolean isDefaultBlockStruct(String structName) { + return structName.startsWith("_RESERVED_IDENTIFIER_FIXUP_") && structName.endsWith("DefaultUniformBlock"); } - private static final class DefaultBlockFinder extends GLSLParserBaseListener { - String structName; - ParserRuleContext structExternalDecl; - ParserRuleContext uniformExternalDecl; - String blockVarName; - final List members = new ArrayList<>(); + private static final class BlockFinder extends GLSLParserBaseListener { + private record PendingStruct(@Nullable UniformBlock source, ParserRuleContext externalDecl, String body, List members) {} + + private final String glslEs; + private final Map sourceBlocks; + private final Map pending = new HashMap<>(); + final List blocks = new ArrayList<>(); + + BlockFinder(String glslEs, Map sourceBlocks) { + this.glslEs = glslEs; + this.sourceBlocks = sourceBlocks; + } @Override public void enterStruct_specifier(GLSLParser.Struct_specifierContext ctx) { - if (structExternalDecl != null) return; final TerminalNode id = ctx.IDENTIFIER(); if (id == null) return; final String idText = id.getText(); - if (!idText.startsWith("_RESERVED_IDENTIFIER_FIXUP_") || !idText.endsWith("DefaultUniformBlock")) return; + final UniformBlock source = sourceBlocks.get(idText); + if (source == null && !isDefaultBlockStruct(idText)) return; - structName = idText; - structExternalDecl = findExternalDeclaration(ctx); + final List members = source != null && source.instanceName() != null ? List.of() : collectMembers(ctx); + final String body = glslEs.substring(ctx.LEFT_BRACE().getSymbol().getStartIndex(), ctx.RIGHT_BRACE().getSymbol().getStopIndex() + 1); + pending.putIfAbsent(idText, new PendingStruct(source, findExternalDeclaration(ctx), body, members)); + } + private static List collectMembers(GLSLParser.Struct_specifierContext ctx) { + final List members = new ArrayList<>(); for (GLSLParser.Struct_declarationContext sd : ctx.struct_declaration_list().struct_declaration()) { final StringBuilder typePrefix = new StringBuilder(); if (sd.type_qualifier() != null) typePrefix.append(textWithSpaces(sd.type_qualifier())).append(' '); @@ -146,11 +177,12 @@ public final class SpirvShaderTranslator { members.add(new MemberInfo(typePrefix + arraySuffix, decl.IDENTIFIER().getText())); } } + return members; } @Override public void enterInit_declarator_list(GLSLParser.Init_declarator_listContext ctx) { - if (structName == null || uniformExternalDecl != null) return; + if (pending.isEmpty()) return; final GLSLParser.Single_declarationContext single = ctx.single_declaration(); if (single == null || single.typeless_declaration() == null) return; final GLSLParser.Fully_specified_typeContext fst = single.fully_specified_type(); @@ -159,10 +191,14 @@ public final class SpirvShaderTranslator { if (fst.type_specifier() == null || fst.type_specifier().type_specifier_nonarray() == null) return; final GLSLParser.Type_specifier_nonarrayContext ts = fst.type_specifier().type_specifier_nonarray(); if (ts.type_name() == null) return; - if (!structName.equals(ts.type_name().getText())) return; + final String structName = ts.type_name().getText(); + final PendingStruct struct = pending.remove(structName); + if (struct == null) return; - blockVarName = single.typeless_declaration().IDENTIFIER().getText(); - uniformExternalDecl = findExternalDeclaration(ctx); + final GLSLParser.Typeless_declarationContext declarator = single.typeless_declaration(); + final String declaratorText = glslEs.substring(startIdx(declarator), stopIdx(declarator) + 1); + blocks.add(new BlockInfo(structName, struct.source, struct.externalDecl, struct.body, struct.members, + findExternalDeclaration(ctx), declarator.IDENTIFIER().getText(), declaratorText)); } private static String textWithSpaces(ParseTree tree) { @@ -192,17 +228,18 @@ public final class SpirvShaderTranslator { } private static final class UseSiteRewriter extends GLSLParserBaseListener { - private final @Nullable DefaultBlockInfo block; + private final @Nullable BlockInfo block; private final Set boolNames; + private final Map> bareMembersByVar; private final List edits; private final Set memberNames; - UseSiteRewriter(@Nullable DefaultBlockInfo block, Set boolNames, List edits) { + UseSiteRewriter(@Nullable BlockInfo block, Set memberNames, Set boolNames, Map> bareMembersByVar, List edits) { this.block = block; + this.memberNames = memberNames; this.boolNames = boolNames; + this.bareMembersByVar = bareMembersByVar; this.edits = edits; - this.memberNames = new HashSet<>(); - if (block != null) for (MemberInfo m : block.members) memberNames.add(m.name); } @Override @@ -240,11 +277,11 @@ public final class SpirvShaderTranslator { } } - if (block != null && ctx.getChildCount() == 3 && ctx.DOT() != null && ctx.field_selection() != null) { - final ParseTree inner = ctx.getChild(0); - if (inner.getText().equals(block.blockVarName)) { + if (ctx.getChildCount() == 3 && ctx.DOT() != null && ctx.field_selection() != null) { + final Set bareMembers = bareMembersByVar.get(ctx.getChild(0).getText()); + if (bareMembers != null) { final String member = ctx.field_selection().getText(); - if (memberNames.contains(member) && !coveredByAncestorBoolCompare(ctx)) { + if (bareMembers.contains(member) && !coveredByAncestorBoolCompare(ctx)) { edits.add(new Edit(startIdx(ctx), stopIdx(ctx), member)); } } @@ -254,8 +291,8 @@ public final class SpirvShaderTranslator { private @Nullable String resolveBoolOperand(ParseTree operand) { final String text = operand.getText(); if (boolNames.contains(text)) return text; - if (block != null && text.startsWith(block.blockVarName + ".")) { - final String member = text.substring(block.blockVarName.length() + 1); + if (block != null && text.startsWith(block.varName + ".")) { + final String member = text.substring(block.varName.length() + 1); if (boolNames.contains(member) && memberNames.contains(member)) return member; } return null; diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/ProxyTexture.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/ProxyTexture.java new file mode 100644 index 000000000..3e61c1390 --- /dev/null +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/ProxyTexture.java @@ -0,0 +1,92 @@ +package com.gtnewhorizons.angelica.glsm.texture; + +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL32; +import org.lwjgl.opengl.GL40; + +/** Emulates GL proxy textures for backends that lack them. */ +public final class ProxyTexture { + + private final int maxTextureSize; + private final int max3DTextureSize; + private final int maxCubeMapTextureSize; + private final int maxArrayTextureLayers; + + private int target; + private int width; + private int height; + private int depth; + private int internalFormat; + + public ProxyTexture(int maxTextureSize, int max3DTextureSize, int maxCubeMapTextureSize, int maxArrayTextureLayers) { + this.maxTextureSize = maxTextureSize; + this.max3DTextureSize = max3DTextureSize; + this.maxCubeMapTextureSize = maxCubeMapTextureSize; + this.maxArrayTextureLayers = maxArrayTextureLayers; + } + + public void record(int target, int level, int internalFormat, int width, int height, int depth, boolean formatSupported) { + final boolean layersInHeight = layersInHeight(target); + final boolean layersInDepth = layersInDepth(target); + final int dimLimit = dimensionLimit(target); + final int w = levelZeroEquivalentSize(width, level); + final int h = layersInHeight ? height : levelZeroEquivalentSize(height, level); + final int d = layersInDepth ? depth : levelZeroEquivalentSize(depth, level); + final int hLimit = layersInHeight ? maxArrayTextureLayers : dimLimit; + final int dLimit = layersInDepth ? maxArrayTextureLayers : dimLimit; + final boolean fits = w >= 1 && w <= dimLimit && h >= 1 && h <= hLimit && d >= 1 && d <= dLimit && formatSupported; + if (fits) { + accept(target, w, h, d, internalFormat); + } else { + reject(target); + } + } + + public void reject(int target) { + accept(target, 0, 0, 0, 0); + } + + private void accept(int target, int width, int height, int depth, int internalFormat) { + this.target = target; + this.width = width; + this.height = height; + this.depth = depth; + this.internalFormat = internalFormat; + } + + public int levelParameter(int target, int level, int pname) { + if (this.target != target) return 0; + return switch (pname) { + case GL11.GL_TEXTURE_WIDTH -> TextureInfo.mipLevelSize(width, level); + case GL11.GL_TEXTURE_HEIGHT -> layersInHeight(target) ? height : TextureInfo.mipLevelSize(height, level); + case GL12.GL_TEXTURE_DEPTH -> layersInDepth(target) ? depth : TextureInfo.mipLevelSize(depth, level); + case GL11.GL_TEXTURE_INTERNAL_FORMAT -> internalFormat; + default -> 0; + }; + } + + public int dimensionLimit(int target) { + return switch (target) { + case GL12.GL_PROXY_TEXTURE_3D -> max3DTextureSize; + case GL13.GL_PROXY_TEXTURE_CUBE_MAP, GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY -> maxCubeMapTextureSize; + default -> maxTextureSize; + }; + } + + public static int levelZeroEquivalentSize(int size, int level) { + if (size <= 0 || level < 0 || level >= Integer.SIZE - 1) return 0; + final long scaled = (long) size << level; + return scaled > Integer.MAX_VALUE ? 0 : (int) scaled; + } + + private static boolean layersInHeight(int target) { + return target == GL30.GL_PROXY_TEXTURE_1D_ARRAY; + } + + private static boolean layersInDepth(int target) { + return target == GL30.GL_PROXY_TEXTURE_2D_ARRAY || target == GL32.GL_PROXY_TEXTURE_2D_MULTISAMPLE_ARRAY || target == GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY; + } +} diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureInfo.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureInfo.java index ea5e3e359..cb3c413ce 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureInfo.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureInfo.java @@ -15,6 +15,12 @@ public class TextureInfo { private static final int UNSET_INT = Integer.MIN_VALUE; private static final float UNSET_FLOAT = Float.NEGATIVE_INFINITY; + public static int mipLevelSize(int base, int level) { + if (base <= 0 || level < 0) return 0; + if (level >= Integer.SIZE - 1) return 1; + return Math.max(1, base >> level); + } + @Getter protected final int id; @Getter @Setter protected int target = GL11.GL_TEXTURE_2D; protected int internalFormat = -1; diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/shader/SpirvShaderTranslatorTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/shader/SpirvShaderTranslatorTest.java index 69be2b1ff..28aadef60 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/shader/SpirvShaderTranslatorTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/shader/SpirvShaderTranslatorTest.java @@ -277,6 +277,39 @@ class SpirvShaderTranslatorTest { assertFalse(es.contains("!= 0u"), "no bool use-site should still be comparing against 0u"); } + @Test + void uniformBlock_staysABlockUnderGLES() { + final String src = """ + #version 330 core + layout(std140) uniform Blk { + mat4 u_M; + vec4 u_C; + }; + layout(std140) uniform Named { + vec4 u_N; + } named; + uniform float u_Loose; + struct L { vec4 c; }; + uniform L u_Light; + in vec4 color; + out vec4 fragColor; + void main() { + fragColor = u_M * color * u_C * u_Loose + u_Light.c + named.u_N; + } + """; + final String es = SpirvShaderTranslator.glslToGlslEs(src, GL20.GL_FRAGMENT_SHADER, "uniform_block.frag"); + dumpOnFailure("uniform_block.frag", src, es); + + assertNotNull(es); + assertTrue(Pattern.compile("layout\\s*\\(\\s*std140\\s*\\)\\s*uniform\\s+Blk\\s*\\{").matcher(es).find(), "a source uniform block must reach GLSL ES as a block, so glUniformBlockBinding finds it by name\n\n" + es); + assertFalse(Pattern.compile("uniform\\s+Blk\\s+_").matcher(es).find(), "the block must not be left as a plain struct uniform\n\n" + es); + assertFalse(Pattern.compile("_[0-9]+\\.u_M").matcher(es).find(), "an unnamed source block's members must be accessed bare\n\n" + es); + assertTrue(Pattern.compile("layout\\s*\\(\\s*std140\\s*\\)\\s*uniform\\s+Named\\s*\\{[^}]*\\}\\s*named\\s*;").matcher(es).find(), "an instance-named block keeps its instance name\n\n" + es); + assertTrue(es.contains("named.u_N"), "members of an instance-named block stay qualified\n\n" + es); + assertContainsDecl(es, "float", "u_Loose"); + assertTrue(Pattern.compile("uniform\\s+L\\s+u_Light\\s*;").matcher(es).find(), "a struct uniform is not a block and must stay a plain uniform\n\n" + es); + } + @Test void readonlyImage3D_staysAnImageUnderGLES() { final String src = """ diff --git a/lwjgl3-backend/build.gradle.kts b/lwjgl3-backend/build.gradle.kts index bcd7b4327..d916a5a62 100644 --- a/lwjgl3-backend/build.gradle.kts +++ b/lwjgl3-backend/build.gradle.kts @@ -1,3 +1,5 @@ +import java.time.Duration + plugins { `java-library` `maven-publish` @@ -75,6 +77,80 @@ dependencies { compileOnly(libs.annotations) } +val glesTag = "gles" + +val glsmRedirectAgent: Configuration by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true +} + +val glsmLwjgl2Stubs: Configuration by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true +} + +dependencies { + glsmRedirectAgent(project(":glsm", "redirectAgent")) + glsmLwjgl2Stubs(project(":glsm", "lwjgl2Stubs")) + + testImplementation(platform(libs.junit.bom)) + testImplementation(libs.junit.jupiter) + testImplementation(testFixtures(project(":glsm"))) + testImplementation(project(":glsm")) { + capabilities { requireCapability("${project.group}:glsm-stubs") } + } + listOf(libs.lwjgl3.sdl, libs.lwjgl3.shaderc, libs.lwjgl3.spvc).forEach { + testImplementation(it) + testRuntimeOnly(it) { artifact { classifier = lwjglNatives } } + } + testRuntimeOnly(libs.junit.platform.launcher) + testRuntimeOnly(libs.log4j.core) + testRuntimeOnly(libs.fastutil) + testRuntimeOnly(libs.joml) + testRuntimeOnly(libs.commons.lang3) + testRuntimeOnly(libs.guava) + testRuntimeOnly(libs.celeritas.common) { isTransitive = false } + testRuntimeOnly(libs.asm.tree.test) +} + +tasks.test { + useJUnitPlatform { + excludeTags(glesTag) + } +} + +val unpackLwjgl3Natives = registerLwjgl3NativesUnpack() + +val glesTest by tasks.registering(Test::class) { + description = "Boots GLSM on the LWJGL3 GL backend in a real OpenGL ES 3.2 context." + group = "verification" + testClassesDirs = sourceSets["test"].output.classesDirs + classpath = glsmLwjgl2Stubs.plus( + sourceSets["test"].runtimeClasspath.filter { file -> + !file.name.startsWith("lwjgl-2.") && !file.name.startsWith("lwjgl_util-") && !file.name.startsWith("lwjgl-platform-") + } + ) + useJUnitPlatform { + includeTags(glesTag) + } + systemProperty("glsm.stubs.capabilities", "lwjgl3") + javaAgent(glsmRedirectAgent) + loadLwjgl3NativesFrom(unpackLwjgl3Natives) + outputs.cacheIf { false } + timeout = Duration.ofMinutes(5) + systemProperty("junit.jupiter.execution.timeout.default", "60s") + systemProperty("junit.jupiter.execution.timeout.threaddump.enabled", "true") + testLogging { + events("passed", "skipped", "failed") + exceptionFormat = org.gradle.api.tasks.testing.logging.TestExceptionFormat.FULL + } + mustRunAfter(tasks.test) +} + +if (!providers.environmentVariable("CI").isPresent) { + tasks.check { dependsOn(glesTest) } +} + tasks.named("compileJava") { val classesDir = destinationDirectory doLast { injectLwjgl3Aware(classesDir.get().asFile) } diff --git a/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java b/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java index 3a7ef7e97..db48af785 100644 --- a/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java +++ b/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java @@ -10,6 +10,9 @@ import com.gtnewhorizons.angelica.glsm.GLScreenCapture; import com.gtnewhorizons.angelica.glsm.backend.RenderBackend; import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture; import com.gtnewhorizons.angelica.glsm.backend.VSyncMode; +import com.gtnewhorizons.angelica.glsm.texture.ProxyTexture; +import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache; +import org.jetbrains.annotations.Nullable; import me.eigenraven.lwjgl3ify.api.Lwjgl3Aware; import me.eigenraven.lwjgl3ify.client.MainThreadExec; import org.lwjgl.PointerBuffer; @@ -48,6 +51,7 @@ import org.lwjgl.sdl.SDLError; import org.lwjgl.sdl.SDLEvents; import org.lwjgl.sdl.SDLTimer; import org.lwjgl.sdl.SDLVideo; +import org.lwjgl.system.FunctionProvider; import org.lwjgl.system.JNI; import org.lwjgl.system.MemoryStack; import org.lwjgl.system.MemoryUtil; @@ -97,14 +101,45 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { || normalizedVendor.startsWith("advanced micro devices"); } } + final FunctionProvider functions = GL.getFunctionProvider(); if (RenderSystem.isGLES()) { - pfnClearDepthf = GL.getFunctionProvider().getFunctionAddress("glClearDepthf"); - pfnDepthRangef = GL.getFunctionProvider().getFunctionAddress("glDepthRangef"); + pfnClearDepthf = functions.getFunctionAddress("glClearDepthf"); + pfnDepthRangef = functions.getFunctionAddress("glDepthRangef"); + proxyTexture = new ProxyTexture(GL11C.glGetInteger(GL11C.GL_MAX_TEXTURE_SIZE), GL11C.glGetInteger(GL12C.GL_MAX_3D_TEXTURE_SIZE), + GL11C.glGetInteger(GL13C.GL_MAX_CUBE_MAP_TEXTURE_SIZE), GL11C.glGetInteger(GL30C.GL_MAX_ARRAY_TEXTURE_LAYERS)); } + pfnGenSamplers = requireFunction(functions, "glGenSamplers"); + pfnDeleteSamplers = requireFunction(functions, "glDeleteSamplers"); + pfnBindSampler = requireFunction(functions, "glBindSampler"); + pfnSamplerParameteri = requireFunction(functions, "glSamplerParameteri"); + pfnSamplerParameterf = requireFunction(functions, "glSamplerParameterf"); + pfnIsSampler = requireFunction(functions, "glIsSampler"); + pfnVertexAttribDivisor = requireFunction(functions, "glVertexAttribDivisor"); + } + + private static long requireFunction(FunctionProvider functions, String name) { + final long address = functions.getFunctionAddress(name); + if (address == 0L) throw new IllegalStateException(name + " is not exported by the GL driver"); + return address; } private long pfnClearDepthf; private long pfnDepthRangef; + private @Nullable ProxyTexture proxyTexture; + // GL33C slots stay unloaded on an ES 3.2 context: LWJGL builds desktop caps only up to the reported 3.2. + private long pfnGenSamplers; + private long pfnDeleteSamplers; + private long pfnBindSampler; + private long pfnSamplerParameteri; + private long pfnSamplerParameterf; + private long pfnIsSampler; + private long pfnVertexAttribDivisor; + + private boolean recordProxy(int target, int level, int internalFormat, int width, int height, int depth) { + if (proxyTexture == null || !TextureInfoCache.isProxyTarget(target)) return false; + proxyTexture.record(target, level, internalFormat, width, height, depth, true); + return true; + } @Override public void shutdown() { @@ -565,26 +600,31 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { @Override public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, ByteBuffer pixels) { + if (recordProxy(target, level, internalFormat, width, height, 1)) return; GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels); } @Override public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, DoubleBuffer pixels) { + if (recordProxy(target, level, internalFormat, width, height, 1)) return; GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels); } @Override public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, FloatBuffer pixels) { + if (recordProxy(target, level, internalFormat, width, height, 1)) return; GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels); } @Override public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, IntBuffer pixels) { + if (recordProxy(target, level, internalFormat, width, height, 1)) return; GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixels); } @Override public void texImage2D(int target, int level, int internalFormat, int width, int height, int border, int format, int type, long pixelBufferOffset) { + if (recordProxy(target, level, internalFormat, width, height, 1)) return; GL11C.glTexImage2D(target, level, internalFormat, width, height, border, format, type, pixelBufferOffset); } @@ -605,16 +645,19 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { @Override public void texImage1D(int target, int level, int internalFormat, int width, int border, int format, int type, ByteBuffer pixels) { + if (recordProxy(target, level, internalFormat, width, 1, 1)) return; GL11C.glTexImage1D(target, level, internalFormat, width, border, format, type, pixels); } @Override public void texImage3D(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, ByteBuffer pixels) { + if (recordProxy(target, level, internalFormat, width, height, depth)) return; GL12C.glTexImage3D(target, level, internalFormat, width, height, depth, border, format, type, pixels); } @Override public void texImage3D(int target, int level, int internalFormat, int width, int height, int depth, int border, int format, int type, IntBuffer pixels) { + if (recordProxy(target, level, internalFormat, width, height, depth)) return; GL12C.glTexImage3D(target, level, internalFormat, width, height, depth, border, format, type, pixels); } @@ -686,6 +729,7 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { @Override public int getTexLevelParameteri(int target, int level, int pname) { + if (proxyTexture != null && TextureInfoCache.isProxyTarget(target)) return proxyTexture.levelParameter(target, level, pname); return GL11C.glGetTexLevelParameteri(target, level, pname); } @@ -706,27 +750,33 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { @Override public int genSamplers() { - return GL33C.glGenSamplers(); + try (MemoryStack stack = MemoryStack.stackPush()) { + final IntBuffer ids = stack.mallocInt(1); + JNI.callPV(1, MemoryUtil.memAddress(ids), pfnGenSamplers); + return ids.get(0); + } } @Override public void deleteSamplers(int sampler) { - GL33C.glDeleteSamplers(sampler); + try (MemoryStack stack = MemoryStack.stackPush()) { + JNI.callPV(1, MemoryUtil.memAddress(stack.ints(sampler)), pfnDeleteSamplers); + } } @Override public void bindSampler(int unit, int sampler) { - GL33C.glBindSampler(unit, sampler); + JNI.callV(unit, sampler, pfnBindSampler); } @Override public void samplerParameteri(int sampler, int pname, int param) { - GL33C.glSamplerParameteri(sampler, pname, param); + JNI.callV(sampler, pname, param, pfnSamplerParameteri); } @Override public void samplerParameterf(int sampler, int pname, float param) { - GL33C.glSamplerParameterf(sampler, pname, param); + JNI.callV(sampler, pname, param, pfnSamplerParameterf); } @Override @@ -1371,7 +1421,7 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { @Override public boolean isFramebuffer(int framebuffer) { return GL30C.glIsFramebuffer(framebuffer); } @Override public boolean isRenderbuffer(int renderbuffer) { return GL30C.glIsRenderbuffer(renderbuffer); } - @Override public boolean isSampler(int sampler) { return GL33C.glIsSampler(sampler); } + @Override public boolean isSampler(int sampler) { return JNI.callZ(sampler, pfnIsSampler); } @Override public boolean isQuery(int query) { return GL15C.glIsQuery(query); } @Override @@ -1441,7 +1491,7 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { @Override public void vertexAttribDivisor(int index, int divisor) { - GL33C.glVertexAttribDivisor(index, divisor); + JNI.callV(index, divisor, pfnVertexAttribDivisor); } @Override diff --git a/lwjgl3-backend/src/test/java/com/gtnewhorizons/angelica/lwjgl3/GlesSmokeTest.java b/lwjgl3-backend/src/test/java/com/gtnewhorizons/angelica/lwjgl3/GlesSmokeTest.java new file mode 100644 index 000000000..2b79f23b2 --- /dev/null +++ b/lwjgl3-backend/src/test/java/com/gtnewhorizons/angelica/lwjgl3/GlesSmokeTest.java @@ -0,0 +1,201 @@ +package com.gtnewhorizons.angelica.lwjgl3; + +import com.gtnewhorizons.angelica.glsm.GLSMCurrentAttribs; +import com.gtnewhorizons.angelica.glsm.GLSMBlend; +import com.gtnewhorizons.angelica.glsm.GLSMDraws; +import com.gtnewhorizons.angelica.glsm.GLSMEnable; +import com.gtnewhorizons.angelica.glsm.GLSMFramebuffer; +import com.gtnewhorizons.angelica.glsm.GLSMRasterizer; +import com.gtnewhorizons.angelica.glsm.GLSMTextures; +import com.gtnewhorizons.angelica.glsm.GLSMVertexInput; +import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.RenderSystem; +import com.gtnewhorizons.angelica.glsm.backend.BackendManager; +import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.lwjgl.opengl.GL; +import org.lwjgl.opengl.GL11C; +import org.lwjgl.opengl.GL12C; +import org.lwjgl.opengl.GL20C; +import org.lwjgl.opengl.GL30C; +import org.lwjgl.opengl.GL33C; +import org.lwjgl.sdl.SDLError; +import org.lwjgl.sdl.SDLInit; +import org.lwjgl.sdl.SDLVideo; +import org.lwjgl.system.Configuration; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; +import java.nio.IntBuffer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Tag("gles") +@DisabledOnOs(OS.MAC) +class GlesSmokeTest { + + private static final int SIZE = 64; + private static int fbo; + + @BeforeAll + static void boot() { + Configuration.OPENGL_EXPLICIT_INIT.set(true); + assertTrue(SDLInit.SDL_Init(SDLInit.SDL_INIT_VIDEO), () -> "SDL_Init failed: " + SDLError.SDL_GetError()); + SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_CONTEXT_PROFILE_MASK, SDLVideo.SDL_GL_CONTEXT_PROFILE_ES); + SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_CONTEXT_MAJOR_VERSION, 3); + SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_CONTEXT_MINOR_VERSION, 2); + SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_DEPTH_SIZE, 24); + SDLVideo.SDL_GL_SetAttribute(SDLVideo.SDL_GL_STENCIL_SIZE, 8); + final long window = SDLVideo.SDL_CreateWindow("glesSmoke", SIZE, SIZE, SDLVideo.SDL_WINDOW_OPENGL | SDLVideo.SDL_WINDOW_HIDDEN); + assertNotEquals(0L, window, () -> "SDL_CreateWindow failed: " + SDLError.SDL_GetError()); + final long context = SDLVideo.SDL_GL_CreateContext(window); + assertNotEquals(0L, context, () -> "SDL_GL_CreateContext failed: " + SDLError.SDL_GetError()); + assertTrue(SDLVideo.SDL_GL_MakeCurrent(window, context), () -> "SDL_GL_MakeCurrent failed: " + SDLError.SDL_GetError()); + + GL.create(SDLVideo::SDL_GL_GetProcAddress); + GL.createCapabilities(); + // LWJGL's GL_CONTEXT_PROFILE_MASK probe in createCapabilities leaves INVALID_ENUM on an ES context. + while (GL11C.glGetError() != GL11C.GL_NO_ERROR) {} + + final String version = GL11C.glGetString(GL11C.GL_VERSION); + assertTrue(version != null && version.startsWith("OpenGL ES 3."), () -> "not an ES 3.x context: " + version); + assertTrue(BackendManager.RENDER_BACKEND instanceof Lwjgl3GLRenderBackend, () -> "selected backend is " + BackendManager.RENDER_BACKEND.getName()); + + GLStateManager.initialize(GLSMInitConfig.builder().displaySize(SIZE, SIZE).build()); + GLStateManager.setRunningSplash(false); + GLStateManager.markSplashComplete("glesSmoke"); + + assertTrue(RenderSystem.isGLES()); + assertTrue(RenderSystem.supportsSamplerObjects()); + fbo = createTarget(); + assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError()); + } + + @Test + void samplerLifecycle() { + final int sampler = RenderSystem.genSampler(); + assertNotEquals(0, sampler); + RenderSystem.samplerParameteri(sampler, GL11C.GL_TEXTURE_MIN_FILTER, GL11C.GL_LINEAR); + RenderSystem.samplerParameterf(sampler, GL12C.GL_TEXTURE_MAX_LOD, 4.0f); + RenderSystem.bindSamplerToUnit(0, sampler); + assertEquals(sampler, GL11C.glGetInteger(GL33C.GL_SAMPLER_BINDING)); + assertTrue(BackendManager.RENDER_BACKEND.isSampler(sampler)); + + RenderSystem.destroySampler(sampler); + assertFalse(BackendManager.RENDER_BACKEND.isSampler(sampler)); + assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError()); + } + + @Test + void vertexAttribDivisor() { + final int vao = GLSMVertexInput.glGenVertexArrays(); + GLSMVertexInput.glBindVertexArray(vao); + GLSMVertexInput.glVertexAttribDivisor(5, 1); + assertEquals(1, GL20C.glGetVertexAttribi(5, GL33C.GL_VERTEX_ATTRIB_ARRAY_DIVISOR)); + GLSMVertexInput.glBindVertexArray(0); + assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError()); + } + + @Test + void ffpQuadRenders() { + bindClearedTarget(); + GLSMDraws.glBegin(GL11C.GL_QUADS); + GLSMCurrentAttribs.glColor4f(0.0f, 1.0f, 0.0f, 1.0f); + GLSMDraws.glVertex3f(-1.0f, -1.0f, 0.0f); + GLSMDraws.glVertex3f(1.0f, -1.0f, 0.0f); + GLSMDraws.glVertex3f(1.0f, 1.0f, 0.0f); + GLSMDraws.glVertex3f(-1.0f, 1.0f, 0.0f); + GLSMDraws.glEnd(); + + assertEquals(0xFF00FF00, readCenterPixel()); + assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError()); + } + + @Test + void ffpTexturedQuadRenders() { + final int texel = 0xFF2060A0; + final int texture = GLSMTextures.glGenTextures(); + GLSMTextures.glBindTexture(GL11C.GL_TEXTURE_2D, texture); + GLSMTextures.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MIN_FILTER, GL11C.GL_NEAREST); + GLSMTextures.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MAG_FILTER, GL11C.GL_NEAREST); + final IntBuffer pixels = MemoryUtil.memAllocInt(16); + try { + for (int i = 0; i < 16; i++) pixels.put(i, texel); + GLSMTextures.glTexImage2D(GL11C.GL_TEXTURE_2D, 0, GL11C.GL_RGBA, 4, 4, 0, GL12C.GL_BGRA, GL12C.GL_UNSIGNED_INT_8_8_8_8_REV, pixels); + } finally { + MemoryUtil.memFree(pixels); + } + + bindClearedTarget(); + GLSMTextures.glBindTexture(GL11C.GL_TEXTURE_2D, texture); + GLSMEnable.glEnable(GL11C.GL_TEXTURE_2D); + GLSMDraws.glBegin(GL11C.GL_QUADS); + GLSMCurrentAttribs.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GLSMCurrentAttribs.glTexCoord2f(0.0f, 0.0f); + GLSMDraws.glVertex3f(-1.0f, -1.0f, 0.0f); + GLSMCurrentAttribs.glTexCoord2f(1.0f, 0.0f); + GLSMDraws.glVertex3f(1.0f, -1.0f, 0.0f); + GLSMCurrentAttribs.glTexCoord2f(1.0f, 1.0f); + GLSMDraws.glVertex3f(1.0f, 1.0f, 0.0f); + GLSMCurrentAttribs.glTexCoord2f(0.0f, 1.0f); + GLSMDraws.glVertex3f(-1.0f, 1.0f, 0.0f); + GLSMDraws.glEnd(); + GLSMEnable.glDisable(GL11C.GL_TEXTURE_2D); + + assertEquals(texel, readCenterPixel()); + assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError()); + } + + @Test + void proxyTextureProbeFindsTheMaxTextureSize() { + final int maxSize = Integer.highestOneBit(GL11C.glGetInteger(GL11C.GL_MAX_TEXTURE_SIZE)); + int found = -1; + for (int size = maxSize * 2; size > 0 && found < 0; size >>= 1) { + GLSMTextures.glTexImage2D(GL11C.GL_PROXY_TEXTURE_2D, 0, GL11C.GL_RGBA, size, size, 0, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, (ByteBuffer) null); + if (GLSMTextures.glGetTexLevelParameteri(GL11C.GL_PROXY_TEXTURE_2D, 0, GL11C.GL_TEXTURE_WIDTH) != 0) found = size; + } + assertEquals(maxSize, found); + assertEquals(GL11C.GL_NO_ERROR, GL11C.glGetError()); + } + + private static void bindClearedTarget() { + GLSMFramebuffer.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, fbo); + GLSMRasterizer.glViewport(0, 0, SIZE, SIZE); + GLSMBlend.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + GLSMFramebuffer.glClear(GL11C.GL_COLOR_BUFFER_BIT | GL11C.GL_DEPTH_BUFFER_BIT); + } + + private static int readCenterPixel() { + final ByteBuffer pixel = MemoryUtil.memAlloc(4); + try { + GLSMFramebuffer.glReadPixels(SIZE / 2, SIZE / 2, 1, 1, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, pixel); + return (pixel.get(3) & 0xFF) << 24 | (pixel.get(0) & 0xFF) << 16 | (pixel.get(1) & 0xFF) << 8 | pixel.get(2) & 0xFF; + } finally { + MemoryUtil.memFree(pixel); + } + } + + private static int createTarget() { + final int color = GLSMTextures.glGenTextures(); + GLSMTextures.glBindTexture(GL11C.GL_TEXTURE_2D, color); + GLSMTextures.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MIN_FILTER, GL11C.GL_NEAREST); + GLSMTextures.glTexParameteri(GL11C.GL_TEXTURE_2D, GL11C.GL_TEXTURE_MAG_FILTER, GL11C.GL_NEAREST); + GLSMTextures.glTexImage2D(GL11C.GL_TEXTURE_2D, 0, GL11C.GL_RGBA8, SIZE, SIZE, 0, GL11C.GL_RGBA, GL11C.GL_UNSIGNED_BYTE, (ByteBuffer) null); + final int depth = GLSMFramebuffer.glGenRenderbuffers(); + GLSMFramebuffer.glBindRenderbuffer(GL30C.GL_RENDERBUFFER, depth); + GLSMFramebuffer.glRenderbufferStorage(GL30C.GL_RENDERBUFFER, GL30C.GL_DEPTH24_STENCIL8, SIZE, SIZE); + final int target = GLSMFramebuffer.glGenFramebuffers(); + GLSMFramebuffer.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, target); + GLSMFramebuffer.glFramebufferTexture2D(GL30C.GL_FRAMEBUFFER, GL30C.GL_COLOR_ATTACHMENT0, GL11C.GL_TEXTURE_2D, color, 0); + GLSMFramebuffer.glFramebufferRenderbuffer(GL30C.GL_FRAMEBUFFER, GL30C.GL_DEPTH_STENCIL_ATTACHMENT, GL30C.GL_RENDERBUFFER, depth); + GLSMFramebuffer.glBindFramebuffer(GL30C.GL_FRAMEBUFFER, 0); + return target; + } +} diff --git a/sdl-gpu/build.gradle.kts b/sdl-gpu/build.gradle.kts index e6a26e435..ce88cebd0 100644 --- a/sdl-gpu/build.gradle.kts +++ b/sdl-gpu/build.gradle.kts @@ -106,7 +106,13 @@ val glsmSdlAgent = sourceSets.create("glsmSdlAgent") { java.srcDir("src/glsmSdlAgent/java") } +val glsmLwjgl2Stubs: Configuration by configurations.creating { + isCanBeConsumed = false + isCanBeResolved = true +} + dependencies { + glsmLwjgl2Stubs(project(":glsm", "lwjgl2Stubs")) "glsmSdlAgentCompileOnly"(project(":glsm")) "glsmSdlAgentCompileOnly"(testFixtures(project(":glsm"))) "glsmSdlAgentCompileOnly"(libs.asm.tree.test) @@ -148,7 +154,7 @@ val glsmSdlTest by tasks.registering(Test::class) { description = "Runs the headless GLSM-on-SDL FFP rig in its own JVM, with the SDL GPU gate engaged." group = "verification" testClassesDirs = sourceSets["test"].output.classesDirs - classpath = glsmSdlStubs.output.classesDirs.plus( + classpath = glsmSdlStubs.output.classesDirs.plus(glsmLwjgl2Stubs).plus( sourceSets["test"].runtimeClasspath.filter { file -> !file.name.startsWith("lwjgl-2.") && !file.name.startsWith("lwjgl_util-") && !file.name.startsWith("lwjgl-platform-") } diff --git a/sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/opengl/GLContext.java b/sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/opengl/GLContext.java deleted file mode 100644 index 4be14af54..000000000 --- a/sdl-gpu/src/glsmSdlStubs/java/org/lwjgl/opengl/GLContext.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.lwjgl.opengl; - -public final class GLContext { - - private static final ContextCapabilities CAPABILITIES = new ContextCapabilities(); - - private GLContext() {} - - public static ContextCapabilities getCapabilities() { - return CAPABILITIES; - } -} diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java index 1273e4422..55d36ad12 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java @@ -20,6 +20,7 @@ import com.gtnewhorizons.angelica.glsm.backend.VSyncMode; import com.gtnewhorizons.angelica.glsm.backend.RenderBackend; import com.gtnewhorizons.angelica.glsm.backend.ScreenCapture; import com.gtnewhorizons.angelica.glsm.hooks.GLSMConfig; +import com.gtnewhorizons.angelica.glsm.texture.TextureInfo; import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache; import com.gtnewhorizons.angelica.glsm.texture.TextureStaging; import com.gtnewhorizons.angelica.sdlgpu.compute.ComputeBinder; @@ -1507,52 +1508,8 @@ public class SDLGPURenderBackend extends RenderBackend { return TextureInfoCache.isProxyTarget(target); } - private static boolean proxyLayersInHeight(int target) { - return target == GL30.GL_PROXY_TEXTURE_1D_ARRAY; - } - - private static boolean proxyLayersInDepth(int target) { - return isProxyArrayTarget(target) && !proxyLayersInHeight(target); - } - - private static boolean isProxyArrayTarget(int target) { - return target == GL30.GL_PROXY_TEXTURE_1D_ARRAY || target == GL30.GL_PROXY_TEXTURE_2D_ARRAY || target == GL32.GL_PROXY_TEXTURE_2D_MULTISAMPLE_ARRAY || target == GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY; - } - - public static int proxyLevelSize(int base, int level) { - return PixelOps.mipLevelSize(base, level); - } - - static int proxyDimensionLimit(int target) { - return switch (target) { - case GL12.GL_PROXY_TEXTURE_3D -> MAX_3D_TEXTURE_SIZE; - case GL13.GL_PROXY_TEXTURE_CUBE_MAP, GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY -> MAX_CUBE_MAP_TEXTURE_SIZE; - default -> MAX_TEXTURE_SIZE; - }; - } - - static int levelZeroEquivalentSize(int size, int level) { - if (size <= 0 || level < 0 || level >= Integer.SIZE - 1) return 0; - final long scaled = (long) size << level; - return scaled > Integer.MAX_VALUE ? 0 : (int) scaled; - } - private void recordProxyTexImage(ContextState st, int target, int level, int internalFormat, int width, int height, int depth) { - final boolean layersInHeight = proxyLayersInHeight(target); - final boolean layersInDepth = proxyLayersInDepth(target); - final int dimLimit = proxyDimensionLimit(target); - final int w = levelZeroEquivalentSize(width, level); - final int h = layersInHeight ? height : levelZeroEquivalentSize(height, level); - final int d = layersInDepth ? depth : levelZeroEquivalentSize(depth, level); - final int hLimit = layersInHeight ? MAX_ARRAY_TEXTURE_LAYERS : dimLimit; - final int dLimit = layersInDepth ? MAX_ARRAY_TEXTURE_LAYERS : dimLimit; - final boolean fits = w >= 1 && w <= dimLimit && h >= 1 && h <= hLimit && d >= 1 && d <= dLimit - && resourceManager.isMappableTextureFormat(internalFormat); - if (fits) { - st.proxyTexture.accept(target, w, h, d, internalFormat); - } else { - st.proxyTexture.reject(target); - } + st.proxyTexture.record(target, level, internalFormat, width, height, depth, resourceManager.isMappableTextureFormat(internalFormat)); } private static int boundTextureFor(ContextState st, int target) { @@ -1816,23 +1773,13 @@ public class SDLGPURenderBackend extends RenderBackend { } @Override public int getTexLevelParameteri(int target, int level, int pname) { final ContextState cs = s(); - if (isProxyTarget(target)) { - final ContextState.ProxyTextureState proxy = cs.proxyTexture; - if (proxy.target != target) return 0; - return switch (pname) { - case GL11.GL_TEXTURE_WIDTH -> proxyLevelSize(proxy.width, level); - case GL11.GL_TEXTURE_HEIGHT -> proxyLayersInHeight(target) ? proxy.height : proxyLevelSize(proxy.height, level); - case GL12.GL_TEXTURE_DEPTH -> proxyLayersInDepth(target) ? proxy.depth : proxyLevelSize(proxy.depth, level); - case GL11.GL_TEXTURE_INTERNAL_FORMAT -> proxy.internalFormat; - default -> 0; - }; - } + if (isProxyTarget(target)) return cs.proxyTexture.levelParameter(target, level, pname); final int glId = cs.boundTextures[cs.activeTextureUnit]; final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(glId); if (meta == null) return 0; return switch (pname) { - case GL11.GL_TEXTURE_WIDTH -> PixelOps.mipLevelSize(meta.width(), level); - case GL11.GL_TEXTURE_HEIGHT -> PixelOps.mipLevelSize(meta.height(), level); + case GL11.GL_TEXTURE_WIDTH -> TextureInfo.mipLevelSize(meta.width(), level); + case GL11.GL_TEXTURE_HEIGHT -> TextureInfo.mipLevelSize(meta.height(), level); case GL11.GL_TEXTURE_INTERNAL_FORMAT -> { final int stored = meta.glFormat(); yield TextureInfoCache.isGenericCompressedInternalFormat(stored) ? GL11.GL_RGBA8 : stored; @@ -1987,8 +1934,8 @@ public class SDLGPURenderBackend extends RenderBackend { if (meta != null) { fbo.colorFormats[colorIdx] = meta.sdlFormat(); if (colorIdx == 0) { - fbo.width = PixelOps.mipLevelSize(meta.width(), level); - fbo.height = PixelOps.mipLevelSize(meta.height(), level); + fbo.width = TextureInfo.mipLevelSize(meta.width(), level); + fbo.height = TextureInfo.mipLevelSize(meta.height(), level); } } fbo.colorAttachmentCount = Math.max(fbo.colorAttachmentCount, colorIdx + 1); @@ -2216,8 +2163,8 @@ public class SDLGPURenderBackend extends RenderBackend { out.level = fbo.colorLevels[fbo.readBufferIndex]; out.sdlFormat = meta != null ? meta.sdlFormat() : SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; out.glBaseFormat = meta != null ? PixelPack.glBaseFormat(meta.glFormat()) : GL11.GL_RGBA; - out.width = meta != null ? PixelOps.mipLevelSize(meta.width(), out.level) : fbo.width; - out.height = meta != null ? PixelOps.mipLevelSize(meta.height(), out.level) : fbo.height; + out.width = meta != null ? TextureInfo.mipLevelSize(meta.width(), out.level) : fbo.width; + out.height = meta != null ? TextureInfo.mipLevelSize(meta.height(), out.level) : fbo.height; return out; } @@ -2283,8 +2230,8 @@ public class SDLGPURenderBackend extends RenderBackend { final int glId = boundTextureFor(cs, target); final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(glId); if (meta == null || level < 0 || level >= meta.levels()) return; - final int width = PixelOps.mipLevelSize(meta.width(), level); - final int height = PixelOps.mipLevelSize(meta.height(), level); + final int width = TextureInfo.mipLevelSize(meta.width(), level); + final int height = TextureInfo.mipLevelSize(meta.height(), level); if (width <= 0 || height <= 0) return; checkReadbackCapacity(format, type, width, height, loadPackState(cs), capacity); final int kind = readbackKind(format); @@ -4166,8 +4113,8 @@ public class SDLGPURenderBackend extends RenderBackend { final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture); if (meta == null) return 0; return switch (pname) { - case GL11.GL_TEXTURE_WIDTH -> PixelOps.mipLevelSize(meta.width(), level); - case GL11.GL_TEXTURE_HEIGHT -> PixelOps.mipLevelSize(meta.height(), level); + case GL11.GL_TEXTURE_WIDTH -> TextureInfo.mipLevelSize(meta.width(), level); + case GL11.GL_TEXTURE_HEIGHT -> TextureInfo.mipLevelSize(meta.height(), level); case GL11.GL_TEXTURE_INTERNAL_FORMAT -> { final int stored = meta.glFormat(); yield TextureInfoCache.isGenericCompressedInternalFormat(stored) ? GL11.GL_RGBA8 : stored; @@ -4246,10 +4193,10 @@ public class SDLGPURenderBackend extends RenderBackend { } static final int MAX_UNIFORM_COMPONENTS_PER_STAGE = 4096 / Float.BYTES; - static final int MAX_ARRAY_TEXTURE_LAYERS = 256; - static final int MAX_TEXTURE_SIZE = 16384; - static final int MAX_CUBE_MAP_TEXTURE_SIZE = 16384; - static final int MAX_3D_TEXTURE_SIZE = 2048; + public static final int MAX_ARRAY_TEXTURE_LAYERS = 256; + public static final int MAX_TEXTURE_SIZE = 16384; + public static final int MAX_CUBE_MAP_TEXTURE_SIZE = 16384; + public static final int MAX_3D_TEXTURE_SIZE = 2048; /** D3D12 and Metal both cap anisotropy at 16, and every desktop Vulkan driver reports 16. */ static final float MAX_ANISOTROPY = 16.0f; @@ -4412,9 +4359,9 @@ public class SDLGPURenderBackend extends RenderBackend { if (texHandle == 0 || frameManager.getCommandBuffer() == 0) return; final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(texture); if (meta == null || level >= meta.levels()) return; - final int mipW = PixelOps.mipLevelSize(meta.width(), level); - final int mipH = PixelOps.mipLevelSize(meta.height(), level); - final int mipD = PixelOps.mipLevelSize(meta.depth(), level); + final int mipW = TextureInfo.mipLevelSize(meta.width(), level); + final int mipH = TextureInfo.mipLevelSize(meta.height(), level); + final int mipD = TextureInfo.mipLevelSize(meta.depth(), level); final int bpp = PixelOps.sdlFormatTexelBytes(meta.sdlFormat()); if (mipW <= 0 || mipH <= 0 || bpp <= 0) return; diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/ContextState.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/ContextState.java index 6f6b37ba2..124a87967 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/ContextState.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/ContextState.java @@ -1,6 +1,7 @@ package com.gtnewhorizons.angelica.sdlgpu.frame; import com.gtnewhorizons.angelica.glsm.GLContextState; +import com.gtnewhorizons.angelica.glsm.texture.ProxyTexture; import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend; import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineCache; import com.gtnewhorizons.angelica.sdlgpu.resource.ColorSource; @@ -67,27 +68,8 @@ public final class ContextState { public final PackState packState = new PackState(); public final ColorSource readColor = new ColorSource(); - public static final class ProxyTextureState { - public int target; - public int width; - public int height; - public int depth; - public int internalFormat; - - public void accept(int target, int width, int height, int depth, int internalFormat) { - this.target = target; - this.width = width; - this.height = height; - this.depth = depth; - this.internalFormat = internalFormat; - } - - public void reject(int target) { - accept(target, 0, 0, 0, 0); - } - } - - public final ProxyTextureState proxyTexture = new ProxyTextureState(); + public final ProxyTexture proxyTexture = new ProxyTexture(SDLGPURenderBackend.MAX_TEXTURE_SIZE, SDLGPURenderBackend.MAX_3D_TEXTURE_SIZE, + SDLGPURenderBackend.MAX_CUBE_MAP_TEXTURE_SIZE, SDLGPURenderBackend.MAX_ARRAY_TEXTURE_LAYERS); public static final class VAOState { public final boolean[] attribEnabled = new boolean[MAX_VERTEX_ATTRIBS]; diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/LogicOpEmulator.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/LogicOpEmulator.java index e2f8131a6..f784b5798 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/LogicOpEmulator.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/pipeline/LogicOpEmulator.java @@ -1,10 +1,10 @@ package com.gtnewhorizons.angelica.sdlgpu.pipeline; +import com.gtnewhorizons.angelica.glsm.texture.TextureInfo; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager; import com.gtnewhorizons.angelica.sdlgpu.frame.FrameManager.FrameState; import com.gtnewhorizons.angelica.sdlgpu.resource.FboState; -import com.gtnewhorizons.angelica.sdlgpu.resource.PixelOps; import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager; import com.gtnewhorizons.angelica.sdlgpu.resource.TextureOps; import com.gtnewhorizons.angelica.sdlgpu.shader.LogicOpFormats; @@ -121,13 +121,13 @@ public final class LogicOpEmulator { if (fbo == null) return frameManager.getFbo0Width(); final int db = drawBuffer(fbo, loc); final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(fbo.colorGlIds[db]); - return meta != null ? PixelOps.mipLevelSize(meta.width(), fbo.colorLevels[db]) : fbo.width; + return meta != null ? TextureInfo.mipLevelSize(meta.width(), fbo.colorLevels[db]) : fbo.width; } private int targetHeight(FboState fbo, int loc) { if (fbo == null) return frameManager.getFbo0Height(); final int db = drawBuffer(fbo, loc); final ResourceManager.TextureMeta meta = resourceManager.getTextureMeta(fbo.colorGlIds[db]); - return meta != null ? PixelOps.mipLevelSize(meta.height(), fbo.colorLevels[db]) : fbo.height; + return meta != null ? TextureInfo.mipLevelSize(meta.height(), fbo.colorLevels[db]) : fbo.height; } } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PixelOps.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PixelOps.java index 998e2b992..1bb6c5e12 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PixelOps.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/PixelOps.java @@ -14,12 +14,6 @@ import static org.lwjgl.sdl.SDLGPU.*; public final class PixelOps { private PixelOps() {} - public static int mipLevelSize(int base, int level) { - if (base <= 0 || level < 0) return 0; - if (level >= Integer.SIZE - 1) return 1; - return Math.max(1, base >> level); - } - public static int defaultMipLevels(int width, int height) { final int max = Math.max(1, Math.max(width, height)); return Math.max(1, 32 - Integer.numberOfLeadingZeros(max)); diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureOps.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureOps.java index 101088c15..c40d06d60 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureOps.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureOps.java @@ -1,5 +1,6 @@ package com.gtnewhorizons.angelica.sdlgpu.resource; +import com.gtnewhorizons.angelica.glsm.texture.TextureInfo; import com.gtnewhorizons.angelica.sdlgpu.device.Device; import com.gtnewhorizons.angelica.sdlgpu.device.Submits; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; @@ -156,11 +157,11 @@ public final class TextureOps { } public static int levelWidth(ResourceManager.TextureMeta meta, int level) { - return meta != null ? PixelOps.mipLevelSize(meta.width(), level) : 0; + return meta != null ? TextureInfo.mipLevelSize(meta.width(), level) : 0; } public static int levelHeight(ResourceManager.TextureMeta meta, int level) { - return meta != null ? PixelOps.mipLevelSize(meta.height(), level) : 0; + return meta != null ? TextureInfo.mipLevelSize(meta.height(), level) : 0; } private void copyFromDefaultFramebuffer(ContextState st, int destGlId, long destTex, int level, int xoffset, int yoffset, int x, int y, int width, int height) { diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/ProxyTextureLimitsTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/ProxyTextureLimitsTest.java index bfd2b90d8..7cdaf69de 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/ProxyTextureLimitsTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/ProxyTextureLimitsTest.java @@ -1,5 +1,6 @@ package com.gtnewhorizons.angelica.sdlgpu; +import com.gtnewhorizons.angelica.glsm.texture.ProxyTexture; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -47,11 +48,11 @@ class ProxyTextureLimitsTest { assertEquals(2048, SDLGPURenderBackend.deviceLimit(GL12.GL_MAX_3D_TEXTURE_SIZE)); assertEquals(16384, SDLGPURenderBackend.deviceLimit(GL13.GL_MAX_CUBE_MAP_TEXTURE_SIZE)); - assertEquals(16384, SDLGPURenderBackend.proxyDimensionLimit(GL11.GL_PROXY_TEXTURE_2D)); - assertEquals(16384, SDLGPURenderBackend.proxyDimensionLimit(GL11.GL_PROXY_TEXTURE_1D)); - assertEquals(2048, SDLGPURenderBackend.proxyDimensionLimit(GL12.GL_PROXY_TEXTURE_3D)); - assertEquals(16384, SDLGPURenderBackend.proxyDimensionLimit(GL13.GL_PROXY_TEXTURE_CUBE_MAP)); - assertEquals(16384, SDLGPURenderBackend.proxyDimensionLimit(GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY)); + assertEquals(16384, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL11.GL_PROXY_TEXTURE_2D)); + assertEquals(16384, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL11.GL_PROXY_TEXTURE_1D)); + assertEquals(2048, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL12.GL_PROXY_TEXTURE_3D)); + assertEquals(16384, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL13.GL_PROXY_TEXTURE_CUBE_MAP)); + assertEquals(16384, SdlTestRig.contextState().proxyTexture.dimensionLimit(GL40.GL_PROXY_TEXTURE_CUBE_MAP_ARRAY)); } @Test @@ -99,13 +100,13 @@ class ProxyTextureLimitsTest { @Test void levelZeroEquivalentSizeRejectsOverflowAndNonsense() { - assertEquals(4, SDLGPURenderBackend.levelZeroEquivalentSize(4, 0)); - assertEquals(32, SDLGPURenderBackend.levelZeroEquivalentSize(4, 3)); - assertEquals(0, SDLGPURenderBackend.levelZeroEquivalentSize(4, -1)); - assertEquals(0, SDLGPURenderBackend.levelZeroEquivalentSize(0, 2)); - assertEquals(0, SDLGPURenderBackend.levelZeroEquivalentSize(4, 31)); - assertEquals(0, SDLGPURenderBackend.levelZeroEquivalentSize(4, 30)); - assertEquals(1 << 30, SDLGPURenderBackend.levelZeroEquivalentSize(1, 30)); + assertEquals(4, ProxyTexture.levelZeroEquivalentSize(4, 0)); + assertEquals(32, ProxyTexture.levelZeroEquivalentSize(4, 3)); + assertEquals(0, ProxyTexture.levelZeroEquivalentSize(4, -1)); + assertEquals(0, ProxyTexture.levelZeroEquivalentSize(0, 2)); + assertEquals(0, ProxyTexture.levelZeroEquivalentSize(4, 31)); + assertEquals(0, ProxyTexture.levelZeroEquivalentSize(4, 30)); + assertEquals(1 << 30, ProxyTexture.levelZeroEquivalentSize(1, 30)); } @Test diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/ProxyTextureTargetTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/ProxyTextureTargetTest.java index 31420d2fe..4553a9fe3 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/ProxyTextureTargetTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/ProxyTextureTargetTest.java @@ -1,5 +1,6 @@ package com.gtnewhorizons.angelica.sdlgpu; +import com.gtnewhorizons.angelica.glsm.texture.TextureInfo; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -81,7 +82,7 @@ class ProxyTextureTargetTest { @ParameterizedTest(name = "{0}") @MethodSource("levelSizeCases") void levelSizeMatchesExpectation(String label, int base, int level, int expected) { - assertEquals(expected, SDLGPURenderBackend.proxyLevelSize(base, level), label); + assertEquals(expected, TextureInfo.mipLevelSize(base, level), label); } @Test