From 1c5dc1d68f9f49900bb8d85ea294177785112770 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Fri, 2 Oct 2026 17:24:25 +0200 Subject: [PATCH 1/3] Restore unknown texture and framebuffer bindings after invalidation --- .../java/com/jme3/renderer/RenderContext.java | 42 +- .../com/jme3/renderer/opengl/GLRenderer.java | 40 +- .../renderer/RenderContextBindingTest.java | 108 ++++ .../GlRendererBindingInvalidationTest.java | 480 ++++++++++++++++++ 4 files changed, 660 insertions(+), 10 deletions(-) create mode 100644 jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java create mode 100644 jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererBindingInvalidationTest.java diff --git a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java index 15a202a6a5..8c04b83be0 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java @@ -207,12 +207,15 @@ private static WeakReference[] newWeakReferenceArray(int size) { public Shader boundShader; /** - * ID of the bound FrameBuffer. + * ID of the bound FrameBuffer. This value is untrusted while + * {@link #isFrameBufferBindingValid()} is false. * * @see Renderer#setFrameBuffer(com.jme3.texture.FrameBuffer) */ public int boundFBO; + private boolean frameBufferBindingValid = true; + /** * Currently bound FrameBuffer. * @@ -289,7 +292,7 @@ private static WeakReference[] newWeakReferenceArray(int size) { public final IDList textureIndexList = new IDList(); /** - * Currently bound texture unit. + * Currently bound texture unit, or -1 when the active unit is unknown. * * @see Renderer#setTexture(int, com.jme3.texture.Texture) */ @@ -374,6 +377,40 @@ public RenderContext() { init(); } + /** + * Tests whether the cached framebuffer binding can suppress a GL bind. + * + * @return true if the binding is known + */ + public boolean isFrameBufferBindingValid() { + return frameBufferBindingValid; + } + + /** + * Updates the validity of the cached framebuffer binding after applying + * native state or restoring a previously saved cache. + * + * @param valid true if the cached framebuffer binding is known + */ + public void setFrameBufferBindingValid(boolean valid) { + frameBufferBindingValid = valid; + } + + /** + * Invalidates texture and framebuffer bindings after external GL changes. + * This does not modify native state. Unlike reset(), it does not assume + * that texture unit zero or the default framebuffer is bound. + */ + public void invalidateBindings() { + boundTextureUnit = -1; + frameBufferBindingValid = false; + boundFB = null; + for (int i = 0; i < boundTextures.length; i++) { + boundTextures[i] = null; + } + textureIndexList.reset(); + } + private void init() { cullMode = RenderState.FaceCullMode.Off; @@ -399,6 +436,7 @@ private void init() { boundShader = null; boundFBO = 0; boundFB = null; + frameBufferBindingValid = true; boundRB = 0; boundElementArrayVBO = 0; diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index be54cb3092..b0cf187e0a 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -903,6 +903,7 @@ public void initialize() { @Override public void invalidateState() { context.reset(); + context.invalidateBindings(); if (gl2 != null) { context.initialDrawBuf = getInteger(GL2.GL_DRAW_BUFFER); context.initialReadBuf = getInteger(GL2.GL_READ_BUFFER); @@ -2004,6 +2005,13 @@ public void copyFrameBuffer(FrameBuffer src, FrameBuffer dst, boolean copyColor, int prevFBO = context.boundFBO; FrameBuffer prevFB = context.boundFB; + boolean prevBindingValid = context.isFrameBufferBindingValid(); + // Attachment updates below can bind another framebuffer. An unknown + // binding may also have different native read and draw targets. + final int prevReadFramebuffer = prevBindingValid ? prevFBO + : getInteger(GLFbo.GL_READ_FRAMEBUFFER_BINDING_EXT); + final int prevDrawFramebuffer = prevBindingValid ? prevFBO + : getInteger(GLFbo.GL_DRAW_FRAMEBUFFER_BINDING_EXT); if (mainFbOverride != null) { if (src == null) { @@ -2062,10 +2070,18 @@ public void copyFrameBuffer(FrameBuffer src, FrameBuffer dst, boolean copyColor, GL.GL_NEAREST); - glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, prevFBO); + if (prevBindingValid) { + glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, prevFBO); + } else { + glfbo.glBindFramebufferEXT(GLFbo.GL_READ_FRAMEBUFFER_EXT, prevReadFramebuffer); + glfbo.glBindFramebufferEXT(GLFbo.GL_DRAW_FRAMEBUFFER_EXT, prevDrawFramebuffer); + } context.boundFBO = prevFBO; context.boundFB = prevFB; - toggleFramebufferSrgb(prevFB); + context.setFrameBufferBindingValid(prevBindingValid); + if (prevBindingValid) { + toggleFramebufferSrgb(prevFB); + } } else { throw new RendererException("Framebuffer blitting not supported by the video hardware"); } @@ -2220,18 +2236,20 @@ public void updateFrameBufferAttachment(FrameBuffer fb, RenderBuffer rb) { private void bindFrameBuffer(FrameBuffer fb) { if (fb == null) { - if (context.boundFBO != defaultFBO) { + if (!context.isFrameBufferBindingValid() || context.boundFBO != defaultFBO) { glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, defaultFBO); statistics.onFrameBufferUse(null, true); context.boundFBO = defaultFBO; context.boundFB = null; + context.setFrameBufferBindingValid(true); } } else { assert fb.getId() != -1 && fb.getId() != 0; - if (context.boundFBO != fb.getId()) { + if (!context.isFrameBufferBindingValid() || context.boundFBO != fb.getId()) { glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, fb.getId()); context.boundFBO = fb.getId(); context.boundFB = fb; + context.setFrameBufferBindingValid(true); statistics.onFrameBufferUse(fb, true); } else { statistics.onFrameBufferUse(fb, false); @@ -2311,7 +2329,7 @@ public Vector2f[] getFrameBufferSamplePositions(FrameBuffer fb) { @Override public void setMainFrameBufferOverride(FrameBuffer fb) { mainFbOverride = null; - if (context.boundFBO == 0) { + if (context.isFrameBufferBindingValid() && context.boundFBO == 0) { // Main FB is now set to fb, make sure its bound setFrameBuffer(fb); } @@ -2385,7 +2403,13 @@ public void setFrameBuffer(FrameBuffer fb) { fb = mainFbOverride; } - if (context.boundFB == fb) { + if (fb == null && !caps.contains(Caps.FrameBuffer)) { + // Selecting the screen does not require framebuffer-object support. + toggleFramebufferSrgb(null); + return; + } + + if (context.isFrameBufferBindingValid() && context.boundFB == fb) { if (fb == null || !fb.isUpdateNeeded()) { toggleFramebufferSrgb(fb); return; @@ -2488,7 +2512,7 @@ private void deleteRenderBuffer(FrameBuffer fb, RenderBuffer rb) { @Override public void deleteFrameBuffer(FrameBuffer fb) { if (fb.getId() != -1) { - if (context.boundFBO == fb.getId()) { + if (context.isFrameBufferBindingValid() && context.boundFBO == fb.getId()) { glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, 0); context.boundFBO = 0; } @@ -3838,7 +3862,7 @@ public void setMainFrameBufferSrgb(boolean enableSrgb) { } mainFrameBufferSrgb = enableSrgb; - if (context.boundFB == null) { + if (context.isFrameBufferBindingValid() && context.boundFB == null) { toggleFramebufferSrgb(null); } } diff --git a/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java b/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java new file mode 100644 index 0000000000..c1d6b635a9 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import org.junit.jupiter.api.Test; + +/** + * Verifies the distinction between fresh defaults and externally changed bindings. + */ +public class RenderContextBindingTest { + + @Test + public void freshContextHasKnownDefaultBindings() { + RenderContext context = new RenderContext(); + assertDefaults(context); + } + + @Test + public void bindingInvalidationClearsTextureReferencesWithoutAssumingNativeDefaults() { + RenderContext context = new RenderContext(); + Image image = new Image(); + context.boundTextures[0] = image.getWeakRef(); + context.boundTextures[3] = image.getWeakRef(); + context.textureIndexList.moveToNew(3); + context.textureIndexList.copyNewToOld(); + context.boundTextureUnit = 3; + context.boundFBO = 12; + context.boundFB = new FrameBuffer(4, 4, 1); + + context.invalidateBindings(); + + assertEquals(-1, context.boundTextureUnit); + assertEquals(12, context.boundFBO); + assertNull(context.boundFB); + assertFalse(context.isFrameBufferBindingValid()); + for (int unit = 0; unit < context.boundTextures.length; unit++) { + assertNull(context.boundTextures[unit]); + } + assertEquals(0, context.textureIndexList.oldLen); + assertEquals(0, context.textureIndexList.newLen); + } + + @Test + public void resetReturnsBindingsToKnownFreshDefaults() { + RenderContext context = new RenderContext(); + context.boundFBO = 12; + context.boundTextureUnit = 3; + context.invalidateBindings(); + context.reset(); + assertDefaults(context); + } + + @Test + public void framebufferValidationDoesNotValidateTheActiveTextureUnit() { + RenderContext context = new RenderContext(); + context.invalidateBindings(); + context.boundFBO = 12; + context.setFrameBufferBindingValid(true); + assertTrue(context.isFrameBufferBindingValid()); + assertEquals(-1, context.boundTextureUnit); + context.setFrameBufferBindingValid(false); + assertFalse(context.isFrameBufferBindingValid()); + assertEquals(12, context.boundFBO); + } + + private static void assertDefaults(RenderContext context) { + assertEquals(0, context.boundTextureUnit); + assertEquals(0, context.boundFBO); + assertNull(context.boundFB); + assertTrue(context.isFrameBufferBindingValid()); + } +} diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererBindingInvalidationTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererBindingInvalidationTest.java new file mode 100644 index 0000000000..445f982ed8 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererBindingInvalidationTest.java @@ -0,0 +1,480 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer.opengl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.jme3.renderer.Caps; +import com.jme3.renderer.Limits; +import com.jme3.renderer.RenderContext; +import com.jme3.renderer.RendererException; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture2D; +import com.jme3.texture.image.ColorSpace; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.nio.ByteBuffer; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** Test-only model of native bindings; intentionally separate from RenderContext. */ +public class GlRendererBindingInvalidationTest { + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void textureZeroAfterExternalActiveUnit(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + driver.activeUnit = 3; + driver.textureBindings[0] = 16; + driver.renderer.invalidateState(); + driver.renderer.setTexture(0, texture(17)); + assertEquals(17, driver.textureBindings[0]); + assertEquals(0, driver.activeUnit); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void repeatedTextureZeroAfterInvalidation(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + Texture2D texture = texture(17); + driver.renderer.setTexture(0, texture); + driver.activeUnit = 3; + driver.textureBindings[0] = 16; + driver.renderer.invalidateState(); + driver.renderer.setTexture(0, texture); + assertEquals(17, driver.textureBindings[0]); + driver.calls.clear(); + driver.renderer.setTexture(0, texture); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void nonzeroTextureUnitControl(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + driver.activeUnit = 3; + driver.renderer.invalidateState(); + driver.renderer.setTexture(1, texture(17)); + assertEquals(17, driver.textureBindings[1]); + assertEquals(1, driver.activeUnit); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void freshTextureAndRepeatControl(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + Texture2D texture = texture(17); + driver.renderer.setTexture(0, texture); + assertEquals(17, driver.textureBindings[0]); + assertFalse(driver.calls.contains("glActiveTexture")); + driver.calls.clear(); + driver.renderer.setTexture(0, texture); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 7}) + public void defaultFramebufferAfterExternalBinding(int defaultId) throws Exception { + Recording driver = new Recording(false); + driver.setDefaultFramebuffer(defaultId); + driver.setNativeFramebuffer(9); + driver.renderer.invalidateState(); + driver.calls.clear(); + driver.renderer.setFrameBuffer(null); + assertEquals(defaultId, driver.drawFramebuffer); + assertEquals(1, driver.calls.stream().filter("glBindFramebufferEXT"::equals).count()); + driver.calls.clear(); + driver.renderer.setFrameBuffer(null); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 7}) + public void explicitFramebufferControl(int defaultId) throws Exception { + Recording driver = new Recording(false); + driver.setDefaultFramebuffer(defaultId); + driver.setNativeFramebuffer(9); + FrameBuffer target = new FrameBuffer(4, 4, 1); + target.setId(12); + target.clearUpdateNeeded(); + driver.renderer.invalidateState(); + driver.renderer.setFrameBuffer(target); + assertEquals(12, driver.drawFramebuffer); + driver.calls.clear(); + driver.renderer.setFrameBuffer(target); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void invalidationDoesNotMutateBindings(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + driver.activeUnit = 3; + driver.setNativeFramebuffer(9); + driver.renderer.invalidateState(); + assertEquals(3, driver.activeUnit); + assertEquals(9, driver.drawFramebuffer); + assertFalse(driver.calls.contains("glActiveTexture")); + assertFalse(driver.calls.contains("glBindFramebufferEXT")); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void freshDefaultFramebufferElidesBinding(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + driver.renderer.setFrameBuffer(null); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void nullFramebufferDoesNotRequireFramebufferSupport(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + driver.renderer.getCaps().remove(Caps.FrameBuffer); + driver.renderer.setFrameBuffer(null); + driver.renderer.invalidateState(); + driver.calls.clear(); + driver.renderer.setFrameBuffer(null); + driver.renderer.setFrameBuffer(null); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + assertThrows(RendererException.class, () -> driver.renderer.setFrameBuffer(framebuffer(12))); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void uploadAfterInvalidationUsesRequestedUnit(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + Texture2D texture = texture(17); + texture.getImage().setUpdateNeeded(); + driver.activeUnit = 3; + driver.renderer.invalidateState(); + driver.renderer.setTexture(0, texture); + assertEquals(0, driver.activeUnit); + assertEquals(17, driver.textureBindings[0]); + assertEquals(17, driver.lastUploadedTexture); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void parameterChangeAfterRepeatedInvalidationUsesRequestedUnit(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + Texture2D texture = texture(17); + driver.renderer.setTexture(0, texture); + driver.activeUnit = 3; + driver.textureBindings[3] = 19; + driver.renderer.invalidateState(); + driver.renderer.invalidateState(); + texture.setMagFilter(Texture.MagFilter.Bilinear); + driver.renderer.setTexture(0, texture); + assertEquals(0, driver.activeUnit); + assertEquals(17, driver.lastParameterTexture); + assertEquals(19, driver.textureBindings[3]); + driver.calls.clear(); + driver.renderer.setTexture(0, texture); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @ParameterizedTest + @CsvSource({"0, 9, 9", "0, 9, 10", "7, 9, 9", "7, 9, 10"}) + public void unknownCopyRestoresNativeBindingsWithoutValidatingCache( + int defaultId, int readId, int drawId) throws Exception { + Recording driver = new Recording(false); + driver.setDefaultFramebuffer(defaultId); + driver.readFramebuffer = readId; + driver.drawFramebuffer = drawId; + driver.renderer.invalidateState(); + driver.calls.clear(); + driver.renderer.copyFrameBuffer(framebuffer(12), framebuffer(13), true, false); + assertEquals(readId, driver.readFramebuffer); + assertEquals(drawId, driver.drawFramebuffer); + assertFalse(driver.context.isFrameBufferBindingValid()); + assertEquals(2, driver.countCalls("glGetInteger")); + assertEquals(List.of("glGetInteger", "glGetInteger"), driver.calls.subList(0, 2)); + driver.calls.clear(); + driver.renderer.setFrameBuffer(null); + assertEquals(defaultId, driver.readFramebuffer); + assertEquals(defaultId, driver.drawFramebuffer); + assertEquals(1, driver.countCalls("glBindFramebufferEXT")); + driver.calls.clear(); + driver.renderer.setFrameBuffer(null); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void unknownCopySnapshotsBindingsBeforeAttachmentUpdates(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + driver.readFramebuffer = 9; + driver.drawFramebuffer = 10; + driver.renderer.invalidateState(); + FrameBuffer source = new FrameBuffer(4, 4, 1); + source.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.RGBA8)); + FrameBuffer destination = new FrameBuffer(4, 4, 1); + destination.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.RGBA8)); + driver.calls.clear(); + driver.renderer.copyFrameBuffer(source, destination, true, false); + assertTrue(source.getId() > 0); + assertTrue(destination.getId() > 0); + assertEquals(9, driver.readFramebuffer); + assertEquals(10, driver.drawFramebuffer); + assertFalse(driver.context.isFrameBufferBindingValid()); + assertEquals(List.of("glGetInteger", "glGetInteger"), driver.calls.subList(0, 2)); + driver.renderer.setFrameBuffer(null); + assertEquals(0, driver.drawFramebuffer); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void knownCopyUsesCachedBindingAndPreservesElision(boolean desktop) throws Exception { + Recording driver = new Recording(desktop); + FrameBuffer previous = framebuffer(14); + driver.renderer.setFrameBuffer(previous); + driver.calls.clear(); + driver.renderer.copyFrameBuffer(framebuffer(12), framebuffer(13), true, false); + assertEquals(14, driver.readFramebuffer); + assertEquals(14, driver.drawFramebuffer); + assertTrue(driver.context.isFrameBufferBindingValid()); + assertSame(previous, driver.renderer.getCurrentFrameBuffer()); + assertEquals(0, driver.countCalls("glGetInteger")); + assertEquals(3, driver.countCalls("glBindFramebufferEXT")); + driver.calls.clear(); + driver.renderer.setFrameBuffer(previous); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @ParameterizedTest + @ValueSource(ints = {0, 7}) + public void unknownOverrideDefersBindingUntilNullSelection(int defaultId) throws Exception { + Recording driver = new Recording(false); + driver.setDefaultFramebuffer(defaultId); + driver.setNativeFramebuffer(9); + driver.renderer.invalidateState(); + driver.calls.clear(); + FrameBuffer override = framebuffer(12); + driver.renderer.setMainFrameBufferOverride(override); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + assertEquals(9, driver.drawFramebuffer); + driver.renderer.setFrameBuffer(null); + assertEquals(12, driver.drawFramebuffer); + assertTrue(driver.context.isFrameBufferBindingValid()); + driver.calls.clear(); + driver.renderer.setFrameBuffer(null); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @Test + public void knownMainOverrideRetainsImmediateBinding() throws Exception { + Recording driver = new Recording(false); + driver.renderer.setMainFrameBufferOverride(framebuffer(12)); + assertEquals(12, driver.drawFramebuffer); + assertTrue(driver.context.isFrameBufferBindingValid()); + } + + @ParameterizedTest + @CsvSource({"0, 9", "0, 10", "7, 9", "7, 10"}) + public void deletingWhileUnknownDoesNotValidateOrEagerlyRebind( + int defaultId, int deletedId) throws Exception { + Recording driver = new Recording(false); + driver.setDefaultFramebuffer(defaultId); + driver.setNativeFramebuffer(9); + driver.renderer.invalidateState(); + driver.calls.clear(); + driver.renderer.deleteFrameBuffer(framebuffer(deletedId)); + assertEquals(deletedId == 9 ? 0 : 9, driver.drawFramebuffer); + assertEquals(0, driver.countCalls("glBindFramebufferEXT")); + assertFalse(driver.context.isFrameBufferBindingValid()); + driver.renderer.setFrameBuffer(null); + assertEquals(defaultId, driver.drawFramebuffer); + assertTrue(driver.context.isFrameBufferBindingValid()); + } + + @Test + public void unknownCopyRestorationDoesNotAssumeMainSrgbTarget() throws Exception { + Recording driver = new Recording(false); + driver.renderer.getCaps().add(Caps.Srgb); + driver.renderer.getCaps().add(Caps.SrgbWriteControl); + driver.setNativeFramebuffer(9); + driver.renderer.invalidateState(); + driver.renderer.setMainFrameBufferSrgb(true); + driver.calls.clear(); + driver.renderer.copyFrameBuffer(framebuffer(12), framebuffer(13), true, false); + assertEquals(9, driver.drawFramebuffer); + assertEquals(0, driver.countCalls("glEnable")); + assertFalse(driver.context.isFrameBufferBindingValid()); + driver.renderer.setFrameBuffer(null); + assertEquals(1, driver.countCalls("glEnable")); + } + + @Test + public void mainSrgbConfigurationDoesNotToggleAnUnknownTarget() throws Exception { + Recording driver = new Recording(false); + driver.renderer.getCaps().add(Caps.Srgb); + driver.renderer.getCaps().add(Caps.SrgbWriteControl); + driver.setNativeFramebuffer(9); + driver.renderer.invalidateState(); + driver.calls.clear(); + driver.renderer.setMainFrameBufferSrgb(true); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + driver.renderer.setFrameBuffer(null); + assertEquals(1, driver.countCalls("glEnable")); + driver.calls.clear(); + driver.renderer.setMainFrameBufferSrgb(false); + assertEquals(1, driver.countCalls("glDisable")); + } + + private static Texture2D texture(int id) { + Image image = new Image(Image.Format.RGBA8, 1, 1, (ByteBuffer) null, ColorSpace.Linear); + image.setId(id); + Texture2D texture = new Texture2D(image); + texture.setMinFilter(Texture.MinFilter.NearestNoMipMaps); + texture.setMagFilter(Texture.MagFilter.Nearest); + image.clearUpdateNeeded(); + return texture; + } + + private static FrameBuffer framebuffer(int id) { + FrameBuffer framebuffer = new FrameBuffer(4, 4, 1); + framebuffer.setId(id); + framebuffer.clearUpdateNeeded(); + return framebuffer; + } + + private static final class Recording implements InvocationHandler { + private int activeUnit; + private int readFramebuffer; + private int drawFramebuffer; + private int nextObject = 100; + private int lastUploadedTexture; + private int lastParameterTexture; + private final int[] textureBindings = new int[16]; + private final List calls = new ArrayList<>(); + private final GLRenderer renderer; + private final RenderContext context; + + private Recording(boolean desktop) throws ReflectiveOperationException { + Class[] types = {desktop ? GL2.class : GL.class, GLExt.class, GLFbo.class}; + Object api = Proxy.newProxyInstance(GL.class.getClassLoader(), types, this); + renderer = new GLRenderer((GL) api, (GLExt) api, (GLFbo) api); + renderer.getCaps().add(Caps.FrameBuffer); + renderer.getCaps().add(Caps.FrameBufferBlit); + renderer.getCaps().add(desktop ? Caps.OpenGL20 : Caps.OpenGLES20); + renderer.getLimits().put(Limits.RenderBufferSize, 1024); + renderer.getLimits().put(Limits.FrameBufferAttachments, 16); + renderer.getLimits().put(Limits.TextureSize, 1024); + Field contextField = GLRenderer.class.getDeclaredField("context"); + contextField.setAccessible(true); + context = (RenderContext) contextField.get(renderer); + Field textureUtilField = GLRenderer.class.getDeclaredField("texUtil"); + textureUtilField.setAccessible(true); + TextureUtil textureUtil = (TextureUtil) textureUtilField.get(renderer); + textureUtil.initialize(renderer.getCaps()); + } + + private void setDefaultFramebuffer(int value) throws ReflectiveOperationException { + Field field = GLRenderer.class.getDeclaredField("defaultFBO"); + field.setAccessible(true); + field.setInt(renderer, value); + } + + private void setNativeFramebuffer(int value) { + readFramebuffer = value; + drawFramebuffer = value; + } + + private long countCalls(String name) { + return calls.stream().filter(name::equals).count(); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + String name = method.getName(); + calls.add(name); + if (name.equals("glActiveTexture")) { + activeUnit = (Integer) args[0] - GL.GL_TEXTURE0; + } else if (name.equals("glBindTexture")) { + textureBindings[activeUnit] = (Integer) args[1]; + } else if (name.equals("glTexImage2D")) { + lastUploadedTexture = textureBindings[activeUnit]; + } else if (name.equals("glTexParameteri")) { + lastParameterTexture = textureBindings[activeUnit]; + } else if (name.equals("glBindFramebufferEXT")) { + int target = (Integer) args[0]; + int framebuffer = (Integer) args[1]; + assertTrue(framebuffer >= 0, "Unknown framebuffer ID passed to GL"); + if (target != GLFbo.GL_DRAW_FRAMEBUFFER_EXT) { + readFramebuffer = framebuffer; + } + if (target != GLFbo.GL_READ_FRAMEBUFFER_EXT) { + drawFramebuffer = framebuffer; + } + } else if (name.equals("glGetInteger")) { + int parameter = (Integer) args[0]; + int result = parameter == GLFbo.GL_READ_FRAMEBUFFER_BINDING_EXT ? readFramebuffer + : parameter == GLFbo.GL_DRAW_FRAMEBUFFER_BINDING_EXT ? drawFramebuffer : 0; + ((IntBuffer) args[1]).put(0, result); + } else if (name.equals("glGenFramebuffersEXT") || name.equals("glGenRenderbuffersEXT") + || name.equals("glGenTextures")) { + ((IntBuffer) args[0]).put(0, nextObject++); + } else if (name.equals("glDeleteFramebuffersEXT")) { + int framebuffer = ((IntBuffer) args[0]).get(0); + if (readFramebuffer == framebuffer) { + readFramebuffer = 0; + } + if (drawFramebuffer == framebuffer) { + drawFramebuffer = 0; + } + } else if (name.equals("glCheckFramebufferStatusEXT")) { + return GLFbo.GL_FRAMEBUFFER_COMPLETE_EXT; + } + if (method.getReturnType() == Integer.TYPE) { + return 0; + } + if (method.getReturnType() == Boolean.TYPE) { + return false; + } + return null; + } + } +} From 809b98829044eb698364dc1fb88242a97b7a6dbe Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Fri, 2 Oct 2026 18:15:58 +0200 Subject: [PATCH 2/3] Record framebuffer bindings together and verify native target identity --- .../java/com/jme3/renderer/RenderContext.java | 31 ++++++--- .../com/jme3/renderer/opengl/GLRenderer.java | 30 ++++---- .../renderer/RenderContextBindingTest.java | 46 +++++++++++-- .../GlRendererBindingInvalidationTest.java | 69 ++++++++++++++++++- 4 files changed, 147 insertions(+), 29 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java index 8c04b83be0..1d8391101e 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java @@ -217,7 +217,10 @@ private static WeakReference[] newWeakReferenceArray(int size) { private boolean frameBufferBindingValid = true; /** - * Currently bound FrameBuffer. + * Java object associated with the known framebuffer binding, or null when + * there is no associated object or the binding is unknown. A null value + * does not imply a native framebuffer name of zero; presentation and + * externally managed framebuffers can have nonzero names. * * @see Renderer#setFrameBuffer(com.jme3.texture.FrameBuffer) */ @@ -387,13 +390,26 @@ public boolean isFrameBufferBindingValid() { } /** - * Updates the validity of the cached framebuffer binding after applying - * native state or restoring a previously saved cache. + * Records the object and native name of a known framebuffer binding. + * Call after establishing native state or restoring a known binding. * - * @param valid true if the cached framebuffer binding is known + * @param fb the associated Java framebuffer, or null if there is none + * @param fbo the known native framebuffer name */ - public void setFrameBufferBindingValid(boolean valid) { - frameBufferBindingValid = valid; + public void setFrameBufferBinding(FrameBuffer fb, int fbo) { + boundFB = fb; + boundFBO = fbo; + frameBufferBindingValid = true; + } + + /** + * Invalidates only the framebuffer binding, leaving texture bindings alone. + * The numeric name is retained as an untrusted value, and the associated + * Java object is cleared. This does not modify native state. + */ + public void invalidateFrameBufferBinding() { + frameBufferBindingValid = false; + boundFB = null; } /** @@ -403,8 +419,7 @@ public void setFrameBufferBindingValid(boolean valid) { */ public void invalidateBindings() { boundTextureUnit = -1; - frameBufferBindingValid = false; - boundFB = null; + invalidateFrameBufferBinding(); for (int i = 0; i < boundTextures.length; i++) { boundTextures[i] = null; } diff --git a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java index b0cf187e0a..9f558b0bde 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java +++ b/jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java @@ -2072,15 +2072,15 @@ public void copyFrameBuffer(FrameBuffer src, FrameBuffer dst, boolean copyColor, if (prevBindingValid) { glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, prevFBO); + context.setFrameBufferBinding(prevFB, prevFBO); + if (prevFB != null || prevFBO == defaultFBO) { + toggleFramebufferSrgb(prevFB); + } } else { glfbo.glBindFramebufferEXT(GLFbo.GL_READ_FRAMEBUFFER_EXT, prevReadFramebuffer); glfbo.glBindFramebufferEXT(GLFbo.GL_DRAW_FRAMEBUFFER_EXT, prevDrawFramebuffer); - } - context.boundFBO = prevFBO; - context.boundFB = prevFB; - context.setFrameBufferBindingValid(prevBindingValid); - if (prevBindingValid) { - toggleFramebufferSrgb(prevFB); + context.invalidateFrameBufferBinding(); + context.boundFBO = prevFBO; } } else { throw new RendererException("Framebuffer blitting not supported by the video hardware"); @@ -2239,17 +2239,13 @@ private void bindFrameBuffer(FrameBuffer fb) { if (!context.isFrameBufferBindingValid() || context.boundFBO != defaultFBO) { glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, defaultFBO); statistics.onFrameBufferUse(null, true); - context.boundFBO = defaultFBO; - context.boundFB = null; - context.setFrameBufferBindingValid(true); + context.setFrameBufferBinding(null, defaultFBO); } } else { assert fb.getId() != -1 && fb.getId() != 0; if (!context.isFrameBufferBindingValid() || context.boundFBO != fb.getId()) { glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, fb.getId()); - context.boundFBO = fb.getId(); - context.boundFB = fb; - context.setFrameBufferBindingValid(true); + context.setFrameBufferBinding(fb, fb.getId()); statistics.onFrameBufferUse(fb, true); } else { statistics.onFrameBufferUse(fb, false); @@ -2409,7 +2405,8 @@ public void setFrameBuffer(FrameBuffer fb) { return; } - if (context.isFrameBufferBindingValid() && context.boundFB == fb) { + if (context.isFrameBufferBindingValid() && context.boundFB == fb + && context.boundFBO == (fb == null ? defaultFBO : fb.getId())) { if (fb == null || !fb.isUpdateNeeded()) { toggleFramebufferSrgb(fb); return; @@ -2467,7 +2464,7 @@ && isMipmapGenerationSupported(tex.getImage().getFormat(), assert fb.getId() > 0; assert context.boundFBO == fb.getId(); - context.boundFB = fb; + context.setFrameBufferBinding(fb, fb.getId()); if (debug && caps.contains(Caps.GLDebug)) { if (fb.getName() != null) glext.glObjectLabel(GL3.GL_FRAMEBUFFER, fb.getId(), fb.getName()); } @@ -2514,7 +2511,7 @@ public void deleteFrameBuffer(FrameBuffer fb) { if (fb.getId() != -1) { if (context.isFrameBufferBindingValid() && context.boundFBO == fb.getId()) { glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, 0); - context.boundFBO = 0; + context.setFrameBufferBinding(null, 0); } if (fb.getDepthTarget() != null) { @@ -3862,7 +3859,8 @@ public void setMainFrameBufferSrgb(boolean enableSrgb) { } mainFrameBufferSrgb = enableSrgb; - if (context.isFrameBufferBindingValid() && context.boundFB == null) { + if (context.isFrameBufferBindingValid() && context.boundFB == null + && context.boundFBO == defaultFBO) { toggleFramebufferSrgb(null); } } diff --git a/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java b/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java index c1d6b635a9..b8cb586143 100644 --- a/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java +++ b/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java @@ -34,6 +34,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import com.jme3.texture.FrameBuffer; @@ -87,16 +88,53 @@ public void resetReturnsBindingsToKnownFreshDefaults() { } @Test - public void framebufferValidationDoesNotValidateTheActiveTextureUnit() { + public void knownFramebufferRecordingUpdatesTheObjectNameAndValidityTogether() { RenderContext context = new RenderContext(); context.invalidateBindings(); - context.boundFBO = 12; - context.setFrameBufferBindingValid(true); + FrameBuffer framebuffer = new FrameBuffer(4, 4, 1); + framebuffer.setId(12); + + context.setFrameBufferBinding(framebuffer, framebuffer.getId()); + + assertSame(framebuffer, context.boundFB); + assertEquals(12, context.boundFBO); assertTrue(context.isFrameBufferBindingValid()); assertEquals(-1, context.boundTextureUnit); - context.setFrameBufferBindingValid(false); + } + + @Test + public void knownNativeFramebufferDoesNotRequireAJavaObject() { + RenderContext context = new RenderContext(); + context.invalidateBindings(); + + context.setFrameBufferBinding(null, 7); + + assertNull(context.boundFB); + assertEquals(7, context.boundFBO); + assertTrue(context.isFrameBufferBindingValid()); + } + + @Test + public void framebufferOnlyInvalidationPreservesTextureKnowledgeAndUntrustedName() { + RenderContext context = new RenderContext(); + FrameBuffer framebuffer = new FrameBuffer(4, 4, 1); + context.setFrameBufferBinding(framebuffer, 12); + Image image = new Image(); + context.boundTextures[3] = image.getWeakRef(); + context.boundTextureUnit = 3; + context.textureIndexList.moveToNew(3); + context.textureIndexList.copyNewToOld(); + context.textureIndexList.moveToNew(0); + + context.invalidateFrameBufferBinding(); + + assertNull(context.boundFB); assertFalse(context.isFrameBufferBindingValid()); assertEquals(12, context.boundFBO); + assertEquals(3, context.boundTextureUnit); + assertSame(image.getWeakRef(), context.boundTextures[3]); + assertEquals(1, context.textureIndexList.oldLen); + assertEquals(1, context.textureIndexList.newLen); } private static void assertDefaults(RenderContext context) { diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererBindingInvalidationTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererBindingInvalidationTest.java index 445f982ed8..6b17676f59 100644 --- a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererBindingInvalidationTest.java +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererBindingInvalidationTest.java @@ -33,6 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -265,6 +266,30 @@ public void unknownCopySnapshotsBindingsBeforeAttachmentUpdates(boolean desktop) assertEquals(0, driver.drawFramebuffer); } + @Test + public void unknownCopyPreservesTheUntrustedNameAndKnownTextures() throws Exception { + Recording driver = new Recording(false); + Texture2D texture = texture(17); + driver.renderer.setTexture(3, texture); + driver.context.setFrameBufferBinding(framebuffer(27), 27); + driver.context.invalidateFrameBufferBinding(); + driver.setNativeFramebuffer(9); + + driver.renderer.copyFrameBuffer(framebuffer(12), framebuffer(13), true, false); + + assertEquals(9, driver.readFramebuffer); + assertEquals(9, driver.drawFramebuffer); + assertEquals(27, driver.context.boundFBO); + assertFalse(driver.context.isFrameBufferBindingValid()); + assertEquals(3, driver.context.boundTextureUnit); + assertSame(texture.getImage().getWeakRef(), driver.context.boundTextures[3]); + driver.calls.clear(); + driver.renderer.setTexture(3, texture); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + driver.renderer.setFrameBuffer(null); + assertEquals(0, driver.drawFramebuffer); + } + @ParameterizedTest @ValueSource(booleans = {false, true}) public void knownCopyUsesCachedBindingAndPreservesElision(boolean desktop) throws Exception { @@ -330,6 +355,46 @@ public void deletingWhileUnknownDoesNotValidateOrEagerlyRebind( assertTrue(driver.context.isFrameBufferBindingValid()); } + @ParameterizedTest + @ValueSource(ints = {0, 77}) + public void deletingKnownFramebufferKeepsCacheConsistentAndRestoresMain(int defaultId) throws Exception { + Recording driver = new Recording(false); + driver.setDefaultFramebuffer(defaultId); + FrameBuffer framebuffer = framebuffer(12); + driver.renderer.setFrameBuffer(framebuffer); + + driver.renderer.deleteFrameBuffer(framebuffer); + + assertEquals(driver.drawFramebuffer, driver.context.boundFBO); + assertNull(driver.context.boundFB); + assertTrue(driver.context.isFrameBufferBindingValid()); + driver.renderer.setFrameBuffer(null); + assertEquals(defaultId, driver.drawFramebuffer); + driver.calls.clear(); + driver.renderer.setFrameBuffer(null); + assertTrue(driver.calls.isEmpty(), driver.calls.toString()); + } + + @Test + public void knownUntrackedFramebufferDoesNotReceiveMainSrgbConfiguration() throws Exception { + Recording driver = new Recording(false); + driver.setDefaultFramebuffer(77); + driver.renderer.getCaps().add(Caps.Srgb); + driver.renderer.getCaps().add(Caps.SrgbWriteControl); + driver.context.setFrameBufferBinding(null, 0); + driver.calls.clear(); + + driver.renderer.setMainFrameBufferSrgb(true); + driver.renderer.copyFrameBuffer(framebuffer(12), framebuffer(13), true, false); + + assertEquals(0, driver.drawFramebuffer); + assertEquals(0, driver.countCalls("glEnable")); + assertEquals(0, driver.countCalls("glGetInteger")); + driver.renderer.setFrameBuffer(null); + assertEquals(77, driver.drawFramebuffer); + assertEquals(1, driver.countCalls("glEnable")); + } + @Test public void unknownCopyRestorationDoesNotAssumeMainSrgbTarget() throws Exception { Recording driver = new Recording(false); @@ -432,7 +497,9 @@ public Object invoke(Object proxy, Method method, Object[] args) { String name = method.getName(); calls.add(name); if (name.equals("glActiveTexture")) { - activeUnit = (Integer) args[0] - GL.GL_TEXTURE0; + int unit = (Integer) args[0] - GL.GL_TEXTURE0; + assertTrue(unit >= 0, "Unknown texture unit passed to GL"); + activeUnit = unit; } else if (name.equals("glBindTexture")) { textureBindings[activeUnit] = (Integer) args[1]; } else if (name.equals("glTexImage2D")) { From dd85c0e839fec28f208edbfea8e5b392cae2c600 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Sat, 3 Oct 2026 00:07:00 +0200 Subject: [PATCH 3/3] Remove redundant binding bookkeeping tests --- .../renderer/RenderContextBindingTest.java | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java b/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java index b8cb586143..e7c897a1cb 100644 --- a/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java +++ b/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java @@ -46,12 +46,6 @@ */ public class RenderContextBindingTest { - @Test - public void freshContextHasKnownDefaultBindings() { - RenderContext context = new RenderContext(); - assertDefaults(context); - } - @Test public void bindingInvalidationClearsTextureReferencesWithoutAssumingNativeDefaults() { RenderContext context = new RenderContext(); @@ -87,33 +81,6 @@ public void resetReturnsBindingsToKnownFreshDefaults() { assertDefaults(context); } - @Test - public void knownFramebufferRecordingUpdatesTheObjectNameAndValidityTogether() { - RenderContext context = new RenderContext(); - context.invalidateBindings(); - FrameBuffer framebuffer = new FrameBuffer(4, 4, 1); - framebuffer.setId(12); - - context.setFrameBufferBinding(framebuffer, framebuffer.getId()); - - assertSame(framebuffer, context.boundFB); - assertEquals(12, context.boundFBO); - assertTrue(context.isFrameBufferBindingValid()); - assertEquals(-1, context.boundTextureUnit); - } - - @Test - public void knownNativeFramebufferDoesNotRequireAJavaObject() { - RenderContext context = new RenderContext(); - context.invalidateBindings(); - - context.setFrameBufferBinding(null, 7); - - assertNull(context.boundFB); - assertEquals(7, context.boundFBO); - assertTrue(context.isFrameBufferBindingValid()); - } - @Test public void framebufferOnlyInvalidationPreservesTextureKnowledgeAndUntrustedName() { RenderContext context = new RenderContext();