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..1d8391101e 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java @@ -207,14 +207,20 @@ 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. + * 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) */ @@ -289,7 +295,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 +380,52 @@ 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; + } + + /** + * Records the object and native name of a known framebuffer binding. + * Call after establishing native state or restoring a known binding. + * + * @param fb the associated Java framebuffer, or null if there is none + * @param fbo the known native framebuffer name + */ + 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; + } + + /** + * 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; + invalidateFrameBufferBinding(); + for (int i = 0; i < boundTextures.length; i++) { + boundTextures[i] = null; + } + textureIndexList.reset(); + } + private void init() { cullMode = RenderState.FaceCullMode.Off; @@ -399,6 +451,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..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 @@ -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); - context.boundFBO = prevFBO; - context.boundFB = prevFB; - toggleFramebufferSrgb(prevFB); + 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.invalidateFrameBufferBinding(); + context.boundFBO = prevFBO; + } } else { throw new RendererException("Framebuffer blitting not supported by the video hardware"); } @@ -2220,18 +2236,16 @@ 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.setFrameBufferBinding(null, defaultFBO); } } 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.setFrameBufferBinding(fb, fb.getId()); statistics.onFrameBufferUse(fb, true); } else { statistics.onFrameBufferUse(fb, false); @@ -2311,7 +2325,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 +2399,14 @@ 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 + && context.boundFBO == (fb == null ? defaultFBO : fb.getId())) { if (fb == null || !fb.isUpdateNeeded()) { toggleFramebufferSrgb(fb); return; @@ -2443,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()); } @@ -2488,9 +2509,9 @@ 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; + context.setFrameBufferBinding(null, 0); } if (fb.getDepthTarget() != null) { @@ -3838,7 +3859,8 @@ public void setMainFrameBufferSrgb(boolean enableSrgb) { } mainFrameBufferSrgb = enableSrgb; - if (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 new file mode 100644 index 0000000000..e7c897a1cb --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/RenderContextBindingTest.java @@ -0,0 +1,113 @@ +/* + * 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.assertSame; +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 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 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) { + 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..6b17676f59 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererBindingInvalidationTest.java @@ -0,0 +1,547 @@ +/* + * 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.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; + +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); + } + + @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 { + 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()); + } + + @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); + 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")) { + 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")) { + 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; + } + } +}