From 4b21bc77b6f48333a6c8e1539d55ba95ddf2f47d Mon Sep 17 00:00:00 2001 From: Jason Mitchell Date: Sat, 8 Aug 2026 18:29:26 -0700 Subject: [PATCH] A few fixes (#1994) --- build.gradle.kts | 1 + buildSrc/build.gradle.kts | 1 + .../src/main/kotlin/Lwjgl3AwareInjection.kt | 49 +++++++ .../streaming/TessellatorStreamingDrawer.java | 1 + gradle/libs.versions.toml | 2 + lwjgl3-backend/build.gradle.kts | 5 + .../lwjgl3/Lwjgl3GLRenderBackend.java | 4 +- sdl-gpu/build.gradle.kts | 5 + .../compute/ChunkCullSpirvCompileTest.java | 2 +- .../culling/GpuDrivenChunkCuller.java | 2 +- .../angelica/shaders/culling/chunk_cull.csh | 2 +- .../arch/Lwjgl3AwareBoundaryTest.java | 127 ++++++++++++++++++ src/test/resources/archunit.properties | 2 + 13 files changed, 198 insertions(+), 5 deletions(-) create mode 100644 buildSrc/src/main/kotlin/Lwjgl3AwareInjection.kt create mode 100644 src/test/java/com/gtnewhorizons/angelica/arch/Lwjgl3AwareBoundaryTest.java create mode 100644 src/test/resources/archunit.properties diff --git a/build.gradle.kts b/build.gradle.kts index 532fbbb92..760778b9f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -201,6 +201,7 @@ fun Test.configureAngelicaJava8() { tasks.test { useJUnitPlatform { excludeTags = setOf("gl-core") } configureAngelicaJava8() + dependsOn(":glsm:classes", ":lwjgl3-backend:classes", ":sdl-gpu:classes") } val downgradeRootMainClasses = tasks.named("downgradeMainClasses") diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 81747e0fc..ebc46fcd7 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -10,4 +10,5 @@ repositories { // Re-export Gradle's generated typed catalog accessors so applied scripts (dependencies.gradle.kts) can use them. dependencies { implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) + implementation(libs.asm.build) } diff --git a/buildSrc/src/main/kotlin/Lwjgl3AwareInjection.kt b/buildSrc/src/main/kotlin/Lwjgl3AwareInjection.kt new file mode 100644 index 000000000..54ab2cef1 --- /dev/null +++ b/buildSrc/src/main/kotlin/Lwjgl3AwareInjection.kt @@ -0,0 +1,49 @@ +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import java.io.File + +private const val LWJGL3_AWARE_DESC = "Lme/eigenraven/lwjgl3ify/api/Lwjgl3Aware;" + +/** + * Stamps @Lwjgl3Aware onto every class under [classesDir] - classes that already carry the annotation are left untouched. + */ +fun injectLwjgl3Aware(classesDir: File) { + if (!classesDir.isDirectory) return + + classesDir.walkTopDown() + .filter { it.isFile && it.extension == "class" && it.name != "module-info.class" } + .forEach { classFile -> + val reader = ClassReader(classFile.readBytes()) + if (hasLwjgl3Aware(reader)) return@forEach + + val writer = ClassWriter(reader, 0) + reader.accept(object : ClassVisitor(Opcodes.ASM9, writer) { + override fun visit( + version: Int, + access: Int, + name: String?, + signature: String?, + superName: String?, + interfaces: Array? + ) { + super.visit(version, access, name, signature, superName, interfaces) + super.visitAnnotation(LWJGL3_AWARE_DESC, true)?.visitEnd() + } + }, 0) + classFile.writeBytes(writer.toByteArray()) + } +} + +private fun hasLwjgl3Aware(reader: ClassReader): Boolean { + var found = false + reader.accept(object : ClassVisitor(Opcodes.ASM9) { + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + if (descriptor == LWJGL3_AWARE_DESC) found = true + return null + } + }, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES) + return found +} diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/streaming/TessellatorStreamingDrawer.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/streaming/TessellatorStreamingDrawer.java index e4b68de15..baeb88c74 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/streaming/TessellatorStreamingDrawer.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/streaming/TessellatorStreamingDrawer.java @@ -282,6 +282,7 @@ public class TessellatorStreamingDrawer { } drawWithQuadConversion(drawMode, firstVertex, vertexCount); + GLStateManager.glBindVertexArray(0); } finally { if (locked) GLStateManager.releaseDrawLock(); Tracy.endZone(); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d9dcb5870..9685f6234 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,7 @@ commonsLang3 = "3.17.0" jvmDowngrader = "1.3.5" junit = "5.14.1" asmTest = "9.7" +asmBuild = "9.7" mockito = "3.12.4" archunit = "1.4.1" ant = "1.8.2" @@ -59,6 +60,7 @@ archunit-junit5 = { module = "com.tngtech.archunit:archunit-junit5", version.ref ant = { module = "org.apache.ant:ant", version.ref = "ant" } asm-tree-compile = { module = "org.ow2.asm:asm-tree", version.ref = "asmCompile" } asm-tree-test = { module = "org.ow2.asm:asm-tree", version.ref = "asmTest" } +asm-build = { module = "org.ow2.asm:asm", version.ref = "asmBuild" } lwjgl2 = { module = "org.lwjgl.lwjgl:lwjgl", version.ref = "lwjgl2" } lwjgl2-util = { module = "org.lwjgl.lwjgl:lwjgl_util", version.ref = "lwjgl2" } lwjgl3 = { module = "org.lwjgl:lwjgl", version.ref = "lwjgl3" } diff --git a/lwjgl3-backend/build.gradle.kts b/lwjgl3-backend/build.gradle.kts index 661eabfd3..a5e2888f3 100644 --- a/lwjgl3-backend/build.gradle.kts +++ b/lwjgl3-backend/build.gradle.kts @@ -74,6 +74,11 @@ dependencies { compileOnly(libs.annotations) } +tasks.named("compileJava") { + val classesDir = destinationDirectory + doLast { injectLwjgl3Aware(classesDir.get().asFile) } +} + tasks.named("jar") { manifest { attributes( 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 8da92b3e8..a6c3a32ca 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 @@ -1613,9 +1613,9 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { @Override public void bindImageTexture(int unit, int texture, int level, boolean layered, int layer, int access, int format) { - if (GLStateManager.capabilities.OpenGL42 || GLStateManager.capabilities.GL_ARB_shader_image_load_store) { + if (caps.OpenGL42 || caps.GL_ARB_shader_image_load_store) { GL42C.glBindImageTexture(unit, texture, level, layered, layer, access, format); - } else if (GLStateManager.capabilities.GL_EXT_shader_image_load_store) { + } else if (caps.GL_EXT_shader_image_load_store) { EXTShaderImageLoadStore.glBindImageTextureEXT(unit, texture, level, layered, layer, access, format); } else { GLStateManager.warnOnce("bindImageTexture", "glBindImageTexture ignored: no image load/store support"); diff --git a/sdl-gpu/build.gradle.kts b/sdl-gpu/build.gradle.kts index b0a52592b..1c7291999 100644 --- a/sdl-gpu/build.gradle.kts +++ b/sdl-gpu/build.gradle.kts @@ -91,6 +91,11 @@ tasks.test { } } +tasks.named("compileJava") { + val classesDir = destinationDirectory + doLast { injectLwjgl3Aware(classesDir.get().asFile) } +} + tasks.named("jar") { manifest { attributes( diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/compute/ChunkCullSpirvCompileTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/compute/ChunkCullSpirvCompileTest.java index e02d6d07e..6d1d37a7f 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/compute/ChunkCullSpirvCompileTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/compute/ChunkCullSpirvCompileTest.java @@ -32,7 +32,7 @@ class ChunkCullSpirvCompileTest { final ShaderManager.ComputeBindingMap map = ShaderManager.remapSpirvForComputeSDLGPU(spirv); assertEquals(0, map.samplerGlSlots().length, "the frustum cull samples no textures"); assertEquals(1, map.uboGlSlots().length, "Frustum UBO"); - assertEquals(0, map.uboGlSlots()[0], "Frustum UBO is at binding 0"); + assertEquals(1, map.uboGlSlots()[0], "Frustum UBO is at binding 1"); assertEquals(160, map.uboSizes()[0], "Frustum UBO is 160 bytes"); assertEquals(3, map.roSsboGlSlots().length + map.rwSsboGlSlots().length, "visible + meta + indirect"); assertTrue(IntStream.concat(Arrays.stream(map.roSsboGlSlots()), Arrays.stream(map.rwSsboGlSlots())).allMatch(s -> s >= 0 && s < 8), "a storage binding outside 0..7 exceeds the GL 4.3 guaranteed minimum"); diff --git a/src/main/java/com/gtnewhorizons/angelica/rendering/culling/GpuDrivenChunkCuller.java b/src/main/java/com/gtnewhorizons/angelica/rendering/culling/GpuDrivenChunkCuller.java index 619f2021c..306d7d166 100644 --- a/src/main/java/com/gtnewhorizons/angelica/rendering/culling/GpuDrivenChunkCuller.java +++ b/src/main/java/com/gtnewhorizons/angelica/rendering/culling/GpuDrivenChunkCuller.java @@ -30,7 +30,7 @@ public final class GpuDrivenChunkCuller { static final int VISIBLE_SSBO_BINDING = 0; static final int META_SSBO_BINDING = 1; static final int INDIRECT_SSBO_BINDING = 2; - static final int FRUSTUM_UBO_BINDING = 0; + static final int FRUSTUM_UBO_BINDING = 1; private boolean ready; private boolean disabled; diff --git a/src/main/resources/assets/angelica/shaders/culling/chunk_cull.csh b/src/main/resources/assets/angelica/shaders/culling/chunk_cull.csh index 86dbc8886..394e3dc0c 100644 --- a/src/main/resources/assets/angelica/shaders/culling/chunk_cull.csh +++ b/src/main/resources/assets/angelica/shaders/culling/chunk_cull.csh @@ -33,7 +33,7 @@ layout(std430, binding = 2) writeonly buffer IndirectCmds { DrawElementsIndirectCommand cmds[]; } indirect; -layout(std140, binding = 0) uniform Frustum { +layout(std140, binding = 1) uniform Frustum { vec4 planes[6]; // camera-relative planes from `proj * mv` (no translate baked in) // control.x = visibleCount (iteration bound) // control.y = indexPointerMask (0 non-sorted -> firstIndex masked to 0; 0xFFFFFFFF sorted) diff --git a/src/test/java/com/gtnewhorizons/angelica/arch/Lwjgl3AwareBoundaryTest.java b/src/test/java/com/gtnewhorizons/angelica/arch/Lwjgl3AwareBoundaryTest.java new file mode 100644 index 000000000..017e49ef1 --- /dev/null +++ b/src/test/java/com/gtnewhorizons/angelica/arch/Lwjgl3AwareBoundaryTest.java @@ -0,0 +1,127 @@ +package com.gtnewhorizons.angelica.arch; + +import com.tngtech.archunit.core.domain.AccessTarget; +import com.tngtech.archunit.core.domain.JavaAccess; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.core.importer.ImportOption; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Enforces the lwjgl3ify remap boundary. + */ +class Lwjgl3AwareBoundaryTest { + + private static final String AWARE = "me.eigenraven.lwjgl3ify.api.Lwjgl3Aware"; + + /** Packages whose classes lwjgl3ify never transforms, so their descriptors match on both sides. */ + private static final List UNTRANSFORMED_PACKAGES = List.of("org.lwjgl.", "org.lwjglx."); + + /** Modules built against native LWJGL3; every class in them must be aware. */ + private static final List AWARE_MODULES = List.of( + "com.gtnewhorizons.angelica.sdlgpu.", + "com.gtnewhorizons.angelica.lwjgl3."); + + private static final JavaClasses classes = new ClassFileImporter() + .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) + .importPaths( + "build/classes/java/main", + "build/classes/java/mixin", + "glsm/build/classes/java/main", + "lwjgl3-backend/build/classes/java/main", + "sdl-gpu/build/classes/java/main"); + + @Test + void awareClassesDoNotReadRemappedMembers() { + final List violations = new ArrayList<>(); + for (JavaClass origin : classes) { + if (!isAware(origin)) continue; + for (JavaAccess access : origin.getAccessesFromSelf()) { + final JavaClass owner = access.getTargetOwner(); + if (isUntransformed(owner) || isAware(owner)) continue; + if (mentionsLwjgl3Type(access.getTarget())) violations.add(describe(access)); + } + } + assertTrue(violations.isEmpty(), "@Lwjgl3Aware code referencing a remapped member with an org.lwjgl type in its descriptor:\n" + String.join("\n", violations)); + } + + @Test + void remappedClassesDoNotReadAwareMembers() { + final List violations = new ArrayList<>(); + for (JavaClass origin : classes) { + if (isAware(origin) || isUntransformed(origin)) continue; + for (JavaAccess access : origin.getAccessesFromSelf()) { + if (!isAware(access.getTargetOwner())) continue; + if (mentionsLwjgl3Type(access.getTarget())) violations.add(describe(access)); + } + } + assertTrue(violations.isEmpty(), "Remapped code referencing an @Lwjgl3Aware member with an org.lwjgl type in its descriptor:\n" + String.join("\n", violations)); + } + + /** Verifies the build-time injection in :sdl-gpu / :lwjgl3-backend actually ran over the class output. */ + @Test + void nativeLwjgl3ModulesAreFullyAware() { + final List missing = new ArrayList<>(); + for (JavaClass c : classes) { + if (AWARE_MODULES.stream().anyMatch(p -> c.getName().startsWith(p)) && !isAware(c)) { + missing.add(c.getName()); + } + } + assertTrue(missing.isEmpty(), "Classes missing @Lwjgl3Aware:\n" + String.join("\n", missing)); + } + + @Test + void renderBackendSignaturesAreRemapSafe() { + final JavaClass renderBackend = classes.get("com.gtnewhorizons.angelica.glsm.backend.RenderBackend"); + final List violations = new ArrayList<>(); + for (JavaMethod method : renderBackend.getMethods()) { + final List types = new ArrayList<>(method.getRawParameterTypes()); + types.add(method.getRawReturnType()); + if (types.stream().anyMatch(Lwjgl3AwareBoundaryTest::isLwjgl3Type)) { + violations.add(method.getFullName()); + } + } + assertTrue(violations.isEmpty(), "RenderBackend methods with an org.lwjgl type in their signature:\n" + String.join("\n", violations)); + } + + private static boolean isAware(JavaClass c) { + return c.isAnnotatedWith(AWARE); + } + + private static boolean isUntransformed(JavaClass c) { + return UNTRANSFORMED_PACKAGES.stream().anyMatch(p -> c.getName().startsWith(p)); + } + + private static boolean isLwjgl3Type(JavaClass type) { + return type.getName().startsWith("org.lwjgl."); + } + + private static boolean mentionsLwjgl3Type(AccessTarget target) { + return descriptorTypes(target).stream().anyMatch(Lwjgl3AwareBoundaryTest::isLwjgl3Type); + } + + private static List descriptorTypes(AccessTarget target) { + if (target instanceof AccessTarget.FieldAccessTarget field) { + return List.of(field.getRawType()); + } + if (target instanceof AccessTarget.CodeUnitCallTarget call) { + final List types = new ArrayList<>(call.getRawParameterTypes()); + if (target instanceof AccessTarget.MethodCallTarget method) { + types.add(method.getRawReturnType()); + } + return types; + } + return List.of(); + } + + private static String describe(JavaAccess access) { + return " " + access.getOwner().getFullName() + " -> " + access.getTarget().getFullName(); + } +} diff --git a/src/test/resources/archunit.properties b/src/test/resources/archunit.properties new file mode 100644 index 000000000..0675408e0 --- /dev/null +++ b/src/test/resources/archunit.properties @@ -0,0 +1,2 @@ +# The arch tests only need names and descriptors off the imported bytecode. +resolveMissingDependenciesFromClassPath=false