package net.coderbot.iris.postprocess; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.RenderSystem; import it.unimi.dsi.fastutil.ints.IntList; import it.unimi.dsi.fastutil.objects.Object2ObjectMap; import net.coderbot.iris.features.FeatureFlags; import net.coderbot.iris.gl.framebuffer.GlFramebuffer; import net.coderbot.iris.gl.image.GlImage; import net.coderbot.iris.gl.program.ComputeProgram; import net.coderbot.iris.gl.program.Program; import net.coderbot.iris.gl.program.ProgramBuilder; import net.coderbot.iris.gl.program.ProgramSamplers; import net.coderbot.iris.gl.program.ProgramUniforms; import net.coderbot.iris.gl.sampler.SamplerLimits; import net.coderbot.iris.gl.state.FogMode; import net.coderbot.iris.gl.texture.TextureAccess; import net.coderbot.iris.pipeline.PatchedShaderPrinter; import net.coderbot.iris.pipeline.WorldRenderingPipeline; import net.coderbot.iris.pipeline.transform.PatchShaderType; import net.coderbot.iris.pipeline.transform.TransformPatcher; import net.coderbot.iris.rendertarget.IRenderTargetExt; import net.coderbot.iris.rendertarget.RenderTarget; import net.coderbot.iris.rendertarget.RenderTargets; import net.coderbot.iris.samplers.IrisImages; import net.coderbot.iris.samplers.IrisSamplers; import net.coderbot.iris.shaderpack.ComputeSource; import net.coderbot.iris.shaderpack.ProgramDirectives; import net.coderbot.iris.shaderpack.ProgramSet; import net.coderbot.iris.shaderpack.ProgramSource; import net.coderbot.iris.shaderpack.texture.TextureStage; import net.coderbot.iris.shadows.ShadowRenderTargets; import net.coderbot.iris.uniforms.CommonUniforms; import net.coderbot.iris.uniforms.FrameUpdateNotifier; import net.coderbot.iris.uniforms.custom.CustomUniforms; import net.minecraft.client.Minecraft; import net.minecraft.client.shader.Framebuffer; import org.jetbrains.annotations.Nullable; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL13; import org.lwjgl.opengl.GL30; import org.lwjgl.opengl.GL42; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.function.Supplier; public class FinalPassRenderer { private final RenderTargets renderTargets; @Nullable private final Pass finalPass; private final ImmutableList swapPasses; private final GlFramebuffer baseline; private final GlFramebuffer colorHolder; private int lastColorTextureId; private int lastColorTextureVersion; private final TextureAccess noiseTexture; private final FrameUpdateNotifier updateNotifier; private final CenterDepthSampler centerDepthSampler; private final Object2ObjectMap customTextureIds; private final CustomUniforms customUniforms; @Nullable private final Set customImages; @Nullable private final Object2ObjectMap irisCustomTextures; @Nullable private final WorldRenderingPipeline pipeline; public FinalPassRenderer(ProgramSet pack, RenderTargets renderTargets, TextureAccess noiseTexture, FrameUpdateNotifier updateNotifier, ImmutableSet flippedBuffers, CenterDepthSampler centerDepthSampler, Supplier shadowTargetsSupplier, Object2ObjectMap customTextureIds, ImmutableSet flippedAtLeastOnce, CustomUniforms customUniforms, CompletableFuture> precomputedTransformFuture) { this(pack, new ProgramBuildContext(renderTargets, noiseTexture, updateNotifier, centerDepthSampler, shadowTargetsSupplier, customTextureIds, customUniforms, null, null, null), flippedBuffers, flippedAtLeastOnce, precomputedTransformFuture, "final"); } public FinalPassRenderer(ProgramSet pack, ProgramBuildContext context, ImmutableSet flippedBuffers, ImmutableSet flippedAtLeastOnce, CompletableFuture> precomputedTransformFuture, String stageName) { this.updateNotifier = context.updateNotifier(); this.centerDepthSampler = context.centerDepthSampler(); this.customTextureIds = context.customTextureIds(); this.noiseTexture = context.noiseTexture(); this.renderTargets = context.renderTargets(); this.customUniforms = context.customUniforms(); this.customImages = context.customImages(); this.irisCustomTextures = context.irisCustomTextures(); this.pipeline = context.pipeline(); this.finalPass = pack.getCompositeFinal().filter(ProgramSource::isValid).map(source -> { final Pass pass = new Pass(); final ProgramDirectives directives = source.getDirectives(); final Map transformed = getTransformed(source, precomputedTransformFuture, stageName); pass.program = createProgramFromTransformed(source, transformed, flippedBuffers, flippedAtLeastOnce, context.shadowTargetsSupplier()); pass.computes = createComputes(pack.getFinalCompute(), flippedBuffers, flippedAtLeastOnce, context.shadowTargetsSupplier()); pass.stageReadsFromAlt = flippedBuffers; pass.mipmappedBuffers = directives.getMipmappedBuffers(); return pass; }).orElse(null); final IntList buffersToBeCleared = pack.getPackDirectives().getRenderTargetDirectives().getBuffersToBeCleared(); // The name of this method might seem a bit odd here, but we want a framebuffer with color attachments that line // up with whatever was written last (since we're reading from these framebuffers) instead of trying to create // a framebuffer with color attachments different from what was written last (as we do with normal composite // passes that write to framebuffers). final Framebuffer main = Minecraft.getMinecraft().getFramebuffer(); this.baseline = renderTargets.createGbufferFramebuffer(flippedBuffers, new int[] {0}); this.colorHolder = new GlFramebuffer(); this.lastColorTextureId = main.framebufferTexture; this.lastColorTextureVersion = ((IRenderTargetExt)main).iris$getColorBufferVersion(); this.colorHolder.addColorAttachment(0, lastColorTextureId); // TODO: We don't actually fully swap the content, we merely copy it from alt to main // This works for the most part, but it's not perfect. A better approach would be creating secondary // framebuffers for every other frame, but that would be a lot more complex... final ImmutableList.Builder swapPasses = ImmutableList.builder(); flippedBuffers.forEach((i) -> { final int target = i; if (buffersToBeCleared.contains(target)) { return; } final SwapPass swap = new SwapPass(); final RenderTarget target1 = renderTargets.get(target); swap.target = target; swap.width = target1.getWidth(); swap.height = target1.getHeight(); // Non-flipped buffers write to ALT, copy ALT→MAIN to preserve data swap.from = renderTargets.createColorFramebuffer(ImmutableSet.of(), new int[] {target}); swap.targetTexture = renderTargets.get(target).getMainTexture(); swapPasses.add(swap); }); this.swapPasses = swapPasses.build(); GLStateManager.glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, 0); } private Map getTransformed(ProgramSource source, CompletableFuture> precomputedTransformFuture, String stageName) { if (precomputedTransformFuture != null) { try { final Map result = precomputedTransformFuture.join(); if (result != null) { return result; } } catch (CompletionException e) { throw new RuntimeException("Shader transformation failed for '" + source.getName() + "' in stage '" + stageName + "'", e.getCause() != null ? e.getCause() : e); } } return TransformPatcher.patchComposite( source.getVertexSource().orElseThrow(NullPointerException::new), source.getGeometrySource().orElse(null), source.getTessControlSource().orElse(null), source.getTessEvalSource().orElse(null), source.getFragmentSource().orElseThrow(NullPointerException::new), TextureStage.COMPOSITE_AND_FINAL, pipeline != null ? pipeline.getTextureMap() : null); } private static final class Pass { Program program; ComputeProgram[] computes; ImmutableSet stageReadsFromAlt; ImmutableSet mipmappedBuffers; private void destroy() { this.program.destroy(); } } private static final class SwapPass { public int target; public int width; public int height; GlFramebuffer from; int targetTexture; } public void renderFinalPass() { GLStateManager.disableBlend(); GLStateManager.disableAlphaTest(); GLStateManager.glDepthMask(false); final Framebuffer main = Minecraft.getMinecraft().getFramebuffer(); final int baseWidth = main.framebufferWidth; final int baseHeight = main.framebufferHeight; // Note that since DeferredWorldRenderingPipeline uses the depth texture of the main Minecraft framebuffer, // we'll be writing to that depth buffer directly automatically and won't need to futz around with copying // depth buffer content. // // Previously, we had our own depth texture and then copied its content to the main Minecraft framebuffer. // This worked with vanilla, but broke with mods that used the stencil buffer. // // This approach is a fairly succinct solution to the issue of having to deal with the main Minecraft // framebuffer potentially having a depth-stencil buffer or similar - we'll automatically enable that to // work properly since we re-use the depth buffer instead of trying to make our own. // // This is not a concern for depthtex1 / depthtex2 since the copy call extracts the depth values, and the // shader pack only ever uses them to read the depth values. if (((IRenderTargetExt)main).iris$getColorBufferVersion() != lastColorTextureVersion || main.framebufferTexture != lastColorTextureId) { lastColorTextureVersion = ((IRenderTargetExt)main).iris$getColorBufferVersion(); this.lastColorTextureId = main.framebufferTexture; colorHolder.addColorAttachment(0, lastColorTextureId); } if (this.finalPass != null) { // If there is a final pass, we use the shader-based full screen quad rendering pathway instead of just copying the color buffer. colorHolder.bind(); FullScreenQuadRenderer.INSTANCE.begin(); for (ComputeProgram computeProgram : finalPass.computes) { if (computeProgram != null) { computeProgram.use(); this.customUniforms.push(computeProgram); computeProgram.dispatch(baseWidth, baseHeight); } } RenderSystem.memoryBarrier(GL42.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL42.GL_TEXTURE_FETCH_BARRIER_BIT); if (!finalPass.mipmappedBuffers.isEmpty()) { GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); for (int index : finalPass.mipmappedBuffers) { setupMipmapping(renderTargets.get(index), finalPass.stageReadsFromAlt.contains(index)); } } finalPass.program.use(); this.customUniforms.push(finalPass.program); FullScreenQuadRenderer.uploadCompositeMatrices(); FullScreenQuadRenderer.INSTANCE.renderQuad(); FullScreenQuadRenderer.end(); } else { // If there are no passes, we somehow need to transfer the content of the Iris color render targets into // the main Minecraft framebuffer. // // Thus, the following call transfers the content of colortex0 into the main Minecraft framebuffer. // // Note that glCopyTexSubImage2D is not as strict as glBlitFramebuffer, so we don't have to worry about // colortex0 having a weird format. This should just work. // // We could have used a shader here, but it should be about the same performance either way: // https://stackoverflow.com/a/23994979/18166885 this.baseline.bindAsReadBuffer(); RenderSystem.copyTexSubImage2D(main.framebufferTexture, GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 0, baseWidth, baseHeight); } GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); for (int i = 0; i < renderTargets.getRenderTargetCount(); i++) { // Reset mipmapping states at the end of the frame. resetRenderTarget(renderTargets.get(i)); } for (SwapPass swapPass : swapPasses) { // NB: We need to use bind(), not bindAsReadBuffer()... Previously we used bindAsReadBuffer() here which // broke TAA on many packs and on many drivers. // // Note that glCopyTexSubImage2D reads from the current GL_READ_BUFFER (given by glReadBuffer()) for the // current framebuffer bound to GL_FRAMEBUFFER, but that is distinct from the current GL_READ_FRAMEBUFFER, // which is what bindAsReadBuffer() binds. // // Also note that RenderTargets already calls readBuffer(0) for us. swapPass.from.bind(); GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, swapPass.targetTexture); GLStateManager.glCopyTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 0, swapPass.width, swapPass.height); } // Make sure to reset the viewport to how it was before... Otherwise weird issues could occur. // Also bind the "main" framebuffer if it isn't already bound. main.bindFramebuffer(true); ProgramUniforms.clearActiveUniforms(); ProgramSamplers.clearActiveSamplers(); GLStateManager.glUseProgram(0); for (int i = 0; i < SamplerLimits.get().getMaxTextureUnits(); i++) { // Unbind all textures that we may have used. // NB: This is necessary for shader pack reloading to work properly GLStateManager.glActiveTexture(GL13.GL_TEXTURE0 + i); GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); } GLStateManager.glActiveTexture(GL13.GL_TEXTURE0); } public void recalculateSwapPassSize() { for (SwapPass swapPass : swapPasses) { final RenderTarget target = renderTargets.get(swapPass.target); renderTargets.destroyFramebuffer(swapPass.from); // Match constructor logic - swap passes are only for non-flipped buffers // Copy ALT→MAIN to preserve data written to ALT swapPass.from = renderTargets.createColorFramebuffer(ImmutableSet.of(), new int[] {swapPass.target}); swapPass.width = target.getWidth(); swapPass.height = target.getHeight(); swapPass.targetTexture = target.getMainTexture(); } } private static void setupMipmapping(RenderTarget target, boolean readFromAlt) { final int texture = readFromAlt ? target.getAltTexture() : target.getMainTexture(); // TODO: Only generate the mipmap if a valid mipmap hasn't been generated or if we've written to the buffer // (since the last mipmap was generated) // // NB: We leave mipmapping enabled even if the buffer is written to again, this appears to match the // behavior of ShadersMod/OptiFine, however I'm not sure if it's desired behavior. It's possible that a // program could use mipmapped sampling with a stale mipmap, which probably isn't great. However, the // sampling mode is always reset between frames, so this only persists after the first program to use // mipmapping on this buffer. // // Also note that this only applies to one of the two buffers in a render target buffer pair - making it // unlikely that this issue occurs in practice with most shader packs. RenderSystem.generateMipmaps(texture, GL11.GL_TEXTURE_2D); int filter = GL11.GL_LINEAR_MIPMAP_LINEAR; if (target.getInternalFormat().getPixelFormat().isInteger()) { filter = GL11.GL_NEAREST_MIPMAP_NEAREST; } RenderSystem.texParameteri(texture, GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter); } private static void resetRenderTarget(RenderTarget target) { // Resets the sampling mode of the given render target and then unbinds it to prevent accidental sampling of it // elsewhere. int filter = GL11.GL_LINEAR; if (target.getInternalFormat().getPixelFormat().isInteger()) { filter = GL11.GL_NEAREST; } RenderSystem.texParameteri(target.getMainTexture(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter); RenderSystem.texParameteri(target.getAltTexture(), GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filter); GLStateManager.glBindTexture(GL11.GL_TEXTURE_2D, 0); } // TODO: Don't just copy this from DeferredWorldRenderingPipeline private Program createProgramFromTransformed(ProgramSource source, Map transformed, ImmutableSet flipped, ImmutableSet flippedAtLeastOnceSnapshot, Supplier shadowTargetsSupplier) { final String vertex = transformed.get(PatchShaderType.VERTEX); final String geometry = transformed.get(PatchShaderType.GEOMETRY); final String tessControl = transformed.get(PatchShaderType.TESS_CONTROL); final String tessEval = transformed.get(PatchShaderType.TESS_EVAL); final String fragment = transformed.get(PatchShaderType.FRAGMENT); PatchedShaderPrinter.debugPatchedShaders(source.getName(), vertex, geometry, tessControl, tessEval, fragment); Objects.requireNonNull(flipped); final ProgramBuilder builder; try { builder = ProgramBuilder.begin(source.getName(), vertex, geometry, tessControl, tessEval, fragment, IrisSamplers.COMPOSITE_RESERVED_TEXTURE_UNITS); } catch (RuntimeException e) { // TODO: Better error handling throw new RuntimeException("Shader compilation failed!", e); } this.customUniforms.assignTo(builder); final ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = ProgramSamplers.customTextureSamplerInterceptor(builder, customTextureIds, flippedAtLeastOnceSnapshot); CommonUniforms.addDynamicUniforms(builder, FogMode.OFF); IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, () -> flipped, renderTargets, true, pipeline); // Bind custom images as samplers BEFORE render target images IrisSamplers.addCustomImages(customTextureSamplerInterceptor, customImages); // Bind custom textures (PNG files from shader pack) IrisSamplers.addCustomTextures(customTextureSamplerInterceptor, irisCustomTextures); IrisImages.addRenderTargetImages(builder, () -> flipped, renderTargets); // Bind custom images as image units AFTER render target images IrisImages.addCustomImages(builder, customImages); IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, noiseTexture); IrisSamplers.addCompositeSamplers(customTextureSamplerInterceptor, renderTargets); if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowTargetsSupplier.get(), null, pipeline != null && pipeline.hasFeature(FeatureFlags.SEPARATE_HARDWARE_SAMPLERS)); IrisImages.addShadowColorImages(builder, shadowTargetsSupplier.get(), null); } // TODO: Don't duplicate this with CompositeRenderer centerDepthSampler.setUsage(builder.addDynamicSampler(centerDepthSampler::getCenterDepthTexture, "iris_centerDepthSmooth")); Program build = builder.build(); this.customUniforms.mapholderToPass(builder, build); return build; } private ComputeProgram[] createComputes(ComputeSource[] compute, ImmutableSet flipped, ImmutableSet flippedAtLeastOnceSnapshot, Supplier shadowTargetsSupplier) { final ComputeProgram[] programs = new ComputeProgram[compute.length]; for (int i = 0; i < programs.length; i++) { final ComputeSource source = compute[i]; if (source == null || !source.getSource().isPresent()) { continue; } else { // TODO: Properly handle empty shaders Objects.requireNonNull(flipped); final ProgramBuilder builder; try { String transformed = TransformPatcher.patchCompute(source.getName(), source.getSource().orElse(null), TextureStage.COMPOSITE_AND_FINAL, pipeline != null ? pipeline.getTextureMap() : null); PatchedShaderPrinter.debugPatchedShaders(source.getName() + "_compute", null, null, null, transformed); builder = ProgramBuilder.beginCompute(source.getName(), transformed, IrisSamplers.COMPOSITE_RESERVED_TEXTURE_UNITS); } catch (RuntimeException e) { // TODO: Better error handling throw new RuntimeException("Shader compilation failed!", e); } ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = ProgramSamplers.customTextureSamplerInterceptor(builder, customTextureIds, flippedAtLeastOnceSnapshot); customUniforms.assignTo(builder); CommonUniforms.addDynamicUniforms(builder, FogMode.OFF); IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, () -> flipped, renderTargets, true, pipeline); IrisSamplers.addCustomImages(customTextureSamplerInterceptor, customImages); IrisSamplers.addCustomTextures(customTextureSamplerInterceptor, irisCustomTextures); IrisImages.addRenderTargetImages(builder, () -> flipped, renderTargets); IrisImages.addCustomImages(builder, customImages); IrisSamplers.addNoiseSampler(customTextureSamplerInterceptor, noiseTexture); IrisSamplers.addCompositeSamplers(customTextureSamplerInterceptor, renderTargets); if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowTargetsSupplier.get(), null, pipeline != null && pipeline.hasFeature(FeatureFlags.SEPARATE_HARDWARE_SAMPLERS)); IrisImages.addShadowColorImages(builder, shadowTargetsSupplier.get(), null); } // TODO: Don't duplicate this with FinalPassRenderer centerDepthSampler.setUsage(builder.addDynamicSampler(centerDepthSampler::getCenterDepthTexture, "iris_centerDepthSmooth")); programs[i] = builder.buildCompute(); this.customUniforms.mapholderToPass(builder, programs[i]); programs[i].setWorkGroupInfo(source.getWorkGroupRelative(), source.getWorkGroups(), null); } } return programs; } public void destroy() { if (finalPass != null) { finalPass.destroy(); } colorHolder.destroy(); } }