From fa5dad4feb492fe86d6d2d5babb604dfe9fe1115 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Fri, 2 Oct 2026 14:30:17 +0200 Subject: [PATCH 1/2] Fix stencil enable-state caching and preserve parameter updates --- .../java/com/jme3/renderer/RenderContext.java | 20 ++ .../com/jme3/renderer/opengl/GLRenderer.java | 15 +- .../opengl/GlRendererStencilStateTest.java | 234 ++++++++++++++++++ 3 files changed, 266 insertions(+), 3 deletions(-) create mode 100644 jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStencilStateTest.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..414e47cf7a 100644 --- a/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java +++ b/jme3-core/src/main/java/com/jme3/renderer/RenderContext.java @@ -331,6 +331,22 @@ private static WeakReference[] newWeakReferenceArray(int size) { * Stencil test function for back-facing polygons. */ public RenderState.TestFunction backStencilFunction; + /** + * Stencil reference value for front-facing polygons. + */ + public int frontStencilReference; + /** + * Stencil reference value for back-facing polygons. + */ + public int backStencilReference; + /** + * Stencil comparison mask for front-facing polygons. + */ + public int frontStencilMask; + /** + * Stencil comparison mask for back-facing polygons. + */ + public int backStencilMask; /** * Vertex attribs currently bound and enabled. If a slot is null, then @@ -417,6 +433,10 @@ private void init() { backStencilDepthPassOperation = RenderState.StencilOperation.Keep; frontStencilFunction = RenderState.TestFunction.Always; backStencilFunction = RenderState.TestFunction.Always; + frontStencilReference = 0; + backStencilReference = 0; + frontStencilMask = -1; + backStencilMask = -1; depthFunc = RenderState.TestFunction.Less; alphaFunc = RenderState.TestFunction.Greater; 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..8b78f50e4a 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 @@ -1154,9 +1154,14 @@ public void applyRenderState(RenderState state) { || context.backStencilDepthFailOperation != state.getBackStencilDepthFailOperation() || context.backStencilDepthPassOperation != state.getBackStencilDepthPassOperation() || context.frontStencilFunction != state.getFrontStencilFunction() - || context.backStencilFunction != state.getBackStencilFunction()) { - - context.frontStencilStencilFailOperation = state.getFrontStencilStencilFailOperation(); //terrible looking, I know + || context.backStencilFunction != state.getBackStencilFunction() + || context.frontStencilReference != state.getFrontStencilReference() + || context.backStencilReference != state.getBackStencilReference() + || context.frontStencilMask != state.getFrontStencilMask() + || context.backStencilMask != state.getBackStencilMask()) { + + context.stencilTest = state.isStencilTest(); + context.frontStencilStencilFailOperation = state.getFrontStencilStencilFailOperation(); context.frontStencilDepthFailOperation = state.getFrontStencilDepthFailOperation(); context.frontStencilDepthPassOperation = state.getFrontStencilDepthPassOperation(); context.backStencilStencilFailOperation = state.getBackStencilStencilFailOperation(); @@ -1164,6 +1169,10 @@ public void applyRenderState(RenderState state) { context.backStencilDepthPassOperation = state.getBackStencilDepthPassOperation(); context.frontStencilFunction = state.getFrontStencilFunction(); context.backStencilFunction = state.getBackStencilFunction(); + context.frontStencilReference = state.getFrontStencilReference(); + context.backStencilReference = state.getBackStencilReference(); + context.frontStencilMask = state.getFrontStencilMask(); + context.backStencilMask = state.getBackStencilMask(); if (state.isStencilTest()) { gl.glEnable(GL.GL_STENCIL_TEST); diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStencilStateTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStencilStateTest.java new file mode 100644 index 0000000000..2d89a6df31 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStencilStateTest.java @@ -0,0 +1,234 @@ +/* + * 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.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import com.jme3.material.RenderState; +import com.jme3.material.RenderState.StencilOperation; +import com.jme3.material.RenderState.TestFunction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Verifies that cached stencil state matches the calls made to OpenGL. + */ +public class GlRendererStencilStateTest { + + private GL gl; + private GLRenderer renderer; + + @BeforeEach + public void initializeRenderer() { + gl = mock(GL.class); + renderer = new GLRenderer(gl, mock(GLExt.class), mock(GLFbo.class)); + } + + @Test + public void disablingStencilWithUnchangedParametersDisablesTheTest() { + renderer.applyRenderState(stencil(true, TestFunction.Never)); + verify(gl).glEnable(GL.GL_STENCIL_TEST); + clearInvocations(gl); + + renderer.applyRenderState(stencil(false, TestFunction.Never)); + + verify(gl).glDisable(GL.GL_STENCIL_TEST); + } + + @Test + public void applyingUnchangedEnabledStateDoesNotRepeatGlCalls() { + RenderState state = stencil(true, TestFunction.Equal); + renderer.applyRenderState(state); + clearInvocations(gl); + + renderer.applyRenderState(state); + + verifyNoInteractions(gl); + } + + @Test + public void stencilCanBeDisabledAndReenabledWithTheSameParameters() { + RenderState enabled = stencil(true, TestFunction.Never); + RenderState disabled = stencil(false, TestFunction.Never); + renderer.applyRenderState(enabled); + clearInvocations(gl); + renderer.applyRenderState(disabled); + verify(gl).glDisable(GL.GL_STENCIL_TEST); + clearInvocations(gl); + + renderer.applyRenderState(enabled); + + verify(gl).glEnable(GL.GL_STENCIL_TEST); + verify(gl).glStencilFuncSeparate(GL.GL_FRONT, GL.GL_NEVER, 0, Integer.MAX_VALUE); + verify(gl).glStencilFuncSeparate(GL.GL_BACK, GL.GL_NEVER, 0, Integer.MAX_VALUE); + } + + @Test + public void changingOnlyTheFrontReferenceUpdatesTheFunction() { + RenderState state = stencil(true, TestFunction.Equal); + renderer.applyRenderState(state); + clearInvocations(gl); + + state.setFrontStencilReference(2); + renderer.applyRenderState(state); + + verify(gl).glStencilFuncSeparate(GL.GL_FRONT, GL.GL_EQUAL, 2, Integer.MAX_VALUE); + } + + @Test + public void changingOnlyTheBackReferenceUpdatesTheFunction() { + RenderState state = stencil(true, TestFunction.Equal); + renderer.applyRenderState(state); + clearInvocations(gl); + + state.setBackStencilReference(3); + renderer.applyRenderState(state); + + verify(gl).glStencilFuncSeparate(GL.GL_BACK, GL.GL_EQUAL, 3, Integer.MAX_VALUE); + } + + @Test + public void changingOnlyTheFrontMaskUpdatesTheFunction() { + RenderState state = stencil(true, TestFunction.Equal); + renderer.applyRenderState(state); + clearInvocations(gl); + + state.setFrontStencilMask(0x0f); + renderer.applyRenderState(state); + + verify(gl).glStencilFuncSeparate(GL.GL_FRONT, GL.GL_EQUAL, 0, 0x0f); + } + + @Test + public void changingOnlyTheBackMaskUpdatesTheFunction() { + RenderState state = stencil(true, TestFunction.Equal); + renderer.applyRenderState(state); + clearInvocations(gl); + + state.setBackStencilMask(0xf0); + renderer.applyRenderState(state); + + verify(gl).glStencilFuncSeparate(GL.GL_BACK, GL.GL_EQUAL, 0, 0xf0); + } + + @Test + public void changingOnlyTheOperationsUpdatesBothFaces() { + RenderState state = stencil(true, TestFunction.Equal); + renderer.applyRenderState(state); + clearInvocations(gl); + + state.setStencil(true, + StencilOperation.Replace, StencilOperation.Keep, StencilOperation.Increment, + StencilOperation.Zero, StencilOperation.Decrement, StencilOperation.Invert, + TestFunction.Equal, TestFunction.Equal); + renderer.applyRenderState(state); + + verify(gl).glStencilOpSeparate(GL.GL_FRONT, GL.GL_REPLACE, GL.GL_KEEP, GL.GL_INCR); + verify(gl).glStencilOpSeparate(GL.GL_BACK, GL.GL_ZERO, GL.GL_DECR, GL.GL_INVERT); + } + + @Test + public void changingOnlyTheFunctionsUpdatesBothFaces() { + RenderState state = stencil(true, TestFunction.Equal); + renderer.applyRenderState(state); + clearInvocations(gl); + + state.setStencil(true, + StencilOperation.Keep, StencilOperation.Keep, StencilOperation.Keep, + StencilOperation.Keep, StencilOperation.Keep, StencilOperation.Keep, + TestFunction.Never, TestFunction.Always); + renderer.applyRenderState(state); + + verify(gl).glStencilFuncSeparate(GL.GL_FRONT, GL.GL_NEVER, 0, Integer.MAX_VALUE); + verify(gl).glStencilFuncSeparate(GL.GL_BACK, GL.GL_ALWAYS, 0, Integer.MAX_VALUE); + } + + @Test + public void parametersChangedWhileDisabledAreAppliedWhenEnabled() { + RenderState state = stencil(false, TestFunction.Equal); + state.setFrontStencilReference(2); + state.setBackStencilReference(3); + state.setFrontStencilMask(0x0f); + state.setBackStencilMask(0xf0); + renderer.applyRenderState(state); + clearInvocations(gl); + + state.setStencil(true, + StencilOperation.Keep, StencilOperation.Keep, StencilOperation.Keep, + StencilOperation.Keep, StencilOperation.Keep, StencilOperation.Keep, + TestFunction.Equal, TestFunction.Equal); + renderer.applyRenderState(state); + + verify(gl).glEnable(GL.GL_STENCIL_TEST); + verify(gl).glStencilFuncSeparate(GL.GL_FRONT, GL.GL_EQUAL, 2, 0x0f); + verify(gl).glStencilFuncSeparate(GL.GL_BACK, GL.GL_EQUAL, 3, 0xf0); + } + + @Test + public void invalidatingStateReappliesEnabledStencilParameters() { + RenderState state = stencil(true, TestFunction.Equal); + state.setFrontStencilReference(2); + state.setBackStencilMask(0xf0); + renderer.applyRenderState(state); + renderer.invalidateState(); + clearInvocations(gl); + + renderer.applyRenderState(state); + + verify(gl).glEnable(GL.GL_STENCIL_TEST); + verify(gl).glStencilFuncSeparate(GL.GL_FRONT, GL.GL_EQUAL, 2, Integer.MAX_VALUE); + verify(gl).glStencilFuncSeparate(GL.GL_BACK, GL.GL_EQUAL, 0, 0xf0); + } + + @Test + public void enablingStencilWithDefaultParametersAppliesBothFaces() { + renderer.applyRenderState(stencil(true, TestFunction.Always)); + + verify(gl).glEnable(GL.GL_STENCIL_TEST); + verify(gl).glStencilFuncSeparate(GL.GL_FRONT, GL.GL_ALWAYS, 0, Integer.MAX_VALUE); + verify(gl).glStencilFuncSeparate(GL.GL_BACK, GL.GL_ALWAYS, 0, Integer.MAX_VALUE); + verify(gl).glStencilOpSeparate(GL.GL_FRONT, GL.GL_KEEP, GL.GL_KEEP, GL.GL_KEEP); + verify(gl).glStencilOpSeparate(GL.GL_BACK, GL.GL_KEEP, GL.GL_KEEP, GL.GL_KEEP); + } + + private static RenderState stencil(boolean enabled, TestFunction function) { + RenderState state = new RenderState(); + state.setStencil(enabled, + StencilOperation.Keep, StencilOperation.Keep, StencilOperation.Keep, + StencilOperation.Keep, StencilOperation.Keep, StencilOperation.Keep, + function, function); + return state; + } +} From 1730d8b857cca677dcd9c21f507fd8387905c276 Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Sat, 3 Oct 2026 00:04:13 +0200 Subject: [PATCH 2/2] Remove redundant stencil disable test --- .../renderer/opengl/GlRendererStencilStateTest.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStencilStateTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStencilStateTest.java index 2d89a6df31..b0d7da4101 100644 --- a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStencilStateTest.java +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererStencilStateTest.java @@ -56,17 +56,6 @@ public void initializeRenderer() { renderer = new GLRenderer(gl, mock(GLExt.class), mock(GLFbo.class)); } - @Test - public void disablingStencilWithUnchangedParametersDisablesTheTest() { - renderer.applyRenderState(stencil(true, TestFunction.Never)); - verify(gl).glEnable(GL.GL_STENCIL_TEST); - clearInvocations(gl); - - renderer.applyRenderState(stencil(false, TestFunction.Never)); - - verify(gl).glDisable(GL.GL_STENCIL_TEST); - } - @Test public void applyingUnchangedEnabledStateDoesNotRepeatGlCalls() { RenderState state = stencil(true, TestFunction.Equal);