Expand SDL-GPU's GLSM redirector to GLSM and remove unredirected GL workarounds and move action back to master (#2268)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-10-17 17:58:50 -07:00
1 parent ab3bf17696
commit 482dece0f7
17 files changed
+146 -122

No files matched your search

+1 -1
View File
@@ -9,7 +9,7 @@ on:
jobs:
build-and-test:
uses: GTNewHorizons/GTNH-Actions-Workflows/.github/workflows/build-and-test.yml@05e7cd7ce0ea015acda8c10c1c39fb3a20a49e60
uses: GTNewHorizons/GTNH-Actions-Workflows/.github/workflows/build-and-test.yml@master
with:
disable-spotless-pr: true
java-distribution: temurin
+8
View File
@@ -170,7 +170,13 @@ val archScanAsm: Configuration by configurations.creating {
isCanBeResolved = true
}
val glsmRedirectAgent: Configuration by configurations.creating {
isCanBeConsumed = false
isCanBeResolved = true
}
dependencies {
glsmRedirectAgent(project(":glsm", "redirectAgent"))
archScanAsm(libs.asm.tree.test)
archScanAsm(libs.asm.commons.test)
}
@@ -212,6 +218,8 @@ val glCoreTest by tasks.registering(Test::class) {
.plus(downgraded(downgradeRootMainClasses))
.plus(sourceSets["test"].runtimeClasspath.minus(rawMain).minus(rawTest))
javaAgent(glsmRedirectAgent)
configureAngelicaJava8()
}
+8
View File
@@ -0,0 +1,8 @@
import org.gradle.api.file.FileCollection
import org.gradle.api.tasks.testing.Test
import org.gradle.process.CommandLineArgumentProvider
fun Test.javaAgent(jar: FileCollection) {
inputs.files(jar).withPropertyName("javaAgent")
jvmArgumentProviders.add(CommandLineArgumentProvider { listOf("-javaagent:${jar.singleFile.path}") })
}
+17
View File
@@ -267,6 +267,22 @@ tasks.test {
configureGlsmJava8()
}
val redirectAgentJar by tasks.registering(Jar::class) {
archiveClassifier = "redirect-agent"
manifest {
attributes("Premain-Class" to "com.gtnewhorizons.angelica.glsm.GlsmRedirectAgent")
}
}
val redirectAgent by configurations.creating {
isCanBeConsumed = true
isCanBeResolved = false
}
artifacts {
add(redirectAgent.name, redirectAgentJar)
}
val glTestTasks = GL_TASK_TAGS.map { (taskName, tag) ->
tasks.register<Test>(taskName) {
description = "Runs the $tag GL tests in their own JVM, so they get their own Display and GL profile."
@@ -275,6 +291,7 @@ val glTestTasks = GL_TASK_TAGS.map { (taskName, tag) ->
includeTags = setOf(tag)
}
configureGlsmJava8()
javaAgent(files(redirectAgentJar))
}
}
@@ -18,7 +18,7 @@ public class GLSM_VAO_UnitTest {
@AfterEach
void resetClientArrayState() {
glBindVertexArray(0);
UniversalVAO.bindVertexArray(0);
GLStateManager.glClientActiveTexture(GL13.GL_TEXTURE0);
GLSMUtil.setClientState(GL11.GL_NORMAL_ARRAY, false);
}
@@ -66,7 +66,7 @@ public class GLSM_VAO_UnitTest {
int vao = UniversalVAO.genVertexArrays();
Assertions.assertTrue(vao > 0, "GL_VERTEX_ARRAY_BINDING - VAO Created");
glBindVertexArray(vao);
UniversalVAO.bindVertexArray(vao);
verifyState(GL30.GL_VERTEX_ARRAY_BINDING, vao, "GL_VERTEX_ARRAY_BINDING - Bound");
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
@@ -91,7 +91,7 @@ public class GLSM_VAO_UnitTest {
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
// Check if everything got reset to default
glBindVertexArray(0);
UniversalVAO.bindVertexArray(0);
verifyState(GL30.GL_VERTEX_ARRAY_BINDING, GLStateManager.ctx().defaultVAO, "GL_VERTEX_ARRAY_BINDING - Unbound");
verifyState(GL15.GL_ARRAY_BUFFER_BINDING, 0, "GL_ARRAY_BUFFER_BINDING - Unbound");
@@ -103,18 +103,18 @@ public class GLSM_VAO_UnitTest {
verifyState(GL11.GL_NORMAL_ARRAY, true, "GL_NORMAL_ARRAY - Reset");
// Check if the VAO states are still valid after re-binding it
glBindVertexArray(vao);
UniversalVAO.bindVertexArray(vao);
verifyState(GL11.GL_VERTEX_ARRAY, vaoVertexEnabled, "GL_VERTEX_ARRAY - Preserved");
verifyState(GL11.GL_VERTEX_ARRAY_SIZE, vaoVertexSize, "GL_VERTEX_ARRAY_SIZE - Preserved");
verifyState(GL11.GL_VERTEX_ARRAY_TYPE, vaoVertexType, "GL_VERTEX_ARRAY_TYPE - Preserved");
verifyState(GL11.GL_VERTEX_ARRAY_STRIDE, vaoVertexStride, "GL_VERTEX_ARRAY_STRIDE - Preserved");
glBindVertexArray(0);
UniversalVAO.bindVertexArray(0);
GLStateManager.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS);
glBindVertexArray(vao);
UniversalVAO.bindVertexArray(vao);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
GLStateManager.glPopAttrib();
@@ -123,7 +123,7 @@ public class GLSM_VAO_UnitTest {
verifyState(GL15.GL_ARRAY_BUFFER_BINDING, vbo, "GL_ARRAY_BUFFER_BINDING - Not affected by glPushAttrib");
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
UniversalVAO.bindVertexArray(0);
GLStateManager.glDeleteBuffers(vbo);
UniversalVAO.deleteVertexArrays(vao);
@@ -132,7 +132,7 @@ public class GLSM_VAO_UnitTest {
@Test
void testGenericAttributeSupersedesFfpConventional() {
final int vao = UniversalVAO.genVertexArrays();
glBindVertexArray(vao);
UniversalVAO.bindVertexArray(vao);
final int vbo = GL15.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
@@ -145,7 +145,7 @@ public class GLSM_VAO_UnitTest {
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
UniversalVAO.bindVertexArray(0);
GLStateManager.glDeleteBuffers(vbo);
UniversalVAO.deleteVertexArrays(vao);
}
@@ -153,7 +153,7 @@ public class GLSM_VAO_UnitTest {
@Test
void testGenericAttributeWinsRegardlessOfOrder() {
final int vao = UniversalVAO.genVertexArrays();
glBindVertexArray(vao);
UniversalVAO.bindVertexArray(vao);
final int vbo = GL15.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
@@ -166,7 +166,7 @@ public class GLSM_VAO_UnitTest {
GLStateManager.glDisableVertexAttribArray(0);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
UniversalVAO.bindVertexArray(0);
GLStateManager.glDeleteBuffers(vbo);
UniversalVAO.deleteVertexArrays(vao);
}
@@ -174,7 +174,7 @@ public class GLSM_VAO_UnitTest {
@Test
void testFfpConventionalAppliesWithoutGenericAttribute() {
final int vao = UniversalVAO.genVertexArrays();
glBindVertexArray(vao);
UniversalVAO.bindVertexArray(vao);
final int vbo = GL15.glGenBuffers();
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, vbo);
@@ -186,7 +186,7 @@ public class GLSM_VAO_UnitTest {
GLStateManager.glDisableClientState(GL11.GL_VERTEX_ARRAY);
GLStateManager.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
UniversalVAO.bindVertexArray(0);
GLStateManager.glDeleteBuffers(vbo);
UniversalVAO.deleteVertexArrays(vao);
}
@@ -198,12 +198,4 @@ public class GLSM_VAO_UnitTest {
verifyState(GL11.GL_COLOR_ARRAY, false, "GL_COLOR_ARRAY Default State");
verifyState(GL11.GL_INDEX_ARRAY, false, "GL_INDEX_ARRAY Default State");
}
private static void glBindVertexArray(int array) {
final GLContextState glCtx = GLStateManager.ctx();
if (array == 0) array = glCtx.defaultVAO;
glCtx.boundVAO = array;
glCtx.vaos.onBindVertexArrayPre(array);
UniversalVAO.bindVertexArray(array);
}
}
@@ -35,7 +35,6 @@ class WideLineClipGLTest {
@BeforeEach
void setUp() {
FfpFixture.routeVertexFormats(true);
savedEmulation = GLStateManager.wideLineEmulationEnabled;
GLStateManager.wideLineEmulationEnabled = true;
@@ -54,7 +53,6 @@ class WideLineClipGLTest {
@AfterEach
void tearDown() {
FfpFixture.routeVertexFormats(false);
GLStateManager.wideLineEmulationEnabled = savedEmulation;
GLStateManager.glLineWidth(1.0f);
if (vbo != 0) { GLStateManager.glDeleteBuffers(vbo); vbo = 0; }
+1 -1
View File
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_ERR">
<Console name="Console" target="SYSTEM_ERR" follow="true">
<PatternLayout pattern="[%-5level] %logger{36} - %msg%n"/>
</Console>
</Appenders>
@@ -28,6 +28,7 @@ public class GLSMCoreExtension implements BeforeAllCallback, BeforeEachCallback,
@Override
public void beforeAll(ExtensionContext context) throws LWJGLException {
GlsmRedirectAgent.requireInstalled();
if (startFailure != null) {
throw new IllegalStateException("GLSMCoreExtension context setup failed earlier in this JVM", startFailure);
}
@@ -81,6 +82,7 @@ public class GLSMCoreExtension implements BeforeAllCallback, BeforeEachCallback,
public void afterEach(ExtensionContext context) {
final int error = GL11.glGetError();
while (GL11.glGetError() != GL11.GL_NO_ERROR) {}
GlsmRedirectAgent.assertNoFailures();
assertEquals(GL11.GL_NO_ERROR, error, () -> "GL Error: 0x" + Integer.toHexString(error));
}
@@ -61,6 +61,7 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
@Override
public void beforeAll(ExtensionContext context) throws LWJGLException {
GlsmRedirectAgent.requireInstalled();
if (startFailure != null) {
throw new IllegalStateException("GLSMExtension context setup failed earlier in this JVM", startFailure);
}
@@ -179,6 +180,7 @@ public class GLSMExtension implements BeforeAllCallback, BeforeEachCallback, Aft
fail("Test leaked an open display-list recorder! Missing glEndList.");
}
GlsmRedirectAgent.assertNoFailures();
final int error = GL11.glGetError();
assertEquals(GL11.GL_NO_ERROR, error,
() -> "GL Error: 0x" + Integer.toHexString(error));
@@ -0,0 +1,76 @@
package com.gtnewhorizons.angelica.glsm;
import com.gtnewhorizons.angelica.glsm.loading.ClassDump;
import com.gtnewhorizons.angelica.glsm.loading.ModRedirector;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import static org.junit.jupiter.api.Assertions.fail;
public final class GlsmRedirectAgent {
private static final String GTNHLIB = "com/gtnewhorizon/gtnhlib/";
private static final List<Throwable> FAILURES = new CopyOnWriteArrayList<>();
private static final AtomicInteger TRANSFORMED = new AtomicInteger();
private static boolean installed;
private GlsmRedirectAgent() {}
public static void premain(String args, Instrumentation instrumentation) {
install(instrumentation);
}
public static void install(Instrumentation instrumentation) {
final ModRedirector redirector = new ModRedirector(ClassDump.DISABLED);
addTransformer(instrumentation, (className, classfile) -> {
if (!className.startsWith(GTNHLIB)) return null;
final byte[] out = redirector.transform(className.replace('/', '.'), classfile, null);
if (out == classfile) return null;
TRANSFORMED.incrementAndGet();
return out;
});
installed = true;
}
public static void addTransformer(Instrumentation instrumentation, BiFunction<String, byte[], byte[]> transform) {
instrumentation.addTransformer(new Guarded(transform));
}
public static void requireInstalled() {
if (!installed) throw new IllegalStateException("GL tests need the GLSM redirect agent");
}
public static void assertNoFailures() {
if (!FAILURES.isEmpty()) fail("agent transform failures: " + FAILURES, FAILURES.get(0));
}
public static int transformedCount() {
return TRANSFORMED.get();
}
private static final class Guarded implements ClassFileTransformer {
private final BiFunction<String, byte[], byte[]> transform;
private Guarded(BiFunction<String, byte[], byte[]> transform) {
this.transform = transform;
}
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> redefined, ProtectionDomain domain, byte[] classfile) {
if (className == null) return null;
try {
return transform.apply(className, classfile);
} catch (Throwable t) {
FAILURES.add(new IllegalStateException("transforming " + className, t));
return null;
}
}
}
}
@@ -1,8 +1,5 @@
package com.gtnewhorizons.angelica.glsm.ffp;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormat;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement;
import com.gtnewhorizon.gtnhlib.client.renderer.vertex.VertexFormatElement.Usage;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.GlTestSupport;
import org.lwjgl.BufferUtils;
@@ -136,27 +133,6 @@ public final class FfpFixture {
GLStateManager.glVertexAttribPointer(loc, size, type, normalized, stride, offset);
}
private static boolean formatRoutingRegistered;
private static boolean routeFormats;
public static void routeVertexFormats(boolean enabled) {
routeFormats = enabled;
if (formatRoutingRegistered) return;
formatRoutingRegistered = true;
VertexFormat.registerSetupBufferStateOverride((format, offset) -> {
if (!routeFormats) return false;
final int stride = format.getVertexSize();
int elementOffset = (int) offset;
for (VertexFormatElement element : format.elementsArray) {
final Usage usage = element.getUsage();
final boolean normalized = usage == Usage.COLOR || usage == Usage.NORMAL;
attrib(usage.getAttributeLocation(), element.getCount(), element.getType().getGlType(), normalized, stride, elementOffset);
elementOffset += element.getByteSize();
}
return true;
});
}
public static void clear() {
GLStateManager.glClearColor(0f, 0f, 0f, 1f);
GLStateManager.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
+3 -3
View File
@@ -108,6 +108,7 @@ val glsmSdlAgent = sourceSets.create("glsmSdlAgent") {
dependencies {
"glsmSdlAgentCompileOnly"(project(":glsm"))
"glsmSdlAgentCompileOnly"(testFixtures(project(":glsm")))
"glsmSdlAgentCompileOnly"(libs.asm.tree.test)
testImplementation(glsmSdlAgent.output)
}
@@ -155,9 +156,8 @@ val glsmSdlTest by tasks.registering(Test::class) {
useJUnitPlatform {
includeTags(glsmSdlTag)
}
dependsOn(tasks.named("glsmSdlStubsClasses"), glsmSdlRedirectAgentJar)
val agentJarPath = glsmSdlRedirectAgentJar.flatMap { it.archiveFile }.map { it.asFile.path }
jvmArgumentProviders.add(CommandLineArgumentProvider { listOf("-javaagent:${agentJarPath.get()}") })
dependsOn(tasks.named("glsmSdlStubsClasses"))
javaAgent(files(glsmSdlRedirectAgentJar))
jvmArgs(
"-Dangelica.sdlgpu.enable=true",
"-Dangelica.sdlgpu.disablePresenterThread=true",
@@ -1,60 +1,15 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.redirect.GLSMRedirector;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import com.gtnewhorizons.angelica.glsm.GlsmRedirectAgent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public final class GlsmSdlRedirectAgent {
private static final String GTNHLIB = "com/gtnewhorizon/gtnhlib/";
private static final List<String> FAILURES = Collections.synchronizedList(new ArrayList<>());
private static final AtomicInteger TRANSFORMED_COUNT = new AtomicInteger();
private GlsmSdlRedirectAgent() {}
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new Redirector());
}
public static List<String> failures() {
return List.copyOf(FAILURES);
}
public static int transformedCount() {
return TRANSFORMED_COUNT.get();
}
private static final class Redirector implements ClassFileTransformer {
private final GLSMRedirector redirector = new GLSMRedirector();
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> redefined, ProtectionDomain domain, byte[] classfile) {
if (className == null) return null;
try {
final byte[] observed = MovedSamplerProbe.instrument(className, classfile);
if (observed != null) return observed;
if (!className.startsWith(GTNHLIB) || !redirector.shouldTransform(classfile)) return null;
final ClassNode node = new ClassNode();
new ClassReader(classfile).accept(node, 0);
if (!redirector.transformClassNode(className.replace('/', '.'), node)) return null;
final ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
node.accept(writer);
TRANSFORMED_COUNT.incrementAndGet();
return writer.toByteArray();
} catch (Throwable t) {
FAILURES.add(className + ": " + t);
return null;
}
}
GlsmRedirectAgent.install(instrumentation);
GlsmRedirectAgent.addTransformer(instrumentation, MovedSamplerProbe::instrument);
}
}
@@ -1,12 +1,11 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GlsmRedirectAgent;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import static org.junit.jupiter.api.Assertions.assertTrue;
public final class GlsmSdlFrameExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback {
@Override
@@ -22,7 +21,6 @@ public final class GlsmSdlFrameExtension implements BeforeAllCallback, BeforeEac
@Override
public void afterEach(ExtensionContext context) {
GlsmSdlHeadlessRig.endFrame();
assertTrue(GlsmSdlRedirectAgent.failures().isEmpty(),
() -> "redirect agent recorded failures: " + GlsmSdlRedirectAgent.failures());
GlsmRedirectAgent.assertNoFailures();
}
}
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.GlsmRedirectAgent;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import com.gtnewhorizons.angelica.glsm.hooks.GLSMInitConfig;
@@ -71,9 +72,8 @@ public final class GlsmSdlHeadlessRig {
GLStateManager.setRunningSplash(false);
GLStateManager.markSplashComplete("glsmSdlHeadlessRig");
assertTrue(GlsmSdlRedirectAgent.transformedCount() > 0, "redirect agent transformed no classes");
assertTrue(GlsmSdlRedirectAgent.failures().isEmpty(),
() -> "redirect agent recorded failures: " + GlsmSdlRedirectAgent.failures());
assertTrue(GlsmRedirectAgent.transformedCount() > 0, "redirect agent transformed no classes");
GlsmRedirectAgent.assertNoFailures();
createTarget();
booted = true;
@@ -1,12 +1,12 @@
package com.gtnewhorizons.angelica.sdlgpu.glsm;
import com.gtnewhorizons.angelica.glsm.GlsmRedirectAgent;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public final class GlsmSdlOutOfFrameExtension implements BeforeAllCallback, BeforeEachCallback, AfterEachCallback {
@@ -24,6 +24,6 @@ public final class GlsmSdlOutOfFrameExtension implements BeforeAllCallback, Befo
@Override
public void afterEach(ExtensionContext context) {
GlsmSdlHeadlessRig.endFrame();
assertTrue(GlsmSdlRedirectAgent.failures().isEmpty(), () -> "redirect agent recorded failures: " + GlsmSdlRedirectAgent.failures());
GlsmRedirectAgent.assertNoFailures();
}
}
@@ -60,8 +60,7 @@ class RetainedTesrGroupsGLTest {
private static final float[] TRIANGLE = { -0.1f, -0.1f, 0f, 0.1f, -0.1f, 0f, 0f, 0.15f, 0f };
private static final ResourceLocation GLINT_TEXTURE = new ResourceLocation("angelica", "test/glint");
private static boolean overridesRegistered;
private static boolean skipClientArrays;
private static boolean probeRegistered;
private static Runnable onBufferSetup;
private AngelicaBufferSource source;
@@ -82,14 +81,13 @@ class RetainedTesrGroupsGLTest {
private CubeParams[] glintCubes;
@BeforeAll
static void registerClientArrayOverrides() {
if (overridesRegistered) return;
overridesRegistered = true;
static void registerBufferSetupProbe() {
if (probeRegistered) return;
probeRegistered = true;
VertexFormat.registerSetupBufferStateOverride((format, offset) -> {
if (onBufferSetup != null) onBufferSetup.run();
return skipClientArrays;
return false;
});
VertexFormat.registerClearBufferStateOverride(format -> skipClientArrays);
}
@BeforeEach
@@ -119,7 +117,6 @@ class RetainedTesrGroupsGLTest {
@AfterEach
void cleanup() {
onBufferSetup = null;
skipClientArrays = false;
source.discard();
GLStateManager.setFfpInstancing(Instancing.NONE);
GLStateManager.glBindVertexArray(0);
@@ -221,12 +218,8 @@ class RetainedTesrGroupsGLTest {
state.setCurrentBlockEntity(83);
state.setCurrentEntityColor(0, 1, 0, 0.5f);
draws.clear();
skipClientArrays = true;
assertEquals(GL11.GL_QUADS, template.drawMode);
groups.drawLayer(layer);
// FIXME each GL_QUADS fallback draw (VertexBuffer.draw inside MeshBuffer.render) raises this with the
// client arrays skipped; a GL_TRIANGLES template does not.
assertEquals(GL11.GL_INVALID_ENUM, GL11.glGetError());
assertEquals(3, draws.size(), "only differing uniform values split the CPU fallback");
for (int i = 0; i < 3; i++) {
assertArrayEquals(new long[] {45010 + i, 45020 + i, 45030 + i,
@@ -339,7 +332,6 @@ class RetainedTesrGroupsGLTest {
assertEquals(2, group.cubeColumns.runs.size(), "sanity: cube runs A,B,A merge into two heads");
assertNotNull(group.cubeColumns.runs.get(0).next, "sanity: the cube A head must carry a second segment");
skipClientArrays = true;
groups.drawLayer(layer);
assertEquals(0, pipeline.matrixBinds);