From f1646c1e860d920654976920f0525f121ce0652f Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Fri, 2 Oct 2026 12:23:12 +0200 Subject: [PATCH 1/2] Fix framebuffer attachment updates and resource cleanup Track native framebuffer attachments and all owned renderbuffers across target mutation, allocation failure, explicit deletion, managed cleanup, and context reset. Preserve existing storage and guard stale references when native names are reused. Use discovered presentation framebuffer names for copies, restoration, and overrides. Build directly on upstream master without the mipmap/null-storage or GLES3 capability changes from PR #2983. Add 34 recording-GL regressions in three dedicated test classes. Local core/effects validation: 522 passed, one existing test skipped, zero failures. --- .../com/jme3/renderer/opengl/GLRenderer.java | 122 +++++-- .../java/com/jme3/texture/FrameBuffer.java | 19 +- .../com/jme3/util/NativeObjectManager.java | 19 ++ .../opengl/FrameBufferTestSupport.java | 306 ++++++++++++++++++ .../GlRendererDefaultFrameBufferTest.java | 108 +++++++ .../GlRendererFrameBufferAttachmentTest.java | 225 +++++++++++++ .../GlRendererFrameBufferLifecycleTest.java | 225 +++++++++++++ 7 files changed, 993 insertions(+), 31 deletions(-) create mode 100644 jme3-core/src/test/java/com/jme3/renderer/opengl/FrameBufferTestSupport.java create mode 100644 jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererDefaultFrameBufferTest.java create mode 100644 jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferAttachmentTest.java create mode 100644 jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferLifecycleTest.java 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..5544723473 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 @@ -102,6 +102,17 @@ public final class GLRenderer implements Renderer { private FrameBuffer mainFbOverride = null; private int defaultFBO = 0; + + /** Native attachment state, without a reference to the owning FrameBuffer. */ + private static final class FrameBufferState { + final Set attachmentSlots = new HashSet<>(); + final Map renderBuffers = new HashMap<>(); + final ArrayList colorTextures = new ArrayList<>(); + } + + // Destructible framebuffer clones contain only an ID. Keep their resources + // here so allocations and target changes after registration are also tracked. + private final Map frameBufferStates = new HashMap<>(); private final Statistics statistics = new Statistics(); private int vpX, vpY, vpW, vpH; private int clipX, clipY, clipW, clipH; @@ -893,11 +904,10 @@ public void initialize() { gl.glGetInteger(GL.GL_FRAMEBUFFER_BINDING, tmp); tmp.rewind(); int fbOnLoad = tmp.get(); - if(fbOnLoad > 0) - { - // Override default FB to fbOnLoad. Mostly an iOS fix for scene processors and filters. - defaultFBO = fbOnLoad; - } + // The presentation framebuffer can be nonzero, notably on iOS. + defaultFBO = fbOnLoad; + context.boundFBO = fbOnLoad; + context.boundFB = null; } @Override @@ -913,6 +923,12 @@ public void invalidateState() { public void resetGLObjects() { logger.log(Level.FINE, "Resetting objects and invalidating state"); objManager.resetObjects(); + for (FrameBufferState state : frameBufferStates.values()) { + for (RenderBuffer rb : state.renderBuffers.keySet()) { + rb.resetObject(); + } + } + frameBufferStates.clear(); statistics.clearMemory(); invalidateState(); } @@ -2023,7 +2039,7 @@ public void copyFrameBuffer(FrameBuffer src, FrameBuffer dst, boolean copyColor, } if (src == null) { - glfbo.glBindFramebufferEXT(GLFbo.GL_READ_FRAMEBUFFER_EXT, 0); + glfbo.glBindFramebufferEXT(GLFbo.GL_READ_FRAMEBUFFER_EXT, defaultFBO); srcX0 = vpX; srcY0 = vpY; srcX1 = vpX + vpW; @@ -2034,7 +2050,7 @@ public void copyFrameBuffer(FrameBuffer src, FrameBuffer dst, boolean copyColor, srcY1 = src.getHeight(); } if (dst == null) { - glfbo.glBindFramebufferEXT(GLFbo.GL_DRAW_FRAMEBUFFER_EXT, 0); + glfbo.glBindFramebufferEXT(GLFbo.GL_DRAW_FRAMEBUFFER_EXT, defaultFBO); dstX0 = vpX; dstY0 = vpY; dstX1 = vpX + vpW; @@ -2104,6 +2120,8 @@ private void checkFrameBufferError() { private void updateRenderBuffer(FrameBuffer fb, RenderBuffer rb) { int id = rb.getId(); + FrameBufferState state = frameBufferStates.get(fb.getId()); + state.renderBuffers.putIfAbsent(rb, 0); if (id == -1) { glfbo.glGenRenderbuffersEXT(intBuf1); id = intBuf1.get(0); @@ -2122,6 +2140,10 @@ private void updateRenderBuffer(FrameBuffer fb, RenderBuffer rb) { } GLImageFormat glFmt = texUtil.getImageFormatWithError(rb.getFormat(), fb.isSrgb()); + if (state.renderBuffers.get(rb) == glFmt.internalFormat) { + // Reattaching an unchanged buffer must not discard its contents. + return; + } if (fb.getSamples() > 1 && caps.contains(Caps.FrameBufferMultisample)) { int samples = fb.getSamples(); @@ -2140,6 +2162,7 @@ private void updateRenderBuffer(FrameBuffer fb, RenderBuffer rb) { fb.getWidth(), fb.getHeight()); } + state.renderBuffers.put(rb, glFmt.internalFormat); } private int convertAttachmentSlot(int attachmentSlot) { @@ -2201,21 +2224,17 @@ public void updateFrameBufferAttachment(FrameBuffer fb, RenderBuffer rb) { + " is not depth-renderable and cannot be used as a depth attachment."); } - boolean needAttach; if (rb.getTexture() == null) { - // if it hasn't been created yet, then attach is required. - needAttach = rb.getId() == -1; updateRenderBuffer(fb, rb); - } else { - needAttach = false; - updateRenderTexture(fb, rb); - } - if (needAttach) { + // A surviving renderbuffer may have moved to a different color slot. glfbo.glFramebufferRenderbufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, convertAttachmentSlot(rb.getSlot()), GLFbo.GL_RENDERBUFFER_EXT, rb.getId()); + } else { + updateRenderTexture(fb, rb); } + frameBufferStates.get(fb.getId()).attachmentSlots.add(rb.getSlot()); } private void bindFrameBuffer(FrameBuffer fb) { @@ -2265,11 +2284,40 @@ public void updateFrameBuffer(FrameBuffer fb) { glfbo.glGenFramebuffersEXT(intBuf1); id = intBuf1.get(0); fb.setId(id); + frameBufferStates.put(id, new FrameBufferState()); objManager.registerObject(fb); statistics.onNewFrameBuffer(); } bindFrameBuffer(fb); + FrameBufferState state = frameBufferStates.get(id); + Set slots = new HashSet<>(); + Set buffers = new HashSet<>(); + if (fb.getDepthTarget() != null) { + slots.add(fb.getDepthTarget().getSlot()); + buffers.add(fb.getDepthTarget()); + } + for (int i = 0; i < fb.getNumColorTargets(); i++) { + RenderBuffer rb = fb.getColorTarget(i); + slots.add(rb.getSlot()); + buffers.add(rb); + } + for (Iterator it = state.attachmentSlots.iterator(); it.hasNext();) { + int slot = it.next(); + if (!slots.contains(slot)) { + glfbo.glFramebufferRenderbufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, + convertAttachmentSlot(slot), GLFbo.GL_RENDERBUFFER_EXT, 0); + it.remove(); + } + } + for (Iterator it = state.renderBuffers.keySet().iterator(); it.hasNext();) { + RenderBuffer rb = it.next(); + if (!buffers.contains(rb)) { + deleteRenderBuffer(rb); + it.remove(); + } + } + state.colorTextures.clear(); FrameBuffer.RenderBuffer depthBuf = fb.getDepthTarget(); if (depthBuf != null) { @@ -2279,6 +2327,9 @@ public void updateFrameBuffer(FrameBuffer fb) { for (int i = 0; i < fb.getNumColorTargets(); i++) { FrameBuffer.RenderBuffer colorBuf = fb.getColorTarget(i); updateFrameBufferAttachment(fb, colorBuf); + if (colorBuf.getTexture() != null) { + state.colorTextures.add(colorBuf.getTexture()); + } } setReadDrawBuffers(fb); @@ -2311,7 +2362,7 @@ public Vector2f[] getFrameBufferSamplePositions(FrameBuffer fb) { @Override public void setMainFrameBufferOverride(FrameBuffer fb) { mainFbOverride = null; - if (context.boundFBO == 0) { + if (context.boundFBO == defaultFBO) { // Main FB is now set to fb, make sure its bound setFrameBuffer(fb); } @@ -2402,23 +2453,25 @@ public void setFrameBuffer(FrameBuffer fb) { if (boundFB != null && (boundFB.getMipMapsGenerationHint() != null ? boundFB.getMipMapsGenerationHint() : generateMipmapsForFramebuffers)) { - for (int i = 0; i < boundFB.getNumColorTargets(); i++) { - RenderBuffer rb = boundFB.getColorTarget(i); - Texture tex = rb.getTexture(); + // Use the native attachment snapshot: the Java targets may already + // have been replaced with textures that have not been allocated yet. + FrameBufferState state = frameBufferStates.get(boundFB.getId()); + for (Texture tex : state.colorTextures) { if (tex != null && tex.getMinFilter().usesMipMapLevels() && isMipmapGenerationSupported(tex.getImage().getFormat(), linearizeSrgbImages && boundFB.isSrgb() ? ColorSpace.sRGB : ColorSpace.Linear)) { try { final int textureUnitIndex = 0; - setTexture(textureUnitIndex, rb.getTexture()); + setTexture(textureUnitIndex, tex); } catch (TextureUnitException exception) { throw new RuntimeException("Renderer lacks texture units?"); } if (tex.getType() == Texture.Type.CubeMap) { glfbo.glGenerateMipmapEXT(GL.GL_TEXTURE_CUBE_MAP); } else { - int textureType = convertTextureType(tex.getType(), tex.getImage().getMultiSamples(), rb.getFace()); + int textureType = convertTextureType( + tex.getType(), tex.getImage().getMultiSamples(), -1); glfbo.glGenerateMipmapEXT(textureType); } } else if (tex != null && tex.getMinFilter().usesMipMapLevels()) { @@ -2480,28 +2533,37 @@ public void readFrameBufferWithFormat(FrameBuffer fb, ByteBuffer byteBuf, Image. readFrameBufferWithGLFormat(fb, byteBuf, glFormat.format, glFormat.dataType); } - private void deleteRenderBuffer(FrameBuffer fb, RenderBuffer rb) { - intBuf1.put(0, rb.getId()); + private void deleteRenderBuffer(RenderBuffer rb) { + int id = rb.getId(); + if (id == -1 || rb.getTexture() != null) { + return; + } + intBuf1.put(0, id); glfbo.glDeleteRenderbuffersEXT(intBuf1); + if (context.boundRB == id) { + context.boundRB = 0; + } + rb.resetObject(); } @Override public void deleteFrameBuffer(FrameBuffer fb) { if (fb.getId() != -1) { if (context.boundFBO == fb.getId()) { - glfbo.glBindFramebufferEXT(GLFbo.GL_FRAMEBUFFER_EXT, 0); - context.boundFBO = 0; + bindFrameBuffer(null); + toggleFramebufferSrgb(null); } - if (fb.getDepthTarget() != null) { - deleteRenderBuffer(fb, fb.getDepthTarget()); - } - if (fb.getColorTarget() != null) { - deleteRenderBuffer(fb, fb.getColorTarget()); + FrameBufferState state = frameBufferStates.remove(fb.getId()); + if (state != null) { + for (RenderBuffer rb : state.renderBuffers.keySet()) { + deleteRenderBuffer(rb); + } } intBuf1.put(0, fb.getId()); glfbo.glDeleteFramebuffersEXT(intBuf1); + objManager.unregisterObject(fb); fb.resetObject(); statistics.onDeleteFrameBuffer(); diff --git a/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java b/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java index 8f696f4b49..5d1c8c4302 100644 --- a/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java +++ b/jme3-core/src/main/java/com/jme3/texture/FrameBuffer.java @@ -258,12 +258,14 @@ private FrameBuffer() { public void addColorTarget(FrameBufferBufferTarget colorBuf){ colorBuf.slot=colorBufs.size(); colorBufs.add(colorBuf); + setUpdateNeeded(); } public void addColorTarget(FrameBufferTextureTarget colorBuf){ // checkSetTexture(colorBuf.getTexture(), false); // TODO: this won't work for levels. colorBuf.slot=colorBufs.size(); colorBufs.add(colorBuf); + setUpdateNeeded(); } /** @@ -281,6 +283,7 @@ public void replaceColorTarget(int i, FrameBufferTextureTarget colorBuf) { } colorBuf.slot = i; colorBufs.set(i, colorBuf); + setUpdateNeeded(); } /** @@ -293,9 +296,15 @@ public void replaceColorTarget(int i, FrameBufferTextureTarget colorBuf) { */ public void removeColorTarget(int i) { colorBufs.remove(i); + if (colorBufIndex > i) { + colorBufIndex--; + } else if (colorBufIndex == i && colorBufIndex == colorBufs.size()) { + colorBufIndex = Math.max(0, colorBufs.size() - 1); + } for (; i < colorBufs.size(); i++) { colorBufs.get(i).slot = i; } + setUpdateNeeded(); } /** @@ -310,6 +319,7 @@ public void addColorTarget(FrameBufferTextureTarget colorBuf, TextureCubeMap.Fac colorBuf.slot = colorBufs.size(); colorBuf.face = face.ordinal(); colorBufs.add(colorBuf); + setUpdateNeeded(); } public void setDepthTarget(FrameBufferBufferTarget depthBuf){ @@ -317,12 +327,14 @@ public void setDepthTarget(FrameBufferBufferTarget depthBuf){ throw new IllegalArgumentException("Depth buffer format must be depth."); this.depthBuf = depthBuf; this.depthBuf.slot = this.depthBuf.getFormat().isDepthStencilFormat() ? SLOT_DEPTH_STENCIL : SLOT_DEPTH; + setUpdateNeeded(); } public void setDepthTarget(FrameBufferTextureTarget depthBuf){ checkSetTexture(depthBuf.getTexture(), true); this.depthBuf = depthBuf; this.depthBuf.slot = depthBuf.getTexture().getImage().getFormat().isDepthStencilFormat() ? SLOT_DEPTH_STENCIL : SLOT_DEPTH; + setUpdateNeeded(); } public int getNumColorTargets(){ @@ -474,6 +486,7 @@ public void setMultiTarget(boolean enabled) { } else { colorBufIndex = 0; } + setUpdateNeeded(); } /** @@ -499,7 +512,7 @@ public void setTargetIndex(int index) { throw new IllegalArgumentException("Target index must be between 0 and 16"); } - if (colorBufs.size() < index) { + if (colorBufs.size() <= index) { throw new IllegalArgumentException("The target at " + index + " is not set!"); } @@ -568,6 +581,10 @@ public void setColorTexture(TextureCubeMap tex, TextureCubeMap.Face face) { */ public void clearColorTargets() { colorBufs.clear(); + if (colorBufIndex >= 0) { + colorBufIndex = 0; + } + setUpdateNeeded(); } /** diff --git a/jme3-core/src/main/java/com/jme3/util/NativeObjectManager.java b/jme3-core/src/main/java/com/jme3/util/NativeObjectManager.java index fed89068f6..0f976a9077 100644 --- a/jme3-core/src/main/java/com/jme3/util/NativeObjectManager.java +++ b/jme3-core/src/main/java/com/jme3/util/NativeObjectManager.java @@ -116,6 +116,20 @@ public void registerObject(NativeObject obj) { } } + /** + * Unregisters an object explicitly deleted by a renderer. This is also safe + * when the manager has already unregistered it before calling the renderer. + * + * @param obj the deleted native object (not null, with its original ID) + */ + public void unregisterObject(NativeObject obj) { + NativeObjectRef ref = refMap.remove(obj.getUniqueId()); + if (ref != null) { + ref.clear(); + } + userDeletionQueue.removeIf(queued -> queued == obj); + } + private void deleteNativeObject(Object rendererObject, NativeObject obj, NativeObjectRef ref, boolean deleteGL, boolean deleteBufs) { assert rendererObject != null; @@ -187,6 +201,11 @@ public void deleteUnused(Object rendererObject){ if (ref == null) { break; } + // Explicit deletion may have unregistered an already queued reference. + // Its native name can now belong to a different object. + if (refMap.get(ref.objClone.getUniqueId()) != ref) { + continue; + } deleteNativeObject(rendererObject, ref.objClone, ref, true, false); removed++; diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/FrameBufferTestSupport.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/FrameBufferTestSupport.java new file mode 100644 index 0000000000..fc214e50cb --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/FrameBufferTestSupport.java @@ -0,0 +1,306 @@ +/* + * 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.assertTrue; + +import com.jme3.renderer.RendererException; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import com.jme3.texture.Texture2D; +import com.jme3.util.NativeObjectManager; +import java.lang.ref.Reference; +import java.lang.reflect.Field; +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.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; + +/** Shared recording-GL fixture; does not require or emulate a native GPU. */ +abstract class FrameBufferTestSupport { + static final int GL_STENCIL_ATTACHMENT = 0x8D20; + RecordingGL driver; + GLRenderer renderer; + + @BeforeEach + void initializeRenderer() { + initializeRenderer(0); + } + + void initializeRenderer(int defaultFramebuffer) { + driver = new RecordingGL(defaultFramebuffer); + renderer = new GLRenderer(driver.proxy(GL2.class), + driver.proxy(GLExt.class), driver.proxy(GLFbo.class)); + renderer.initialize(); + } + + static Texture2D texture() { + return new Texture2D(8, 8, Image.Format.RGBA8); + } + + static FrameBuffer framebuffer(Texture2D... textures) { + FrameBuffer fb = new FrameBuffer(8, 8, 1); + for (Texture2D texture : textures) { + fb.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(texture)); + } + return fb; + } + + static FrameBuffer bufferedFramebuffer(int colorCount) { + FrameBuffer fb = new FrameBuffer(8, 8, 1); + for (int i = 0; i < colorCount; ++i) { + fb.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.RGBA8)); + } + fb.setDepthTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.Depth)); + return fb; + } + + Reference registeredReference(FrameBuffer framebuffer) throws ReflectiveOperationException { + Field managerField = GLRenderer.class.getDeclaredField("objManager"); + managerField.setAccessible(true); + NativeObjectManager manager = (NativeObjectManager) managerField.get(renderer); + Field mapField = NativeObjectManager.class.getDeclaredField("refMap"); + mapField.setAccessible(true); + Map map = (Map) mapField.get(manager); + return (Reference) map.get(framebuffer.getUniqueId()); + } + + static final class RecordingGL implements InvocationHandler { + final int defaultFramebuffer; + final Map> attachments = new HashMap<>(); + final Set liveRenderbuffers = new HashSet<>(); + final Set liveFramebuffers = new HashSet<>(); + final Set liveTextures = new HashSet<>(); + final List deletedRenderbuffers = new ArrayList<>(); + final List renderbufferStorageCalls = new ArrayList<>(); + final Map renderbufferFormats = new HashMap<>(); + final Map> drawBuffers = new HashMap<>(); + final List mipmapGenerations = new ArrayList<>(); + final Map boundTextures = new HashMap<>(); + int nextObjectId = 100; + int reuseRenderbufferId = -1; + int reuseFramebufferId = -1; + int boundRenderbuffer; + int drawFramebuffer; + int readFramebuffer; + int blitDrawFramebuffer = -1; + int blitReadFramebuffer = -1; + int activeTextureUnit; + int framebufferStatus = GLFbo.GL_FRAMEBUFFER_COMPLETE_EXT; + boolean failNextStorage; + + RecordingGL(int defaultFramebuffer) { + this.defaultFramebuffer = defaultFramebuffer; + drawFramebuffer = defaultFramebuffer; + readFramebuffer = defaultFramebuffer; + } + + T proxy(Class api) { + return api.cast(Proxy.newProxyInstance(api.getClassLoader(), new Class[] {api}, this)); + } + + int attachment(FrameBuffer fb, int colorSlot) { + return attachmentAt(fb, GLFbo.GL_COLOR_ATTACHMENT0_EXT + colorSlot); + } + + int attachmentAt(FrameBuffer fb, int glSlot) { + return attachments.get(fb.getId()).getOrDefault(glSlot, 0); + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + switch (method.getName()) { + case "glGetString": + return getString((Integer) args[0]); + case "glGetInteger": + ((IntBuffer) args[1]).put(0, getInteger((Integer) args[0])); + return null; + case "glGenTextures": + int texture = nextObjectId++; + liveTextures.add(texture); + ((IntBuffer) args[0]).put(0, texture); + return null; + case "glDeleteTextures": + liveTextures.remove(((IntBuffer) args[0]).get(0)); + return null; + case "glActiveTexture": + activeTextureUnit = (Integer) args[0] - GL.GL_TEXTURE0; + return null; + case "glBindTexture": + int textureName = (Integer) args[1]; + assertTrue(textureName >= 0, "Cannot bind an unallocated image name"); + boundTextures.put(activeTextureUnit, textureName); + return null; + case "glGenerateMipmapEXT": + int generated = boundTextures.getOrDefault(activeTextureUnit, 0); + assertTrue(liveTextures.contains(generated), "Mipmap generation needs allocated storage"); + mipmapGenerations.add(generated); + return null; + case "glGenFramebuffersEXT": + int framebuffer = reuseFramebufferId >= 0 ? reuseFramebufferId : nextObjectId++; + reuseFramebufferId = -1; + liveFramebuffers.add(framebuffer); + attachments.put(framebuffer, new HashMap<>()); + ((IntBuffer) args[0]).put(0, framebuffer); + return null; + case "glGenRenderbuffersEXT": + int renderbuffer = reuseRenderbufferId >= 0 ? reuseRenderbufferId : nextObjectId++; + reuseRenderbufferId = -1; + liveRenderbuffers.add(renderbuffer); + ((IntBuffer) args[0]).put(0, renderbuffer); + return null; + case "glBindRenderbufferEXT": + boundRenderbuffer = (Integer) args[1]; + return null; + case "glRenderbufferStorageEXT": + case "glRenderbufferStorageMultisampleEXT": + assertTrue(liveRenderbuffers.contains(boundRenderbuffer), + "Storage requires a live, bound renderbuffer"); + if (failNextStorage) { + failNextStorage = false; + throw new RendererException("Simulated storage allocation failure"); + } + renderbufferStorageCalls.add(boundRenderbuffer); + int formatIndex = method.getName().equals("glRenderbufferStorageEXT") ? 1 : 2; + renderbufferFormats.put(boundRenderbuffer, (Integer) args[formatIndex]); + return null; + case "glDeleteRenderbuffersEXT": + int deleted = ((IntBuffer) args[0]).get(0); + assertTrue(liveRenderbuffers.remove(deleted), + "Deleting a stale renderbuffer name " + deleted); + deletedRenderbuffers.add(deleted); + if (boundRenderbuffer == deleted) { + boundRenderbuffer = 0; + } + return null; + case "glDeleteFramebuffersEXT": + int deletedFramebuffer = ((IntBuffer) args[0]).get(0); + assertTrue(liveFramebuffers.remove(deletedFramebuffer), + "Deleting a stale framebuffer name"); + attachments.remove(deletedFramebuffer); + return null; + case "glBindFramebufferEXT": + int target = (Integer) args[0]; + int id = (Integer) args[1]; + if (target == GLFbo.GL_FRAMEBUFFER_EXT || target == GLFbo.GL_DRAW_FRAMEBUFFER_EXT) { + drawFramebuffer = id; + } + if (target == GLFbo.GL_FRAMEBUFFER_EXT || target == GLFbo.GL_READ_FRAMEBUFFER_EXT) { + readFramebuffer = id; + } + return null; + case "glFramebufferTexture2DEXT": + case "glFramebufferRenderbufferEXT": + int slot = (Integer) args[1]; + int attachment = (Integer) args[3]; + if (slot == GL3.GL_DEPTH_STENCIL_ATTACHMENT) { + attachments.get(drawFramebuffer).put(GLFbo.GL_DEPTH_ATTACHMENT_EXT, attachment); + attachments.get(drawFramebuffer).put(GL_STENCIL_ATTACHMENT, attachment); + } else { + attachments.get(drawFramebuffer).put(slot, attachment); + } + return null; + case "glDrawBuffer": + drawBuffers.put(drawFramebuffer, java.util.Collections.singletonList((Integer) args[0])); + return null; + case "glDrawBuffers": + List targets = new ArrayList<>(); + IntBuffer source = ((IntBuffer) args[0]).duplicate(); + while (source.hasRemaining()) { + targets.add(source.get()); + } + drawBuffers.put(drawFramebuffer, targets); + return null; + case "glCheckFramebufferStatusEXT": + return framebufferStatus; + case "glBlitFramebufferEXT": + blitDrawFramebuffer = drawFramebuffer; + blitReadFramebuffer = readFramebuffer; + return null; + default: + if (method.getReturnType() == void.class) { + return null; + } + if (method.getReturnType() == boolean.class) { + return false; + } + throw new AssertionError("Unexpected GL query: " + method.getName()); + } + } + + private static String getString(int name) { + switch (name) { + case GL.GL_VERSION: + return "2.1"; + case GL.GL_SHADING_LANGUAGE_VERSION: + return "1.20"; + case GL.GL_EXTENSIONS: + return "GL_EXT_framebuffer_object GL_EXT_framebuffer_blit GL_EXT_packed_depth_stencil " + + "GL_EXT_texture_sRGB GL_ARB_framebuffer_sRGB"; + case GL.GL_VENDOR: + case GL.GL_RENDERER: + return "Recording GL"; + default: + throw new AssertionError("Unexpected GL string: " + name); + } + } + + private int getInteger(int name) { + switch (name) { + case GL.GL_FRAMEBUFFER_BINDING: + return defaultFramebuffer; + case GL.GL_MAX_TEXTURE_SIZE: + case GL.GL_MAX_CUBE_MAP_TEXTURE_SIZE: + case GLFbo.GL_MAX_RENDERBUFFER_SIZE_EXT: + return 1024; + case GL.GL_MAX_TEXTURE_IMAGE_UNITS: + case GL.GL_MAX_VERTEX_ATTRIBS: + return 16; + case GLFbo.GL_MAX_COLOR_ATTACHMENTS_EXT: + case GLExt.GL_MAX_DRAW_BUFFERS_ARB: + return 4; + case GL2.GL_DRAW_BUFFER: + case GL2.GL_READ_BUFFER: + return GL.GL_BACK; + default: + return 0; + } + } + } +} diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererDefaultFrameBufferTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererDefaultFrameBufferTest.java new file mode 100644 index 0000000000..7956a67b82 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererDefaultFrameBufferTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.renderer.opengl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.jme3.texture.FrameBuffer; +import org.junit.jupiter.api.Test; + +/** Tests the platform presentation framebuffer when its name is nonzero. */ +public class GlRendererDefaultFrameBufferTest extends FrameBufferTestSupport { + @Test + void copyingToScreenUsesDiscoveredFramebuffer() { + initializeRenderer(77); + FrameBuffer fb = framebuffer(texture()); + renderer.setFrameBuffer(fb); + renderer.setFrameBuffer(null); + assertEquals(77, driver.drawFramebuffer); + renderer.copyFrameBuffer(fb, null, true, false); + assertEquals(77, driver.blitDrawFramebuffer); + } + + @Test + void copyingFromScreenUsesDiscoveredFramebuffer() { + initializeRenderer(77); + FrameBuffer fb = framebuffer(texture()); + renderer.setFrameBuffer(fb); + renderer.setFrameBuffer(null); + assertEquals(77, driver.readFramebuffer); + renderer.copyFrameBuffer(null, fb, true, false); + assertEquals(77, driver.blitReadFramebuffer); + } + + @Test + void zeroDefaultCopyRestoresPreviousFramebuffer() { + FrameBuffer fb = framebuffer(texture()); + renderer.setFrameBuffer(fb); + renderer.copyFrameBuffer(fb, null, true, false); + assertEquals(0, driver.blitDrawFramebuffer); + assertEquals(fb.getId(), driver.drawFramebuffer); + assertEquals(fb.getId(), driver.readFramebuffer); + } + + @Test + void copyImmediatelyAfterInitializationRestoresDiscoveredFramebuffer() { + initializeRenderer(77); + FrameBuffer fb = framebuffer(texture()); + renderer.copyFrameBuffer(fb, null, true, false); + assertEquals(77, driver.blitDrawFramebuffer); + assertEquals(77, driver.drawFramebuffer); + assertEquals(77, driver.readFramebuffer); + assertNull(renderer.getCurrentFrameBuffer()); + } + + @Test + void deletingBoundFramebufferRestoresScreenAndClearsObjectCache() { + initializeRenderer(77); + FrameBuffer fb = bufferedFramebuffer(1); + renderer.setFrameBuffer(fb); + renderer.deleteFrameBuffer(fb); + assertEquals(77, driver.drawFramebuffer); + assertEquals(77, driver.readFramebuffer); + assertNull(renderer.getCurrentFrameBuffer()); + renderer.setFrameBuffer(null); + assertEquals(77, driver.drawFramebuffer); + } + + @Test + void mainOverrideIsBoundWhenDiscoveredDefaultIsCurrent() { + initializeRenderer(77); + FrameBuffer override = framebuffer(texture()); + renderer.setMainFrameBufferOverride(override); + assertEquals(override.getId(), driver.drawFramebuffer); + renderer.copyFrameBuffer(null, null, true, false); + assertEquals(override.getId(), driver.blitReadFramebuffer); + assertEquals(override.getId(), driver.blitDrawFramebuffer); + } +} diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferAttachmentTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferAttachmentTest.java new file mode 100644 index 0000000000..b19ff5124e --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferAttachmentTest.java @@ -0,0 +1,225 @@ +/* + * 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.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import com.jme3.texture.Texture; +import com.jme3.texture.Texture2D; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +/** Tests updates to already initialized framebuffer attachments. */ +public class GlRendererFrameBufferAttachmentTest extends FrameBufferTestSupport { + @Test + void initialAttachmentAndExplicitDirtyReplacement() { + Texture2D first = texture(); + Texture2D replacement = texture(); + FrameBuffer fb = framebuffer(first); + renderer.setFrameBuffer(fb); + assertEquals(first.getImage().getId(), driver.attachment(fb, 0)); + fb.replaceColorTarget(0, FrameBuffer.FrameBufferTarget.newTarget(replacement)); + fb.setUpdateNeeded(); + renderer.setFrameBuffer(fb); + assertEquals(replacement.getImage().getId(), driver.attachment(fb, 0)); + assertNotEquals(first.getImage().getId(), replacement.getImage().getId()); + } + + @Test + void replacementWhileBoundUpdatesNativeAttachment() throws Exception { + Texture2D replacement = texture(); + renderer.setTexture(0, replacement); + FrameBuffer fb = framebuffer(texture()); + renderer.setFrameBuffer(fb); + fb.replaceColorTarget(0, FrameBuffer.FrameBufferTarget.newTarget(replacement)); + renderer.setFrameBuffer(fb); + assertEquals(replacement.getImage().getId(), driver.attachment(fb, 0)); + } + + @Test + void replacementAfterSwitchingAwayUpdatesNativeAttachment() throws Exception { + Texture2D replacement = texture(); + renderer.setTexture(0, replacement); + FrameBuffer fb = framebuffer(texture()); + renderer.setFrameBuffer(fb); + renderer.setFrameBuffer(null); + fb.replaceColorTarget(0, FrameBuffer.FrameBufferTarget.newTarget(replacement)); + renderer.setFrameBuffer(fb); + assertEquals(replacement.getImage().getId(), driver.attachment(fb, 0)); + } + + @Test + void removedTextureIsDetached() { + FrameBuffer fb = framebuffer(texture(), texture()); + renderer.setFrameBuffer(fb); + fb.removeColorTarget(1); + renderer.setFrameBuffer(fb); + assertEquals(0, driver.attachment(fb, 1)); + } + + @Test + void removalReattachesShiftedRenderbufferWithoutReallocatingIt() { + FrameBuffer fb = bufferedFramebuffer(2); + renderer.setFrameBuffer(fb); + FrameBuffer.RenderBuffer removed = fb.getColorTarget(0); + final int survivor = fb.getColorTarget(1).getId(); + final int allocations = driver.renderbufferStorageCalls.size(); + fb.removeColorTarget(0); + renderer.setFrameBuffer(fb); + assertEquals(-1, removed.getId()); + assertEquals(survivor, driver.attachment(fb, 0)); + assertEquals(0, driver.attachment(fb, 1)); + assertEquals(allocations, driver.renderbufferStorageCalls.size()); + } + + @Test + void addingColorTargetAllocatesOnlyNewStorage() { + FrameBuffer fb = bufferedFramebuffer(1); + renderer.setFrameBuffer(fb); + fb.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.RGBA8)); + renderer.setFrameBuffer(fb); + assertEquals(3, driver.renderbufferStorageCalls.size()); + assertEquals(fb.getColorTarget(1).getId(), driver.attachment(fb, 1)); + } + + @Test + void clearingColorsLeavesDepthOnlyFramebuffer() { + FrameBuffer fb = bufferedFramebuffer(2); + renderer.setFrameBuffer(fb); + fb.clearColorTargets(); + renderer.setFrameBuffer(fb); + assertEquals(0, driver.attachment(fb, 0)); + assertEquals(0, driver.attachment(fb, 1)); + assertEquals(1, driver.liveRenderbuffers.size()); + assertEquals(Arrays.asList(GL.GL_NONE), driver.drawBuffers.get(fb.getId())); + } + + @Test + void removingSelectedLastTargetKeepsSelectionValid() { + FrameBuffer fb = framebuffer(texture(), texture()); + fb.setTargetIndex(1); + renderer.setFrameBuffer(fb); + fb.removeColorTarget(1); + renderer.setFrameBuffer(fb); + assertEquals(0, fb.getTargetIndex()); + assertEquals(Arrays.asList(GLFbo.GL_COLOR_ATTACHMENT0_EXT), driver.drawBuffers.get(fb.getId())); + assertThrows(IllegalArgumentException.class, () -> fb.setTargetIndex(1)); + } + + @Test + void removingEarlierTargetKeepsSelectedSurvivor() { + FrameBuffer fb = framebuffer(texture(), texture(), texture()); + fb.setTargetIndex(2); + renderer.setFrameBuffer(fb); + final int selected = driver.attachment(fb, 2); + fb.removeColorTarget(0); + renderer.setFrameBuffer(fb); + assertEquals(1, fb.getTargetIndex()); + assertEquals(selected, driver.attachment(fb, 1)); + assertEquals(0, driver.attachment(fb, 2)); + } + + @Test + void mrtChangesUpdateDrawBuffersWithoutDiscardingStorage() { + FrameBuffer fb = bufferedFramebuffer(2); + renderer.setFrameBuffer(fb); + final int allocations = driver.renderbufferStorageCalls.size(); + fb.setMultiTarget(true); + renderer.setFrameBuffer(fb); + assertEquals(Arrays.asList(GLFbo.GL_COLOR_ATTACHMENT0_EXT, GLFbo.GL_COLOR_ATTACHMENT0_EXT + 1), + driver.drawBuffers.get(fb.getId())); + fb.setMultiTarget(false); + renderer.setFrameBuffer(fb); + assertEquals(Arrays.asList(GLFbo.GL_COLOR_ATTACHMENT0_EXT), driver.drawBuffers.get(fb.getId())); + assertEquals(allocations, driver.renderbufferStorageCalls.size()); + } + + @Test + void replacingPackedDepthStencilDetachesOldStencil() { + FrameBuffer fb = framebuffer(texture()); + fb.setDepthTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.Depth24Stencil8)); + renderer.setFrameBuffer(fb); + int oldDepth = fb.getDepthTarget().getId(); + assertEquals(oldDepth, driver.attachmentAt(fb, GL_STENCIL_ATTACHMENT)); + fb.setDepthTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.Depth)); + renderer.setFrameBuffer(fb); + assertFalse(driver.liveRenderbuffers.contains(oldDepth)); + assertEquals(0, driver.attachmentAt(fb, GL_STENCIL_ATTACHMENT)); + assertEquals(fb.getDepthTarget().getId(), driver.attachmentAt(fb, GLFbo.GL_DEPTH_ATTACHMENT_EXT)); + } + + @Test + void replacingDepthBufferWithTextureUpdatesAttachment() { + FrameBuffer fb = bufferedFramebuffer(1); + renderer.setFrameBuffer(fb); + Texture2D depth = new Texture2D(8, 8, Image.Format.Depth); + fb.setDepthTarget(FrameBuffer.FrameBufferTarget.newTarget(depth)); + renderer.setFrameBuffer(fb); + assertEquals(depth.getImage().getId(), driver.attachmentAt(fb, GLFbo.GL_DEPTH_ATTACHMENT_EXT)); + assertEquals(1, driver.liveRenderbuffers.size()); + } + + @Test + void mipmappedReplacementGeneratesOldRenderedTextureBeforeAllocatingNewOne() { + Texture2D old = texture(); + old.setMinFilter(Texture.MinFilter.Trilinear); + Texture2D replacement = texture(); + replacement.setMinFilter(Texture.MinFilter.Trilinear); + FrameBuffer fb = framebuffer(old); + renderer.setFrameBuffer(fb); + fb.replaceColorTarget(0, FrameBuffer.FrameBufferTarget.newTarget(replacement)); + renderer.setFrameBuffer(fb); + assertEquals(Arrays.asList(old.getImage().getId()), driver.mipmapGenerations); + assertEquals(replacement.getImage().getId(), driver.attachment(fb, 0)); + renderer.setFrameBuffer(null); + assertEquals(Arrays.asList(old.getImage().getId(), replacement.getImage().getId()), + driver.mipmapGenerations); + } + + @Test + void explicitSrgbStorageChangeStillReallocatesInternalFormat() { + FrameBuffer fb = bufferedFramebuffer(1); + renderer.setFrameBuffer(fb); + int id = fb.getColorTarget(0).getId(); + final int oldFormat = driver.renderbufferFormats.get(id); + fb.setSrgb(true); + fb.setUpdateNeeded(); + renderer.setFrameBuffer(fb); + assertNotEquals(oldFormat, driver.renderbufferFormats.get(id)); + assertEquals(GLExt.GL_SRGB8_ALPHA8_EXT, driver.renderbufferFormats.get(id)); + } +} diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferLifecycleTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferLifecycleTest.java new file mode 100644 index 0000000000..b3c0ad00a5 --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferLifecycleTest.java @@ -0,0 +1,225 @@ +/* + * 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.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.jme3.renderer.RendererException; +import com.jme3.texture.FrameBuffer; +import com.jme3.texture.Image; +import java.lang.ref.Reference; +import org.junit.jupiter.api.Test; + +/** Tests ownership, deletion, native-name reuse, and context reset. */ +public class GlRendererFrameBufferLifecycleTest extends FrameBufferTestSupport { + @Test + void disposalDeletesEveryColorAndDepthRenderbuffer() { + FrameBuffer fb = bufferedFramebuffer(2); + renderer.setFrameBuffer(fb); + assertEquals(3, driver.liveRenderbuffers.size()); + fb.dispose(); + renderer.postFrame(); + assertTrue(driver.liveRenderbuffers.isEmpty()); + assertTrue(driver.liveFramebuffers.isEmpty()); + } + + @Test + void managedCleanupDeletesOwnedRenderbuffersFromDestructibleClone() { + FrameBuffer fb = bufferedFramebuffer(1); + renderer.setFrameBuffer(fb); + assertEquals(2, driver.liveRenderbuffers.size()); + renderer.cleanup(); + assertTrue(driver.liveFramebuffers.isEmpty()); + assertTrue(driver.liveRenderbuffers.isEmpty()); + } + + @Test + void singleTargetDisposalDeletesBothBuffers() { + FrameBuffer fb = bufferedFramebuffer(1); + renderer.setFrameBuffer(fb); + fb.dispose(); + renderer.postFrame(); + assertEquals(2, driver.deletedRenderbuffers.size()); + } + + @Test + void disposalBeforeRebindStillDeletesRemovedBuffers() { + FrameBuffer fb = bufferedFramebuffer(2); + renderer.setFrameBuffer(fb); + fb.removeColorTarget(0); + fb.setDepthTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.Depth)); + fb.dispose(); + renderer.postFrame(); + assertTrue(driver.liveRenderbuffers.isEmpty()); + assertEquals(3, driver.deletedRenderbuffers.size()); + } + + @Test + void managedCleanupIncludesRenderbuffersAddedAfterRegistration() { + FrameBuffer fb = bufferedFramebuffer(1); + renderer.setFrameBuffer(fb); + fb.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.RGBA8)); + renderer.setFrameBuffer(fb); + renderer.cleanup(); + assertTrue(driver.liveRenderbuffers.isEmpty()); + assertEquals(3, driver.deletedRenderbuffers.size()); + } + + @Test + void removedBufferCanBeAddedAgainWithFreshNativeStorage() { + FrameBuffer fb = bufferedFramebuffer(1); + FrameBuffer.FrameBufferBufferTarget color + = (FrameBuffer.FrameBufferBufferTarget) fb.getColorTarget(0); + renderer.setFrameBuffer(fb); + final int original = color.getId(); + fb.clearColorTargets(); + renderer.setFrameBuffer(fb); + assertEquals(-1, color.getId()); + fb.addColorTarget(color); + renderer.setFrameBuffer(fb); + assertNotEquals(original, color.getId()); + assertEquals(color.getId(), driver.attachment(fb, 0)); + assertEquals(3, driver.renderbufferStorageCalls.size()); + } + + @Test + void resetClearsRemovedWrappersAndDoesNotDeleteOldContextNames() { + FrameBuffer fb = bufferedFramebuffer(2); + renderer.setFrameBuffer(fb); + FrameBuffer.RenderBuffer removed = fb.getColorTarget(0); + fb.removeColorTarget(0); + renderer.resetGLObjects(); + assertEquals(-1, removed.getId()); + assertEquals(-1, fb.getId()); + // The old driver context and all of its names no longer exist. + driver.liveRenderbuffers.clear(); + driver.liveFramebuffers.clear(); + driver.attachments.clear(); + driver.boundRenderbuffer = 0; + renderer.setFrameBuffer(fb); + fb.dispose(); + renderer.postFrame(); + assertEquals(2, driver.deletedRenderbuffers.size()); + assertTrue(driver.liveRenderbuffers.isEmpty()); + } + + @Test + void deletingBoundRenderbufferInvalidatesCacheBeforeNameReuse() { + FrameBuffer first = bufferedFramebuffer(1); + renderer.setFrameBuffer(first); + final int reused = driver.boundRenderbuffer; + first.dispose(); + renderer.postFrame(); + assertEquals(0, driver.boundRenderbuffer); + FrameBuffer second = new FrameBuffer(8, 8, 1); + second.addColorTarget(FrameBuffer.FrameBufferTarget.newTarget(Image.Format.RGBA8)); + driver.reuseRenderbufferId = reused; + renderer.setFrameBuffer(second); + assertEquals(reused, driver.boundRenderbuffer); + assertEquals(reused, second.getColorTarget(0).getId()); + } + + @Test + void directDeletionUnregistersOldCloneBeforeFramebufferNameReuse() throws Exception { + FrameBuffer first = bufferedFramebuffer(1); + renderer.setFrameBuffer(first); + final int oldName = first.getId(); + Reference staleReference = registeredReference(first); + // Queue the old reference before direct deletion to exercise the queue guard. + assertTrue(staleReference.enqueue()); + first.dispose(); + renderer.deleteFrameBuffer(first); + driver.reuseFramebufferId = oldName; + FrameBuffer second = bufferedFramebuffer(2); + renderer.setFrameBuffer(second); + renderer.postFrame(); + assertEquals(oldName, second.getId()); + assertEquals(3, driver.liveRenderbuffers.size()); + renderer.cleanup(); + assertTrue(driver.liveRenderbuffers.isEmpty()); + assertTrue(driver.liveFramebuffers.isEmpty()); + } + + @Test + void simulatedGcQueueDeletesAllOwnedBuffers() throws Exception { + FrameBuffer fb = bufferedFramebuffer(2); + renderer.setFrameBuffer(fb); + renderer.setFrameBuffer(null); + assertTrue(registeredReference(fb).enqueue()); + renderer.postFrame(); + assertTrue(driver.liveRenderbuffers.isEmpty()); + assertTrue(driver.liveFramebuffers.isEmpty()); + } + + @Test + void storageAllocationFailureKeepsNameTrackedForCleanup() { + FrameBuffer fb = bufferedFramebuffer(1); + driver.failNextStorage = true; + assertThrows(RendererException.class, () -> renderer.setFrameBuffer(fb)); + assertEquals(1, driver.liveRenderbuffers.size()); + renderer.cleanup(); + assertTrue(driver.liveRenderbuffers.isEmpty()); + assertTrue(driver.liveFramebuffers.isEmpty()); + } + + @Test + void storageAllocationFailureCanRetryExistingName() { + FrameBuffer fb = bufferedFramebuffer(1); + driver.failNextStorage = true; + assertThrows(RendererException.class, () -> renderer.setFrameBuffer(fb)); + renderer.setFrameBuffer(fb); + assertEquals(2, driver.renderbufferStorageCalls.size()); + assertEquals(2, driver.liveRenderbuffers.size()); + } + + @Test + void incompleteFramebufferFailureStillCleansEveryAllocation() { + FrameBuffer fb = bufferedFramebuffer(2); + driver.framebufferStatus = GLFbo.GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT; + assertThrows(IllegalStateException.class, () -> renderer.setFrameBuffer(fb)); + renderer.cleanup(); + assertTrue(driver.liveRenderbuffers.isEmpty()); + assertTrue(driver.liveFramebuffers.isEmpty()); + } + + @Test + void textureAttachmentsAreNotDeletedAsRenderbufferNames() { + FrameBuffer fb = framebuffer(texture()); + renderer.setFrameBuffer(fb); + renderer.deleteFrameBuffer(fb); + assertTrue(driver.deletedRenderbuffers.isEmpty()); + assertEquals(1, driver.liveTextures.size()); + } +} From ca0de8963afae238f89c56e0bf21c8cde71e2e6e Mon Sep 17 00:00:00 2001 From: Mr Robot Date: Sat, 3 Oct 2026 00:02:50 +0200 Subject: [PATCH 2/2] Remove redundant framebuffer disposal test --- .../opengl/GlRendererFrameBufferLifecycleTest.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferLifecycleTest.java b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferLifecycleTest.java index b3c0ad00a5..b61d3a534a 100644 --- a/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferLifecycleTest.java +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferLifecycleTest.java @@ -65,15 +65,6 @@ void managedCleanupDeletesOwnedRenderbuffersFromDestructibleClone() { assertTrue(driver.liveRenderbuffers.isEmpty()); } - @Test - void singleTargetDisposalDeletesBothBuffers() { - FrameBuffer fb = bufferedFramebuffer(1); - renderer.setFrameBuffer(fb); - fb.dispose(); - renderer.postFrame(); - assertEquals(2, driver.deletedRenderbuffers.size()); - } - @Test void disposalBeforeRebindStillDeletesRemovedBuffers() { FrameBuffer fb = bufferedFramebuffer(2);