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..b61d3a534a --- /dev/null +++ b/jme3-core/src/test/java/com/jme3/renderer/opengl/GlRendererFrameBufferLifecycleTest.java @@ -0,0 +1,216 @@ +/* + * 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 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()); + } +}