package net.coderbot.iris.rendertarget; import com.google.common.collect.ImmutableSet; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.RenderSystem; import lombok.Getter; import net.coderbot.iris.Iris; import net.coderbot.iris.gl.framebuffer.GlFramebuffer; import net.coderbot.iris.gl.texture.DepthBufferFormat; import net.coderbot.iris.gl.texture.DepthCopyStrategy; import net.coderbot.iris.shaderpack.PackDirectives; import net.coderbot.iris.shaderpack.PackRenderTargetDirectives; import org.joml.Vector2i; import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.List; import java.util.Map; public class RenderTargets { private final RenderTarget[] targets; private int currentDepthTexture; private DepthBufferFormat currentDepthFormat; @Getter private final DepthTexture noTranslucents; private final DepthTexture noHand; private final GlFramebuffer depthSourceFb; private final GlFramebuffer noTranslucentsDestFb; private final GlFramebuffer noHandDestFb; private DepthCopyStrategy copyStrategy; private final List ownedFramebuffers; private int cachedWidth; private int cachedHeight; @Getter private boolean fullClearRequired; private boolean translucentDepthDirty; private boolean handDepthDirty; private int cachedDepthBufferVersion; public RenderTargets(int width, int height, int depthTexture, int depthBufferVersion, DepthBufferFormat depthFormat, Map renderTargets, PackDirectives packDirectives) { targets = new RenderTarget[renderTargets.size()]; renderTargets.forEach((index, settings) -> { // TODO: Handle mipmapping? Vector2i dimensions = packDirectives.getTextureScaleOverride(index, width, height); // Apply format fallback for opengl versions with limited color-renderable format support (e.g., macOS GL 2.1) var requestedFormat = settings.getInternalFormat(); var actualFormat = requestedFormat.getColorRenderableFallback(); if (actualFormat != requestedFormat) { Iris.logger.info("Render target {} using fallback format {} (requested {})", index, actualFormat, requestedFormat); } targets[index] = RenderTarget.builder().setDimensions(dimensions.x, dimensions.y) .setInternalFormat(actualFormat) .setPixelFormat(actualFormat.getPixelFormat()).build(); }); this.currentDepthTexture = depthTexture; this.currentDepthFormat = depthFormat; this.copyStrategy = DepthCopyStrategy.fastest(currentDepthFormat.isCombinedStencil()); this.cachedWidth = width; this.cachedHeight = height; this.cachedDepthBufferVersion = depthBufferVersion; this.ownedFramebuffers = new ArrayList<>(); // NB: Make sure all buffers are cleared so that they don't contain undefined // data. Otherwise very weird things can happen. fullClearRequired = true; this.depthSourceFb = createFramebufferWritingToMain(new int[] {0}); this.noTranslucents = new DepthTexture(width, height, currentDepthFormat); this.noHand = new DepthTexture(width, height, currentDepthFormat); this.noTranslucentsDestFb = createFramebufferWritingToMain(new int[] {0}); this.noTranslucentsDestFb.addDepthAttachment(this.noTranslucents.getTextureId()); this.noHandDestFb = createFramebufferWritingToMain(new int[] {0}); this.noHandDestFb.addDepthAttachment(this.noHand.getTextureId()); this.translucentDepthDirty = true; this.handDepthDirty = true; } public void destroy() { for (GlFramebuffer owned : ownedFramebuffers) { owned.destroy(); } for (RenderTarget target : targets) { target.destroy(); } noTranslucents.destroy(); noHand.destroy(); } public int getRenderTargetCount() { return targets.length; } public RenderTarget get(int index) { return targets[index]; } public int getDepthTexture() { return currentDepthTexture; } public DepthTexture getDepthTextureNoTranslucents() { return noTranslucents; } public DepthTexture getDepthTextureNoHand() { return noHand; } public boolean resizeIfNeeded(int newDepthBufferVersion, int newDepthTextureId, int newWidth, int newHeight, DepthBufferFormat newDepthFormat, PackDirectives packDirectives) { boolean recreateDepth = false; if (cachedDepthBufferVersion != newDepthBufferVersion) { recreateDepth = true; currentDepthTexture = newDepthTextureId; cachedDepthBufferVersion = newDepthBufferVersion; } boolean sizeChanged = newWidth != cachedWidth || newHeight != cachedHeight; boolean depthFormatChanged = newDepthFormat != currentDepthFormat; if (depthFormatChanged) { currentDepthFormat = newDepthFormat; // Might need a new copy strategy copyStrategy = DepthCopyStrategy.fastest(currentDepthFormat.isCombinedStencil()); } if (recreateDepth) { // Re-attach the depth textures with the new depth texture ID, since Minecraft re-creates // the depth texture when resizing its render targets. // // I'm not sure if our framebuffers holding on to the old depth texture between frames // could be a concern, in the case of resizing and similar. I think it should work // based on what I've seen of the spec, though - it seems like deleting a texture // automatically detaches it from its framebuffers. for (GlFramebuffer framebuffer : ownedFramebuffers) { if (framebuffer == noHandDestFb || framebuffer == noTranslucentsDestFb) { // NB: Do not change the depth attachment of these framebuffers // as it is intentionally different continue; } if (framebuffer.hasDepthAttachment()) { framebuffer.addDepthAttachment(newDepthTextureId); } } } if (depthFormatChanged || sizeChanged) { // Reallocate depth buffers noTranslucents.resize(newWidth, newHeight, newDepthFormat); noHand.resize(newWidth, newHeight, newDepthFormat); this.translucentDepthDirty = true; this.handDepthDirty = true; } if (sizeChanged) { cachedWidth = newWidth; cachedHeight = newHeight; for (int i = 0; i < targets.length; i++) { targets[i].resize(packDirectives.getTextureScaleOverride(i, newWidth, newHeight)); } fullClearRequired = true; } return sizeChanged; } public void copyPreTranslucentDepth() { if (translucentDepthDirty) { translucentDepthDirty = false; GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, noTranslucents.getTextureId()); depthSourceFb.bindAsReadBuffer(); RenderSystem.copyTexImage2D(GL11.GL_TEXTURE_2D, 0, currentDepthFormat.getGlInternalFormat(), 0, 0, cachedWidth, cachedHeight, 0); } else { copyStrategy.copy(depthSourceFb, getDepthTexture(), noTranslucentsDestFb, noTranslucents.getTextureId(), getCurrentWidth(), getCurrentHeight()); } } public void copyPreHandDepth() { if (handDepthDirty) { handDepthDirty = false; GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, noHand.getTextureId()); depthSourceFb.bindAsReadBuffer(); RenderSystem.copyTexImage2D(GL11.GL_TEXTURE_2D, 0, currentDepthFormat.getGlInternalFormat(), 0, 0, cachedWidth, cachedHeight, 0); } else { copyStrategy.copy(depthSourceFb, getDepthTexture(), noHandDestFb, noHand.getTextureId(), getCurrentWidth(), getCurrentHeight()); } } public void onFullClear() { fullClearRequired = false; } public GlFramebuffer createFramebufferWritingToMain(int[] drawBuffers) { return createFullFramebuffer(false, drawBuffers); } public GlFramebuffer createFramebufferWritingToAlt(int[] drawBuffers) { return createFullFramebuffer(true, drawBuffers); } public GlFramebuffer createClearFramebuffer(boolean alt, int[] clearBuffers) { ImmutableSet stageWritesToMain = ImmutableSet.of(); if (!alt) { stageWritesToMain = invert(ImmutableSet.of(), clearBuffers); } return createColorFramebuffer(stageWritesToMain, clearBuffers); } private ImmutableSet invert(ImmutableSet base, int[] relevant) { ImmutableSet.Builder inverted = ImmutableSet.builder(); for (int i : relevant) { if (!base.contains(i)) { inverted.add(i); } } return inverted.build(); } private GlFramebuffer createEmptyFramebuffer() { GlFramebuffer framebuffer = new GlFramebuffer(); ownedFramebuffers.add(framebuffer); framebuffer.addDepthAttachment(currentDepthTexture); // NB: Before OpenGL 3.0, all framebuffers are required to have a color attachment no matter what. framebuffer.addColorAttachment(0, get(0).getMainTexture()); framebuffer.noDrawBuffers(); return framebuffer; } public GlFramebuffer createDHFramebuffer(ImmutableSet stageWritesToAlt, int[] drawBuffers) { if (drawBuffers.length == 0) { return createEmptyFramebuffer(); } ImmutableSet stageWritesToMain = invert(stageWritesToAlt, drawBuffers); return createColorFramebuffer(stageWritesToMain, drawBuffers); } public GlFramebuffer createGbufferFramebuffer(ImmutableSet stageWritesToAlt, int[] drawBuffers) { if (drawBuffers.length == 0) { return createEmptyFramebuffer(); } ImmutableSet stageWritesToMain = invert(stageWritesToAlt, drawBuffers); GlFramebuffer framebuffer = createColorFramebuffer(stageWritesToMain, drawBuffers); framebuffer.addDepthAttachment(currentDepthTexture); return framebuffer; } private GlFramebuffer createFullFramebuffer(boolean clearsAlt, int[] drawBuffers) { if (drawBuffers.length == 0) { return createEmptyFramebuffer(); } ImmutableSet stageWritesToMain = ImmutableSet.of(); if (!clearsAlt) { stageWritesToMain = invert(ImmutableSet.of(), drawBuffers); } return createColorFramebufferWithDepth(stageWritesToMain, drawBuffers); } public GlFramebuffer createColorFramebufferWithDepth(ImmutableSet stageWritesToMain, int[] drawBuffers) { final GlFramebuffer framebuffer = createColorFramebuffer(stageWritesToMain, drawBuffers); framebuffer.addDepthAttachment(currentDepthTexture); return framebuffer; } public GlFramebuffer createColorFramebuffer(ImmutableSet stageWritesToMain, int[] drawBuffers) { if (drawBuffers.length == 0) { throw new IllegalArgumentException("Framebuffer must have at least one color buffer"); } final GlFramebuffer framebuffer = new GlFramebuffer(); ownedFramebuffers.add(framebuffer); final int[] actualDrawBuffers = new int[drawBuffers.length]; for (int i = 0; i < drawBuffers.length; i++) { actualDrawBuffers[i] = i; if (drawBuffers[i] >= getRenderTargetCount()) { // TODO: This causes resource leaks, also we should really verify this in the shaderpack parser... throw new IllegalStateException("Render target with index " + drawBuffers[i] + " is not supported, only " + getRenderTargetCount() + " render targets are supported."); } final RenderTarget target = this.get(drawBuffers[i]); final int textureId = stageWritesToMain.contains(drawBuffers[i]) ? target.getMainTexture() : target.getAltTexture(); framebuffer.addColorAttachment(i, textureId); } framebuffer.drawBuffers(actualDrawBuffers); framebuffer.readBuffer(0); if (!framebuffer.isComplete()) { throw new IllegalStateException("Unexpected error while creating framebuffer"); } return framebuffer; } public void destroyFramebuffer(GlFramebuffer framebuffer) { framebuffer.destroy(); ownedFramebuffers.remove(framebuffer); } public int getCurrentWidth() { return cachedWidth; } public int getCurrentHeight() { return cachedHeight; } }