diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLContextState.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLContextState.java index 738c8f0e3..cd5f4391e 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLContextState.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLContextState.java @@ -3,6 +3,7 @@ package com.gtnewhorizons.angelica.glsm; import com.gtnewhorizon.gtnhlib.client.renderer.stacks.IStateStack; import it.unimi.dsi.fastutil.ints.Int2IntMap; import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import com.gtnewhorizons.angelica.glsm.stacks.AlphaStateStack; import com.gtnewhorizons.angelica.glsm.stacks.BlendStateStack; import com.gtnewhorizons.angelica.glsm.stacks.BooleanStateStack; @@ -257,6 +258,7 @@ public class GLContextState { public boolean vanillaBlendEnabledBefore; public int drawFramebuffer = 0; public int readFramebuffer = 0; + public final Int2ObjectOpenHashMap framebufferTextures = new Int2ObjectOpenHashMap<>(); public int texGenGeneration; public int defaultVAO; public final VAOManager vaos = new VAOManager(); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java index 55ffad535..ba80b5113 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/GLStateManager.java @@ -75,8 +75,14 @@ import com.gtnewhorizons.angelica.glsm.streaming.TessellatorStreamingDrawer; import com.gtnewhorizons.angelica.glsm.texture.TextureInfo; import com.gtnewhorizons.angelica.glsm.texture.TextureInfoCache; import com.gtnewhorizons.angelica.glsm.texture.TextureStaging; +import com.gtnewhorizons.angelica.glsm.texture.TextureTargets; import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2LongMap; +import it.unimi.dsi.fastutil.ints.Int2LongMaps; +import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongIterator; import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; @@ -106,6 +112,7 @@ import org.lwjgl.opengl.GL30; import org.lwjgl.opengl.GL31; import org.lwjgl.opengl.GL32; import org.lwjgl.opengl.GL33; +import org.lwjgl.opengl.GL40; import org.lwjgl.opengl.GL42; import org.lwjgl.opengl.GL43; import org.lwjgl.opengl.GLContext; @@ -430,8 +437,24 @@ public class GLStateManager { } // Deferred texture deletion: names kept valid until glGenTextures recycles them. // Emulates Mesa/compat-profile behavior where delete-then-bind doesn't error. - private static final IntOpenHashSet deferredDeleteTextures = new IntOpenHashSet(); - private static final IntOpenHashSet unshrinkableTextures = new IntOpenHashSet(); + private static final Int2LongOpenHashMap deferredDeleteTextures = new Int2LongOpenHashMap(); + private static final long DEFERRED_ABSENT = Long.MIN_VALUE; + private static final long DEFERRED_ATTACHED = -1; + private static final long DEFERRED_RELEASED = -2; + private static final int RELEASE_DELAY_FRAMES = 3; + private static long frameIndex; + private static long nextReleaseFrame = Long.MAX_VALUE; + private static final Int2IntOpenHashMap non2DTextureTargets = new Int2IntOpenHashMap(); + private static final Int2IntOpenHashMap textureFramebufferRefs = new Int2IntOpenHashMap(); + private static final FloatBuffer TRANSPARENT_BLACK = BufferUtils.createFloatBuffer(4); + private static final Tracy.ZoneId Z_TEX_DELETE = Tracy.zoneId("texDelete", Tracy.COLOR_FFP); + private static final Tracy.ZoneId Z_TEX_RELEASE_STORAGE = Tracy.zoneId("texReleaseStorage", Tracy.COLOR_FFP); + private static final Tracy.ZoneId Z_TEX_RESCUE_RESET = Tracy.zoneId("texRescueReset", Tracy.COLOR_FFP); + private static final Tracy.ZoneId Z_TEX_FLUSH_DELETES = Tracy.zoneId("texFlushDeletes", Tracy.COLOR_FFP); + static { + deferredDeleteTextures.defaultReturnValue(DEFERRED_ABSENT); + non2DTextureTargets.defaultReturnValue(GL11.GL_TEXTURE_2D); + } private static final IntOpenHashSet writeMappedBuffers = new IntOpenHashSet(); @Getter private static Vendor VENDOR; @@ -2376,18 +2399,28 @@ public class GLStateManager { } } // Rebinding a deferred-delete texture rescues it (Mesa/compat-profile behavior) + long rescued = DEFERRED_ABSENT; + boolean rejected = false; if (texture != 0) { final boolean locked = acquireDrawLock(); try { - deferredDeleteTextures.remove(texture); - if (target != GL11.GL_TEXTURE_2D) unshrinkableTextures.add(texture); + final long state = deferredDeleteTextures.isEmpty() ? DEFERRED_ABSENT : deferredDeleteTextures.get(texture); + if (state == DEFERRED_ABSENT) { + recordTarget(texture, target); + } else if (non2DTextureTargets.get(texture) == target) { + rescued = deferredDeleteTextures.remove(texture); + } else { + rejected = true; + } } finally { if (locked) releaseDrawLock(); } } + if (rejected) return; if (!isCachingEnabled()) { RENDER_BACKEND.bindTexture(target, texture); + if (rescued != DEFERRED_ABSENT) resetRescuedTexture(target, texture, rescued); return; } @@ -2413,6 +2446,54 @@ public class GLStateManager { glCtx.lockBindCallback = false; } } + if (rescued != DEFERRED_ABSENT) resetRescuedTexture(target, texture, rescued); + } + + private static void bindTextureUnitByName(int unit, int texture) { + if (isDeferredDelete(texture)) return; + final CommandRecorder paused = DisplayListManager.pauseRecording(); + final int saved = getActiveTextureUnit(); + try { + glActiveTexture(GL13.GL_TEXTURE0 + unit); + if (texture == 0) { + // Read before the 2D unbind, which overwrites the unit's cached target. + final int target = ctx().textures.getTextureUnitBindings(unit).getTarget(); + glBindTexture(GL11.GL_TEXTURE_2D, 0); + if (!is2D(target)) glBindTexture(target, 0); + } else { + glBindTexture(non2DTextureTargets.get(texture), texture); + } + } finally { + glActiveTexture(GL13.GL_TEXTURE0 + saved); + if (paused != null) DisplayListManager.resumeRecording(paused); + } + } + + public static void glBindTextures(int first, int count, IntBuffer textures) { + if (first < 0 || !isValidTextureUnit(first + count - 1, GL13.GL_TEXTURE0 + first + count - 1, "glBindTextures")) return; + if (textures != null) requireRemaining(textures, count); + for (int i = 0; i < count; i++) bindTextureUnitByName(first + i, textures == null ? 0 : textures.get(textures.position() + i)); + } + + public static void glBindTextures(int first, IntBuffer textures) { + if (textures != null) glBindTextures(first, textures.remaining(), textures); + } + + public static void glBindTextures(int first, int[] textures) { + if (textures != null) glBindTextures(first, textures.length, IntBuffer.wrap(textures)); + } + + public static void glBindTextureUnit(int unit, int texture) { + if (isValidTextureUnit(unit, GL13.GL_TEXTURE0 + unit, "glBindTextureUnit")) bindTextureUnitByName(unit, texture); + } + + public static void glBindMultiTextureEXT(int texunit, int target, int texture) { + if (DisplayListManager.isRecording()) throw DisplayListManager.unsupportedInList("glBindMultiTextureEXT"); + if (!isValidTextureUnit(texunit - GL13.GL_TEXTURE0, texunit, "glBindMultiTextureEXT")) return; + final int saved = getActiveTextureUnit(); + glActiveTexture(texunit); + glBindTexture(target, texture); + glActiveTexture(GL13.GL_TEXTURE0 + saved); } private static int changeFormatIfDeprecated(int internalformat) { @@ -2620,15 +2701,26 @@ public class GLStateManager { } public static void glDeleteTextures(int id) { - deferDeleteTexture(id); - onDeleteTexture(id); + deleteTexture(id); } public static void glDeleteTextures(IntBuffer ids) { for (int i = 0; i < ids.remaining(); i++) { - final int id = ids.get(ids.position() + i); + deleteTexture(ids.get(ids.position() + i)); + } + } + + public static void glDeleteTextures(int[] ids) { + for (int id : ids) deleteTexture(id); + } + + private static void deleteTexture(int id) { + if (Tracy.ENABLED) Tracy.beginZone(Z_TEX_DELETE); + try { deferDeleteTexture(id); onDeleteTexture(id); + } finally { + if (Tracy.ENABLED) Tracy.endZone(); } } @@ -2637,26 +2729,21 @@ public class GLStateManager { * Why you might ask? In compat profile if you bind a texture that doesn't exist, ie a deleted texture, * mesa will silently create it for you, in core this is an error. Why would you be binding a deleted texture? * Damned if I know, but vanilla and mods do it... they create one, delete it, and then bind it.... - * - * So... we shrink the texture to 1x1 to free GPU memory, unbind from all units, but keep the name valid. * */ private static void deferDeleteTexture(int id) { final GLContextState glCtx = ctx(); if (id == 0) return; final int savedUnit = glCtx.activeTextureUnit.getValue(); - final TextureBindingStack savedUnitBindings = glCtx.textures.getTextureUnitBindings(savedUnit); - final int savedBinding = savedUnitBindings.getBinding(); - final int savedTarget = savedUnitBindings.getTarget(); - boolean changedUnit = false; + int activeUnit = savedUnit; // Unbind from all units that have this texture for (int i = 0; i <= glCtx.maxBoundTextureUnit; i++) { final TextureBindingStack unitBindings = glCtx.textures.getTextureUnitBindings(i); if (unitBindings.getBinding() == id) { - if (i != savedUnit) { + if (i != activeUnit) { RENDER_BACKEND.activeTexture(GL13.GL_TEXTURE0 + i); - changedUnit = true; + activeUnit = i; } final int unitTarget = unitBindings.getTarget(); RENDER_BACKEND.bindTexture(unitTarget == 0 ? GL11.GL_TEXTURE_2D : unitTarget, 0); @@ -2670,48 +2757,245 @@ public class GLStateManager { } } - // Restore active unit, then shrink the texture via a temporary bind - if (changedUnit) { + if (activeUnit != savedUnit) { RENDER_BACKEND.activeTexture(GL13.GL_TEXTURE0 + savedUnit); } - final boolean shrinkable; + final TextureBindingStack savedBindings = glCtx.textures.getTextureUnitBindings(savedUnit); + if (!is2D(savedBindings.getTarget()) && previous2D(savedBindings) == id) restoreUnitAfter2DBind(savedBindings, id, id); + final boolean locked = acquireDrawLock(); try { - shrinkable = !unshrinkableTextures.contains(id); - deferredDeleteTextures.add(id); + detachFromFramebuffer(glCtx, GL30.GL_DRAW_FRAMEBUFFER, glCtx.drawFramebuffer, id); + if (glCtx.readFramebuffer != glCtx.drawFramebuffer) detachFromFramebuffer(glCtx, GL30.GL_READ_FRAMEBUFFER, glCtx.readFramebuffer, id); + if (textureFramebufferRefs.get(id) > 0) deferredDeleteTextures.put(id, DEFERRED_ATTACHED); + else scheduleRelease(id); } finally { if (locked) releaseDrawLock(); } - if (!shrinkable) return; + } - RENDER_BACKEND.bindTexture(GL11.GL_TEXTURE_2D, id); - RENDER_BACKEND.texParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0); + private static void scheduleRelease(int id) { + deferredDeleteTextures.put(id, frameIndex); + nextReleaseFrame = Math.min(nextReleaseFrame, frameIndex + RELEASE_DELAY_FRAMES); + } + + private static boolean isDeferredDelete(int texture) { + if (texture == 0 || deferredDeleteTextures.isEmpty()) return false; + final boolean locked = acquireDrawLock(); + try { + return deferredDeleteTextures.containsKey(texture); + } finally { + if (locked) releaseDrawLock(); + } + } + + public static void onFrameEnd() { + frameIndex++; + if (!splashComplete || frameIndex < nextReleaseFrame) return; + nextReleaseFrame = Long.MAX_VALUE; + for (Int2LongMap.Entry entry : Int2LongMaps.fastIterable(deferredDeleteTextures)) { + final long since = entry.getLongValue(); + if (since < 0) continue; + if (frameIndex - since < RELEASE_DELAY_FRAMES) { + nextReleaseFrame = Math.min(nextReleaseFrame, since + RELEASE_DELAY_FRAMES); + continue; + } + final int id = entry.getIntKey(); + if (non2DTextureTargets.get(id) == GL11.GL_TEXTURE_2D) releaseStorage2D(id); + entry.setValue(DEFERRED_RELEASED); + } + } + + public static int deferredTexturesAwaitingRelease() { + int count = 0; + final LongIterator it = deferredDeleteTextures.values().iterator(); + while (it.hasNext()) { + if (it.nextLong() != DEFERRED_RELEASED) count++; + } + return count; + } + + private static void releaseStorage2D(int id) { + if (Tracy.ENABLED) Tracy.beginZone(Z_TEX_RELEASE_STORAGE); + try { + final TextureBindingStack unit = ctx().textures.getTextureUnitBindings(getActiveTextureUnit()); + final int previous2D = previous2D(unit); + RENDER_BACKEND.bindTexture(GL11.GL_TEXTURE_2D, id); + if (bindTook(GL11.GL_TEXTURE_2D, id)) releaseBoundStorage2D(); + restoreUnitAfter2DBind(unit, id, previous2D); + } finally { + if (Tracy.ENABLED) Tracy.endZone(); + } + } + + private static void restoreUnitAfter2DBind(TextureBindingStack unit, int deleted, int previous2D) { + RENDER_BACKEND.bindTexture(GL11.GL_TEXTURE_2D, previous2D == deleted ? 0 : previous2D); + final int target = unit.getTarget(); + if (is2D(target)) return; + RENDER_BACKEND.bindTexture(target, unit.getBinding() == deleted ? 0 : unit.getBinding()); + } + + private static boolean is2D(int target) { + return target == 0 || target == GL11.GL_TEXTURE_2D; + } + + private static int previous2D(TextureBindingStack unit) { + return is2D(unit.getTarget()) ? unit.getBinding() : RENDER_BACKEND.getInteger(GL11.GL_TEXTURE_BINDING_2D); + } + + private static void recordTarget(int texture, int target) { + if (target != GL11.GL_TEXTURE_2D) non2DTextureTargets.put(texture, target); + } + + private static void releaseBoundStorage2D() { + if (RenderSystem.supportsTextureStorage() && RENDER_BACKEND.getTexParameteri(GL11.GL_TEXTURE_2D, GL42.GL_TEXTURE_IMMUTABLE_FORMAT) != 0) return; + final int size = Math.max(RENDER_BACKEND.getTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_WIDTH), RENDER_BACKEND.getTexLevelParameteri(GL11.GL_TEXTURE_2D, 0, GL11.GL_TEXTURE_HEIGHT)); + final int levels = Math.max(1, 32 - Integer.numberOfLeadingZeros(size)); suspendPixelUnpackBuffer(); - RENDER_BACKEND.texImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_R8, 1, 1, 0, GL11.GL_RED, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); + for (int level = 0; level < levels; level++) { + RENDER_BACKEND.texImage2D(GL11.GL_TEXTURE_2D, level, GL11.GL_RGBA8, 0, 0, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); + } restorePixelUnpackBuffer(); + } - // Restore previous binding on active unit - final boolean savedIs2D = savedTarget == GL11.GL_TEXTURE_2D || savedTarget == 0; - RENDER_BACKEND.bindTexture(GL11.GL_TEXTURE_2D, savedBinding == id || !savedIs2D ? 0 : savedBinding); + private static boolean bindTook(int target, int texture) { + return RENDER_BACKEND.getInteger(TextureTargets.bindingQuery(target)) == texture; + } + + private static void resetRescuedTexture(int target, int texture, long state) { + if (Tracy.ENABLED) Tracy.beginZone(Z_TEX_RESCUE_RESET); + try { + if (!bindTook(target, texture)) return; + if (target == GL11.GL_TEXTURE_2D && state >= 0) releaseBoundStorage2D(); + if (target == GL31.GL_TEXTURE_BUFFER || target == GL32.GL_TEXTURE_2D_MULTISAMPLE || target == GL32.GL_TEXTURE_2D_MULTISAMPLE_ARRAY) return; + final boolean rectangle = target == GL31.GL_TEXTURE_RECTANGLE; + final int wrap = rectangle ? GL12.GL_CLAMP_TO_EDGE : GL11.GL_REPEAT; + RENDER_BACKEND.texParameteri(target, GL11.GL_TEXTURE_MIN_FILTER, rectangle ? GL11.GL_LINEAR : GL11.GL_NEAREST_MIPMAP_LINEAR); + RENDER_BACKEND.texParameteri(target, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); + RENDER_BACKEND.texParameteri(target, GL11.GL_TEXTURE_WRAP_S, wrap); + RENDER_BACKEND.texParameteri(target, GL11.GL_TEXTURE_WRAP_T, wrap); + RENDER_BACKEND.texParameteri(target, GL12.GL_TEXTURE_WRAP_R, wrap); + RENDER_BACKEND.texParameteri(target, GL12.GL_TEXTURE_BASE_LEVEL, 0); + if (!rectangle) RENDER_BACKEND.texParameteri(target, GL12.GL_TEXTURE_MAX_LEVEL, 1000); + RENDER_BACKEND.texParameterf(target, GL12.GL_TEXTURE_MIN_LOD, -1000.0f); + RENDER_BACKEND.texParameterf(target, GL12.GL_TEXTURE_MAX_LOD, 1000.0f); + RENDER_BACKEND.texParameteri(target, GL14.GL_TEXTURE_COMPARE_MODE, GL11.GL_NONE); + RENDER_BACKEND.texParameteri(target, GL14.GL_TEXTURE_COMPARE_FUNC, GL11.GL_LEQUAL); + RENDER_BACKEND.texParameteri(target, GL33.GL_TEXTURE_SWIZZLE_R, GL11.GL_RED); + RENDER_BACKEND.texParameteri(target, GL33.GL_TEXTURE_SWIZZLE_G, GL11.GL_GREEN); + RENDER_BACKEND.texParameteri(target, GL33.GL_TEXTURE_SWIZZLE_B, GL11.GL_BLUE); + RENDER_BACKEND.texParameteri(target, GL33.GL_TEXTURE_SWIZZLE_A, GL11.GL_ALPHA); + if (!RenderSystem.isGLES()) { + RENDER_BACKEND.texParameterf(target, GL14.GL_TEXTURE_LOD_BIAS, 0.0f); + RENDER_BACKEND.texParameterfv(target, GL11.GL_TEXTURE_BORDER_COLOR, TRANSPARENT_BLACK); + } + if (RenderSystem.supportsStencilTexturing()) RENDER_BACKEND.texParameteri(target, GL43.GL_DEPTH_STENCIL_TEXTURE_MODE, GL11.GL_DEPTH_COMPONENT); + if (RENDER_BACKEND.isAnisotropicSupported()) RENDER_BACKEND.texParameterf(target, EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT, 1.0f); + } finally { + if (Tracy.ENABLED) Tracy.endZone(); + } } /** Flush deferred deletes so the driver can recycle names. */ private static void flushDeferredTextureDeletes() { if (!splashComplete || deferredDeleteTextures.isEmpty()) return; - final GLContextState primary = primaryContext(); - final GLContextState[] drawables = drawableContextStates; - final var it = deferredDeleteTextures.iterator(); - while (it.hasNext()) { - final int id = it.nextInt(); - RENDER_BACKEND.deleteTextures(id); - unshrinkableTextures.remove(id); - clearTextureBindings(primary, id); - for (int i = 0; i < drawables.length; i++) { - clearTextureBindings(drawables[i], id); + if (Tracy.ENABLED) Tracy.beginZone(Z_TEX_FLUSH_DELETES); + try { + final GLContextState primary = primaryContext(); + final GLContextState[] drawables = drawableContextStates; + final var it = Int2LongMaps.fastIterator(deferredDeleteTextures); + while (it.hasNext()) { + final Int2LongMap.Entry entry = it.next(); + if (entry.getLongValue() == DEFERRED_ATTACHED) continue; + final int id = entry.getIntKey(); + it.remove(); + RENDER_BACKEND.deleteTextures(id); + non2DTextureTargets.remove(id); + clearTextureBindings(primary, id); + for (int i = 0; i < drawables.length; i++) { + clearTextureBindings(drawables[i], id); + } } + } finally { + if (Tracy.ENABLED) Tracy.endZone(); + } + } + + private static final int DEPTH_SLOT = 16; + private static final int STENCIL_SLOT = 17; + private static final int FRAMEBUFFER_SLOTS = 18; + + private static int attachmentSlot(int attachment) { + return switch (attachment) { + case GL30.GL_DEPTH_ATTACHMENT -> DEPTH_SLOT; + case GL30.GL_STENCIL_ATTACHMENT -> STENCIL_SLOT; + default -> attachment >= GL30.GL_COLOR_ATTACHMENT0 && attachment <= GL30.GL_COLOR_ATTACHMENT15 ? attachment - GL30.GL_COLOR_ATTACHMENT0 : -1; + }; + } + + private static int slotAttachment(int slot) { + return switch (slot) { + case DEPTH_SLOT -> GL30.GL_DEPTH_ATTACHMENT; + case STENCIL_SLOT -> GL30.GL_STENCIL_ATTACHMENT; + default -> GL30.GL_COLOR_ATTACHMENT0 + slot; + }; + } + + private static boolean beginAttach(int target, int attachment, int texture) { + final GLContextState glCtx = ctx(); + return beginAttach(glCtx, target == GL30.GL_READ_FRAMEBUFFER ? glCtx.readFramebuffer : glCtx.drawFramebuffer, attachment, texture); + } + + private static boolean beginAttach(GLContextState glCtx, int framebuffer, int attachment, int texture) { + if (isDeferredDelete(texture)) return false; + TextureInfoCache.INSTANCE.onWrittenOutsideUploads(texture); + trackFramebufferAttachment(glCtx, framebuffer, attachment, texture); + return true; + } + + private static void trackFramebufferAttachment(GLContextState glCtx, int framebuffer, int attachment, int texture) { + if (framebuffer == 0) return; + if (attachment == GL30.GL_DEPTH_STENCIL_ATTACHMENT) { + trackFramebufferAttachment(glCtx, framebuffer, GL30.GL_DEPTH_ATTACHMENT, texture); + trackFramebufferAttachment(glCtx, framebuffer, GL30.GL_STENCIL_ATTACHMENT, texture); + return; + } + final int slot = attachmentSlot(attachment); + if (slot < 0) return; + int[] slots = glCtx.framebufferTextures.get(framebuffer); + if (slots == null) { + if (texture == 0) return; + slots = new int[FRAMEBUFFER_SLOTS]; + glCtx.framebufferTextures.put(framebuffer, slots); + } + setFramebufferSlot(slots, slot, texture); + } + + private static void setFramebufferSlot(int[] slots, int slot, int texture) { + final int previous = slots[slot]; + if (previous == texture) return; + slots[slot] = texture; + final boolean locked = acquireDrawLock(); + try { + if (texture != 0) textureFramebufferRefs.addTo(texture, 1); + if (previous != 0 && textureFramebufferRefs.addTo(previous, -1) == 1) { + textureFramebufferRefs.remove(previous); + if (deferredDeleteTextures.get(previous) == DEFERRED_ATTACHED) scheduleRelease(previous); + } + } finally { + if (locked) releaseDrawLock(); + } + } + + private static void detachFromFramebuffer(GLContextState glCtx, int target, int framebuffer, int texture) { + final int[] slots = glCtx.framebufferTextures.get(framebuffer); + if (slots == null) return; + for (int slot = 0; slot < slots.length; slot++) { + if (slots[slot] != texture) continue; + RENDER_BACKEND.framebufferTexture2D(target, slotAttachment(slot), GL11.GL_TEXTURE_2D, 0, 0); + setFramebufferSlot(slots, slot, 0); } - deferredDeleteTextures.clear(); } public static int glGenTextures() { @@ -2726,6 +3010,32 @@ public class GLStateManager { RENDER_BACKEND.genTextures(textures); } + public static void glGenTextures(int[] textures) { + requireContext("glGenTextures"); + flushDeferredTextureDeletes(); + for (int i = 0; i < textures.length; i++) textures[i] = RENDER_BACKEND.genTextures(); + } + + public static int glCreateTextures(int target) { + requireContext("glCreateTextures"); + final int id = RENDER_BACKEND.createTextures(target); + final boolean locked = acquireDrawLock(); + try { + recordTarget(id, target); + } finally { + if (locked) releaseDrawLock(); + } + return id; + } + + public static void glCreateTextures(int target, IntBuffer textures) { + for (int i = textures.position(); i < textures.limit(); i++) textures.put(i, glCreateTextures(target)); + } + + public static void glCreateTextures(int target, int[] textures) { + for (int i = 0; i < textures.length; i++) textures[i] = glCreateTextures(target); + } + public static void enableTexture() { final RecordMode mode = DisplayListManager.getRecordMode(); if (mode != RecordMode.NONE) { @@ -5891,14 +6201,6 @@ public class GLStateManager { } public static void glTexStorage2D(int target, int levels, int internalFormat, int width, int height) { RENDER_BACKEND.texStorage2D(target, levels, internalFormat, width, height); - if (target == GL11.GL_TEXTURE_2D) { - final boolean locked = acquireDrawLock(); - try { - unshrinkableTextures.add(getBoundTextureForServerState()); - } finally { - if (locked) releaseDrawLock(); - } - } TextureInfoCache.INSTANCE.onTexStorage2D(target, internalFormat, width, height); } public static void glClearTexImage(int texture, int level, int format, int type, ByteBuffer data) { @@ -5950,13 +6252,15 @@ public class GLStateManager { for (int i = 0; i < count; i++) glBindSampler(first + i, 0); return; } - if (samplers.remaining() < count) { - throw new IllegalArgumentException("Number of remaining buffer elements is " + samplers.remaining() + ", must be at least " + count); - } + requireRemaining(samplers, count); final int base = samplers.position(); for (int i = 0; i < count; i++) glBindSampler(first + i, samplers.get(base + i)); } + private static void requireRemaining(IntBuffer buffer, int count) { + if (buffer.remaining() < count) throw new IllegalArgumentException("Number of remaining buffer elements is " + buffer.remaining() + ", must be at least " + count); + } + public static void glBindSamplers(int first, IntBuffer samplers) { if (samplers != null) glBindSamplers(first, samplers.remaining(), samplers); } @@ -7739,7 +8043,7 @@ public class GLStateManager { public static void glClearBufferSubData(int target, int internalFormat, long offset, long size, int format, int type, ByteBuffer data) { onBufferRespec(target); RENDER_BACKEND.clearBufferSubData(target, internalFormat, offset, size, format, type, data); } public static void glClearBufferData(int target, int internalformat, int format, int type, ByteBuffer data) { onBufferRespec(target); RENDER_BACKEND.clearBufferData(target, internalformat, format, type, data); } public static boolean glIsBuffer(int buffer) { return RENDER_BACKEND.isBuffer(buffer); } - public static boolean glIsTexture(int texture) { return RENDER_BACKEND.isTexture(texture); } + public static boolean glIsTexture(int texture) { return !isDeferredDelete(texture) && RENDER_BACKEND.isTexture(texture); } public static boolean glIsFramebuffer(int framebuffer) { return RENDER_BACKEND.isFramebuffer(framebuffer); } public static boolean glIsRenderbuffer(int renderbuffer) { return RENDER_BACKEND.isRenderbuffer(renderbuffer); } public static boolean glIsSampler(int sampler) { return RENDER_BACKEND.isSampler(sampler); } @@ -7872,9 +8176,21 @@ public class GLStateManager { final GLContextState glCtx = ctx(); if (glCtx.drawFramebuffer == framebuffer) { glCtx.drawFramebuffer = 0; glCtx.drawFramebufferGeneration++; } if (glCtx.readFramebuffer == framebuffer) glCtx.readFramebuffer = 0; + final int[] slots = glCtx.framebufferTextures.remove(framebuffer); + if (slots != null) { + for (int slot = 0; slot < slots.length; slot++) setFramebufferSlot(slots, slot, 0); + } RENDER_BACKEND.deleteFramebuffers(framebuffer); } + public static void glDeleteFramebuffers(IntBuffer framebuffers) { + for (int i = framebuffers.position(); i < framebuffers.limit(); i++) glDeleteFramebuffers(framebuffers.get(i)); + } + + public static void glDeleteFramebuffers(int[] framebuffers) { + for (int framebuffer : framebuffers) glDeleteFramebuffers(framebuffer); + } + public static int glGenFramebuffers() { requireContext("glGenFramebuffers"); return RENDER_BACKEND.genFramebuffers(); @@ -7889,16 +8205,91 @@ public class GLStateManager { } public static void glFramebufferTexture2D(int target, int attachment, int textarget, int texture, int level) { - TextureInfoCache.INSTANCE.onWrittenOutsideUploads(texture); + if (!beginAttach(target, attachment, texture)) return; RENDER_BACKEND.framebufferTexture2D(target, attachment, textarget, texture, level); } + public static void glFramebufferTexture1D(int target, int attachment, int textarget, int texture, int level) { + if (!beginAttach(target, attachment, texture)) return; + RENDER_BACKEND.framebufferTexture1D(target, attachment, textarget, texture, level); + } + + public static void glFramebufferTexture3D(int target, int attachment, int textarget, int texture, int level, int zoffset) { + if (!beginAttach(target, attachment, texture)) return; + RENDER_BACKEND.framebufferTexture3D(target, attachment, textarget, texture, level, zoffset); + } + + public static void glFramebufferTextureLayer(int target, int attachment, int texture, int level, int layer) { + if (!beginAttach(target, attachment, texture)) return; + RENDER_BACKEND.framebufferTextureLayer(target, attachment, texture, level, layer); + } + public static void glFramebufferTexture(int target, int attachment, int texture, int level) { - TextureInfoCache.INSTANCE.onWrittenOutsideUploads(texture); + if (!beginAttach(target, attachment, texture)) return; RENDER_BACKEND.framebufferTexture(target, attachment, texture, level); } + public static void glFramebufferTextureFace(int target, int attachment, int texture, int level, int face) { + glFramebufferTexture2D(target, attachment, face, texture, level); + } + + public static void glNamedFramebufferTexture(int framebuffer, int attachment, int texture, int level) { + if (!beginAttach(ctx(), framebuffer, attachment, texture)) return; + RENDER_BACKEND.namedFramebufferTexture(framebuffer, attachment, texture, level); + } + + private static int swapDrawFramebuffer(int framebuffer) { + final GLContextState glCtx = ctx(); + final int previous = glCtx.drawFramebuffer; + glCtx.drawFramebuffer = framebuffer; + RENDER_BACKEND.bindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, framebuffer); + return previous; + } + + public static void glNamedFramebufferTextureLayer(int framebuffer, int attachment, int texture, int level, int layer) { + final int previous = swapDrawFramebuffer(framebuffer); + glFramebufferTextureLayer(GL30.GL_DRAW_FRAMEBUFFER, attachment, texture, level, layer); + swapDrawFramebuffer(previous); + } + + public static void glNamedFramebufferRenderbuffer(int framebuffer, int attachment, int renderbuffertarget, int renderbuffer) { + final int previous = swapDrawFramebuffer(framebuffer); + glFramebufferRenderbuffer(GL30.GL_DRAW_FRAMEBUFFER, attachment, renderbuffertarget, renderbuffer); + swapDrawFramebuffer(previous); + } + + public static void glNamedFramebufferTexture1DEXT(int framebuffer, int attachment, int textarget, int texture, int level) { + final int previous = swapDrawFramebuffer(framebuffer); + glFramebufferTexture1D(GL30.GL_DRAW_FRAMEBUFFER, attachment, textarget, texture, level); + swapDrawFramebuffer(previous); + } + + public static void glNamedFramebufferTexture2DEXT(int framebuffer, int attachment, int textarget, int texture, int level) { + final int previous = swapDrawFramebuffer(framebuffer); + glFramebufferTexture2D(GL30.GL_DRAW_FRAMEBUFFER, attachment, textarget, texture, level); + swapDrawFramebuffer(previous); + } + + public static void glNamedFramebufferTexture3DEXT(int framebuffer, int attachment, int textarget, int texture, int level, int zoffset) { + final int previous = swapDrawFramebuffer(framebuffer); + glFramebufferTexture3D(GL30.GL_DRAW_FRAMEBUFFER, attachment, textarget, texture, level, zoffset); + swapDrawFramebuffer(previous); + } + + public static void glNamedFramebufferTextureEXT(int framebuffer, int attachment, int texture, int level) { + final int previous = swapDrawFramebuffer(framebuffer); + glFramebufferTexture(GL30.GL_DRAW_FRAMEBUFFER, attachment, texture, level); + swapDrawFramebuffer(previous); + } + + public static void glNamedFramebufferTextureFaceEXT(int framebuffer, int attachment, int texture, int level, int face) { + glNamedFramebufferTexture2DEXT(framebuffer, attachment, face, texture, level); + } + public static void attachTextureForReading(int attachment, int texture) { + if (isDeferredDelete(texture)) return; + final GLContextState glCtx = ctx(); + trackFramebufferAttachment(glCtx, glCtx.readFramebuffer, attachment, texture); RENDER_BACKEND.framebufferTexture2D(GL30.GL_READ_FRAMEBUFFER, attachment, GL11.GL_TEXTURE_2D, texture, 0); } @@ -7910,7 +8301,10 @@ public class GLStateManager { public static void glBindRenderbuffer(int target, int renderbuffer) { RENDER_BACKEND.bindRenderbuffer(target, renderbuffer); } public static void glRenderbufferStorage(int target, int internalformat, int width, int height) { RENDER_BACKEND.renderbufferStorage(target, internalformat, width, height); } public static void glRenderbufferStorageMultisample(int target, int samples, int internalformat, int width, int height) { RENDER_BACKEND.renderbufferStorageMultisample(target, samples, internalformat, width, height); } - public static void glFramebufferRenderbuffer(int target, int attachment, int renderbuffertarget, int renderbuffer) { RENDER_BACKEND.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer); } + public static void glFramebufferRenderbuffer(int target, int attachment, int renderbuffertarget, int renderbuffer) { + beginAttach(target, attachment, 0); + RENDER_BACKEND.framebufferRenderbuffer(target, attachment, renderbuffertarget, renderbuffer); + } public static void glGenQueries(IntBuffer ids) { requireContext("glGenQueries"); @@ -8713,6 +9107,7 @@ public class GLStateManager { } public static void glBindImageTexture(int unit, int texture, int level, boolean layered, int layer, int access, int format) { + if (isDeferredDelete(texture)) return; final GLContextState glCtx = ctx(); final ImageUnitBinding binding = glCtx.imageUnits.get(unit); if (binding == null) { @@ -8725,6 +9120,50 @@ public class GLStateManager { RENDER_BACKEND.bindImageTexture(unit, texture, level, layered, layer, access, format); } + public static void glBindImageTextures(int first, int count, IntBuffer textures) { + if (first < 0 || first + count > ctx().imageUnits.size()) return; + if (textures == null) { + for (int i = 0; i < count; i++) bindImageUnitByName(first + i, 0); + return; + } + requireRemaining(textures, count); + for (int i = 0; i < count; i++) bindImageUnitByName(first + i, textures.get(textures.position() + i)); + } + + public static void glBindImageTextures(int first, IntBuffer textures) { + if (textures != null) glBindImageTextures(first, textures.remaining(), textures); + } + + public static void glBindImageTextures(int first, int[] textures) { + if (textures != null) glBindImageTextures(first, textures.length, IntBuffer.wrap(textures)); + } + + private static void bindImageUnitByName(int unit, int texture) { + if (texture == 0) { + glBindImageTexture(unit, 0, 0, false, 0, GL15.GL_READ_ONLY, GL30.GL_R8); + return; + } + if (isDeferredDelete(texture)) return; + final int format = TextureInfoCache.INSTANCE.getInfo(texture).getInternalFormat(); + if (!isImageFormat(format)) return; + final boolean layered = switch (non2DTextureTargets.get(texture)) { + case GL30.GL_TEXTURE_1D_ARRAY, GL30.GL_TEXTURE_2D_ARRAY, GL12.GL_TEXTURE_3D, GL13.GL_TEXTURE_CUBE_MAP, GL40.GL_TEXTURE_CUBE_MAP_ARRAY, GL32.GL_TEXTURE_2D_MULTISAMPLE_ARRAY -> true; + default -> false; + }; + glBindImageTexture(unit, texture, 0, layered, 0, GL15.GL_READ_WRITE, format); + } + + private static boolean isImageFormat(int format) { + return switch (format) { + case GL30.GL_RGBA32F, GL30.GL_RGBA16F, GL30.GL_RG32F, GL30.GL_RG16F, GL30.GL_R11F_G11F_B10F, GL30.GL_R32F, GL30.GL_R16F, + GL30.GL_RGBA32UI, GL30.GL_RGBA16UI, GL33.GL_RGB10_A2UI, GL30.GL_RGBA8UI, GL30.GL_RG32UI, GL30.GL_RG16UI, GL30.GL_RG8UI, GL30.GL_R32UI, GL30.GL_R16UI, GL30.GL_R8UI, + GL30.GL_RGBA32I, GL30.GL_RGBA16I, GL30.GL_RGBA8I, GL30.GL_RG32I, GL30.GL_RG16I, GL30.GL_RG8I, GL30.GL_R32I, GL30.GL_R16I, GL30.GL_R8I, + GL11.GL_RGBA16, GL11.GL_RGB10_A2, GL11.GL_RGBA8, GL30.GL_RG16, GL30.GL_RG8, GL30.GL_R16, GL30.GL_R8, + GL31.GL_RGBA16_SNORM, GL31.GL_RGBA8_SNORM, GL31.GL_RG16_SNORM, GL31.GL_RG8_SNORM, GL31.GL_R16_SNORM, GL31.GL_R8_SNORM -> true; + default -> false; + }; + } + public static ImageUnitBinding getImageUnitBinding(int unit) { return ctx().imageUnits.get(unit); } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java index 214a97b25..604c718e0 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/RenderSystem.java @@ -46,6 +46,8 @@ public class RenderSystem { private static boolean supportsMultiDrawIndirect; private static boolean supportsBufferStorage; private static boolean supportsClearTexture; + private static boolean supportsStencilTexturing; + private static boolean supportsTextureStorage; private static boolean supportsTesselation; private static boolean supportsSamplerObjects; private static boolean supportsProgramBinary; @@ -128,6 +130,8 @@ public class RenderSystem { supportsMultiDrawIndirect = GLStateManager.capabilities.OpenGL43 || GLStateManager.capabilities.GL_ARB_multi_draw_indirect; supportsBufferStorage = !isGLES && (GLStateManager.capabilities.OpenGL44 || GLStateManager.capabilities.GL_ARB_buffer_storage); supportsClearTexture = GLStateManager.capabilities.OpenGL44 || GLStateManager.capabilities.GL_ARB_clear_texture; + supportsStencilTexturing = GLStateManager.capabilities.OpenGL43 || GLStateManager.capabilities.GL_ARB_stencil_texturing; + supportsTextureStorage = isGLES ? glesVersion >= 300 : GLStateManager.capabilities.OpenGL42 || GLStateManager.capabilities.GL_ARB_texture_storage; hasClipCullDistance = !isGLES || hasExtension("GL_EXT_clip_cull_distance"); // Cache maximum image units @@ -307,14 +311,14 @@ public class RenderSystem { } public static void texImage1D(int texture, int target, int level, int internalformat, int width, int border, int format, int type, ByteBuffer pixels) { - RENDER_BACKEND.bindTexture(target, texture); + GLStateManager.glBindTexture(target, texture); GLStateManager.suspendPixelUnpackBuffer(); RENDER_BACKEND.texImage1D(target, level, internalformat, width, border, format, type, pixels); GLStateManager.restorePixelUnpackBuffer(); } public static void texImage3D(int texture, int target, int level, int internalformat, int width, int height, int depth, int border, int format, int type, ByteBuffer pixels) { - RENDER_BACKEND.bindTexture(target, texture); + GLStateManager.glBindTexture(target, texture); GLStateManager.suspendPixelUnpackBuffer(); RENDER_BACKEND.texImage3D(target, level, internalformat, width, height, depth, border, format, type, pixels); GLStateManager.restorePixelUnpackBuffer(); @@ -363,7 +367,6 @@ public class RenderSystem { } public static void framebufferTexture2D(int fb, int fbtarget, int attachment, int target, int texture, int levels) { - TextureInfoCache.INSTANCE.onWrittenOutsideUploads(texture); dsaState.framebufferTexture2D(fb, fbtarget, attachment, target, texture, levels); } @@ -459,6 +462,14 @@ public class RenderSystem { return supportsClearTexture; } + public static boolean supportsStencilTexturing() { + return supportsStencilTexturing; + } + + public static boolean supportsTextureStorage() { + return supportsTextureStorage; + } + public static int getMaxSSBOBindings() { return maxSSBOBindings; } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/Lwjgl2GLRenderBackend.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/Lwjgl2GLRenderBackend.java index cc36a86e8..6741dee65 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/Lwjgl2GLRenderBackend.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/Lwjgl2GLRenderBackend.java @@ -609,6 +609,21 @@ public final class Lwjgl2GLRenderBackend extends RenderBackend { GL30.glFramebufferTexture2D(target, attachment, textarget, texture, level); } + @Override + public void framebufferTexture1D(int target, int attachment, int textarget, int texture, int level) { + GL30.glFramebufferTexture1D(target, attachment, textarget, texture, level); + } + + @Override + public void framebufferTexture3D(int target, int attachment, int textarget, int texture, int level, int zoffset) { + GL30.glFramebufferTexture3D(target, attachment, textarget, texture, level, zoffset); + } + + @Override + public void framebufferTextureLayer(int target, int attachment, int texture, int level, int layer) { + GL30.glFramebufferTextureLayer(target, attachment, texture, level, layer); + } + @Override public void framebufferTexture(int target, int attachment, int texture, int level) { GL32.glFramebufferTexture(target, attachment, texture, level); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/RenderBackend.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/RenderBackend.java index 2d498af4f..3309643fd 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/RenderBackend.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/RenderBackend.java @@ -457,6 +457,9 @@ public abstract class RenderBackend { public abstract void deleteFramebuffers(int framebuffer); public abstract void bindFramebuffer(int target, int framebuffer); public abstract void framebufferTexture2D(int target, int attachment, int textarget, int texture, int level); + public abstract void framebufferTexture1D(int target, int attachment, int textarget, int texture, int level); + public abstract void framebufferTexture3D(int target, int attachment, int textarget, int texture, int level, int zoffset); + public abstract void framebufferTextureLayer(int target, int attachment, int texture, int level, int layer); public abstract void framebufferTexture(int target, int attachment, int texture, int level); public abstract int genRenderbuffers(); public abstract void deleteRenderbuffers(int renderbuffer); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAARB.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAARB.java index 40b6dcddd..02a561844 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAARB.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAARB.java @@ -128,7 +128,7 @@ public class DSAARB extends DSAUnsupported { @Override public void framebufferTexture2D(int fb, int fbtarget, int attachment, int target, int texture, int levels) { - RENDER_BACKEND.namedFramebufferTexture(fb, attachment, texture, levels); + GLStateManager.glNamedFramebufferTexture(fb, attachment, texture, levels); } @Override @@ -139,7 +139,6 @@ public class DSAARB extends DSAUnsupported { @Override public int createTexture(int target) { - GLStateManager.requireContext("glCreateTextures"); - return RENDER_BACKEND.createTextures(target); + return GLStateManager.glCreateTextures(target); } } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAUnsupported.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAUnsupported.java index 12ef4aa8f..4afbef548 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAUnsupported.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/dsa/DSAUnsupported.java @@ -2,13 +2,12 @@ package com.gtnewhorizons.angelica.glsm.dsa; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.stacks.TextureBindingStack; +import com.gtnewhorizons.angelica.glsm.texture.TextureTargets; import org.lwjgl.opengl.GL11; import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND; -import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL30; -import org.lwjgl.opengl.GL31; import java.nio.ByteBuffer; import java.nio.FloatBuffer; @@ -153,7 +152,7 @@ public class DSAUnsupported implements DSAAccess { @Override public void framebufferTexture2D(int fb, int fbtarget, int attachment, int target, int texture, int levels) { GLStateManager.glBindFramebuffer(fbtarget, fb); - RENDER_BACKEND.framebufferTexture2D(fbtarget, attachment, target, texture, levels); + GLStateManager.glFramebufferTexture2D(fbtarget, attachment, target, texture, levels); } @Override @@ -166,23 +165,12 @@ public class DSAUnsupported implements DSAAccess { @Override public int createTexture(int target) { final int texture = GLStateManager.glGenTextures(); - GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, texture); + GLStateManager.glBindTexture(target, texture); return texture; } - private static int bindingQuery(int target) { - return switch (target) { - case GL11.GL_TEXTURE_1D -> GL11.GL_TEXTURE_BINDING_1D; - case GL12.GL_TEXTURE_3D -> GL12.GL_TEXTURE_BINDING_3D; - case GL13.GL_TEXTURE_CUBE_MAP -> GL13.GL_TEXTURE_BINDING_CUBE_MAP; - case GL30.GL_TEXTURE_2D_ARRAY -> GL30.GL_TEXTURE_BINDING_2D_ARRAY; - case GL31.GL_TEXTURE_RECTANGLE -> GL31.GL_TEXTURE_BINDING_RECTANGLE; - default -> GL11.GL_TEXTURE_BINDING_2D; - }; - } - private static int previousBinding(int target) { - return target == GL11.GL_TEXTURE_2D ? previous2D() : RENDER_BACKEND.getInteger(bindingQuery(target)); + return target == GL11.GL_TEXTURE_2D ? previous2D() : RENDER_BACKEND.getInteger(TextureTargets.bindingQuery(target)); } private static int previous2D() { diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/FrameHooks.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/FrameHooks.java index 03c121175..69e8e6e47 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/FrameHooks.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/hooks/FrameHooks.java @@ -26,6 +26,7 @@ public final class FrameHooks { } public static void frameEnd() { + GLStateManager.onFrameEnd(); popFrameGroup(); RENDER_BACKEND.onFrameEnd(); } diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMRedirector.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMRedirector.java index a7574f3cd..a1db2a37e 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMRedirector.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMRedirector.java @@ -167,6 +167,14 @@ public class GLSMRedirector { private static final String EXTTimerQuery = "org/lwjgl/opengl/EXTTimerQuery"; private static final String ARBSamplerObjects = "org/lwjgl/opengl/ARBSamplerObjects"; private static final String ARBMultiBind = "org/lwjgl/opengl/ARBMultiBind"; + private static final String ARBDirectStateAccess = "org/lwjgl/opengl/ARBDirectStateAccess"; + private static final String ARBGeometryShader4 = "org/lwjgl/opengl/ARBGeometryShader4"; + private static final String EXTGeometryShader4 = "org/lwjgl/opengl/EXTGeometryShader4"; + private static final String NVGeometryProgram4 = "org/lwjgl/opengl/NVGeometryProgram4"; + private static final String EXTTextureArray = "org/lwjgl/opengl/EXTTextureArray"; + private static final String EXTDirectStateAccess = "org/lwjgl/opengl/EXTDirectStateAccess"; + private static final String ARBShaderImageLoadStore = "org/lwjgl/opengl/ARBShaderImageLoadStore"; + private static final String EXTShaderImageLoadStore = "org/lwjgl/opengl/EXTShaderImageLoadStore"; // Redirect VAO related calls from NHLib private static final String UniversalVAO = "com/gtnewhorizon/gtnhlib/client/opengl/UniversalVAO"; @@ -513,7 +521,10 @@ public class GLSMRedirector { .add("glDeleteFramebuffers") .add("glGenFramebuffers") .add("glCheckFramebufferStatus") + .add("glFramebufferTexture1D") .add("glFramebufferTexture2D") + .add("glFramebufferTexture3D") + .add("glFramebufferTextureLayer") .add("glVertexAttribIPointer") .add("glGenerateMipmap") .add("glGetFramebufferAttachmentParameteri") @@ -567,11 +578,18 @@ public class GLSMRedirector { final var gl44 = RedirectMap.newMap() .add("glBufferStorage") .add("glBindSamplers") - .add("glClearTexImage"); + .add("glClearTexImage") + .add("glBindTextures") + .add("glBindImageTextures"); final var gl45 = RedirectMap.newMap() .add("glCreateBuffers") .add("glNamedBufferData") - .add("glNamedBufferSubData"); + .add("glNamedBufferSubData") + .add("glNamedFramebufferTexture") + .add("glNamedFramebufferTextureLayer") + .add("glNamedFramebufferRenderbuffer") + .add("glCreateTextures") + .add("glBindTextureUnit"); final var gl46 = RedirectMap.newMap() .add("glPolygonOffsetClamp") .add("glMultiDrawArraysIndirectCount") @@ -630,7 +648,10 @@ public class GLSMRedirector { .add("glDeleteFramebuffers") .add("glGenFramebuffers") .add("glCheckFramebufferStatus") + .add("glFramebufferTexture1D") .add("glFramebufferTexture2D") + .add("glFramebufferTexture3D") + .add("glFramebufferTextureLayer") .add("glBindRenderbuffer") .add("glDeleteRenderbuffers") .add("glGenRenderbuffers") @@ -643,7 +664,9 @@ public class GLSMRedirector { .add("glDeleteFramebuffersEXT", "glDeleteFramebuffers") .add("glGenFramebuffersEXT", "glGenFramebuffers") .add("glCheckFramebufferStatusEXT", "glCheckFramebufferStatus") + .add("glFramebufferTexture1DEXT", "glFramebufferTexture1D") .add("glFramebufferTexture2DEXT", "glFramebufferTexture2D") + .add("glFramebufferTexture3DEXT", "glFramebufferTexture3D") .add("glBindRenderbufferEXT", "glBindRenderbuffer") .add("glDeleteRenderbuffersEXT", "glDeleteRenderbuffers") .add("glGenRenderbuffersEXT", "glGenRenderbuffers") @@ -793,6 +816,45 @@ public class GLSMRedirector { ); methodRedirects.put(ARBMultiBind, RedirectMap.newMap() .add("glBindSamplers") + .add("glBindTextures") + .add("glBindImageTextures") + ); + methodRedirects.put(ARBDirectStateAccess, RedirectMap.newMap() + .add("glNamedFramebufferTexture") + .add("glNamedFramebufferTextureLayer") + .add("glNamedFramebufferRenderbuffer") + .add("glCreateTextures") + .add("glBindTextureUnit") + ); + methodRedirects.put(ARBGeometryShader4, RedirectMap.newMap() + .add("glFramebufferTextureARB", "glFramebufferTexture") + .add("glFramebufferTextureLayerARB", "glFramebufferTextureLayer") + .add("glFramebufferTextureFaceARB", "glFramebufferTextureFace") + ); + final var geometryShader4 = RedirectMap.newMap() + .add("glFramebufferTextureEXT", "glFramebufferTexture") + .add("glFramebufferTextureLayerEXT", "glFramebufferTextureLayer") + .add("glFramebufferTextureFaceEXT", "glFramebufferTextureFace"); + methodRedirects.put(EXTGeometryShader4, geometryShader4); + methodRedirects.put(NVGeometryProgram4, geometryShader4); + methodRedirects.put(EXTTextureArray, RedirectMap.newMap() + .add("glFramebufferTextureLayerEXT", "glFramebufferTextureLayer") + ); + methodRedirects.put(EXTDirectStateAccess, RedirectMap.newMap() + .add("glNamedFramebufferTexture1DEXT") + .add("glNamedFramebufferTexture2DEXT") + .add("glNamedFramebufferTexture3DEXT") + .add("glNamedFramebufferTextureEXT") + .add("glNamedFramebufferTextureLayerEXT", "glNamedFramebufferTextureLayer") + .add("glNamedFramebufferTextureFaceEXT") + .add("glNamedFramebufferRenderbufferEXT", "glNamedFramebufferRenderbuffer") + .add("glBindMultiTextureEXT") + ); + methodRedirects.put(ARBShaderImageLoadStore, RedirectMap.newMap() + .add("glBindImageTexture") + ); + methodRedirects.put(EXTShaderImageLoadStore, RedirectMap.newMap() + .add("glBindImageTextureEXT", "glBindImageTexture") ); // APPLE diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureTargets.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureTargets.java index c99b0b547..a11d5ded5 100644 --- a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureTargets.java +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/texture/TextureTargets.java @@ -27,4 +27,20 @@ public final class TextureTargets { default -> false; }; } + + public static int bindingQuery(int target) { + return switch (target) { + case GL11.GL_TEXTURE_1D -> GL11.GL_TEXTURE_BINDING_1D; + case GL12.GL_TEXTURE_3D -> GL12.GL_TEXTURE_BINDING_3D; + case GL13.GL_TEXTURE_CUBE_MAP -> GL13.GL_TEXTURE_BINDING_CUBE_MAP; + case GL30.GL_TEXTURE_1D_ARRAY -> GL30.GL_TEXTURE_BINDING_1D_ARRAY; + case GL30.GL_TEXTURE_2D_ARRAY -> GL30.GL_TEXTURE_BINDING_2D_ARRAY; + case GL31.GL_TEXTURE_RECTANGLE -> GL31.GL_TEXTURE_BINDING_RECTANGLE; + case GL31.GL_TEXTURE_BUFFER -> GL31.GL_TEXTURE_BINDING_BUFFER; + case GL32.GL_TEXTURE_2D_MULTISAMPLE -> GL32.GL_TEXTURE_BINDING_2D_MULTISAMPLE; + case GL32.GL_TEXTURE_2D_MULTISAMPLE_ARRAY -> GL32.GL_TEXTURE_BINDING_2D_MULTISAMPLE_ARRAY; + case GL40.GL_TEXTURE_CUBE_MAP_ARRAY -> GL40.GL_TEXTURE_BINDING_CUBE_MAP_ARRAY; + default -> GL11.GL_TEXTURE_BINDING_2D; + }; + } } diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DeferredTextureDelete_GLTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DeferredTextureDelete_GLTest.java index fe159ce24..7f1d06003 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DeferredTextureDelete_GLTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/GLSM_DeferredTextureDelete_GLTest.java @@ -1,9 +1,20 @@ package com.gtnewhorizons.angelica.glsm; +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.lwjgl.BufferUtils; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL13; +import org.lwjgl.opengl.GL14; import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL33; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assumptions.assumeTrue; @@ -11,6 +22,10 @@ import static org.junit.jupiter.api.Assumptions.assumeTrue; @GLCoreTest public class GLSM_DeferredTextureDelete_GLTest { + private static final int RELEASE_DELAY_FRAMES = Reflect.getStatic(GLStateManager.class, "RELEASE_DELAY_FRAMES"); + private static final int SIZE = 4; + private static final int LEVELS = 3; + private static int immutableTexture() { assumeTrue(GLStateManager.capabilities.OpenGL42, "no glTexStorage2D on this context"); final int tex = GLStateManager.glGenTextures(); @@ -20,6 +35,292 @@ public class GLSM_DeferredTextureDelete_GLTest { return tex; } + private static int mipmappedTexture() { + final int tex = GLStateManager.glGenTextures(); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex); + for (int level = 0; level < LEVELS; level++) { + GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, level, GL11.GL_RGBA8, SIZE >> level, SIZE >> level, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); + } + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + return tex; + } + + private static int levelWidth(int tex, int level) { + final int previous = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, tex); + final int width = GL11.glGetTexLevelParameteri(GL11.GL_TEXTURE_2D, level, GL11.GL_TEXTURE_WIDTH); + GL11.glBindTexture(GL11.GL_TEXTURE_2D, previous); + return width; + } + + private static int cachedBinding(int unit) { + return GLStateManager.getTextures().getTextureUnitBindings(unit).getBinding(); + } + + private static void framesPass(int frames) { + for (int i = 0; i < frames; i++) GLStateManager.onFrameEnd(); + } + + enum Attachment { + NONE { + @Override int attach(int tex) { return 0; } + }, + FRAMEBUFFER { + @Override int attach(int tex) { + final int fbo = GLStateManager.glGenFramebuffers(); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo); + GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, tex, 0); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + return fbo; + } + }, + NAMED_EXT { + @Override int attach(int tex) { + final int fbo = GLStateManager.glGenFramebuffers(); + GLStateManager.glNamedFramebufferTexture2DEXT(fbo, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, tex, 0); + assertEquals(0, GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING), "named attach must restore the draw binding"); + return fbo; + } + }; + + abstract int attach(int tex); + } + + @ParameterizedTest + @EnumSource(Attachment.class) + void storageIsReleasedOnlyAfterTheDelayFramesUnattached(Attachment attachment) { + framesPass(RELEASE_DELAY_FRAMES); + final int before = GLStateManager.deferredTexturesAwaitingRelease(); + final int tex = mipmappedTexture(); + final int fbo = attachment.attach(tex); + + GLStateManager.glDeleteTextures(tex); + assertEquals(before + 1, GLStateManager.deferredTexturesAwaitingRelease()); + framesPass(RELEASE_DELAY_FRAMES - 1); + assertEquals(SIZE, levelWidth(tex, 0), "storage must outlive the delete by the delay frames"); + if (fbo != 0) { + framesPass(RELEASE_DELAY_FRAMES); + assertEquals(SIZE, levelWidth(tex, 0), "an attached texture keeps its storage"); + assertEquals(before + 1, GLStateManager.deferredTexturesAwaitingRelease()); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo); + GLStateManager.glDeleteTextures(GLStateManager.glGenTextures()); + assertEquals(tex, GL30.glGetFramebufferAttachmentParameteri(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME), "a flush must not delete an attached texture"); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + GLStateManager.glDeleteFramebuffers(BufferUtils.createIntBuffer(1).put(fbo).flip()); + framesPass(RELEASE_DELAY_FRAMES - 1); + assertEquals(SIZE, levelWidth(tex, 0), "storage must outlive the detach by the delay frames"); + } + framesPass(1); + + for (int level = 0; level < LEVELS; level++) assertEquals(0, levelWidth(tex, level), "level " + level); + assertEquals(before, GLStateManager.deferredTexturesAwaitingRelease(), "released storage leaves nothing awaiting release"); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } + + enum Rejection { ATTACH, BIND_TEXTURE_UNIT } + + @ParameterizedTest + @EnumSource(Rejection.class) + void deletedNamesAreRejected(Rejection rejection) { + final int tex = mipmappedTexture(); + final int other = mipmappedTexture(); + GLStateManager.glDeleteTextures(tex); + try { + switch (rejection) { + case ATTACH -> { + final int fbo = GLStateManager.glGenFramebuffers(); + try { + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, fbo); + GLStateManager.glFramebufferTexture2D(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, tex, 0); + assertEquals(GL11.GL_NONE, GL30.glGetFramebufferAttachmentParameteri(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE)); + } finally { + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + GLStateManager.glDeleteFramebuffers(fbo); + } + } + case BIND_TEXTURE_UNIT -> { + GLStateManager.glBindTextureUnit(2, other); + GLStateManager.glBindTextureUnit(2, tex); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE2); + assertEquals(other, GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "driver binding"); + assertEquals(other, GLStateManager.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "cached binding"); + } + } + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } finally { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + GLStateManager.glBindTextureUnit(2, 0); + GLStateManager.glDeleteTextures(other); + } + } + + @Test + void bindTexturesBindsEachTextureToItsTarget() { + final int t2d = mipmappedTexture(); + final int t3d = GLStateManager.glGenTextures(); + GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, t3d); + GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, 0); + try { + GLStateManager.glBindTextures(1, new int[] {t2d, t3d}); + + assertEquals(GL13.GL_TEXTURE0, GL11.glGetInteger(GL13.GL_ACTIVE_TEXTURE), "active unit unchanged"); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE1); + assertEquals(t2d, GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "unit 1 driver binding"); + assertEquals(t2d, GLStateManager.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "unit 1 cached binding"); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE2); + assertEquals(t3d, GL11.glGetInteger(GL12.GL_TEXTURE_BINDING_3D), "unit 2 driver binding"); + assertEquals(t3d, cachedBinding(2), "unit 2 cached binding"); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + + final int unit0 = GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D); + GLStateManager.glBindTextures(GLStateManager.MAX_TEXTURE_UNITS - 1, new int[] {t3d, t2d}); + assertEquals(unit0, GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "an out-of-range multi-bind must not land on the active unit"); + + GLStateManager.glDeleteTextures(t2d); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE1); + assertEquals(0, GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "delete unbinds a multi-bound texture"); + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + } finally { + GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); + GLStateManager.glBindTextures(1, new int[] {0, 0}); + GLStateManager.glDeleteTextures(t3d); + } + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } + + @Test + void dsaCreated3DTextureIsBoundAs3D() { + final int tex = RenderSystem.createTexture(GL12.GL_TEXTURE_3D); + final int other = GLStateManager.glGenTextures(); + GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, other); + RenderSystem.texImage3D(tex, GL12.GL_TEXTURE_3D, 0, GL11.GL_RGBA8, SIZE, SIZE, SIZE, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); + assertEquals(tex, GL11.glGetInteger(GL12.GL_TEXTURE_BINDING_3D), "texImage3D binds the texture as 3D"); + assertEquals(tex, cachedBinding(0), "cached 3D binding"); + GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, 0); + GLStateManager.glDeleteTextures(tex); + GLStateManager.glDeleteTextures(other); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } + + @Test + void createdNon2DTextureIsNotReleasedAs2D() { + assumeTrue(GLStateManager.capabilities.OpenGL45, "no glCreateTextures on this context"); + final int tex = GLStateManager.glCreateTextures(GL12.GL_TEXTURE_3D); + GLStateManager.glDeleteTextures(tex); + framesPass(RELEASE_DELAY_FRAMES); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } + + @Test + void deletingDetachesOnlyFromBoundFramebuffers() { + final int tex = mipmappedTexture(); + final int unbound = Attachment.FRAMEBUFFER.attach(tex); + final int bound = Attachment.FRAMEBUFFER.attach(tex); + try { + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, bound); + + GLStateManager.glDeleteTextures(tex); + + assertEquals(GL11.GL_NONE, GL30.glGetFramebufferAttachmentParameteri(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE)); + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, unbound); + assertEquals(tex, GL30.glGetFramebufferAttachmentParameteri(GL30.GL_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL30.GL_FRAMEBUFFER_ATTACHMENT_OBJECT_NAME)); + } finally { + GLStateManager.glBindFramebuffer(GL30.GL_FRAMEBUFFER, 0); + GLStateManager.glDeleteFramebuffers(bound); + GLStateManager.glDeleteFramebuffers(unbound); + } + } + + enum Rescue { PENDING, RELEASED, ATTACHED } + + @ParameterizedTest + @EnumSource(Rescue.class) + void rescuedTextureIsEmptyWithDefaultParameters(Rescue rescue) { + final int tex = mipmappedTexture(); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_NEAREST); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_BASE_LEVEL, 1); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 2); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_R, GL11.GL_GREEN); + GLStateManager.glTexParameterf(GL11.GL_TEXTURE_2D, GL14.GL_TEXTURE_LOD_BIAS, 1.5f); + GLStateManager.glTexParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_BORDER_COLOR, (FloatBuffer) BufferUtils.createFloatBuffer(4).put(new float[] {1, 1, 1, 1}).flip()); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + final int fbo = rescue == Rescue.ATTACHED ? Attachment.FRAMEBUFFER.attach(tex) : 0; + GLStateManager.glDeleteTextures(tex); + if (rescue != Rescue.PENDING) framesPass(RELEASE_DELAY_FRAMES); + + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex); + try { + assertEquals(rescue == Rescue.ATTACHED ? SIZE : 0, levelWidth(tex, 0), "only an attached texture keeps its storage"); + final int[][] defaults = { + {GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_NEAREST_MIPMAP_LINEAR}, + {GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR}, + {GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT}, + {GL12.GL_TEXTURE_BASE_LEVEL, 0}, + {GL12.GL_TEXTURE_MAX_LEVEL, 1000}, + {GL33.GL_TEXTURE_SWIZZLE_R, GL11.GL_RED}, + }; + for (int[] pair : defaults) { + assertEquals(pair[1], GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, pair[0]), "driver pname 0x" + Integer.toHexString(pair[0])); + assertEquals(pair[1], GLStateManager.glGetTexParameteri(GL11.GL_TEXTURE_2D, pair[0]), "cached pname 0x" + Integer.toHexString(pair[0])); + } + assertEquals(0.0f, GL11.glGetTexParameterf(GL11.GL_TEXTURE_2D, GL14.GL_TEXTURE_LOD_BIAS)); + final FloatBuffer border = BufferUtils.createFloatBuffer(16); + GL11.glGetTexParameter(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_BORDER_COLOR, border); + assertEquals(0.0f, border.get(0) + border.get(1) + border.get(2) + border.get(3), "border color"); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } finally { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + GLStateManager.glDeleteTextures(tex); + if (fbo != 0) GLStateManager.glDeleteFramebuffers(fbo); + } + } + + @Test + void rescuingWithADifferentTargetLeavesTheBoundTextureAlone() { + final int t3d = GLStateManager.glGenTextures(); + final int tex = mipmappedTexture(); + try { + GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, t3d); + GLStateManager.glTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GLStateManager.glDeleteTextures(tex); + + GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, tex); + + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError(), "GLSM drops the bind"); + assertEquals(t3d, cachedBinding(0), "cached 3D binding"); + assertEquals(GL11.GL_LINEAR, GL11.glGetTexParameteri(GL12.GL_TEXTURE_3D, GL11.GL_TEXTURE_MIN_FILTER)); + framesPass(RELEASE_DELAY_FRAMES); + assertEquals(t3d, GL11.glGetInteger(GL12.GL_TEXTURE_BINDING_3D), "the release must not unbind the 3D texture"); + assertEquals(0, levelWidth(tex, 0), "the mismatched bind must leave the texture deferred"); + } finally { + GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, 0); + GLStateManager.glDeleteTextures(t3d); + } + } + + @Test + void deletingAStaleNameKeepsTheBoundTexture() { + final int live = mipmappedTexture(); + final int stale = GLStateManager.glGenTextures(); + try { + GLStateManager.glDeleteTextures(stale); + GLStateManager.glDeleteTextures(GLStateManager.glGenTextures()); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, live); + + GLStateManager.glDeleteTextures(stale); + framesPass(RELEASE_DELAY_FRAMES); + + assertEquals(SIZE, levelWidth(live, 0), "a stale delete must not empty the bound texture"); + assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); + } finally { + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); + GLStateManager.glDeleteTextures(live); + } + } + @Test void deletingAnImmutableTextureRaisesNoError() { final int tex = immutableTexture(); @@ -30,33 +331,40 @@ public class GLSM_DeferredTextureDelete_GLTest { } @Test - void deletingARescuedImmutableTextureRaisesNoError() { + void rescuedImmutableTextureResetsParametersAndDeletesAgain() { final int tex = immutableTexture(); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); GLStateManager.glDeleteTextures(tex); GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex); + assertEquals(GL11.GL_NEAREST_MIPMAP_LINEAR, GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER)); GLStateManager.glDeleteTextures(tex); assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); } - @Test - void deletingA2DTextureWhileA3DTextureIsBoundLeavesNothingBoundTo2D() { + @ParameterizedTest + @ValueSource(booleans = {true, false}) + void deletingA2DTextureWhileA3DTextureIsBound(boolean deletedIsHidden2D) { final int t2d = GLStateManager.glGenTextures(); final int t3d = GLStateManager.glGenTextures(); + final int other = GLStateManager.glGenTextures(); try { GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, t2d); GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, t3d); - GLStateManager.glDeleteTextures(t2d); + GLStateManager.glDeleteTextures(deletedIsHidden2D ? t2d : other); assertEquals(GL11.GL_NO_ERROR, GL11.glGetError()); - assertEquals(0, GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "the deleted texture must not stay bound"); + assertEquals(deletedIsHidden2D ? 0 : t2d, GL11.glGetInteger(GL11.GL_TEXTURE_BINDING_2D), "only the deleted texture leaves the hidden 2D binding"); assertEquals(t3d, GL11.glGetInteger(GL12.GL_TEXTURE_BINDING_3D)); } finally { GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, 0); GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); GLStateManager.glDeleteTextures(t3d); + GLStateManager.glDeleteTextures(deletedIsHidden2D ? other : t2d); } } } diff --git a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMRedirectorTest.java b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMRedirectorTest.java index 5e3f02122..337f929a3 100644 --- a/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMRedirectorTest.java +++ b/glsm/src/test/java/com/gtnewhorizons/angelica/glsm/redirect/GLSMRedirectorTest.java @@ -230,6 +230,7 @@ class GLSMRedirectorTest { @ParameterizedTest(name = "{0}.{1}{2}") @CsvSource(delimiter = '|', value = { "org/lwjgl/opengl/EXTFramebufferObject | glBindFramebufferEXT | (II)V | glBindFramebuffer", + "org/lwjgl/opengl/EXTFramebufferObject | glFramebufferTexture3DEXT | (IIIIII)V | glFramebufferTexture3D", "org/lwjgl/opengl/ARBFramebufferObject | glGenerateMipmap | (I)V | glGenerateMipmap", "org/lwjgl/opengl/EXTFramebufferObject | glGenerateMipmapEXT | (I)V | glGenerateMipmap", "org/lwjgl/opengl/GL11 | glReadPixels | (IIIIIILjava/nio/IntBuffer;)V | glReadPixels", @@ -261,9 +262,15 @@ class GLSMRedirectorTest { "org/lwjgl/opengl/GL11 | glLoadMatrixf | (Ljava/nio/FloatBuffer;)V | glLoadMatrix", "org/lwjgl/opengl/ARBVertexArrayObject | glGenVertexArrays | ()I | glGenVertexArrays", "org/lwjgl/opengl/APPLEVertexArrayObject | glGenVertexArraysAPPLE | ()I | glGenVertexArrays", + "org/lwjgl/opengl/ARBGeometryShader4 | glFramebufferTextureFaceARB | (IIIII)V | glFramebufferTextureFace", + "org/lwjgl/opengl/EXTGeometryShader4 | glFramebufferTextureEXT | (IIII)V | glFramebufferTexture", + "org/lwjgl/opengl/EXTTextureArray | glFramebufferTextureLayerEXT | (IIIII)V | glFramebufferTextureLayer", + "org/lwjgl/opengl/EXTShaderImageLoadStore | glBindImageTextureEXT | (IIIZIII)V | glBindImageTexture", + "org/lwjgl/opengl/EXTFramebufferObject | glDeleteFramebuffersEXT | (Ljava/nio/IntBuffer;)V | glDeleteFramebuffers", }) - void aliasIsRedirected(String owner, String name, String desc, String expectedName) { + void aliasIsRedirected(String owner, String name, String desc, String expectedName) throws ClassNotFoundException { assertEquals(desc, assertRedirected(owner, name, desc, expectedName).desc); + assertTrue(glsmDeclares(expectedName, desc), () -> "GLStateManager lacks " + expectedName + desc); assertTrue(debugSeen().isEmpty()); } @@ -303,6 +310,32 @@ class GLSMRedirectorTest { "org/lwjgl/opengl/ARBSamplerObjects | glSamplerParameteri | (III)V", "org/lwjgl/opengl/ARBSamplerObjects | glSamplerParameterf | (IIF)V", "org/lwjgl/opengl/GL43 | glGetDebugMessageLog | (ILjava/nio/IntBuffer;Ljava/nio/IntBuffer;Ljava/nio/IntBuffer;Ljava/nio/IntBuffer;Ljava/nio/IntBuffer;Ljava/nio/ByteBuffer;)I", + "org/lwjgl/opengl/GL30 | glFramebufferTexture1D | (IIIII)V", + "org/lwjgl/opengl/GL30 | glFramebufferTexture3D | (IIIIII)V", + "org/lwjgl/opengl/GL30 | glFramebufferTextureLayer | (IIIII)V", + "org/lwjgl/opengl/GL30C | glDeleteFramebuffers | ([I)V", + "org/lwjgl/opengl/GL11C | glDeleteTextures | ([I)V", + "org/lwjgl/opengl/GL11C | glGenTextures | ([I)V", + "org/lwjgl/opengl/GL44 | glBindTextures | (IILjava/nio/IntBuffer;)V", + "org/lwjgl/opengl/GL44C | glBindTextures | (ILjava/nio/IntBuffer;)V", + "org/lwjgl/opengl/GL44 | glBindTextures | (I[I)V", + "org/lwjgl/opengl/GL44 | glBindImageTextures | (IILjava/nio/IntBuffer;)V", + "org/lwjgl/opengl/GL44C | glBindImageTextures | (ILjava/nio/IntBuffer;)V", + "org/lwjgl/opengl/GL44 | glBindImageTextures | (I[I)V", + "org/lwjgl/opengl/GL45 | glBindTextureUnit | (II)V", + "org/lwjgl/opengl/GL45 | glCreateTextures | (I)I", + "org/lwjgl/opengl/GL45C | glCreateTextures | (ILjava/nio/IntBuffer;)V", + "org/lwjgl/opengl/GL45C | glCreateTextures | (I[I)V", + "org/lwjgl/opengl/GL45 | glNamedFramebufferTexture | (IIII)V", + "org/lwjgl/opengl/GL45 | glNamedFramebufferTextureLayer | (IIIII)V", + "org/lwjgl/opengl/ARBDirectStateAccess | glNamedFramebufferRenderbuffer | (IIII)V", + "org/lwjgl/opengl/EXTDirectStateAccess | glNamedFramebufferTexture1DEXT | (IIIII)V", + "org/lwjgl/opengl/EXTDirectStateAccess | glNamedFramebufferTexture2DEXT | (IIIII)V", + "org/lwjgl/opengl/EXTDirectStateAccess | glNamedFramebufferTexture3DEXT | (IIIIII)V", + "org/lwjgl/opengl/EXTDirectStateAccess | glNamedFramebufferTextureEXT | (IIII)V", + "org/lwjgl/opengl/EXTDirectStateAccess | glNamedFramebufferTextureFaceEXT | (IIIII)V", + "org/lwjgl/opengl/EXTDirectStateAccess | glBindMultiTextureEXT | (III)V", + "org/lwjgl/opengl/ARBShaderImageLoadStore | glBindImageTexture | (IIIZIII)V", }) void redirectsToADeclaredGlsmTarget(String owner, String name, String desc) throws ClassNotFoundException { for (boolean aware : new boolean[]{false, true}) { diff --git a/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java b/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java index 0192596ac..e4f835fba 100644 --- a/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java +++ b/lwjgl3-backend/src/main/java/com/gtnewhorizons/angelica/lwjgl3/Lwjgl3GLRenderBackend.java @@ -747,6 +747,21 @@ public final class Lwjgl3GLRenderBackend extends RenderBackend { GL30C.glFramebufferTexture2D(target, attachment, textarget, texture, level); } + @Override + public void framebufferTexture1D(int target, int attachment, int textarget, int texture, int level) { + GL30C.glFramebufferTexture1D(target, attachment, textarget, texture, level); + } + + @Override + public void framebufferTexture3D(int target, int attachment, int textarget, int texture, int level, int zoffset) { + GL30C.glFramebufferTexture3D(target, attachment, textarget, texture, level, zoffset); + } + + @Override + public void framebufferTextureLayer(int target, int attachment, int texture, int level, int layer) { + GL30C.glFramebufferTextureLayer(target, attachment, texture, level, layer); + } + @Override public void framebufferTexture(int target, int attachment, int texture, int level) { GL32C.glFramebufferTexture(target, attachment, texture, level); diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java index fb27fee92..a359e7c5d 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/SDLGPURenderBackend.java @@ -1557,8 +1557,8 @@ public class SDLGPURenderBackend extends RenderBackend { return isProxyTarget(target) ? 0 : st.boundTextures[st.activeTextureUnit]; } - private int requestedMipLevels(int glId) { - return Math.max(1, resourceManager.getOrCreateTexSamplerState(glId).maxLevel + 1); + private int requestedMipLevels(int glId, int width, int height) { + return Math.max(1, Math.min(resourceManager.getOrCreateTexSamplerState(glId).maxLevel + 1, PixelOps.defaultMipLevels(width, height))); } private int fullMipLevels(int glId, int width, int height) { @@ -1576,7 +1576,7 @@ public class SDLGPURenderBackend extends RenderBackend { } private void ensureLevel0Storage(ContextState st, int glId, int target, int internalFormat, int width, int height, int depth, int format, boolean mipmapped) { - final int numLevels = mipmapped ? requestedMipLevels(glId) : 1; + final int numLevels = mipmapped ? requestedMipLevels(glId, width, height) : 1; final boolean bgraSwizzle = format == GL12.GL_BGRA && (internalFormat == GL11.GL_RGBA || internalFormat == GL11.GL_RGBA8); final int targetSdlFormat = bgraSwizzle ? SDLGPU.SDL_GPU_TEXTUREFORMAT_B8G8R8A8_UNORM @@ -1594,6 +1594,15 @@ public class SDLGPURenderBackend extends RenderBackend { resourceManager.refreshTextureReferences(glId); } + private boolean dropStorageIfEmpty(ContextState st, int glId, int level, int width, int height, int depth) { + if (width > 0 && height > 0 && depth > 0) return false; + if (level == 0) { + textureOps.releaseTextureForRealloc(st, glId); + resourceManager.refreshTextureReferences(glId); + } + return true; + } + private static boolean canReuseTexture(ResourceManager.TextureMeta m, long handle, int target, int glFormat, int sdlFormat, int w, int h, int d, int levels) { return m != null && handle != 0 @@ -1613,7 +1622,7 @@ public class SDLGPURenderBackend extends RenderBackend { return; } final int glId = st.boundTextures[st.activeTextureUnit]; - if (glId == 0) return; + if (glId == 0 || dropStorageIfEmpty(st, glId, level, width, height, 1)) return; if (level == 0) { ensureLevel0Storage(st, glId, target, internalFormat, width, height, 1, format, true); @@ -1639,7 +1648,7 @@ public class SDLGPURenderBackend extends RenderBackend { return; } final int glId = st.boundTextures[st.activeTextureUnit]; - if (glId == 0) return; + if (glId == 0 || dropStorageIfEmpty(st, glId, level, width, height, depth)) return; if (level == 0) { ensureLevel0Storage(st, glId, target, internalFormat, width, height, depth, GL11.GL_NONE, false); @@ -1669,7 +1678,7 @@ public class SDLGPURenderBackend extends RenderBackend { return; } final int glId = st.boundTextures[st.activeTextureUnit]; - if (glId == 0) return; + if (glId == 0 || dropStorageIfEmpty(st, glId, level, width, 1, 1)) return; if (level == 0) { ensureLevel0Storage(st, glId, target, internalFormat, width, 1, 1, GL11.GL_NONE, false); @@ -1685,7 +1694,7 @@ public class SDLGPURenderBackend extends RenderBackend { return; } final int glId = st.boundTextures[st.activeTextureUnit]; - if (glId == 0) return; + if (glId == 0 || dropStorageIfEmpty(st, glId, level, width, height, 1)) return; if (level == 0) { ensureLevel0Storage(st, glId, target, internalFormat, width, height, 1, format, true); @@ -1768,7 +1777,7 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public void copyTexImage2D(int target, int level, int internalFormat, int x, int y, int width, int height, int border) { final ContextState cs = s(); final int destGlId = boundTextureFor(cs, target); - if (destGlId == 0) return; + if (destGlId == 0 || dropStorageIfEmpty(cs, destGlId, level, width, height, 1)) return; if (level == 0) { ensureLevel0Storage(cs, destGlId, target, internalFormat, width, height, 1, GL11.GL_NONE, true); @@ -2023,6 +2032,18 @@ public class SDLGPURenderBackend extends RenderBackend { } } + @Override public void framebufferTexture1D(int target, int attachment, int textarget, int texture, int level) { + framebufferTexture2D(target, attachment, GL11.GL_TEXTURE_2D, texture, level); + } + + @Override public void framebufferTexture3D(int target, int attachment, int textarget, int texture, int level, int zoffset) { + throw new UnsupportedOperationException("SDL-GPU FboState attachments store only texture and mip level; no depth-plane (layer_or_depth_plane) slot for glFramebufferTexture3D"); + } + + @Override public void framebufferTextureLayer(int target, int attachment, int texture, int level, int layer) { + throw new UnsupportedOperationException("SDL-GPU FboState attachments store only texture and mip level; no array-layer slot for glFramebufferTextureLayer"); + } + @Override public void framebufferTexture(int target, int attachment, int texture, int level) { framebufferTexture2D(target, attachment, GL11.GL_TEXTURE_2D, texture, level); } @@ -3832,6 +3853,7 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public void textureImage2DEXT(int texture, int target, int level, int internalformat, int width, int height, int border, int format, int type, ByteBuffer pixels) { if (texture == 0) return; final ContextState st = s(); + if (dropStorageIfEmpty(st, texture, level, width, height, 1)) return; if (level == 0) { ensureLevel0Storage(st, texture, target, internalformat, width, height, 1, format, true); } else if (!growForLevel(texture, level)) { @@ -4068,7 +4090,7 @@ public class SDLGPURenderBackend extends RenderBackend { case GL11.GL_TEXTURE_WRAP_S -> ss.wrapS; case GL11.GL_TEXTURE_WRAP_T -> ss.wrapT; case GL12.GL_TEXTURE_WRAP_R -> ss.wrapR; - case GL12.GL_TEXTURE_MAX_LEVEL -> ss.maxLevel; + case GL12.GL_TEXTURE_MAX_LEVEL -> ss.maxLevel < 0 ? 1000 : ss.maxLevel; default -> 0; }; } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerState.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerState.java index 935804942..ab2572293 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerState.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/TextureSamplerState.java @@ -33,7 +33,8 @@ public final class TextureSamplerState { case GL11.GL_TEXTURE_WRAP_S -> { if (wrapS == param) return false; wrapS = param; } case GL11.GL_TEXTURE_WRAP_T -> { if (wrapT == param) return false; wrapT = param; } case GL12.GL_TEXTURE_WRAP_R -> { if (wrapR == param) return false; wrapR = param; } - case GL12.GL_TEXTURE_MAX_LEVEL -> { maxLevel = param; return false; } + // 1000 must map to unset, or every rescued texture allocates a full mip chain. + case GL12.GL_TEXTURE_MAX_LEVEL -> { maxLevel = param >= 1000 ? -1 : param; return false; } case GL14.GL_TEXTURE_COMPARE_MODE -> { if (compareMode == param) return false; compareMode = param; } case GL14.GL_TEXTURE_COMPARE_FUNC -> { if (compareFunc == param) return false; compareFunc = param; } case GL12.GL_TEXTURE_MIN_LOD, GL12.GL_TEXTURE_MAX_LOD, EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT -> { return setf(pname, (float) param); } diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlSamplerOverflowTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlSamplerOverflowTest.java index f0854f1f9..d945e7e6f 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlSamplerOverflowTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlSamplerOverflowTest.java @@ -47,6 +47,7 @@ class GlsmSdlSamplerOverflowTest { private static final int FETCH_GREEN = 17; private static final int FILTERED_BLUE = 15; private static final int EXPECTED = 0xFF4080C0; + private static final int RELEASE_DELAY_FRAMES = Reflect.getStatic(GLStateManager.class, "RELEASE_DELAY_FRAMES"); private final int[] textures = new int[SAMPLERS]; private int program; @@ -282,9 +283,12 @@ class GlsmSdlSamplerOverflowTest { GLStateManager.glDeleteTextures(textures[FETCH_RED]); textures[FETCH_RED] = 0; - GlsmSdlHeadlessRig.beginFrame(); - GlsmSdlHeadlessRig.endFrame(); - assertEquals(beforeDelete - 1, liveTextureCount(), "the copy is released while GLSM retains a 1x1 placeholder for the deleted name"); + for (int i = 0; i < RELEASE_DELAY_FRAMES; i++) { + GlsmSdlHeadlessRig.beginFrame(); + GLStateManager.onFrameEnd(); + GlsmSdlHeadlessRig.endFrame(); + } + assertEquals(beforeDelete - 2, liveTextureCount(), "the deleted texture and its copy are released after the delay"); } @Test diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlVanillaTextureTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlVanillaTextureTest.java index 19b0c5de7..03fa20c9b 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlVanillaTextureTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/glsm/GlsmSdlVanillaTextureTest.java @@ -1,6 +1,9 @@ package com.gtnewhorizons.angelica.sdlgpu.glsm; import com.gtnewhorizons.angelica.glsm.GLStateManager; +import com.gtnewhorizons.angelica.glsm.backend.BackendManager; +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; +import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -11,6 +14,7 @@ import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL14; +import java.nio.ByteBuffer; import java.nio.IntBuffer; import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.SIZE; @@ -18,6 +22,7 @@ import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.describe import static com.gtnewhorizons.angelica.sdlgpu.glsm.GlsmSdlHeadlessRig.pixelAt; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @Tag("glsm-sdl") @@ -37,6 +42,7 @@ class GlsmSdlVanillaTextureTest { }; private static final int LIGHTMAP_SIZE = 16; + private static final int RELEASE_DELAY_FRAMES = Reflect.getStatic(GLStateManager.class, "RELEASE_DELAY_FRAMES"); private static final int CLEAR_COLOR = 0xFF000000; private static int atlasTexture; @@ -259,4 +265,66 @@ class GlsmSdlVanillaTextureTest { final int actual = center(); assertColorNear(expected, actual, 2, "display list with bindings outside"); } + + private static void texImageEmpty(int level) { + GLStateManager.glTexImage2D(GL11.GL_TEXTURE_2D, level, GL11.GL_RGBA8, 0, 0, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, (ByteBuffer) null); + } + + @Test + void zeroSizedLevelZeroDropsStorageAndRespecifyRecreatesIt() { + final ResourceManager rm = GlsmSdlHeadlessRig.resourceManager(); + final int tex = GlsmSdlHeadlessRig.newNearestTexture(); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 2); + GlsmSdlHeadlessRig.uploadSolidLevel(0, 4, LEVEL_COLORS[0]); + assertEquals(3, rm.getTextureMeta(tex).levels(), "MAX_LEVEL 2 allocates the 4x4 chain"); + final long handle = rm.getTextureHandle(tex); + + texImageEmpty(1); + assertEquals(handle, rm.getTextureHandle(tex), "zero-sized level 1 must leave storage alone"); + + texImageEmpty(0); + assertEquals(0L, rm.getTextureHandle(tex), "zero-sized level 0 must release the handle"); + assertNull(rm.getTextureMeta(tex), "zero-sized level 0 must drop the meta"); + texImageEmpty(1); + assertEquals(0L, rm.getTextureHandle(tex), "zero-sized level 1 on an empty texture must not allocate"); + + GlsmSdlHeadlessRig.uploadSolidLevel(0, 4, LEVEL_COLORS[0]); + assertNotEquals(0L, rm.getTextureHandle(tex), "respecified level 0 must recreate storage"); + assertEquals(4, rm.getTextureMeta(tex).width(), "recreated width"); + assertEquals(3, rm.getTextureMeta(tex).levels(), "MAX_LEVEL survives the release"); + GLStateManager.glDeleteTextures(tex); + } + + @Test + void rescuedTextureAllocatesOneLevelUntilAHigherLevelIsUploaded() { + final ResourceManager rm = GlsmSdlHeadlessRig.resourceManager(); + final int tex = GlsmSdlHeadlessRig.newNearestTexture(); + GLStateManager.glTexParameteri(GL11.GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 2); + GLStateManager.glDeleteTextures(tex); + GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, tex); + GlsmSdlHeadlessRig.uploadSolidLevel(0, 4, LEVEL_COLORS[0]); + assertEquals(1, rm.getTextureMeta(tex).levels(), "level-0 upload"); + GlsmSdlHeadlessRig.uploadSolidLevel(1, 2, LEVEL_COLORS[1]); + assertEquals(3, rm.getTextureMeta(tex).levels(), "level-1 upload grows to the full chain"); + GLStateManager.glDeleteTextures(tex); + } + + @Test + void deletingA2DTextureKeepsTheUnitsNon2DBinding() { + final ResourceManager rm = GlsmSdlHeadlessRig.resourceManager(); + final int deleted = GlsmSdlHeadlessRig.newNearestTexture(); + GlsmSdlHeadlessRig.uploadSolidLevel(0, 4, LEVEL_COLORS[0]); + final int volume = GLStateManager.glGenTextures(); + GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, volume); + + GLStateManager.glDeleteTextures(deleted); + assertEquals(volume, BackendManager.RENDER_BACKEND.getInteger(GL12.GL_TEXTURE_BINDING_3D), "after the delete"); + for (int i = 0; i < RELEASE_DELAY_FRAMES; i++) GLStateManager.onFrameEnd(); + + assertEquals(0L, rm.getTextureHandle(deleted), "the frame-end release must have run"); + assertEquals(volume, BackendManager.RENDER_BACKEND.getInteger(GL12.GL_TEXTURE_BINDING_3D), "after the release"); + GLStateManager.glBindTexture(GL12.GL_TEXTURE_3D, 0); + GLStateManager.glDeleteTextures(volume); + } + } diff --git a/src/main/java/com/gtnewhorizons/angelica/profiling/TracyFramePlots.java b/src/main/java/com/gtnewhorizons/angelica/profiling/TracyFramePlots.java index ff654f92f..fb32ccb35 100644 --- a/src/main/java/com/gtnewhorizons/angelica/profiling/TracyFramePlots.java +++ b/src/main/java/com/gtnewhorizons/angelica/profiling/TracyFramePlots.java @@ -35,6 +35,7 @@ public final class TracyFramePlots { private static final PlotDelta P_GL_DRAW_CALLS = new PlotDelta("gl.drawCalls"); private static final PlotDelta P_GL_TEX_BIND_MISSES = new PlotDelta("gl.texBindMisses"); private static final PlotDelta P_GL_PROGRAM_SWITCHES = new PlotDelta("gl.programSwitches"); + private static final long P_GL_TEX_AWAITING_RELEASE = Tracy.plotHandle("gl.texAwaitingRelease"); private static final PlotDelta P_GLSM_BRACKETS = new PlotDelta("glsm.brackets"); private static final PlotDelta P_GLSM_COW_SLOTS = new PlotDelta("glsm.cowSlots"); private static final PlotDelta P_GLSM_POP_RESTORES = new PlotDelta("glsm.popRestores"); @@ -191,6 +192,7 @@ public final class TracyFramePlots { P_GL_DRAW_CALLS.plot(GLStateManager.drawCalls); P_GL_TEX_BIND_MISSES.plot(GLStateManager.texBindMisses); P_GL_PROGRAM_SWITCHES.plot(GLStateManager.programSwitches); + Tracy.plotInt(P_GL_TEX_AWAITING_RELEASE, GLStateManager.deferredTexturesAwaitingRelease()); P_GLSM_BRACKETS.plot(GLStateManager.attribPushes); P_GLSM_COW_SLOTS.plot(GLStateManager.attribSlotsSaved); P_GLSM_POP_RESTORES.plot(GLStateManager.attribValueRestores);