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..3acadcbaf8 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java @@ -39,12 +39,25 @@ import com.jme3.texture.FrameBuffer; import com.jme3.texture.Image; import java.lang.ref.WeakReference; +import java.util.EnumSet; /** * Represents the current state of the graphics library. This class is used * internally to reduce state changes. NOTE: This class is specific to OpenGL. */ public class RenderContext { + /** + * Independently cached categories managed by Renderer.applyRenderState. + * A disabled category need not have its inactive parameters applied yet. + */ + public enum RenderStateCategory { + Wireframe, DepthTest, DepthFunction, DepthWrite, ColorWrite, PolygonOffset, + FaceCull, BlendMode, BlendEquations, BlendFactors, Stencil, LineWidth + } + + private final EnumSet validRenderState + = EnumSet.allOf(RenderStateCategory.class); + @SuppressWarnings("unchecked") private static WeakReference[] newWeakReferenceArray(int size) { return (WeakReference[]) new WeakReference[size]; @@ -368,81 +381,109 @@ private static WeakReference[] newWeakReferenceArray(int size) { public ColorRGBA clearColor = new ColorRGBA(0, 0, 0, 0); /** - * Instantiates a context with appropriate default values. + * Instantiates a context matching the defaults of a fresh GL context. */ public RenderContext() { - init(); + resetRenderState(); + resetOtherState(); + } + + /** + * Tests whether a render-state category can be trusted for suppressing + * redundant GL calls. The stored values must not be trusted while invalid. + * + * @param category the category to inspect (not null) + * @return true if the category has known state + */ + public boolean isRenderStateValid(RenderStateCategory category) { + return validRenderState.contains(category); } + /** + * Marks a category valid after its required GL state has been applied and + * the corresponding cached values have been updated. + * + * @param category the category that was applied (not null) + */ + public void setRenderStateValid(RenderStateCategory category) { + validRenderState.add(category); + } - private void init() { + /** + * Invalidates cached render-state values without replacing them with + * assumed GL defaults. Inactive parameters stay unknown until applied. + * Other renderer caches retain their existing reset behavior. + */ + public void invalidate() { + validRenderState.clear(); + resetOtherState(); + } + + /** + * Resets this cache to the known defaults of a fresh GL context. + * This does not modify GL state. Use invalidate() after external GL changes. + */ + public void reset() { + resetRenderState(); + resetOtherState(); + } + + private void resetRenderState() { cullMode = RenderState.FaceCullMode.Off; depthTestEnabled = false; depthWriteEnabled = true; colorWriteEnabled = true; - clipRectEnabled = false; polyOffsetEnabled = false; polyOffsetFactor = 0; polyOffsetUnits = 0; - pointSize = 1; lineWidth = 1; blendMode = RenderState.BlendMode.Off; blendEquation = RenderState.BlendEquation.Add; blendEquationAlpha = RenderState.BlendEquationAlpha.InheritColor; sfactorRGB = RenderState.BlendFunc.One; - dfactorRGB = RenderState.BlendFunc.One; + dfactorRGB = RenderState.BlendFunc.Zero; sfactorAlpha = RenderState.BlendFunc.One; - dfactorAlpha = RenderState.BlendFunc.One; + dfactorAlpha = RenderState.BlendFunc.Zero; wireframe = false; + stencilTest = false; + frontStencilStencilFailOperation = RenderState.StencilOperation.Keep; + frontStencilDepthFailOperation = RenderState.StencilOperation.Keep; + frontStencilDepthPassOperation = RenderState.StencilOperation.Keep; + backStencilStencilFailOperation = RenderState.StencilOperation.Keep; + backStencilDepthFailOperation = RenderState.StencilOperation.Keep; + backStencilDepthPassOperation = RenderState.StencilOperation.Keep; + frontStencilFunction = RenderState.TestFunction.Always; + backStencilFunction = RenderState.TestFunction.Always; + depthFunc = RenderState.TestFunction.Less; + validRenderState.addAll(EnumSet.allOf(RenderStateCategory.class)); + } + private void resetOtherState() { + clipRectEnabled = false; + pointSize = 1; boundShaderProgram = 0; boundShader = null; boundFBO = 0; boundFB = null; boundRB = 0; - boundElementArrayVBO = 0; boundVertexArray = 0; boundArrayVBO = 0; boundPixelPackPBO = 0; numTexturesSet = 0; boundTextureUnit = 0; - stencilTest = false; - - frontStencilStencilFailOperation = RenderState.StencilOperation.Keep; - frontStencilDepthFailOperation = RenderState.StencilOperation.Keep; - frontStencilDepthPassOperation = RenderState.StencilOperation.Keep; - backStencilStencilFailOperation = RenderState.StencilOperation.Keep; - backStencilDepthFailOperation = RenderState.StencilOperation.Keep; - backStencilDepthPassOperation = RenderState.StencilOperation.Keep; - frontStencilFunction = RenderState.TestFunction.Always; - backStencilFunction = RenderState.TestFunction.Always; - - depthFunc = RenderState.TestFunction.Less; alphaFunc = RenderState.TestFunction.Greater; - cullMode = RenderState.FaceCullMode.Off; - srgbWriteEnabled = false; - clearColor.set(0, 0, 0, 0); - } - - /** - * Resets the RenderContext to default GL state. - */ - public void reset() { - init(); for (int i = 0; i < boundTextures.length; i++) { boundTextures[i] = null; } - textureIndexList.reset(); for (int i = 0; i < boundAttribs.length; i++) { boundAttribs[i] = null; } - attribIndexList.reset(); } } 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..e37104462e 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 @@ -38,6 +38,7 @@ import com.jme3.material.RenderState.TestFunction; import com.jme3.math.*; import com.jme3.renderer.*; +import com.jme3.renderer.RenderContext.RenderStateCategory; import com.jme3.scene.Mesh; import com.jme3.scene.Mesh.Mode; import com.jme3.scene.VertexBuffer; @@ -121,7 +122,6 @@ public final class GLRenderer implements Renderer { private boolean debug = false; private int debugGroupId = 0; - public GLRenderer(GL gl, GLExt glext, GLFbo glfbo) { this.gl = gl; this.gl2 = gl instanceof GL2 ? (GL2)gl : null; @@ -900,10 +900,20 @@ public void initialize() { } } + private boolean isDesktopGl() { + // Some GLES adapters expose GL2 as well. Prefer the runtime profile, + // falling back to the adapter type before capabilities are loaded. + return gl2 != null + && !caps.contains(Caps.OpenGLES20) + && !caps.contains(Caps.OpenGLES30) + && !caps.contains(Caps.WebGL) + && (caps.contains(Caps.OpenGL20) || !(gl instanceof GLES_30)); + } + @Override public void invalidateState() { - context.reset(); - if (gl2 != null) { + context.invalidate(); + if (isDesktopGl()) { context.initialDrawBuf = getInteger(GL2.GL_DRAW_BUFFER); context.initialReadBuf = getInteger(GL2.GL_READ_BUFFER); } @@ -938,9 +948,10 @@ public void clearBuffers(boolean color, boolean depth, boolean stencil) { int bits = 0; if (color) { //See explanations of the depth below, we must enable color write to be able to clear the color buffer - if (context.colorWriteEnabled == false) { + if (!context.isRenderStateValid(RenderStateCategory.ColorWrite) || !context.colorWriteEnabled) { gl.glColorMask(true, true, true, true); context.colorWriteEnabled = true; + context.setRenderStateValid(RenderStateCategory.ColorWrite); } bits = GL.GL_COLOR_BUFFER_BIT; } @@ -949,9 +960,10 @@ public void clearBuffers(boolean color, boolean depth, boolean stencil) { // Here is a link to the openGL discussion: // http://www.opengl.org/discussion_boards/ubbthreads.php?ubb=showflat&Number=257223 // If depth clear is requested, we enable the depth mask. - if (context.depthWriteEnabled == false) { + if (!context.isRenderStateValid(RenderStateCategory.DepthWrite) || !context.depthWriteEnabled) { gl.glDepthMask(true); context.depthWriteEnabled = true; + context.setRenderStateValid(RenderStateCategory.DepthWrite); } bits |= GL.GL_DEPTH_BUFFER_BIT; } @@ -994,46 +1006,49 @@ public void setAlphaToCoverage(boolean value) { @Override public void applyRenderState(RenderState state) { - if (gl2 != null) { - if (state.isWireframe() && !context.wireframe) { - gl2.glPolygonMode(GL.GL_FRONT_AND_BACK, GL2.GL_LINE); - context.wireframe = true; - } else if (!state.isWireframe() && context.wireframe) { - gl2.glPolygonMode(GL.GL_FRONT_AND_BACK, GL2.GL_FILL); - context.wireframe = false; + if (isDesktopGl() + && (!context.isRenderStateValid(RenderStateCategory.Wireframe) + || state.isWireframe() != context.wireframe)) { + gl2.glPolygonMode(GL.GL_FRONT_AND_BACK, state.isWireframe() ? GL2.GL_LINE : GL2.GL_FILL); + context.wireframe = state.isWireframe(); + context.setRenderStateValid(RenderStateCategory.Wireframe); + } + + if (!context.isRenderStateValid(RenderStateCategory.DepthTest) + || state.isDepthTest() != context.depthTestEnabled) { + if (state.isDepthTest()) { + gl.glEnable(GL.GL_DEPTH_TEST); + } else { + gl.glDisable(GL.GL_DEPTH_TEST); } + context.depthTestEnabled = state.isDepthTest(); + context.setRenderStateValid(RenderStateCategory.DepthTest); } - - if (state.isDepthTest() && !context.depthTestEnabled) { - gl.glEnable(GL.GL_DEPTH_TEST); - context.depthTestEnabled = true; - } else if (!state.isDepthTest() && context.depthTestEnabled) { - gl.glDisable(GL.GL_DEPTH_TEST); - context.depthTestEnabled = false; - } - if (state.isDepthTest() && state.getDepthFunc() != context.depthFunc) { + if (state.isDepthTest() && (!context.isRenderStateValid(RenderStateCategory.DepthFunction) + || state.getDepthFunc() != context.depthFunc)) { gl.glDepthFunc(convertTestFunction(state.getDepthFunc())); context.depthFunc = state.getDepthFunc(); + context.setRenderStateValid(RenderStateCategory.DepthFunction); } - if (state.isDepthWrite() && !context.depthWriteEnabled) { - gl.glDepthMask(true); - context.depthWriteEnabled = true; - } else if (!state.isDepthWrite() && context.depthWriteEnabled) { - gl.glDepthMask(false); - context.depthWriteEnabled = false; + if (!context.isRenderStateValid(RenderStateCategory.DepthWrite) + || state.isDepthWrite() != context.depthWriteEnabled) { + gl.glDepthMask(state.isDepthWrite()); + context.depthWriteEnabled = state.isDepthWrite(); + context.setRenderStateValid(RenderStateCategory.DepthWrite); } - if (state.isColorWrite() && !context.colorWriteEnabled) { - gl.glColorMask(true, true, true, true); - context.colorWriteEnabled = true; - } else if (!state.isColorWrite() && context.colorWriteEnabled) { - gl.glColorMask(false, false, false, false); - context.colorWriteEnabled = false; + if (!context.isRenderStateValid(RenderStateCategory.ColorWrite) + || state.isColorWrite() != context.colorWriteEnabled) { + boolean colorWrite = state.isColorWrite(); + gl.glColorMask(colorWrite, colorWrite, colorWrite, colorWrite); + context.colorWriteEnabled = colorWrite; + context.setRenderStateValid(RenderStateCategory.ColorWrite); } if (state.isPolyOffset()) { - if (!context.polyOffsetEnabled) { + if (!context.isRenderStateValid(RenderStateCategory.PolygonOffset) + || !context.polyOffsetEnabled) { gl.glEnable(GL.GL_POLYGON_OFFSET_FILL); gl.glPolygonOffset(state.getPolyOffsetFactor(), state.getPolyOffsetUnits()); @@ -1050,7 +1065,7 @@ public void applyRenderState(RenderState state) { } } } else { - if (context.polyOffsetEnabled) { + if (!context.isRenderStateValid(RenderStateCategory.PolygonOffset) || context.polyOffsetEnabled) { gl.glDisable(GL.GL_POLYGON_OFFSET_FILL); context.polyOffsetEnabled = false; context.polyOffsetFactor = 0; @@ -1058,7 +1073,10 @@ public void applyRenderState(RenderState state) { } } - if (state.getFaceCullMode() != context.cullMode) { + context.setRenderStateValid(RenderStateCategory.PolygonOffset); + + if (!context.isRenderStateValid(RenderStateCategory.FaceCull) + || state.getFaceCullMode() != context.cullMode) { if (state.getFaceCullMode() == RenderState.FaceCullMode.Off) { gl.glDisable(GL.GL_CULL_FACE); } else { @@ -1083,6 +1101,7 @@ public void applyRenderState(RenderState state) { } context.cullMode = state.getFaceCullMode(); + context.setRenderStateValid(RenderStateCategory.FaceCull); } // Always update the blend equations and factors when using custom blend mode. @@ -1097,7 +1116,8 @@ public void applyRenderState(RenderState state) { blendEquationSeparate(state.getBlendEquation(), state.getBlendEquationAlpha()); // Update the blend equations and factors only on a mode change for all the other (common) blend modes. - } else if (state.getBlendMode() != context.blendMode) { + } else if (!context.isRenderStateValid(RenderStateCategory.BlendMode) + || state.getBlendMode() != context.blendMode) { changeBlendMode(state.getBlendMode()); switch (state.getBlendMode()) { @@ -1146,7 +1166,8 @@ public void applyRenderState(RenderState state) { blendEquationSeparate(RenderState.BlendEquation.Add, RenderState.BlendEquationAlpha.InheritColor); } - if (context.stencilTest != state.isStencilTest() + if (!context.isRenderStateValid(RenderStateCategory.Stencil) + || context.stencilTest != state.isStencilTest() || context.frontStencilStencilFailOperation != state.getFrontStencilStencilFailOperation() || context.frontStencilDepthFailOperation != state.getFrontStencilDepthFailOperation() || context.frontStencilDepthPassOperation != state.getFrontStencilDepthPassOperation() @@ -1184,27 +1205,37 @@ public void applyRenderState(RenderState state) { } else { gl.glDisable(GL.GL_STENCIL_TEST); } + context.setRenderStateValid(RenderStateCategory.Stencil); } - if (context.lineWidth != state.getLineWidth()) { - gl.glLineWidth(state.getLineWidth()); - context.lineWidth = state.getLineWidth(); + setLineWidth(state.getLineWidth()); + } + + private void setLineWidth(float lineWidth) { + if (!context.isRenderStateValid(RenderStateCategory.LineWidth) || context.lineWidth != lineWidth) { + gl.glLineWidth(lineWidth); + context.lineWidth = lineWidth; + context.setRenderStateValid(RenderStateCategory.LineWidth); } } private void changeBlendMode(RenderState.BlendMode blendMode) { - if (blendMode != context.blendMode) { + if (!context.isRenderStateValid(RenderStateCategory.BlendMode) || blendMode != context.blendMode) { if (blendMode == RenderState.BlendMode.Off) { gl.glDisable(GL.GL_BLEND); - } else if (context.blendMode == RenderState.BlendMode.Off) { + } else if (!context.isRenderStateValid(RenderStateCategory.BlendMode) + || context.blendMode == RenderState.BlendMode.Off) { gl.glEnable(GL.GL_BLEND); } context.blendMode = blendMode; + context.setRenderStateValid(RenderStateCategory.BlendMode); } } private void blendEquationSeparate(RenderState.BlendEquation blendEquation, RenderState.BlendEquationAlpha blendEquationAlpha) { - if (blendEquation != context.blendEquation || blendEquationAlpha != context.blendEquationAlpha) { + if (!context.isRenderStateValid(RenderStateCategory.BlendEquations) + || blendEquation != context.blendEquation + || blendEquationAlpha != context.blendEquationAlpha) { int glBlendEquation = convertBlendEquation(blendEquation); int glBlendEquationAlpha = blendEquationAlpha == RenderState.BlendEquationAlpha.InheritColor ? glBlendEquation @@ -1212,11 +1243,13 @@ private void blendEquationSeparate(RenderState.BlendEquation blendEquation, Rend gl.glBlendEquationSeparate(glBlendEquation, glBlendEquationAlpha); context.blendEquation = blendEquation; context.blendEquationAlpha = blendEquationAlpha; + context.setRenderStateValid(RenderStateCategory.BlendEquations); } } private void blendFunc(RenderState.BlendFunc sfactor, RenderState.BlendFunc dfactor) { - if (sfactor != context.sfactorRGB + if (!context.isRenderStateValid(RenderStateCategory.BlendFactors) + || sfactor != context.sfactorRGB || dfactor != context.dfactorRGB || sfactor != context.sfactorAlpha || dfactor != context.dfactorAlpha) { @@ -1228,12 +1261,14 @@ private void blendFunc(RenderState.BlendFunc sfactor, RenderState.BlendFunc dfac context.dfactorRGB = dfactor; context.sfactorAlpha = sfactor; context.dfactorAlpha = dfactor; + context.setRenderStateValid(RenderStateCategory.BlendFactors); } } private void blendFuncSeparate(RenderState.BlendFunc sfactorRGB, RenderState.BlendFunc dfactorRGB, RenderState.BlendFunc sfactorAlpha, RenderState.BlendFunc dfactorAlpha) { - if (sfactorRGB != context.sfactorRGB + if (!context.isRenderStateValid(RenderStateCategory.BlendFactors) + || sfactorRGB != context.sfactorRGB || dfactorRGB != context.dfactorRGB || sfactorAlpha != context.sfactorAlpha || dfactorAlpha != context.dfactorAlpha) { @@ -1247,6 +1282,7 @@ private void blendFuncSeparate(RenderState.BlendFunc sfactorRGB, RenderState.Ble context.dfactorRGB = dfactorRGB; context.sfactorAlpha = sfactorAlpha; context.dfactorAlpha = dfactorAlpha; + context.setRenderStateValid(RenderStateCategory.BlendFactors); } } @@ -3809,9 +3845,8 @@ public void renderMesh(Mesh mesh, int lod, int count, VertexBuffer[] instanceDat throw new RendererException("Mesh instancing is not supported by the video hardware"); } - if (mesh.getLineWidth() != 1f && context.lineWidth != mesh.getLineWidth()) { - gl.glLineWidth(mesh.getLineWidth()); - context.lineWidth = mesh.getLineWidth(); + if (!context.isRenderStateValid(RenderStateCategory.LineWidth) || mesh.getLineWidth() != 1f) { + setLineWidth(mesh.getLineWidth()); } if (gl4 != null && mesh.getMode().equals(Mode.Patch)) { diff --git a/jme3-core/src/test/java/com/jme3/renderer/RenderContextStateTest.java b/jme3-core/src/test/java/com/jme3/renderer/RenderContextStateTest.java new file mode 100644 index 0000000000..bee81cc95d --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/RenderContextStateTest.java @@ -0,0 +1,229 @@ +/* + * 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.material.RenderState; +import com.jme3.math.ColorRGBA; +import com.jme3.renderer.RenderContext.RenderStateCategory; +import com.jme3.scene.VertexBuffer; +import com.jme3.shader.bufferobject.BufferObject; +import com.jme3.texture.Image; +import java.lang.ref.WeakReference; +import org.junit.jupiter.api.Test; + +/** + * Verifies cache-owned validity without changing the public cached field types. + */ +public class RenderContextStateTest { + + @Test + public void freshContextHasKnownGlDefaults() { + RenderContext context = new RenderContext(); + + assertFreshDefaults(context); + assertAllValid(context, true); + } + + @Test + public void invalidationPreservesCachedValuesButMakesThemUnknown() { + RenderContext context = new RenderContext(); + setNondefaultDrawState(context); + + context.invalidate(); + + assertEquals(RenderState.FaceCullMode.Front, context.cullMode); + assertTrue(context.depthTestEnabled); + assertFalse(context.depthWriteEnabled); + assertFalse(context.colorWriteEnabled); + assertEquals(RenderState.TestFunction.Never, context.depthFunc); + assertEquals(RenderState.BlendMode.Custom, context.blendMode); + assertEquals(RenderState.BlendFunc.Src_Alpha, context.sfactorRGB); + assertEquals(RenderState.BlendEquation.Subtract, context.blendEquation); + assertTrue(context.stencilTest); + assertEquals(RenderState.TestFunction.Equal, context.frontStencilFunction); + assertTrue(context.polyOffsetEnabled); + assertEquals(2f, context.polyOffsetFactor); + assertEquals(3f, context.polyOffsetUnits); + assertEquals(3f, context.lineWidth); + assertTrue(context.wireframe); + assertAllValid(context, false); + } + + @Test + public void aPartialWriteValidatesOnlyItsOwnCategory() { + RenderContext context = new RenderContext(); + context.invalidate(); + context.depthWriteEnabled = true; + + context.setRenderStateValid(RenderStateCategory.DepthWrite); + + for (RenderStateCategory category : RenderStateCategory.values()) { + assertEquals(category == RenderStateCategory.DepthWrite, + context.isRenderStateValid(category), category.toString()); + } + context.invalidate(); + assertAllValid(context, false); + } + + @Test + public void resetRestoresKnownFreshDefaults() { + RenderContext context = new RenderContext(); + setNondefaultDrawState(context); + context.invalidate(); + + context.reset(); + + assertFreshDefaults(context); + assertAllValid(context, true); + } + + @Test + public void invalidationRetainsExistingCleanupForOtherCaches() { + RenderContext context = new RenderContext(); + context.clipRectEnabled = true; + context.pointSize = 8f; + context.boundShaderProgram = 21; + context.boundFBO = 22; + context.boundRB = 23; + context.boundElementArrayVBO = 24; + context.boundVertexArray = 25; + context.boundArrayVBO = 26; + context.boundPixelPackPBO = 27; + context.numTexturesSet = 3; + context.boundTextureUnit = 4; + context.alphaFunc = RenderState.TestFunction.Never; + context.srgbWriteEnabled = true; + context.clearColor.set(ColorRGBA.Red); + context.boundTextures[0] = new WeakReference(null); + context.boundAttribs[0] = new WeakReference(null); + context.textureIndexList.moveToNew(3); + context.textureIndexList.copyNewToOld(); + context.attribIndexList.moveToNew(2); + context.attribIndexList.copyNewToOld(); + WeakReference previousBuffer = new WeakReference<>(null); + context.boundBO[0] = previousBuffer; + + context.invalidate(); + + assertFalse(context.clipRectEnabled); + assertEquals(1f, context.pointSize); + assertEquals(0, context.boundShaderProgram); + assertEquals(0, context.boundFBO); + assertEquals(0, context.boundRB); + assertEquals(0, context.boundElementArrayVBO); + assertEquals(0, context.boundVertexArray); + assertEquals(0, context.boundArrayVBO); + assertEquals(0, context.boundPixelPackPBO); + assertEquals(0, context.numTexturesSet); + assertEquals(0, context.boundTextureUnit); + assertEquals(RenderState.TestFunction.Greater, context.alphaFunc); + assertFalse(context.srgbWriteEnabled); + assertEquals(new ColorRGBA(0f, 0f, 0f, 0f), context.clearColor); + assertNull(context.boundTextures[0]); + assertNull(context.boundAttribs[0]); + assertEquals(0, context.textureIndexList.oldLen); + assertEquals(0, context.textureIndexList.newLen); + assertEquals(0, context.attribIndexList.oldLen); + assertEquals(0, context.attribIndexList.newLen); + // The legacy reset did not change boundBO; this revision preserves that scope. + assertSame(previousBuffer, context.boundBO[0]); + } + + @Test + public void constructorDoesNotCallOverridableResetMethods() { + RenderContext context = new RenderContext() { + @Override + public void reset() { + throw new AssertionError("Constructor called public reset"); + } + + @Override + public void invalidate() { + throw new AssertionError("Constructor called public invalidate"); + } + }; + + assertFreshDefaults(context); + assertAllValid(context, true); + } + + private static void assertAllValid(RenderContext context, boolean valid) { + for (RenderStateCategory category : RenderStateCategory.values()) { + assertEquals(valid, context.isRenderStateValid(category), category.toString()); + } + } + + private static void assertFreshDefaults(RenderContext context) { + assertEquals(RenderState.FaceCullMode.Off, context.cullMode); + assertFalse(context.depthTestEnabled); + assertTrue(context.depthWriteEnabled); + assertTrue(context.colorWriteEnabled); + assertEquals(RenderState.TestFunction.Less, context.depthFunc); + assertEquals(RenderState.BlendMode.Off, context.blendMode); + assertEquals(RenderState.BlendFunc.One, context.sfactorRGB); + assertEquals(RenderState.BlendFunc.Zero, context.dfactorRGB); + assertEquals(RenderState.BlendFunc.One, context.sfactorAlpha); + assertEquals(RenderState.BlendFunc.Zero, context.dfactorAlpha); + assertEquals(RenderState.BlendEquation.Add, context.blendEquation); + assertFalse(context.stencilTest); + assertEquals(RenderState.TestFunction.Always, context.frontStencilFunction); + assertFalse(context.polyOffsetEnabled); + assertEquals(0f, context.polyOffsetFactor); + assertEquals(0f, context.polyOffsetUnits); + assertEquals(1f, context.lineWidth); + assertFalse(context.wireframe); + } + + private static void setNondefaultDrawState(RenderContext context) { + context.cullMode = RenderState.FaceCullMode.Front; + context.depthTestEnabled = true; + context.depthWriteEnabled = false; + context.colorWriteEnabled = false; + context.depthFunc = RenderState.TestFunction.Never; + context.blendMode = RenderState.BlendMode.Custom; + context.sfactorRGB = RenderState.BlendFunc.Src_Alpha; + context.blendEquation = RenderState.BlendEquation.Subtract; + context.stencilTest = true; + context.frontStencilFunction = RenderState.TestFunction.Equal; + context.polyOffsetEnabled = true; + context.polyOffsetFactor = 2f; + context.polyOffsetUnits = 3f; + context.lineWidth = 3f; + context.wireframe = true; + } +} diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStateInvalidationTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStateInvalidationTest.java new file mode 100644 index 0000000000..94de7565f8 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStateInvalidationTest.java @@ -0,0 +1,620 @@ +/* + * 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.assertAll; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.jme3.material.RenderState; +import com.jme3.material.RenderState.BlendEquation; +import com.jme3.material.RenderState.BlendEquationAlpha; +import com.jme3.material.RenderState.BlendFunc; +import com.jme3.material.RenderState.BlendMode; +import com.jme3.material.RenderState.FaceCullMode; +import com.jme3.material.RenderState.StencilOperation; +import com.jme3.material.RenderState.TestFunction; +import com.jme3.renderer.Caps; +import com.jme3.scene.Mesh; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.nio.IntBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +/** + * Models native GL state independently of the renderer's cached RenderContext. + */ +public class GlRendererStateInvalidationTest { + + enum Surface { + DESKTOP(true), DESKTOP_ADAPTER(true), GLES2(false), GLES3(false), + GLES_ADAPTER(false), WEBGL_ADAPTER(false); + + final boolean desktop; + + Surface(boolean desktop) { + this.desktop = desktop; + } + } + + @ParameterizedTest + @EnumSource(Surface.class) + public void invalidationRestoresDisabledAndDefaultDrawState(Surface surface) { + RecordingGl recording = new RecordingGl(surface); + RenderState state = disabledState(); + recording.renderer.applyRenderState(state); + recording.changeNativeState(); + recording.renderer.invalidateState(); + recording.calls.clear(); + + recording.renderer.applyRenderState(state); + + assertAll( + () -> assertFalse(recording.enabled(GL.GL_DEPTH_TEST), "depth test"), + () -> recording.assertState("glDepthMask", true), + () -> recording.assertState("glColorMask", true, true, true, true), + () -> assertFalse(recording.enabled(GL.GL_CULL_FACE), "face culling"), + () -> assertFalse(recording.enabled(GL.GL_BLEND), "blending"), + () -> assertFalse(recording.enabled(GL.GL_STENCIL_TEST), "stencil test"), + () -> assertFalse(recording.enabled(GL.GL_POLYGON_OFFSET_FILL), "polygon offset"), + () -> recording.assertState("glLineWidth", 1f)); + if (surface.desktop) { + recording.assertState("glPolygonMode", GL.GL_FRONT_AND_BACK, GL2.GL_FILL); + } else { + assertFalse(recording.calls.contains("glPolygonMode")); + } + recording.calls.clear(); + recording.renderer.applyRenderState(state); + assertTrue(recording.calls.isEmpty(), recording.calls.toString()); + } + + @ParameterizedTest + @EnumSource(Surface.class) + public void disabledDepthDoesNotValidateAnUnknownComparisonFunction(Surface surface) { + RecordingGl recording = new RecordingGl(surface); + RenderState state = disabledState(); + recording.renderer.applyRenderState(state); + recording.gl.glDepthFunc(GL.GL_NEVER); + recording.renderer.invalidateState(); + recording.renderer.applyRenderState(state); + + state.setDepthTest(true); + recording.renderer.applyRenderState(state); + + assertTrue(recording.enabled(GL.GL_DEPTH_TEST)); + recording.assertState("glDepthFunc", GL.GL_LESS); + recording.calls.clear(); + recording.renderer.applyRenderState(state); + assertTrue(recording.calls.isEmpty(), recording.calls.toString()); + } + + @ParameterizedTest + @EnumSource(BlendMode.class) + public void disabledBlendDoesNotValidateUnknownFactors(BlendMode mode) { + RecordingGl recording = new RecordingGl(Surface.GLES2); + RenderState state = disabledState(); + recording.renderer.applyRenderState(state); + recording.gl.glBlendFunc(GL.GL_ZERO, GL.GL_ZERO); + recording.gl.glBlendEquationSeparate(GL2.GL_FUNC_SUBTRACT, GL2.GL_FUNC_REVERSE_SUBTRACT); + recording.gl.glEnable(GL.GL_BLEND); + recording.renderer.invalidateState(); + recording.renderer.applyRenderState(state); + + state.setBlendMode(mode); + recording.renderer.applyRenderState(state); + + assertEquals(mode != BlendMode.Off, recording.enabled(GL.GL_BLEND)); + recording.assertState("glBlendEquationSeparate", GL2.GL_FUNC_ADD, GL2.GL_FUNC_ADD); + if (mode != BlendMode.Off) { + int[] factors = expectedFactors(mode); + recording.assertState("glBlendFuncSeparate", factors[0], factors[1], factors[2], factors[3]); + } + recording.calls.clear(); + recording.renderer.applyRenderState(state); + assertTrue(recording.calls.isEmpty(), recording.calls.toString()); + } + + @Test + public void customBlendRestoresFactorsEqualToTheNativeDefaultsAfterADisabledApply() { + RecordingGl recording = new RecordingGl(Surface.GLES2); + RenderState state = disabledState(); + state.setCustomBlendFactors(BlendFunc.One, BlendFunc.Zero, BlendFunc.One, BlendFunc.Zero); + recording.gl.glBlendFunc(GL.GL_ZERO, GL.GL_ONE); + recording.renderer.invalidateState(); + recording.renderer.applyRenderState(state); + + state.setBlendMode(BlendMode.Custom); + recording.renderer.applyRenderState(state); + + recording.assertState("glBlendFuncSeparate", GL.GL_ONE, GL.GL_ZERO, GL.GL_ONE, GL.GL_ZERO); + recording.calls.clear(); + recording.renderer.applyRenderState(state); + assertTrue(recording.calls.isEmpty(), recording.calls.toString()); + } + + @ParameterizedTest + @EnumSource(Surface.class) + public void invalidationRestoresEnabledNondefaultDrawState(Surface surface) { + RecordingGl recording = new RecordingGl(surface); + RenderState state = enabledState(); + recording.renderer.applyRenderState(state); + recording.restoreNativeDefaults(); + recording.renderer.invalidateState(); + recording.calls.clear(); + + recording.renderer.applyRenderState(state); + + assertAll( + () -> assertTrue(recording.enabled(GL.GL_DEPTH_TEST), "depth test"), + () -> recording.assertState("glDepthFunc", GL.GL_GREATER), + () -> recording.assertState("glDepthMask", false), + () -> recording.assertState("glColorMask", false, false, false, false), + () -> assertTrue(recording.enabled(GL.GL_CULL_FACE), "face culling"), + () -> recording.assertState("glCullFace", GL.GL_FRONT), + () -> assertTrue(recording.enabled(GL.GL_BLEND), "blending"), + () -> recording.assertState("glBlendEquationSeparate", GL2.GL_FUNC_SUBTRACT, + GL2.GL_FUNC_REVERSE_SUBTRACT), + () -> recording.assertState("glBlendFuncSeparate", GL.GL_SRC_ALPHA, + GL.GL_ONE_MINUS_SRC_ALPHA, GL.GL_ONE, GL.GL_ZERO), + () -> assertTrue(recording.enabled(GL.GL_STENCIL_TEST), "stencil test"), + () -> recording.assertState("glStencilFuncSeparate:" + GL.GL_FRONT, + GL.GL_FRONT, GL.GL_EQUAL, 2, 0x0f), + () -> recording.assertState("glStencilFuncSeparate:" + GL.GL_BACK, + GL.GL_BACK, GL.GL_NOTEQUAL, 3, 0xf0), + () -> recording.assertState("glStencilOpSeparate:" + GL.GL_FRONT, + GL.GL_FRONT, GL.GL_REPLACE, GL.GL_INCR, GL.GL_DECR), + () -> recording.assertState("glStencilOpSeparate:" + GL.GL_BACK, + GL.GL_BACK, GL.GL_ZERO, GL.GL_INVERT, GL.GL_KEEP), + () -> assertTrue(recording.enabled(GL.GL_POLYGON_OFFSET_FILL), "polygon offset"), + () -> recording.assertState("glPolygonOffset", 2f, 3f), + () -> recording.assertState("glLineWidth", 2f)); + if (surface.desktop) { + recording.assertState("glPolygonMode", GL.GL_FRONT_AND_BACK, GL2.GL_LINE); + } else { + assertFalse(recording.calls.contains("glPolygonMode")); + } + } + + @ParameterizedTest + @EnumSource(Surface.class) + public void freshDefaultsAndUnchangedApplicationsNeedNoGlCalls(Surface surface) { + RecordingGl recording = new RecordingGl(surface); + RenderState state = disabledState(); + + recording.renderer.applyRenderState(state); + recording.renderer.applyRenderState(state); + + assertTrue(recording.calls.isEmpty(), recording.calls.toString()); + } + + @ParameterizedTest + @EnumSource(BlendMode.class) + public void freshBlendModesUseNativeFactorDefaults(BlendMode mode) { + RecordingGl recording = new RecordingGl(Surface.GLES2); + RenderState state = disabledState(); + state.setBlendMode(mode); + + recording.renderer.applyRenderState(state); + + assertEquals(mode != BlendMode.Off, recording.enabled(GL.GL_BLEND)); + if (mode == BlendMode.Off) { + recording.assertState("glBlendFuncSeparate", GL.GL_ONE, GL.GL_ZERO, GL.GL_ONE, GL.GL_ZERO); + } else { + int[] factors = expectedFactors(mode); + recording.assertState("glBlendFuncSeparate", factors[0], factors[1], factors[2], factors[3]); + } + recording.calls.clear(); + recording.renderer.applyRenderState(state); + assertTrue(recording.calls.isEmpty(), recording.calls.toString()); + } + + @ParameterizedTest + @EnumSource(Surface.class) + public void invalidationQueriesOnlySupportedDesktopBufferState(Surface surface) { + RecordingGl recording = new RecordingGl(surface); + + recording.renderer.invalidateState(); + + if (surface.desktop) { + assertEquals(Arrays.asList(GL2.GL_DRAW_BUFFER, GL2.GL_READ_BUFFER), recording.queries); + } else { + assertTrue(recording.queries.isEmpty(), recording.queries.toString()); + } + } + + @Test + public void uninitializedGlesAdapterDoesNotUseDesktopState() { + RecordingGl recording = new RecordingGl(Surface.GLES_ADAPTER); + recording.renderer.getCaps().clear(); + RenderState state = disabledState(); + state.setWireframe(true); + + recording.renderer.invalidateState(); + recording.renderer.applyRenderState(state); + + assertTrue(recording.queries.isEmpty()); + assertFalse(recording.calls.contains("glPolygonMode")); + } + + @Test + public void clearingOneBufferDoesNotValidateTheOtherWriteMask() { + RecordingGl recording = new RecordingGl(Surface.GLES2); + recording.gl.glDepthMask(false); + recording.gl.glColorMask(false, false, false, false); + recording.renderer.invalidateState(); + + recording.renderer.clearBuffers(true, false, false); + + recording.assertState("glColorMask", true, true, true, true); + recording.assertState("glDepthMask", false); + recording.renderer.clearBuffers(false, true, false); + recording.assertState("glDepthMask", true); + recording.calls.clear(); + recording.renderer.clearBuffers(false, false, false); + assertTrue(recording.calls.isEmpty()); + } + + @Test + public void meshLineWidthAndRenderStateShareTheSameCache() { + final RecordingGl recording = new RecordingGl(Surface.GLES2); + final Mesh mesh = lineMesh(2f); + RenderState state = disabledState(); + state.setLineWidth(2f); + recording.renderer.applyRenderState(state); + recording.gl.glLineWidth(5f); + recording.renderer.invalidateState(); + + recording.renderer.renderMesh(mesh, 0, 1, null); + + recording.assertState("glLineWidth", 2f); + recording.calls.clear(); + recording.renderer.applyRenderState(state); + assertFalse(recording.calls.contains("glLineWidth")); + recording.calls.clear(); + recording.renderer.renderMesh(mesh, 0, 1, null); + assertFalse(recording.calls.contains("glLineWidth")); + state.setLineWidth(1f); + recording.renderer.applyRenderState(state); + recording.assertState("glLineWidth", 1f); + } + + @Test + public void defaultMeshWidthRestoresAnUnknownLineWidthWithoutAMaterialApply() { + RecordingGl recording = new RecordingGl(Surface.GLES2); + recording.gl.glLineWidth(5f); + recording.renderer.invalidateState(); + recording.calls.clear(); + Mesh mesh = lineMesh(1f); + + recording.renderer.renderMesh(mesh, 0, 1, null); + + recording.assertState("glLineWidth", 1f); + assertTrue(recording.calls.contains("glLineWidth")); + recording.calls.clear(); + recording.renderer.renderMesh(mesh, 0, 1, null); + assertFalse(recording.calls.contains("glLineWidth")); + } + + @Test + public void defaultMeshWidthPreservesTheKnownMaterialLineWidth() { + RecordingGl recording = new RecordingGl(Surface.GLES2); + recording.gl.glLineWidth(5f); + recording.renderer.invalidateState(); + RenderState state = disabledState(); + state.setLineWidth(3f); + recording.renderer.applyRenderState(state); + recording.calls.clear(); + + recording.renderer.renderMesh(lineMesh(1f), 0, 1, null); + + recording.assertState("glLineWidth", 3f); + assertFalse(recording.calls.contains("glLineWidth")); + } + + @Test + public void clearingAfterInvalidationRestoresOnlyTheRequestedWriteMasks() { + RecordingGl recording = new RecordingGl(Surface.GLES2); + recording.renderer.applyRenderState(disabledState()); + recording.gl.glDepthMask(false); + recording.gl.glColorMask(false, true, false, true); + recording.gl.glEnable(GL.GL_DEPTH_TEST); + recording.renderer.invalidateState(); + + recording.renderer.clearBuffers(true, true, false); + + recording.assertState("glDepthMask", true); + recording.assertState("glColorMask", true, true, true, true); + recording.assertState("glClear", GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT); + recording.calls.clear(); + recording.renderer.clearBuffers(true, true, false); + assertEquals(Arrays.asList("glClear"), recording.calls); + recording.renderer.applyRenderState(disabledState()); + assertFalse(recording.enabled(GL.GL_DEPTH_TEST)); + } + + @Test + public void disabledStencilIgnoresItsUnusedParametersAfterInvalidation() { + RecordingGl recording = new RecordingGl(Surface.GLES2); + RenderState state = disabledState(); + state.setStencil(false, null, null, null, null, null, null, null, null); + recording.gl.glEnable(GL.GL_STENCIL_TEST); + recording.renderer.invalidateState(); + recording.calls.clear(); + + recording.renderer.applyRenderState(state); + + assertFalse(recording.enabled(GL.GL_STENCIL_TEST)); + assertFalse(recording.calls.contains("glStencilFuncSeparate")); + assertFalse(recording.calls.contains("glStencilOpSeparate")); + } + + @Test + public void enablingPolygonOffsetAfterDisabledRecoveryRestoresItsParameters() { + RecordingGl recording = new RecordingGl(Surface.GLES2); + RenderState state = disabledState(); + recording.gl.glPolygonOffset(9f, 8f); + recording.renderer.invalidateState(); + recording.renderer.applyRenderState(state); + + state.setPolyOffset(2f, 3f); + recording.renderer.applyRenderState(state); + + assertTrue(recording.enabled(GL.GL_POLYGON_OFFSET_FILL)); + recording.assertState("glPolygonOffset", 2f, 3f); + } + + @Test + public void objectResetAndCleanupInvalidateDrawState() { + RecordingGl recording = new RecordingGl(Surface.GLES2); + RenderState state = disabledState(); + recording.renderer.applyRenderState(state); + recording.gl.glDepthMask(false); + recording.renderer.resetGLObjects(); + recording.renderer.applyRenderState(state); + recording.assertState("glDepthMask", true); + recording.gl.glColorMask(false, false, false, false); + recording.renderer.cleanup(); + recording.renderer.applyRenderState(state); + recording.assertState("glColorMask", true, true, true, true); + } + + private static Mesh lineMesh(float lineWidth) { + Mesh mesh = new Mesh() { + @Override + public int getVertexCount() { + return 2; + } + + @Override + public int getTriangleCount() { + return 1; + } + }; + mesh.setMode(Mesh.Mode.Lines); + mesh.setLineWidth(lineWidth); + return mesh; + } + + private static RenderState disabledState() { + RenderState state = new RenderState(); + state.setDepthTest(false); + state.setDepthFunc(TestFunction.Less); + state.setFaceCullMode(FaceCullMode.Off); + state.setFrontStencilMask(-1); + state.setBackStencilMask(-1); + return state; + } + + private static RenderState enabledState() { + RenderState state = disabledState(); + state.setWireframe(true); + state.setDepthTest(true); + state.setDepthFunc(TestFunction.Greater); + state.setDepthWrite(false); + state.setColorWrite(false); + state.setFaceCullMode(FaceCullMode.Front); + state.setBlendMode(BlendMode.Custom); + state.setBlendEquation(BlendEquation.Subtract); + state.setBlendEquationAlpha(BlendEquationAlpha.ReverseSubtract); + state.setCustomBlendFactors(BlendFunc.Src_Alpha, BlendFunc.One_Minus_Src_Alpha, + BlendFunc.One, BlendFunc.Zero); + state.setStencil(true, StencilOperation.Replace, StencilOperation.Increment, + StencilOperation.Decrement, + StencilOperation.Zero, StencilOperation.Invert, StencilOperation.Keep, + TestFunction.Equal, TestFunction.NotEqual); + state.setFrontStencilReference(2); + state.setBackStencilReference(3); + state.setFrontStencilMask(0x0f); + state.setBackStencilMask(0xf0); + state.setPolyOffset(2f, 3f); + state.setLineWidth(2f); + return state; + } + + private static int[] expectedFactors(BlendMode mode) { + switch (mode) { + case Additive: + case Custom: + return new int[]{GL.GL_ONE, GL.GL_ONE, GL.GL_ONE, GL.GL_ONE}; + case AlphaAdditive: + return new int[]{GL.GL_SRC_ALPHA, GL.GL_ONE, GL.GL_SRC_ALPHA, GL.GL_ONE}; + case Alpha: + return new int[]{GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA, + GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA}; + case AlphaSumA: + return new int[]{GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA, GL.GL_ONE, GL.GL_ONE}; + case PremultAlpha: + return new int[]{GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA, GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA}; + case Modulate: + return new int[]{GL.GL_DST_COLOR, GL.GL_ZERO, GL.GL_DST_COLOR, GL.GL_ZERO}; + case ModulateX2: + return new int[]{GL.GL_DST_COLOR, GL.GL_SRC_COLOR, GL.GL_DST_COLOR, GL.GL_SRC_COLOR}; + case Color: + case Screen: + return new int[]{GL.GL_ONE, GL.GL_ONE_MINUS_SRC_COLOR, GL.GL_ONE, GL.GL_ONE_MINUS_SRC_COLOR}; + case Exclusion: + return new int[]{GL.GL_ONE_MINUS_DST_COLOR, GL.GL_ONE_MINUS_SRC_COLOR, + GL.GL_ONE_MINUS_DST_COLOR, GL.GL_ONE_MINUS_SRC_COLOR}; + default: + throw new AssertionError(mode); + } + } + + private static final class RecordingGl implements InvocationHandler { + private final Surface surface; + private final GL gl; + private final GLRenderer renderer; + private final Map> state = new HashMap<>(); + private final Map enables = new HashMap<>(); + private final List calls = new ArrayList<>(); + private final List queries = new ArrayList<>(); + + private RecordingGl(Surface surface) { + this.surface = surface; + Class glType = surface.desktop ? GL3.class + : surface == Surface.GLES2 ? GL.class : GLES_30.class; + Class adapterType; + if (surface == Surface.DESKTOP_ADAPTER) { + adapterType = GLES_30.class; + } else if (surface == Surface.GLES_ADAPTER || surface == Surface.WEBGL_ADAPTER) { + adapterType = GL2.class; + } else { + adapterType = GLExt.class; + } + gl = (GL) Proxy.newProxyInstance(GL.class.getClassLoader(), + new Class[]{glType, adapterType, GLFbo.class}, this); + GLExt extension = (GLExt) Proxy.newProxyInstance(GLExt.class.getClassLoader(), + new Class[]{GLExt.class}, this); + restoreNativeDefaults(); + renderer = new GLRenderer(gl, extension, (GLFbo) gl); + if (surface.desktop) { + renderer.getCaps().add(Caps.OpenGL20); + renderer.getCaps().add(Caps.OpenGL30); + } else if (surface == Surface.WEBGL_ADAPTER) { + renderer.getCaps().add(Caps.WebGL); + } else { + renderer.getCaps().add(Caps.OpenGLES20); + if (surface != Surface.GLES2) { + renderer.getCaps().add(Caps.OpenGLES30); + } + } + calls.clear(); + } + + private boolean enabled(int capability) { + return Boolean.TRUE.equals(enables.get(capability)); + } + + private void assertState(String name, Object... expected) { + assertEquals(Arrays.asList(expected), state.get(name), name); + } + + private void restoreNativeDefaults() { + gl.glDisable(GL.GL_DEPTH_TEST); + gl.glDepthFunc(GL.GL_LESS); + gl.glDepthMask(true); + gl.glColorMask(true, true, true, true); + gl.glDisable(GL.GL_CULL_FACE); + gl.glCullFace(GL.GL_BACK); + gl.glDisable(GL.GL_BLEND); + gl.glBlendEquationSeparate(GL2.GL_FUNC_ADD, GL2.GL_FUNC_ADD); + gl.glBlendFunc(GL.GL_ONE, GL.GL_ZERO); + gl.glDisable(GL.GL_STENCIL_TEST); + gl.glStencilFuncSeparate(GL.GL_FRONT, GL.GL_ALWAYS, 0, -1); + gl.glStencilFuncSeparate(GL.GL_BACK, GL.GL_ALWAYS, 0, -1); + gl.glStencilOpSeparate(GL.GL_FRONT, GL.GL_KEEP, GL.GL_KEEP, GL.GL_KEEP); + gl.glStencilOpSeparate(GL.GL_BACK, GL.GL_KEEP, GL.GL_KEEP, GL.GL_KEEP); + gl.glDisable(GL.GL_POLYGON_OFFSET_FILL); + gl.glPolygonOffset(0f, 0f); + gl.glLineWidth(1f); + if (surface.desktop) { + ((GL2) gl).glPolygonMode(GL.GL_FRONT_AND_BACK, GL2.GL_FILL); + } + } + + private void changeNativeState() { + gl.glEnable(GL.GL_DEPTH_TEST); + gl.glDepthFunc(GL.GL_NEVER); + gl.glDepthMask(false); + gl.glColorMask(false, true, false, true); + gl.glEnable(GL.GL_CULL_FACE); + gl.glCullFace(GL.GL_FRONT); + gl.glEnable(GL.GL_BLEND); + gl.glBlendEquationSeparate(GL2.GL_FUNC_SUBTRACT, GL2.GL_FUNC_REVERSE_SUBTRACT); + gl.glBlendFunc(GL.GL_ZERO, GL.GL_ZERO); + gl.glEnable(GL.GL_STENCIL_TEST); + gl.glStencilFuncSeparate(GL.GL_FRONT, GL.GL_NEVER, 1, 0x0f); + gl.glStencilFuncSeparate(GL.GL_BACK, GL.GL_NEVER, 2, 0xf0); + gl.glEnable(GL.GL_POLYGON_OFFSET_FILL); + gl.glPolygonOffset(8f, 9f); + gl.glLineWidth(3f); + if (surface.desktop) { + ((GL2) gl).glPolygonMode(GL.GL_FRONT_AND_BACK, GL2.GL_LINE); + } + } + + @Override + public Object invoke(Object proxy, Method method, Object[] arguments) { + String name = method.getName(); + if (name.equals("glGetInteger")) { + int parameter = (Integer) arguments[0]; + queries.add(parameter); + if (!surface.desktop + && (parameter == GL2.GL_DRAW_BUFFER || parameter == GL2.GL_READ_BUFFER)) { + throw new AssertionError("Unsupported GLES query: " + parameter); + } + ((IntBuffer) arguments[1]).put(0, 0); + return null; + } + calls.add(name); + if (name.equals("glEnable") || name.equals("glDisable")) { + enables.put((Integer) arguments[0], name.equals("glEnable")); + } else if (name.equals("glBlendFunc")) { + state.put("glBlendFuncSeparate", Arrays.asList(arguments[0], arguments[1], + arguments[0], arguments[1])); + } else { + String key = name.startsWith("glStencil") ? name + ":" + arguments[0] : name; + state.put(key, Arrays.asList(arguments)); + } + return null; + } + } +}