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 8f57da01c..f48d8a82c 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 @@ -276,6 +276,10 @@ public abstract class RenderBackend { public void voxelizeRegion(long pass, boolean rebindVertexBuffer, float x, float y, float z, int rangeBase, int rangeCount, int vertexTotal) {} public void endVoxelizationBatch(long pass) {} + public void setVertexWriteReplay(int graphicsProgram, VertexWriteReplaySetup setup) {} + + public boolean supportsComputeImageAtomics() { return true; } + public abstract void enable(int cap); public abstract void enablei(int cap, int index); public abstract void disable(int cap); diff --git a/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/VertexWriteReplaySetup.java b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/VertexWriteReplaySetup.java new file mode 100644 index 000000000..118be84d4 --- /dev/null +++ b/glsm/src/main/java/com/gtnewhorizons/angelica/glsm/backend/VertexWriteReplaySetup.java @@ -0,0 +1,23 @@ +package com.gtnewhorizons.angelica.glsm.backend; + +import java.util.function.IntSupplier; + +/** + * How to replay a graphics program's vertex-stage image writes. + * + * @param inputLocations per input, the declared location or -1 for whatever the graphics program linked + * @param images per image the replay uses, in binding order: the written ones, then the read-only ones + * @param writtenImageCount how many of {@code images} the replay writes + * @param samplerNames samplers the graphics program may not have, because only the stripped writes read them + * @param samplerUnits per sampler name, the texture unit it reads at each draw, or -1 to use samplerTextures + * @param samplerTextures per sampler name, its texture when samplerUnits is -1 + * @param samplerObjects per sampler name, the GL sampler object to sample it with when samplerUnits is -1, or 0 + * @param vertexBufferBinding storage buffer binding of the first vertex buffer the replay program declares + * @param vertexBufferCount how many vertex buffers it declares, at consecutive bindings + * @param indexBufferBinding storage buffer binding of its index buffer + */ +public record VertexWriteReplaySetup(int replayProgram, String[] inputNames, int[] inputLocations, + IntSupplier[] images, int writtenImageCount, + String[] samplerNames, int[] samplerUnits, IntSupplier[] samplerTextures, int[] samplerObjects, + int vertexBufferBinding, int vertexBufferCount, int indexBufferBinding) { +} 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 f22939b9d..a7dbb9007 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 @@ -1,6 +1,7 @@ package com.gtnewhorizons.angelica.sdlgpu; import com.gtnewhorizons.angelica.glsm.hooks.GLSMHooks; +import com.gtnewhorizons.angelica.sdlgpu.compute.VertexWriteReplayer; import com.gtnewhorizons.angelica.sdlgpu.compute.VoxelizationDispatcher; import com.gtnewhorizons.angelica.config.SystemProperties; import com.gtnewhorizons.angelica.glsm.CaptureGate; @@ -10,6 +11,7 @@ import com.gtnewhorizons.angelica.glsm.profiling.Tracy; import com.gtnewhorizons.angelica.glsm.backend.BackendOptions; import com.gtnewhorizons.angelica.glsm.backend.GLDebugMessageListener; import com.gtnewhorizons.angelica.glsm.backend.MainThreadPump; +import com.gtnewhorizons.angelica.glsm.backend.VertexWriteReplaySetup; import me.eigenraven.lwjgl3ify.client.MainThreadExec; import org.lwjglx.Lwjgl3ifyEventLoop; import com.gtnewhorizons.angelica.glsm.backend.VSyncMode; @@ -164,6 +166,7 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public long nextSeq() { return TransferThread.nextSeq(); } }); private final PipelineApplier pipelineApplier = new PipelineApplier(frameManager, resourceManager, shaderManager, pipelineStore, fboClearTracker, persistentSync, samplerBinder, storageTextureBinder, storageBufferBinder); + private final VertexWriteReplayer vertexWriteReplayer = new VertexWriteReplayer(shaderManager, resourceManager, pipelineApplier, computeBinder, SDLGPURenderBackend::s); private final AttachmentClear attachmentClear = new AttachmentClear(frameManager, pipelineStore, shaderManager); private final DepthStencilReadback depthStencilReadback = new DepthStencilReadback(frameManager, resourceManager, pipelineStore, shaderManager, fboClearTracker); private final DrawDispatch drawDispatch = new DrawDispatch(device, frameManager, resourceManager, pipelineApplier, this::enqueuePreCopied); @@ -301,6 +304,8 @@ public class SDLGPURenderBackend extends RenderBackend { } }); pipelineApplier.setDeferredUploadSink(this::enqueuePreCopied); + frameManager.setRenderPassEndHook(vertexWriteReplayer::flush); + resourceManager.setTextureReplaceHook(this::flushVertexWritesBeforeTextureChange); frameManager.setPreRenderPassHook(() -> { final ContextState st = s(); pipelineApplier.flushUniformBlocks(st); @@ -1207,9 +1212,11 @@ public class SDLGPURenderBackend extends RenderBackend { if (st.rasterizerDiscard) return; final FrameState f = frameManager.frame(); if (!f.frameActive) { f.droppedDrawsThisFrame++; return; } + prepareVertexWrites(st); if (mode == GL11.GL_TRIANGLE_FAN && count >= 3) { if (!drawDispatch.drawTriangleFanAsTriangleList(st, first, count)) { f.droppedDrawsThisFrame++; return; } if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "arraysFan", mode, count, 0, first, 0); + captureVertexWrites(st, first, count, 1, 0, 0, 0); return; } drawDispatch.setPrimitiveTypeForDraw(st, FormatMap.mapPrimitiveType(mode)); @@ -1218,6 +1225,7 @@ public class SDLGPURenderBackend extends RenderBackend { if (!pipelineApplier.applyPipelineAndState(st, f)) { f.droppedDrawsThisFrame++; return; } if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "arrays", mode, count, 0, first, 0); SDL_DrawGPUPrimitives(f.renderPass, count, 1, first, 0); + captureVertexWrites(st, first, count, 1, 0, 0, 0); } @Override public void drawElements(int mode, int count, int type, long indices) { @@ -1227,6 +1235,7 @@ public class SDLGPURenderBackend extends RenderBackend { final int firstIndex = (int) (indices / FormatMap.indexElementSize(type)); if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "elements", mode, count, type, firstIndex, 0); drawDispatch.issueIndexedDraw(st, rp, st.currentVao.elementBuffer, type, count, 1, firstIndex, 0); + captureVertexWrites(st, 0, count, 1, type, indices, 0); } @Override public void drawElementsInstanced(int mode, int count, int type, long indices, int primcount) { @@ -1236,6 +1245,7 @@ public class SDLGPURenderBackend extends RenderBackend { final int firstIndex = (int) (indices / FormatMap.indexElementSize(type)); if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "elementsInstanced", mode, count, type, firstIndex, 0); drawDispatch.issueIndexedDraw(st, rp, st.currentVao.elementBuffer, type, count, primcount, firstIndex, 0); + captureVertexWrites(st, 0, count, primcount, type, indices, 0); } @@ -1246,10 +1256,12 @@ public class SDLGPURenderBackend extends RenderBackend { final int firstIndex = (int) (indices / FormatMap.indexElementSize(type)); if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "elementsBaseVertex", mode, count, type, firstIndex, baseVertex); drawDispatch.issueIndexedDraw(st, rp, st.currentVao.elementBuffer, type, count, 1, firstIndex, baseVertex); + captureVertexWrites(st, 0, count, 1, type, indices, baseVertex); } private long prepareIndexedDrawBind(ContextState st, int mode, int type, String opName) { if (st.rasterizerDiscard) return 0; + prepareVertexWrites(st); if (!drawDispatch.prepareIndexedDraw(st, mode, type)) return 0; final int ebo = st.currentVao.elementBuffer; final long eboHandle = resourceManager.getBufferHandle(ebo); @@ -1278,6 +1290,7 @@ public class SDLGPURenderBackend extends RenderBackend { final int baseVertex = MemoryUtil.memGetInt(pBaseVertex + offset); if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "multiElementsBaseVertex", mode, count, type, firstIndex, baseVertex); drawDispatch.issueIndexedDraw(st, rp, ebo, type, count, 1, firstIndex, baseVertex); + captureVertexWrites(st, 0, count, 1, type, indices, baseVertex); } } @@ -1304,6 +1317,7 @@ public class SDLGPURenderBackend extends RenderBackend { } if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "elementsIndirect", mode, drawcount, type, (int) indirect, 0); SDL_DrawGPUIndexedPrimitivesIndirect(rp, indirectHandle, (int) indirect, drawcount); + if (st.boundProgramObj != null && st.boundProgramObj.vertexWriteReplay != null) vertexWriteReplayer.noteIndirectDraw(); } finally { Tracy.endZone(); } @@ -1418,6 +1432,11 @@ public class SDLGPURenderBackend extends RenderBackend { resourceManager.deleteTexture(texture); } + /** Captured vertex-write replays name textures by GL id, which a delete or reallocation would make mean something else. */ + private void flushVertexWritesBeforeTextureChange() { + if (vertexWriteReplayer.hasPending()) frameManager.endRenderPassIfActive(FrameManager.PASS_END_COMPUTE); + } + @Override public void bindTexture(int target, int texture) { if (isProxyTarget(target)) return; @@ -4271,6 +4290,10 @@ public class SDLGPURenderBackend extends RenderBackend { @Override public void memoryBarrier(int barriers) { // Graphics storage bindings are read-only on SDL_GPU; RW is compute-pass only frameManager.endCopyPassIfActive(); + // Replayed vertex image writes land when the render pass ends, and a barrier promises them to the draws after it + if ((barriers & (GL42.GL_SHADER_IMAGE_ACCESS_BARRIER_BIT | GL42.GL_TEXTURE_FETCH_BARRIER_BIT)) != 0 && vertexWriteReplayer.hasPending()) { + frameManager.endRenderPassIfActive(FrameManager.PASS_END_COMPUTE); + } } @Override public void copyImageSubData(int srcName, int srcTarget, int srcLevel, int srcX, int srcY, int srcZ, @@ -4417,18 +4440,41 @@ public class SDLGPURenderBackend extends RenderBackend { voxelizationDispatcher.endBatch(pass); } + @Override public void setVertexWriteReplay(int graphicsProgram, VertexWriteReplaySetup setup) { + vertexWriteReplayer.setReplay(graphicsProgram, setup); + } + + @Override public boolean supportsComputeImageAtomics() { + return device.supportsSpirv() || !device.supportsMsl(); + } + + private void prepareVertexWrites(ContextState st) { + final ShaderManager.ProgramObject prog = st.boundProgramObj; + if (prog != null && prog.vertexWriteReplay != null) vertexWriteReplayer.ensureImageUsage(prog.vertexWriteReplay); + } + + private void captureVertexWrites(ContextState st, int first, int count, int instances, int indexType, long indexOffset, int baseVertex) { + final ShaderManager.ProgramObject prog = st.boundProgramObj; + if (prog != null && prog.vertexWriteReplay != null) { + vertexWriteReplayer.capture(st, first, count, instances, indexType, indexOffset, baseVertex); + if (!frameManager.isRenderPassActive()) vertexWriteReplayer.flush(); + } + } + @Override public void drawArraysInstanced(int mode, int first, int count, int primcount) { if (count <= 0 || primcount <= 0) return; final ContextState st = s(); if (st.rasterizerDiscard) return; if (!frameManager.isFrameActive()) return; + prepareVertexWrites(st); drawDispatch.setPrimitiveTypeForDraw(st, FormatMap.mapPrimitiveType(mode)); pipelineApplier.ensureDrawRenderPass(st); if (!frameManager.isRenderPassActive()) return; if (!pipelineApplier.applyPipelineAndState(st)) return; if (SystemProperties.FFP_TRACE) ffpTrace.trace(st, "arraysInstanced", mode, count, 0, first, 0); SDL_DrawGPUPrimitives(frameManager.getRenderPass(), count, primcount, first, 0); + captureVertexWrites(st, first, count, primcount, 0, 0, 0); } @Override diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/ComputeBinder.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/ComputeBinder.java index e665a6e5c..a7b8a760d 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/ComputeBinder.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/ComputeBinder.java @@ -45,6 +45,27 @@ public final class ComputeBinder implements ComputeDispatchSink { private final ShaderManager shaderManager; private final SamplerBinder samplerBinder; private boolean droppedDispatchWarned; + private ShaderManager.ProgramObject lastUniformProgram; + private long lastUniformCb; + private long[] samplerOverrides; + private long[] rwBufferOverrides; + private ByteBuffer[] uniformBlockOverrides; + + public void setSamplerOverrides(long[] byUnit) { + samplerOverrides = byUnit; + } + + public void setReadWriteStorageBufferOverrides(long[] byGlSlot) { + rwBufferOverrides = byGlSlot; + } + + public void setUniformBlockOverrides(ByteBuffer[] byIndex) { + uniformBlockOverrides = byIndex; + } + + public long samplerHandle(int samplerObject, int glTexId) { + return samplerBinder.getSampler(samplerObject, glTexId); + } public ComputeBinder(FrameManager frameManager, ResourceManager resourceManager, ShaderManager shaderManager, SamplerBinder samplerBinder) { this.frameManager = frameManager; @@ -153,6 +174,43 @@ public final class ComputeBinder implements ComputeDispatchSink { @Override public long beginBatchedComputeDispatch(ContextState st, boolean cycleRwBuffers) { + return beginComputePass(st, cycleRwBuffers, true); + } + + public long beginComputePassWithoutStorageBuffers(ContextState st) { + return beginComputePass(st, false, false); + } + + public boolean bindRoStorageBufferHandles(long pass, ContextState st, long[] handlesByGlSlot) { + if (pass == 0) return false; + final ShaderManager.ProgramObject prog = shaderManager.getProgram(st.boundProgram); + if (prog == null || !prog.linked) return false; + final int[] slots = prog.computeBindingMap.roSsboGlSlots(); + if (slots.length == 0) return true; + try (MemoryStack stack = MemoryStack.stackPush()) { + final PointerBuffer ptrs = stack.mallocPointer(slots.length); + for (int i = 0; i < slots.length; i++) { + long handle = slots[i] < handlesByGlSlot.length ? handlesByGlSlot[slots[i]] : 0; + if (handle == 0) handle = resourceManager.getOrCreateFallbackStorageBuffer(); + if (handle == 0) return false; + ptrs.put(i, handle); + } + SDL_BindGPUComputeStorageBuffers(pass, 0, ptrs); + } + return true; + } + + public boolean rebindSamplers(long pass, ContextState st) { + if (pass == 0) return false; + final ShaderManager.ProgramObject prog = shaderManager.getProgram(st.boundProgram); + if (prog == null || !prog.linked) return false; + final ShaderManager.ComputeBindingMap map = prog.computeBindingMap; + try (MemoryStack stack = MemoryStack.stackPush()) { + return bindComputeSamplers(st, prog, pass, map.samplerGlSlots(), map.samplerNames(), stack); + } + } + + private long beginComputePass(ContextState st, boolean cycleRwBuffers, boolean bindRoBuffers) { final int programId = st.boundProgram; if (programId == 0) return 0; final ShaderManager.ProgramObject prog = shaderManager.getProgram(programId); @@ -196,7 +254,7 @@ public final class ComputeBinder implements ComputeDispatchSink { final boolean ok = bindComputeSamplers(st, prog, pass, samplerSlots, map.samplerNames(), stack) && bindRoStorageTextures(st, prog, pass, map, stack) - && bindRoStorageBuffers(st, pass, roBufSlots, stack); + && (!bindRoBuffers || bindRoStorageBuffers(st, pass, roBufSlots, stack)); if (!ok) { SDL_EndGPUComputePass(pass); frameManager.noteComputePassEnded(); @@ -298,7 +356,7 @@ public final class ComputeBinder implements ComputeDispatchSink { private SDL_GPUStorageBufferReadWriteBinding.Buffer buildRwBufferBindings(ContextState st, int[] slots, MemoryStack stack, boolean cycle) { final SDL_GPUStorageBufferReadWriteBinding.Buffer buf = SDL_GPUStorageBufferReadWriteBinding.calloc(slots.length, stack); for (int i = 0; i < slots.length; i++) { - final long handle = resolveStorageBufHandle(st, slots[i]); + final long handle = rwBufferOverrides != null && slots[i] < rwBufferOverrides.length ? rwBufferOverrides[slots[i]] : resolveStorageBufHandle(st, slots[i]); if (handle == 0) { warnMissingBinding(st, 1, "RW storage buffer slot", slots[i], true); return null; } buf.get(i).buffer(handle).cycle(cycle); @@ -319,7 +377,8 @@ public final class ComputeBinder implements ComputeDispatchSink { if (texHandle == 0) { warnMissingBinding(st, 2, "sampler texture unit", unit, true); return false; } warnMissingBinding(st, 2, "sampler texture unit", unit, false); } - long sampHandle = samplerBinder.getSamplerForUnit(st, unit, texGlId); + final long override = samplerOverrides != null && unit >= 0 && unit < samplerOverrides.length ? samplerOverrides[unit] : 0; + long sampHandle = override != 0 ? override : samplerBinder.getSamplerForUnit(st, unit, texGlId); if (sampHandle == 0) { sampHandle = resourceManager.getOrCreateDefaultSampler(); if (sampHandle == 0) { warnMissingBinding(st, 3, "sampler unit", unit, true); return false; } @@ -374,6 +433,28 @@ public final class ComputeBinder implements ComputeDispatchSink { return true; } + public ByteBuffer uniformBlockView(ContextState st, ShaderManager.ProgramObject prog, int i) { + final ShaderManager.ComputeBindingMap map = prog.computeBindingMap; + final int[] uboSlots = map.uboGlSlots(); + final int[] declaredSizes = map.uboSizes(); + if (i >= uboSlots.length) return null; + final int slot = uboSlots[i]; + final int bufGlId = st.boundUboByIndex[slot]; + if (bufGlId == 0) return null; + final ByteBuffer shadow = resourceManager.getUboShadow(bufGlId); + if (shadow == null) return null; + final int offset = st.uboRangeOffset[slot]; + if (offset < 0 || offset >= shadow.capacity()) return null; + int size = (i < declaredSizes.length && declaredSizes[i] > 0) ? declaredSizes[i] : shadow.capacity() - offset; + final int rangeSize = st.uboRangeSize[slot]; + if (rangeSize > 0) size = Math.min(size, rangeSize); + size = Math.min(size, shadow.capacity() - offset); + if (size <= 0) return null; + final ByteBuffer view = frameManager.getUboPushView(bufGlId, shadow); + view.position(offset).limit(offset + size); + return view; + } + private void pushComputeUboData(ContextState st, long cb, ShaderManager.ProgramObject prog, int[] uboSlots) { if (prog.lastComputeUboHash == null || prog.lastComputeUboHash.length != uboSlots.length) { prog.lastComputeUboHash = new long[uboSlots.length]; @@ -381,16 +462,18 @@ public final class ComputeBinder implements ComputeDispatchSink { prog.lastComputeFrame = 0; } final long curFrame = frameManager.getFrameNumber(); - final boolean cacheValid = cb == prog.lastComputeCb && curFrame == prog.lastComputeFrame; + final boolean cacheValid = cb == prog.lastComputeCb && curFrame == prog.lastComputeFrame + && lastUniformProgram == prog && lastUniformCb == cb; final long[] cache = prog.lastComputeUboHash; if (!cacheValid) { for (int i = 0; i < cache.length; i++) cache[i] = 0L; prog.lastComputeCb = cb; prog.lastComputeFrame = curFrame; } + lastUniformProgram = prog; + lastUniformCb = cb; final ShaderManager.ComputeBindingMap map = prog.computeBindingMap; final boolean[] isDefault = map.uboIsDefaultBlock(); - final int[] declaredSizes = map.uboSizes(); final UniformStaging us = st.uniformStaging(prog); for (int i = 0; i < uboSlots.length; i++) { final ByteBuffer view; @@ -398,21 +481,13 @@ public final class ComputeBinder implements ComputeDispatchSink { if (us.fsUniformBuf == null || prog.fragmentUboSize <= 0) continue; view = us.fsUniformBuf; view.position(0).limit(prog.fragmentUboSize); + } else if (uniformBlockOverrides != null && i < uniformBlockOverrides.length && uniformBlockOverrides[i] != null) { + view = uniformBlockOverrides[i]; + view.position(0); + if (!view.hasRemaining()) continue; } else { - final int slot = uboSlots[i]; - final int bufGlId = st.boundUboByIndex[slot]; - if (bufGlId == 0) continue; - final ByteBuffer shadow = resourceManager.getUboShadow(bufGlId); - if (shadow == null) continue; - final int offset = st.uboRangeOffset[slot]; - if (offset < 0 || offset >= shadow.capacity()) continue; - int size = (i < declaredSizes.length && declaredSizes[i] > 0) ? declaredSizes[i] : shadow.capacity() - offset; - final int rangeSize = st.uboRangeSize[slot]; - if (rangeSize > 0) size = Math.min(size, rangeSize); - size = Math.min(size, shadow.capacity() - offset); - if (size <= 0) continue; - view = frameManager.getUboPushView(bufGlId, shadow); - view.position(offset).limit(offset + size); + view = uniformBlockView(st, prog, i); + if (view == null) continue; } final int pos = view.position(); final int len = view.remaining(); diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/InvocationDispatch.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/InvocationDispatch.java new file mode 100644 index 000000000..aee73c184 --- /dev/null +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/InvocationDispatch.java @@ -0,0 +1,24 @@ +package com.gtnewhorizons.angelica.sdlgpu.compute; + +import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; + +public final class InvocationDispatch { + static final int MAX_GROUPS_PER_DISPATCH = 65535; + static final int WORKGROUP_SIZE = 64; + + @FunctionalInterface + public interface IntUniformWriter { + void write(ContextState st, int location, int value); + } + + private InvocationDispatch() {} + + static void dispatch(ComputeDispatchSink sink, ContextState st, long pass, long invocations, int invocationBaseLocation, IntUniformWriter writer) { + final long groups = (invocations + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE; + for (long g0 = 0; g0 < groups; g0 += MAX_GROUPS_PER_DISPATCH) { + if (invocationBaseLocation >= 0) writer.write(st, invocationBaseLocation, (int) (g0 * WORKGROUP_SIZE)); + sink.pushPendingComputeUniforms(st); + sink.dispatchInBatch(pass, (int) Math.min(MAX_GROUPS_PER_DISPATCH, groups - g0), 1, 1); + } + } +} diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/VertexWriteReplayer.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/VertexWriteReplayer.java new file mode 100644 index 000000000..f22ed30ae --- /dev/null +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/VertexWriteReplayer.java @@ -0,0 +1,635 @@ +package com.gtnewhorizons.angelica.sdlgpu.compute; + +import com.gtnewhorizons.angelica.glsm.backend.VertexWriteReplaySetup; +import com.gtnewhorizons.angelica.sdlgpu.SDLGPURenderBackend; +import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; +import com.gtnewhorizons.angelica.sdlgpu.pipeline.PipelineApplier; +import com.gtnewhorizons.angelica.sdlgpu.resource.ResourceManager; +import com.gtnewhorizons.angelica.sdlgpu.shader.ShaderManager; +import com.gtnewhorizons.angelica.sdlgpu.shader.UniformStaging; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import org.lwjgl.opengl.GL15; +import org.lwjgl.opengl.GL30; +import org.lwjgl.opengl.GL33; +import org.lwjgl.opengl.GL41; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +/** + * Performs the image writes of a vertex shader that SDL-GPU strips (graphics stages may only read storage images). + * Every draw made with a program that has a {@link Replay} is captured: its vertex and index buffers, attribute layout, + * draw range, the uniform values the replay reads and the textures it samples. When the render pass ends, the replay + * compute shader re-runs the vertex shader for each captured vertex and performs the writes. + */ +public final class VertexWriteReplayer { + private static final Logger LOG = LogManager.getLogger("Angelica-SDLGPU"); + + private static final int MAX_CAPTURES = 1 << 16; + private static final int[] COMPONENT_SIZE = { 4, 2, 1, 1, 2, 2, 4, 4, 4, 4, 4, 4 }; + private static final int FIRST_PACKED_TYPE = 9; + private static final int PACKED_10F_11F_11F = 11; + + private static final int ENABLED = 0x2000; + private static final int BGRA = 0x4000; + private static final int NO_ELEMENT = 0x8000; + private static final int MAX_DIVISOR = 0xFFFF; + private static final int UNCHANGED = -1; + private static final int TEXTURE_UNITS = 32; + private static final int SAMPLER_OBJECT_OF_DRAW = -1; + + public static final class Replay { + final int replayProgram; + final VertexWriteReplaySetup setup; + + private boolean resolved; + private boolean broken; + int[] inputLocations; + int[] uniformSources; + int[] uniformTargets; + int[] samplerUnits; + IntSupplier[] samplerSources; + int[] samplerObjects; + int[] packStorageSlots; + int[] packRwStorageSlots; + int[] uniformBlockIndices; + int uniformBlockCount; + int locAttr, locAttrDefault, locDraw, locIndex, locRestart, locInvocationBase; + long[] lastHashes; + boolean lastHashesApplied; + + public Replay(VertexWriteReplaySetup setup) { + this.replayProgram = setup.replayProgram(); + this.setup = setup; + } + + IntSupplier[] images() { + return setup.images(); + } + + boolean resolve(ShaderManager.ProgramObject graphics, ShaderManager.ProgramObject replay) { + if (resolved) return !broken; + resolved = true; + + final String[] inputNames = setup.inputNames(); + inputLocations = new int[inputNames.length]; + for (int i = 0; i < inputNames.length; i++) { + int loc = graphics.resolvedAttribLocations.getInt(inputNames[i]); + if (loc < 0) loc = graphics.attribLocationBindings.getInt(inputNames[i]); + if (loc < 0) loc = setup.inputLocations()[i]; + inputLocations[i] = loc; + } + + final IntArrayList sources = new IntArrayList(); + final IntArrayList targets = new IntArrayList(); + final List samplerNames = new ArrayList<>(); + for (int target = 0; target < replay.uniformSlotCount; target++) { + final String name = replay.locationName[target]; + if (name == null || name.startsWith("_vg_")) continue; + if (replay.locationKind[target] == ShaderManager.ProgramObject.LOCATION_KIND_SAMPLER) { + samplerNames.add(name); + continue; + } + if (replay.locationKind[target] != ShaderManager.ProgramObject.LOCATION_KIND_PLAIN) continue; + final int source = graphics.nameToLocation.getInt(name); + if (source < 0) continue; + sources.add(source); + targets.add(target); + } + uniformSources = sources.toIntArray(); + uniformTargets = targets.toIntArray(); + lastHashes = new long[uniformSources.length]; + resolveSamplers(graphics, replay, samplerNames); + + final IntArrayList storage = new IntArrayList(); + for (int slot : replay.computeBindingMap.roSsboGlSlots()) { + final boolean vertexBuffer = slot >= setup.vertexBufferBinding() && slot < setup.vertexBufferBinding() + setup.vertexBufferCount(); + if (!vertexBuffer && slot != setup.indexBufferBinding()) storage.add(slot); + } + packStorageSlots = storage.toIntArray(); + packRwStorageSlots = replay.computeBindingMap.rwSsboGlSlots().clone(); + final IntArrayList blocks = new IntArrayList(); + final boolean[] isDefault = replay.computeBindingMap.uboIsDefaultBlock(); + for (int i = 0; i < replay.computeBindingMap.uboGlSlots().length; i++) { + if (i >= isDefault.length || !isDefault[i]) blocks.add(i); + } + uniformBlockIndices = blocks.toIntArray(); + uniformBlockCount = replay.computeBindingMap.uboGlSlots().length; + + locAttr = replay.nameToLocation.getInt("_vg_attr"); + locAttrDefault = replay.nameToLocation.getInt("_vg_attrDefault"); + locDraw = replay.nameToLocation.getInt("_vg_draw"); + locIndex = replay.nameToLocation.getInt("_vg_index"); + locRestart = replay.nameToLocation.getInt("_vg_restart"); + locInvocationBase = replay.nameToLocation.getInt("_vg_invocationBase"); + if (locDraw < 0 || locInvocationBase < 0) { + broken = true; + LOG.warn("Vertex write replay program {} is missing its dispatch uniforms; its writes are dropped", replayProgram); + } + return !broken; + } + + private void resolveSamplers(ShaderManager.ProgramObject graphics, ShaderManager.ProgramObject replay, List names) { + final IntArrayList units = new IntArrayList(); + final List sources = new ArrayList<>(); + final IntArrayList objects = new IntArrayList(); + final IntArrayList ownTextureIndices = new IntArrayList(); + final List ownTextureNames = new ArrayList<>(); + final List pipelineNames = Arrays.asList(setup.samplerNames()); + for (String name : names) { + int unit = graphics.samplerTextureUnits.getInt(name); + if (unit < 0) { + final int i = pipelineNames.indexOf(name); + if (i < 0) continue; + if (setup.samplerTextures()[i] != null && setup.samplerUnits()[i] < 0) { + ownTextureNames.add(name); + ownTextureIndices.add(i); + continue; + } + unit = setup.samplerUnits()[i]; + if (unit < 0) continue; + } + replay.samplerTextureUnits.put(name, unit); + if (!units.contains(unit)) { + units.add(unit); + sources.add(null); + objects.add(SAMPLER_OBJECT_OF_DRAW); + } + } + int free = TEXTURE_UNITS - 1; + for (int k = 0; k < ownTextureNames.size(); k++) { + while (free >= 0 && units.contains(free)) free--; + if (free < 0) break; + final int i = ownTextureIndices.getInt(k); + replay.samplerTextureUnits.put(ownTextureNames.get(k), free); + units.add(free); + sources.add(setup.samplerTextures()[i]); + objects.add(setup.samplerObjects()[i]); + } + samplerUnits = units.toIntArray(); + samplerSources = sources.toArray(new IntSupplier[0]); + samplerObjects = objects.toIntArray(); + } + } + + private static final class Capture { + Replay replay; + long[] buffers = new long[0]; + int bufferCount; + long indexBuffer; + float[] attr = new float[0]; + float[] attrDefault = new float[0]; + int[] samplerTextures = new int[0]; + long[] samplerHandles = new long[0]; + int[] imageTextures = new int[0]; + long[] packStorage = new long[0]; + long[] packRwStorage = new long[0]; + ByteBuffer[] uniformBlocks = new ByteBuffer[0]; + int first, count, instances, baseVertex, indexSize, indexOffset, indexCount; + boolean restart; + int restartIndex; + } + + private final ShaderManager shaderManager; + private final ResourceManager resourceManager; + private final PipelineApplier pipelineApplier; + private final ComputeBinder computeBinder; + private final Supplier state; + + private final ArrayList captures = new ArrayList<>(); + private int pending; + private float[] uniformValues = new float[4096]; + private int[] uniformLengths = new int[256]; + private int uniformValueCount; + private int uniformCount; + private boolean flushing; + private final long[] handlesByGlSlot = new long[ContextState.MAX_INDEXED_BUFFERS]; + private final long[] rwHandlesByGlSlot = new long[ContextState.MAX_INDEXED_BUFFERS]; + private final long[] samplerOverrides = new long[TEXTURE_UNITS]; + private final int[] savedImages = new int[ContextState.MAX_IMAGE_UNITS]; + private final int[] savedLevels = new int[ContextState.MAX_IMAGE_UNITS]; + private final int[] savedAccess = new int[ContextState.MAX_IMAGE_UNITS]; + private final int[] savedTextures = new int[TEXTURE_UNITS]; + private int uniformCursor; + private int valueCursor; + private final InvocationDispatch.IntUniformWriter invocationBaseWriter = (st, location, value) -> putIntVector(st, location, 1, value, 0, 0, 0); + private final Set warned = new HashSet<>(); + + public VertexWriteReplayer(ShaderManager shaderManager, ResourceManager resourceManager, PipelineApplier pipelineApplier, ComputeBinder computeBinder, Supplier state) { + this.shaderManager = shaderManager; + this.resourceManager = resourceManager; + this.pipelineApplier = pipelineApplier; + this.computeBinder = computeBinder; + this.state = state; + } + + public void setReplay(int graphicsProgram, VertexWriteReplaySetup setup) { + final ShaderManager.ProgramObject prog = shaderManager.getProgram(graphicsProgram); + if (prog == null) return; + if (setup == null) { + prog.vertexWriteReplay = null; + return; + } + if (setup.images().length > ContextState.MAX_IMAGE_UNITS) { + warnOnce("images:" + setup.replayProgram(), "Vertex write replay program " + setup.replayProgram() + " uses " + setup.images().length + " images, more than the " + ContextState.MAX_IMAGE_UNITS + " image units; its writes are dropped"); + prog.vertexWriteReplay = null; + return; + } + prog.vertexWriteReplay = new Replay(setup); + } + + public void ensureImageUsage(Replay replay) { + final IntSupplier[] images = replay.images(); + final int written = replay.setup.writtenImageCount(); + for (int i = 0; i < images.length; i++) { + final int texture = images[i].getAsInt(); + if (texture != 0) resourceManager.ensureTextureUsage(texture, SDLGPURenderBackend.imageUsageForAccess(imageAccess(i, written))); + } + } + + public void capture(ContextState st, int first, int count, int instances, int indexType, long indexOffset, int baseVertex) { + final ShaderManager.ProgramObject graphics = st.boundProgramObj; + final Replay replay = graphics.vertexWriteReplay; + final ShaderManager.ProgramObject twin = shaderManager.getProgram(replay.replayProgram); + if (twin == null || !twin.linked || twin.sdlComputePipeline == 0) return; + if (!replay.resolve(graphics, twin)) return; + if (count <= 0 || instances <= 0) return; + if (pending == MAX_CAPTURES) { + warnOnce("captures", "More than " + MAX_CAPTURES + " draws to replay vertex image writes for in one render pass; the rest are dropped"); + return; + } + + final IntSupplier[] images = replay.images(); + final Capture c = acquire(); + c.replay = replay; + if (c.buffers.length != replay.setup.vertexBufferCount()) c.buffers = new long[replay.setup.vertexBufferCount()]; + c.bufferCount = 0; + if (!captureInputs(st, replay, c)) return; + + if (indexType != 0) { + c.indexBuffer = resourceManager.getBufferHandle(st.currentVao.elementBuffer); + c.indexSize = indexType == GL11.GL_UNSIGNED_BYTE ? 1 : indexType == GL11.GL_UNSIGNED_SHORT ? 2 : 4; + c.indexOffset = (int) indexOffset; + if (c.indexBuffer == 0) return; + final long indexBytes = resourceManager.getBufferSize(st.currentVao.elementBuffer); + c.indexCount = indexBytes <= 0 ? -1 : (int) Math.min(Math.max(0L, indexBytes - indexOffset) / c.indexSize, Integer.MAX_VALUE); + c.restart = st.primitiveRestartEnabled && indexType != GL11.GL_UNSIGNED_BYTE; + c.restartIndex = indexType == GL11.GL_UNSIGNED_SHORT ? st.primitiveRestartSentinel & 0xFFFF : st.primitiveRestartSentinel; + } else { + c.indexBuffer = 0; + c.indexSize = 0; + c.indexOffset = 0; + c.indexCount = 0; + c.restart = false; + } + c.first = first; + c.count = count; + c.instances = instances; + c.baseVertex = baseVertex; + + if (c.samplerTextures.length != replay.samplerUnits.length) { + c.samplerTextures = new int[replay.samplerUnits.length]; + c.samplerHandles = new long[replay.samplerUnits.length]; + } + for (int i = 0; i < replay.samplerUnits.length; i++) { + final int unit = replay.samplerUnits[i]; + final IntSupplier source = replay.samplerSources[i]; + c.samplerTextures[i] = source != null ? source.getAsInt() : st.boundTextures[unit]; + final int samplerObject = replay.samplerObjects[i] != SAMPLER_OBJECT_OF_DRAW ? replay.samplerObjects[i] : st.boundSamplerObjects[unit]; + c.samplerHandles[i] = computeBinder.samplerHandle(samplerObject, c.samplerTextures[i]); + } + if (c.imageTextures.length != images.length) c.imageTextures = new int[images.length]; + for (int i = 0; i < images.length; i++) c.imageTextures[i] = images[i].getAsInt(); + if (c.packStorage.length != replay.packStorageSlots.length) c.packStorage = new long[replay.packStorageSlots.length]; + for (int i = 0; i < replay.packStorageSlots.length; i++) { + c.packStorage[i] = resourceManager.getBufferHandle(st.boundSsboByIndex[replay.packStorageSlots[i]]); + } + if (c.packRwStorage.length != replay.packRwStorageSlots.length) c.packRwStorage = new long[replay.packRwStorageSlots.length]; + for (int i = 0; i < replay.packRwStorageSlots.length; i++) { + c.packRwStorage[i] = resourceManager.getBufferHandle(st.boundSsboByIndex[replay.packRwStorageSlots[i]]); + } + captureUniformBlocks(st, twin, replay, c); + + captureUniforms(st, graphics, replay); + pending++; + } + + private boolean captureInputs(ContextState st, Replay replay, Capture c) { + final int n = replay.inputLocations.length; + if (c.attr.length < n * 4) { + c.attr = new float[n * 4]; + c.attrDefault = new float[n * 4]; + } + final ContextState.VAOState vao = st.currentVao; + for (int k = 0; k < n; k++) { + final int loc = replay.inputLocations[k]; + int x = 0, y = 0, z = 0, w = 0; + if (loc >= 0 && loc < ContextState.MAX_VERTEX_ATTRIBS) { + System.arraycopy(st.attribDefaults, loc * 4, c.attrDefault, k * 4, 4); + if (vao.attribEnabled[loc]) { + final int type = componentType(vao.attribType[loc]); + final int binding = vao.attribBinding[loc]; + final int buffer = vao.bindingBuffer[binding]; + final long handle = type >= 0 ? resourceManager.getBufferHandle(buffer) : 0; + if (type < 0) warnOnce("type:" + vao.attribType[loc], "Vertex write replay cannot read vertex attribute type 0x" + Integer.toHexString(vao.attribType[loc]) + "; that input reads its default value"); + if (handle != 0) { + final int slot = bufferSlot(c, handle); + if (slot < 0) { + warnOnce("buffers", "A draw reads more than " + c.buffers.length + " vertex buffers; its vertex image writes are dropped"); + return false; + } + final boolean bgra = vao.attribSize[loc] == GL12.GL_BGRA; + final int size = type == PACKED_10F_11F_11F ? 3 : bgra ? 4 : vao.attribSize[loc]; + final int elementBytes = type >= FIRST_PACKED_TYPE ? 4 : size * COMPONENT_SIZE[type]; + final int stride = vao.bindingStride[binding] != 0 ? vao.bindingStride[binding] : elementBytes; + final long offset = vao.bindingOffset[binding] + vao.attribRelativeOffset[loc]; + final int divisor = Math.min(vao.bindingDivisor[binding], MAX_DIVISOR); + final long bufferSize = resourceManager.getBufferSize(buffer); + final boolean noElement = bufferSize > 0 && bufferSize < offset + elementBytes; + x = slot | type << 4 | size << 8 | (vao.attribNormalized[loc] ? 0x800 : 0) | (vao.attribIsInteger[loc] ? 0x1000 : 0) + | ENABLED | (bgra ? BGRA : 0) | (noElement ? NO_ELEMENT : 0) | divisor << 16; + y = (int) offset; + z = stride; + w = lastElement(bufferSize, offset, elementBytes, stride); + } + } + } else { + c.attrDefault[k * 4] = 0f; + c.attrDefault[k * 4 + 1] = 0f; + c.attrDefault[k * 4 + 2] = 0f; + c.attrDefault[k * 4 + 3] = 1f; + } + c.attr[k * 4] = Float.intBitsToFloat(x); + c.attr[k * 4 + 1] = Float.intBitsToFloat(y); + c.attr[k * 4 + 2] = Float.intBitsToFloat(z); + c.attr[k * 4 + 3] = Float.intBitsToFloat(w); + } + return true; + } + + static int lastElement(long bufferSize, long offset, int elementBytes, int stride) { + if (bufferSize <= 0) return -1; + final long room = bufferSize - offset - elementBytes; + if (room < 0 || stride <= 0) return 0; + return (int) Math.min(room / stride, 0xFFFFFFFFL); + } + + private static int bufferSlot(Capture c, long handle) { + for (int i = 0; i < c.bufferCount; i++) { + if (c.buffers[i] == handle) return i; + } + if (c.bufferCount == c.buffers.length) return -1; + c.buffers[c.bufferCount] = handle; + return c.bufferCount++; + } + + static int componentType(int glType) { + return switch (glType) { + case GL11.GL_FLOAT -> 0; + case GL30.GL_HALF_FLOAT -> 1; + case GL11.GL_BYTE -> 2; + case GL11.GL_UNSIGNED_BYTE -> 3; + case GL11.GL_SHORT -> 4; + case GL11.GL_UNSIGNED_SHORT -> 5; + case GL11.GL_INT -> 6; + case GL11.GL_UNSIGNED_INT -> 7; + case GL41.GL_FIXED -> 8; + case GL33.GL_INT_2_10_10_10_REV -> 9; + case GL12.GL_UNSIGNED_INT_2_10_10_10_REV -> 10; + case GL30.GL_UNSIGNED_INT_10F_11F_11F_REV -> PACKED_10F_11F_11F; + default -> -1; + }; + } + + private void captureUniformBlocks(ContextState st, ShaderManager.ProgramObject twin, Replay replay, Capture c) { + if (c.uniformBlocks.length != replay.uniformBlockCount) c.uniformBlocks = new ByteBuffer[replay.uniformBlockCount]; + for (int index : replay.uniformBlockIndices) { + final ByteBuffer view = computeBinder.uniformBlockView(st, twin, index); + if (view == null) { + if (c.uniformBlocks[index] != null) c.uniformBlocks[index].limit(0); + continue; + } + ByteBuffer copy = c.uniformBlocks[index]; + if (copy == null || copy.capacity() < view.remaining()) copy = c.uniformBlocks[index] = ByteBuffer.allocateDirect(view.remaining()).order(ByteOrder.nativeOrder()); + copy.clear(); + copy.put(view); + copy.flip(); + } + } + + private void captureUniforms(ContextState st, ShaderManager.ProgramObject graphics, Replay replay) { + final UniformStaging us = st.uniformStaging(graphics); + final long[] hashes = us.uniformValueHashBySlot; + for (int u = 0; u < replay.uniformSources.length; u++) { + final int source = replay.uniformSources[u]; + final float[] value = us.uniformDataBySlot != null && source < us.uniformDataBySlot.length ? us.uniformDataBySlot[source] : null; + final long hash = hashes != null && source < hashes.length ? hashes[source] : 0L; + if (uniformCount == uniformLengths.length) uniformLengths = Arrays.copyOf(uniformLengths, uniformLengths.length * 2); + if (value != null && replay.lastHashesApplied && hash == replay.lastHashes[u]) { + uniformLengths[uniformCount++] = UNCHANGED; + continue; + } + replay.lastHashes[u] = hash; + final int len = value == null ? 0 : value.length; + uniformLengths[uniformCount++] = len; + if (len == 0) continue; + if (uniformValueCount + len > uniformValues.length) uniformValues = Arrays.copyOf(uniformValues, Math.max(uniformValues.length * 2, uniformValueCount + len)); + System.arraycopy(value, 0, uniformValues, uniformValueCount, len); + uniformValueCount += len; + } + replay.lastHashesApplied = true; + } + + public boolean hasPending() { + return pending > 0; + } + + public void noteIndirectDraw() { + warnOnce("indirect", "An indirect draw used a program whose vertex shader writes images; those writes are dropped"); + } + + private Capture acquire() { + if (pending == captures.size()) captures.add(new Capture()); + return captures.get(pending); + } + + public void flush() { + if (pending == 0 || flushing) return; + flushing = true; + final ContextState st = state.get(); + final int savedProgram = st.boundProgram; + final ShaderManager.ProgramObject savedProgramObj = st.boundProgramObj; + try { + uniformCursor = 0; + valueCursor = 0; + int start = 0; + while (start < pending) { + final Capture first = captures.get(start); + final Replay replay = first.replay; + int end = start; + while (end < pending && captures.get(end).replay == replay && Arrays.equals(captures.get(end).imageTextures, first.imageTextures) + && Arrays.equals(captures.get(end).packRwStorage, first.packRwStorage)) end++; + replayRun(st, replay, start, end); + start = end; + } + } finally { + st.boundProgram = savedProgram; + st.boundProgramObj = savedProgramObj; + for (int i = 0; i < pending; i++) captures.get(i).replay = null; + pending = 0; + uniformCount = 0; + uniformValueCount = 0; + flushing = false; + } + } + + private void replayRun(ContextState st, Replay replay, int start, int end) { + final ShaderManager.ProgramObject twin = shaderManager.getProgram(replay.replayProgram); + if (twin == null || !twin.linked || twin.sdlComputePipeline == 0) { + skipUniforms(replay, start, end); + return; + } + + final Capture first = captures.get(start); + final int[] imageTextures = first.imageTextures; + final int imageCount = imageTextures.length; + final int written = replay.setup.writtenImageCount(); + System.arraycopy(st.boundStorageTextureByUnit, 0, savedImages, 0, imageCount); + System.arraycopy(st.boundStorageTextureLevel, 0, savedLevels, 0, imageCount); + System.arraycopy(st.boundStorageTextureAccess, 0, savedAccess, 0, imageCount); + for (int i = 0; i < replay.samplerUnits.length; i++) savedTextures[i] = st.boundTextures[replay.samplerUnits[i]]; + computeBinder.setSamplerOverrides(samplerOverrides); + Arrays.fill(rwHandlesByGlSlot, 0L); + for (int i = 0; i < replay.packRwStorageSlots.length; i++) rwHandlesByGlSlot[replay.packRwStorageSlots[i]] = first.packRwStorage[i]; + computeBinder.setReadWriteStorageBufferOverrides(rwHandlesByGlSlot); + + try { + for (int i = 0; i < imageCount; i++) { + st.boundStorageTextureByUnit[i] = imageTextures[i]; + st.boundStorageTextureLevel[i] = 0; + st.boundStorageTextureAccess[i] = imageAccess(i, written); + } + applySamplerTextures(st, replay, captures.get(start)); + + st.boundProgram = replay.replayProgram; + st.boundProgramObj = twin; + final long pass = computeBinder.beginComputePassWithoutStorageBuffers(st); + if (pass == 0) { + for (int i = start; i < end; i++) applyUniforms(st, replay); + return; + } + try { + for (int i = start; i < end; i++) { + final Capture c = captures.get(i); + if (i > start && applySamplerTextures(st, replay, c)) computeBinder.rebindSamplers(pass, st); + dispatch(st, pass, replay, c); + } + } finally { + computeBinder.endBatchedComputeDispatch(pass); + } + } finally { + computeBinder.setReadWriteStorageBufferOverrides(null); + computeBinder.setUniformBlockOverrides(null); + System.arraycopy(savedImages, 0, st.boundStorageTextureByUnit, 0, imageCount); + System.arraycopy(savedLevels, 0, st.boundStorageTextureLevel, 0, imageCount); + System.arraycopy(savedAccess, 0, st.boundStorageTextureAccess, 0, imageCount); + for (int i = 0; i < replay.samplerUnits.length; i++) { + st.boundTextures[replay.samplerUnits[i]] = savedTextures[i]; + samplerOverrides[replay.samplerUnits[i]] = 0L; + } + computeBinder.setSamplerOverrides(null); + } + } + + private boolean applySamplerTextures(ContextState st, Replay replay, Capture c) { + boolean changed = false; + for (int i = 0; i < replay.samplerUnits.length; i++) { + final int unit = replay.samplerUnits[i]; + if (st.boundTextures[unit] != c.samplerTextures[i] || samplerOverrides[unit] != c.samplerHandles[i]) { + st.boundTextures[unit] = c.samplerTextures[i]; + samplerOverrides[unit] = c.samplerHandles[i]; + changed = true; + } + } + return changed; + } + + private void skipUniforms(Replay replay, int start, int end) { + replay.lastHashesApplied = false; + for (int i = start; i < end; i++) { + for (int u = 0; u < replay.uniformSources.length; u++) valueCursor += Math.max(0, uniformLengths[uniformCursor++]); + } + } + + private void dispatch(ContextState st, long pass, Replay replay, Capture c) { + Arrays.fill(handlesByGlSlot, 0L); + for (int i = 0; i < replay.packStorageSlots.length; i++) handlesByGlSlot[replay.packStorageSlots[i]] = c.packStorage[i]; + System.arraycopy(c.buffers, 0, handlesByGlSlot, replay.setup.vertexBufferBinding(), c.bufferCount); + handlesByGlSlot[replay.setup.indexBufferBinding()] = c.indexBuffer; + final boolean bound = computeBinder.bindRoStorageBufferHandles(pass, st, handlesByGlSlot); + + applyUniforms(st, replay); + if (!bound) return; + + final int inputs = replay.inputLocations.length; + putFloats(st, replay.locAttr, c.attr, inputs * 4); + putFloats(st, replay.locAttrDefault, c.attrDefault, inputs * 4); + putIntVector(st, replay.locDraw, 4, c.first, c.count, c.instances, c.baseVertex); + putIntVector(st, replay.locIndex, 3, c.indexSize, c.indexOffset, c.indexCount, 0); + putIntVector(st, replay.locRestart, 2, c.restart ? 1 : 0, c.restartIndex, 0, 0); + computeBinder.setUniformBlockOverrides(c.uniformBlocks); + + InvocationDispatch.dispatch(computeBinder, st, pass, (long) c.count * c.instances, replay.locInvocationBase, invocationBaseWriter); + } + + private void putFloats(ContextState st, int location, float[] source, int len) { + if (location < 0 || len == 0) return; + final float[] value = pipelineApplier.reuseOrAlloc(st, location, len); + if (value == null) return; + System.arraycopy(source, 0, value, 0, len); + pipelineApplier.putUniform(st, location, value); + } + + private void applyUniforms(ContextState st, Replay replay) { + for (int u = 0; u < replay.uniformSources.length; u++) { + final int len = uniformLengths[uniformCursor++]; + if (len <= 0) continue; + final float[] value = pipelineApplier.reuseOrAlloc(st, replay.uniformTargets[u], len); + if (value != null) { + System.arraycopy(uniformValues, valueCursor, value, 0, len); + pipelineApplier.putUniform(st, replay.uniformTargets[u], value); + } + valueCursor += len; + } + } + + private static int imageAccess(int index, int writtenImageCount) { + return index < writtenImageCount ? GL15.GL_READ_WRITE : GL15.GL_READ_ONLY; + } + + private void putIntVector(ContextState st, int location, int count, int x, int y, int z, int w) { + if (location < 0) return; + final float[] value = pipelineApplier.reuseOrAlloc(st, location, count); + if (value == null) return; + value[0] = Float.intBitsToFloat(x); + if (count > 1) value[1] = Float.intBitsToFloat(y); + if (count > 2) value[2] = Float.intBitsToFloat(z); + if (count > 3) value[3] = Float.intBitsToFloat(w); + pipelineApplier.putUniform(st, location, value); + } + + private void warnOnce(String key, String message) { + if (warned.add(key)) LOG.warn(message); + } +} diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/VoxelizationDispatcher.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/VoxelizationDispatcher.java index ddde9825f..057077567 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/VoxelizationDispatcher.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/compute/VoxelizationDispatcher.java @@ -4,8 +4,7 @@ import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; public final class VoxelizationDispatcher { - static final int MAX_GROUPS_PER_DISPATCH = 65535; - private static final int WORKGROUP_SIZE = 64; + private static final InvocationDispatch.IntUniformWriter UNIFORM_1I = (st, location, value) -> GLStateManager.glUniform1i(location, value); private final ComputeDispatchSink computeBinder; @@ -25,12 +24,7 @@ public final class VoxelizationDispatcher { if (locBase >= 0) GLStateManager.glUniform1i(locBase, rangeBase); if (locCount >= 0) GLStateManager.glUniform1i(locCount, rangeCount); if (locTotal >= 0) GLStateManager.glUniform1i(locTotal, vertexTotal); - final int groups = (vertexTotal + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE; - for (int g0 = 0; g0 < groups; g0 += MAX_GROUPS_PER_DISPATCH) { - if (locInvBase >= 0) GLStateManager.glUniform1i(locInvBase, g0 * WORKGROUP_SIZE); - computeBinder.pushPendingComputeUniforms(st); - computeBinder.dispatchInBatch(pass, Math.min(MAX_GROUPS_PER_DISPATCH, groups - g0), 1, 1); - } + InvocationDispatch.dispatch(computeBinder, st, pass, vertexTotal, locInvBase, UNIFORM_1I); } public void endBatch(long pass) { diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FrameManager.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FrameManager.java index 7ecef04fd..00793f3af 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FrameManager.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/frame/FrameManager.java @@ -381,11 +381,14 @@ public final class FrameManager { private volatile Runnable preRenderPassHook; + private volatile Runnable renderPassEndHook; private volatile Runnable beforeSubmit; private volatile Runnable afterPresent; public void setPreRenderPassHook(Runnable hook) { this.preRenderPassHook = hook; } + public void setRenderPassEndHook(Runnable hook) { this.renderPassEndHook = hook; } + public void noteComputePassBegun() { final FrameState f = frame(); f.computePassesThisFrame++; @@ -472,6 +475,8 @@ public final class FrameManager { f.currentColorTarget = 0; f.currentDepthTarget = 0; f.activeLayoutHash = 0; + final Runnable hook = renderPassEndHook; + if (hook != null) hook.run(); } } diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/ResourceManager.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/ResourceManager.java index 0b623b39e..50dd1787c 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/ResourceManager.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/resource/ResourceManager.java @@ -248,6 +248,7 @@ public final class ResourceManager { } private long createTextureCore(int glId, int glTarget, int sdlFormat, int glFormat, int width, int height, int depth, int levels, int usage) { + if (getTextureHandle(glId) != 0) beforeTextureReplaced(); usage = reconcileUsageAndWarn(glId, usage); final int sdlType = mapTextureType(glTarget); if (!SDL_GPUTextureSupportsFormat(device.getDevice(), sdlFormat, sdlType, usage)) { @@ -309,7 +310,19 @@ public final class ResourceManager { return sdlFormat; } + private Runnable textureReplaceHook; + + public void setTextureReplaceHook(Runnable hook) { + textureReplaceHook = hook; + } + + private void beforeTextureReplaced() { + final Runnable hook = textureReplaceHook; + if (hook != null) hook.run(); + } + public void deleteTexture(int glId) { + beforeTextureReplaced(); final long handle; wLock.lock(); try { @@ -340,6 +353,7 @@ public final class ResourceManager { } public void releaseTextureHandleForRealloc(int glId) { + beforeTextureReplaced(); final long handle; wLock.lock(); try { diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBinder.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBinder.java index 05458f151..d8cd01bfc 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBinder.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/sampler/SamplerBinder.java @@ -43,8 +43,13 @@ public final class SamplerBinder { } public long getSamplerForUnit(ContextState st, int glUnit, int glTexId) { - if (glUnit >= 0 && glUnit < st.boundSamplerObjects.length && st.boundSamplerObjects[glUnit] != 0) { - final TextureSamplerState ss = resourceManager.getSamplerObject(st.boundSamplerObjects[glUnit]); + final int samplerObject = glUnit >= 0 && glUnit < st.boundSamplerObjects.length ? st.boundSamplerObjects[glUnit] : 0; + return getSampler(samplerObject, glTexId); + } + + public long getSampler(int samplerObject, int glTexId) { + if (samplerObject != 0) { + final TextureSamplerState ss = resourceManager.getSamplerObject(samplerObject); if (ss != null) return getOrCreateSdlSampler(ss); } if (glTexId != 0) return getSamplerForTexture(glTexId); diff --git a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java index a206da3f3..904ea5045 100644 --- a/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java +++ b/sdl-gpu/src/main/java/com/gtnewhorizons/angelica/sdlgpu/shader/ShaderManager.java @@ -11,6 +11,7 @@ import com.gtnewhorizons.angelica.glsm.shader.ShaderCacheIO; import com.gtnewhorizons.angelica.glsm.shader.ShaderDiskCache; import com.gtnewhorizons.angelica.glsm.shader.SpirvCompiler; import com.gtnewhorizons.angelica.glsm.threading.AngelicaWorkers; +import com.gtnewhorizons.angelica.sdlgpu.compute.VertexWriteReplayer; import com.gtnewhorizons.angelica.sdlgpu.device.Device; import com.gtnewhorizons.angelica.sdlgpu.frame.ContextState; import com.gtnewhorizons.angelica.sdlgpu.shader.cross.CrossCompileCache; @@ -2078,6 +2079,7 @@ public final class ShaderManager { public int vertexShader; public int vertexUboSize; public long sdlComputePipeline; + public VertexWriteReplayer.Replay vertexWriteReplay; public long sdlFragmentShader; public long sdlVertexShader; public long lastComputeCb; diff --git a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/compute/VoxelizationDispatcherTest.java b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/compute/VoxelizationDispatcherTest.java index 226738fb8..7c5dd67d4 100644 --- a/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/compute/VoxelizationDispatcherTest.java +++ b/sdl-gpu/src/test/java/com/gtnewhorizons/angelica/sdlgpu/compute/VoxelizationDispatcherTest.java @@ -77,9 +77,9 @@ class VoxelizationDispatcherTest { void dispatchRegion_splitsAboveMaxGroups() { final RecordingSink sink = new RecordingSink(); final VoxelizationDispatcher d = new VoxelizationDispatcher(sink); - region(d, 1, VoxelizationDispatcher.MAX_GROUPS_PER_DISPATCH * 64 + 1, new ContextState()); + region(d, 1, InvocationDispatch.MAX_GROUPS_PER_DISPATCH * 64 + 1, new ContextState()); assertEquals(2, sink.dispatches.size()); - assertEquals(VoxelizationDispatcher.MAX_GROUPS_PER_DISPATCH, sink.dispatches.get(0)[0]); + assertEquals(InvocationDispatch.MAX_GROUPS_PER_DISPATCH, sink.dispatches.get(0)[0]); assertEquals(1, sink.dispatches.get(1)[0]); assertEquals(2, sink.uniformPushCount); } diff --git a/src/main/java/com/gtnewhorizons/angelica/api/EnderDragonBeams.java b/src/main/java/com/gtnewhorizons/angelica/api/EnderDragonBeams.java new file mode 100644 index 000000000..7ec284ba4 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/api/EnderDragonBeams.java @@ -0,0 +1,39 @@ +package com.gtnewhorizons.angelica.api; + +import com.gtnewhorizons.angelica.config.AngelicaConfig; +import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition; +import net.coderbot.iris.layer.GbufferPrograms; +import net.coderbot.iris.shaderpack.materialmap.NamespacedId; +import net.coderbot.iris.uniforms.CapturedRenderingState; + +public final class EnderDragonBeams { + + private static final NamespacedId CRYSTAL_BEAM = new NamespacedId("minecraft", "end_crystal_beam"); + private static final NamespacedId DEATH_RAYS = new NamespacedId("minecraft", "dragon_death_rays"); + + private EnderDragonBeams() {} + + public static void beginCrystalBeam() { + if (!AngelicaConfig.enableIris) return; + CapturedRenderingState.INSTANCE.pushCurrentEntityAndItem(); + CapturedRenderingState.INSTANCE.setCurrentNamedEntity(CRYSTAL_BEAM); + } + + public static void endCrystalBeam() { + if (!AngelicaConfig.enableIris) return; + CapturedRenderingState.INSTANCE.popCurrentEntityAndItem(); + } + + public static void beginDeathRays() { + if (!AngelicaConfig.enableIris) return; + GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.LIGHTNING); + CapturedRenderingState.INSTANCE.pushCurrentEntityAndItem(); + CapturedRenderingState.INSTANCE.setCurrentNamedEntity(DEATH_RAYS); + } + + public static void endDeathRays() { + if (!AngelicaConfig.enableIris) return; + CapturedRenderingState.INSTANCE.popCurrentEntityAndItem(); + GbufferPrograms.teardownSpecialRenderCondition(); + } +} diff --git a/src/main/java/com/gtnewhorizons/angelica/api/EyePassRenderer.java b/src/main/java/com/gtnewhorizons/angelica/api/EyePassRenderer.java new file mode 100644 index 000000000..26b07d158 --- /dev/null +++ b/src/main/java/com/gtnewhorizons/angelica/api/EyePassRenderer.java @@ -0,0 +1,4 @@ +package com.gtnewhorizons.angelica.api; + +public interface EyePassRenderer { +} diff --git a/src/main/java/com/gtnewhorizons/angelica/helpers/RendererLivingEntityHelper.java b/src/main/java/com/gtnewhorizons/angelica/helpers/RendererLivingEntityHelper.java index 981d99972..77da42c20 100644 --- a/src/main/java/com/gtnewhorizons/angelica/helpers/RendererLivingEntityHelper.java +++ b/src/main/java/com/gtnewhorizons/angelica/helpers/RendererLivingEntityHelper.java @@ -2,6 +2,7 @@ package com.gtnewhorizons.angelica.helpers; import static com.gtnewhorizons.angelica.client.font.ColorCodeUtils.FORMATTING_CHAR; +import com.gtnewhorizons.angelica.api.EyePassRenderer; import net.minecraft.client.renderer.entity.RenderDragon; import net.minecraft.client.renderer.entity.RenderEnderman; import net.minecraft.client.renderer.entity.RenderSpider; @@ -15,7 +16,8 @@ public final class RendererLivingEntityHelper { private RendererLivingEntityHelper() {} public static boolean hasEyePass(Object renderer) { - return renderer instanceof RenderSpider || renderer instanceof RenderEnderman || renderer instanceof RenderDragon; + return renderer instanceof RenderSpider || renderer instanceof RenderEnderman || renderer instanceof RenderDragon + || renderer instanceof EyePassRenderer; } public static String getUpsideDownName(EntityLivingBase entity) { diff --git a/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java b/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java index e0d2d68af..00eaae5a8 100644 --- a/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java +++ b/src/main/java/net/coderbot/iris/pipeline/DeferredWorldRenderingPipeline.java @@ -181,6 +181,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R // Custom images and SSBOs private final Set customImages; + private final VertexWriteReplays vertexWriteReplays = new VertexWriteReplays(); private final GlImage[] imagesToClear; @Nullable private final ShaderStorageBufferHolder ssboHolder; @@ -1181,6 +1182,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R ProgramBuilder builder = beginBuilder(source.getName(), transformed); wireGbufferProgram(builder, availability, shadow); final Program program = builder.build(); + vertexWriteReplays.attach(source.getName(), program, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow)); final Pass pass = createPassInner(program, source.getDirectives(), shadow, id); this.customUniforms.mapholderToPass(builder, pass); @@ -1194,17 +1196,20 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R private void wireGbufferProgram(ProgramBuilder builder, InputAvailability availability, boolean shadow) { CommonUniforms.addDynamicUniforms(builder, FogMode.PER_VERTEX); this.customUniforms.assignTo(builder); + wireGbufferResources(builder, builder, availability, shadow); + } + private void wireGbufferResources(SamplerHolder samplers, ImageHolder images, InputAvailability availability, boolean shadow) { final Supplier> flipped = shadow ? flippedShadowGbuffers : flippedGbuffers; TextureStage textureStage = TextureStage.GBUFFERS_AND_SHADOW; ProgramSamplers.CustomTextureSamplerInterceptor customTextureSamplerInterceptor = - ProgramSamplers.customTextureSamplerInterceptor(builder, + ProgramSamplers.customTextureSamplerInterceptor(samplers, customTextureManager.getCustomTextureIdMap(textureStage)); IrisSamplers.addRenderTargetSamplers(customTextureSamplerInterceptor, flipped, renderTargets, false, this); - IrisImages.addRenderTargetImages(builder, flipped, renderTargets); + IrisImages.addRenderTargetImages(images, flipped, renderTargets); if (!shouldBindPBR) { shouldBindPBR = IrisSamplers.hasPBRSamplers(customTextureSamplerInterceptor); @@ -1220,7 +1225,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R IrisSamplers.addCustomImages(customTextureSamplerInterceptor, customImages); IrisSamplers.addCustomTextures(customTextureSamplerInterceptor, customTextureManager.getIrisCustomTextures()); - IrisImages.addCustomImages(builder, customImages); + IrisImages.addCustomImages(images, customImages); if (IrisSamplers.hasShadowSamplers(customTextureSamplerInterceptor)) { if (!shadow) { @@ -1229,7 +1234,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R if (shadowRenderTargets != null) { recordSamplerUsage(IrisSamplers.addShadowSamplers(customTextureSamplerInterceptor, shadowRenderTargets, null, true)); - IrisImages.addShadowColorImages(builder, shadowRenderTargets, null); + IrisImages.addShadowColorImages(images, shadowRenderTargets, null); } } } @@ -1527,6 +1532,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R final ProgramBuilder builder = beginBuilder(sourceName + kind.variantSuffix, transformed); wireGbufferProgram(builder, availability, shadow); final Program variant = builder.build(); + vertexWriteReplays.attach(sourceName + kind.variantSuffix, variant, transformed, (samplers, images) -> wireGbufferResources(samplers, images, availability, shadow)); this.customUniforms.mapholderToPass(builder, variant); return variant; } catch (Exception e) { @@ -1606,6 +1612,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R } } } + vertexWriteReplays.destroy(); if (shadowVoxelizationCompute != null) { shadowVoxelizationCompute.destroy(); shadowVoxelizationCompute = null; diff --git a/src/main/java/net/coderbot/iris/pipeline/VertexWriteReplays.java b/src/main/java/net/coderbot/iris/pipeline/VertexWriteReplays.java new file mode 100644 index 000000000..3acac26bf --- /dev/null +++ b/src/main/java/net/coderbot/iris/pipeline/VertexWriteReplays.java @@ -0,0 +1,161 @@ +package net.coderbot.iris.pipeline; + +import com.gtnewhorizons.angelica.glsm.RenderSystem; +import com.gtnewhorizons.angelica.glsm.backend.BackendManager; +import com.gtnewhorizons.angelica.glsm.backend.VertexWriteReplaySetup; +import com.gtnewhorizons.angelica.glsm.texture.InternalTextureFormat; +import com.gtnewhorizons.angelica.glsm.texture.TextureType; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import net.coderbot.iris.Iris; +import net.coderbot.iris.gl.image.ImageHolder; +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.sampler.GlSampler; +import net.coderbot.iris.gl.sampler.SamplerHolder; +import net.coderbot.iris.gl.state.ValueUpdateNotifier; +import net.coderbot.iris.pipeline.transform.PatchShaderType; +import net.coderbot.iris.pipeline.transform.RwImageStoreExtractor; +import net.coderbot.iris.pipeline.transform.RwImageStoreExtractor.VertexInput; +import net.coderbot.iris.samplers.IrisSamplers; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.IntSupplier; + +/** + * SDL-GPU only allows image writes from compute, so a gbuffers/shadow vertex shader that writes images gets a compute + * twin that the backend runs over every draw made with the program. + */ +final class VertexWriteReplays { + + private final List twins = new ArrayList<>(); + private final IntArrayList attachedPrograms = new IntArrayList(); + + void attach(String name, Program program, Map transformed, BiConsumer wireResources) { + BackendManager.RENDER_BACKEND.setVertexWriteReplay(program.getProgramId(), null); + final String source = transformed.get(PatchShaderType.COMPUTE); + if (RwImageStoreExtractor.parseSentinel(source) != RwImageStoreExtractor.RwExtractMode.VERTEX_REPLAY) return; + + final ComputeProgram twin; + try { + twin = ProgramBuilder.beginCompute(name + "_vertex_replay", source, IrisSamplers.WORLD_RESERVED_TEXTURE_UNITS).buildCompute(); + } catch (RuntimeException e) { + Iris.logger.error("Vertex image write replay for {} failed to build; its image writes are dropped", name, e); + return; + } + + final List writtenImages = RwImageStoreExtractor.parseVertexReplayImages(source); + final List images = new ArrayList<>(writtenImages); + images.addAll(RwImageStoreExtractor.parseVertexReplayReadImages(source)); + final List samplers = new ArrayList<>(); + for (String sampler : RwImageStoreExtractor.parseVertexReplaySamplers(source)) { + if (RenderSystem.getUniformLocation(twin.getProgramId(), sampler) != -1) samplers.add(sampler); + } + final ResourceCollector resources = new ResourceCollector(images, samplers); + wireResources.accept(resources, resources); + for (int i = 0; i < resources.images.length; i++) { + if (resources.images[i] == null) { + Iris.logger.warn("Vertex image writes of {} use image {}, which the pipeline does not provide; they are dropped", name, resources.imageNames.get(i)); + twin.destroy(); + return; + } + } + + final List inputs = RwImageStoreExtractor.parseVertexReplayInputs(source); + final String[] inputNames = new String[inputs.size()]; + final int[] inputLocations = new int[inputs.size()]; + for (int i = 0; i < inputs.size(); i++) { + inputNames[i] = inputs.get(i).name(); + inputLocations[i] = inputs.get(i).location(); + } + BackendManager.RENDER_BACKEND.setVertexWriteReplay(program.getProgramId(), new VertexWriteReplaySetup(twin.getProgramId(), + inputNames, inputLocations, resources.images, writtenImages.size(), + resources.samplerNames.toArray(new String[0]), resources.samplerUnits, resources.samplerTextures, resources.samplerObjects, + RwImageStoreExtractor.VG_REPLAY_VERTEX_BUFFER_BINDING, RwImageStoreExtractor.VG_REPLAY_VERTEX_BUFFER_COUNT, RwImageStoreExtractor.VG_REPLAY_INDEX_BUFFER_BINDING)); + attachedPrograms.add(program.getProgramId()); + twins.add(twin); + } + + void destroy() { + for (int i = 0; i < attachedPrograms.size(); i++) BackendManager.RENDER_BACKEND.setVertexWriteReplay(attachedPrograms.getInt(i), null); + attachedPrograms.clear(); + for (ComputeProgram twin : twins) twin.destroy(); + twins.clear(); + } + + private static final class ResourceCollector implements SamplerHolder, ImageHolder { + final List imageNames; + final IntSupplier[] images; + final List samplerNames; + final int[] samplerUnits; + final IntSupplier[] samplerTextures; + final int[] samplerObjects; + + ResourceCollector(List imageNames, List samplerNames) { + this.imageNames = imageNames; + this.images = new IntSupplier[imageNames.size()]; + this.samplerNames = samplerNames; + this.samplerUnits = new int[samplerNames.size()]; + Arrays.fill(samplerUnits, -1); + this.samplerTextures = new IntSupplier[samplerNames.size()]; + this.samplerObjects = new int[samplerNames.size()]; + } + + @Override + public boolean hasImage(String name) { + return imageNames.contains(name); + } + + @Override + public void addTextureImage(IntSupplier textureID, InternalTextureFormat internalFormat, String name) { + final int index = imageNames.indexOf(name); + if (index >= 0 && images[index] == null) images[index] = textureID; + } + + @Override + public boolean hasSampler(String name) { + return samplerNames.contains(name) || imageNames.contains(name); + } + + @Override + public void addExternalSampler(int textureUnit, String... names) { + for (String name : names) { + final int index = samplerNames.indexOf(name); + if (index >= 0 && unassigned(index)) samplerUnits[index] = textureUnit; + } + } + + @Override + public boolean addDefaultSampler(TextureType type, IntSupplier texture, ValueUpdateNotifier notifier, GlSampler sampler, String... names) { + return addDynamicSampler(type, texture, notifier, sampler, names); + } + + @Override + public boolean addDynamicSampler(TextureType type, IntSupplier texture, GlSampler sampler, String... names) { + return addDynamicSampler(type, texture, null, sampler, names); + } + + @Override + public boolean addDynamicSampler(TextureType type, IntSupplier texture, ValueUpdateNotifier notifier, GlSampler sampler, String... names) { + boolean used = false; + for (String name : names) { + final int index = samplerNames.indexOf(name); + if (index < 0) continue; + used = true; + if (unassigned(index)) { + samplerTextures[index] = texture; + samplerObjects[index] = sampler != null ? sampler.getId() : 0; + } + } + return used; + } + + private boolean unassigned(int index) { + return samplerUnits[index] < 0 && samplerTextures[index] == null; + } + } +} diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/PreRasterComputeDispatcher.java b/src/main/java/net/coderbot/iris/pipeline/transform/PreRasterComputeDispatcher.java index 551bc0419..a7950472a 100644 --- a/src/main/java/net/coderbot/iris/pipeline/transform/PreRasterComputeDispatcher.java +++ b/src/main/java/net/coderbot/iris/pipeline/transform/PreRasterComputeDispatcher.java @@ -3,6 +3,7 @@ package net.coderbot.iris.pipeline.transform; import com.gtnewhorizons.angelica.glsm.GLStateManager; import com.gtnewhorizons.angelica.glsm.RenderSystem; import net.coderbot.iris.gl.program.ComputeProgram; +import net.coderbot.iris.postprocess.FullScreenQuadRenderer; import net.coderbot.iris.uniforms.custom.CustomUniforms; import org.lwjgl.opengl.GL20; import org.lwjgl.opengl.GL42; @@ -16,6 +17,7 @@ public final class PreRasterComputeDispatcher { final int prevProgram = GLStateManager.glGetInteger(GL20.GL_CURRENT_PROGRAM); compute.use(); uniforms.push(compute); + if (mode == RwImageStoreExtractor.RwExtractMode.COMPOSITE_VSH) FullScreenQuadRenderer.uploadCompositeMatrices(); int targetSizeLoc = cachedTargetSizeLoc; if (mode == RwImageStoreExtractor.RwExtractMode.COMPOSITE_FSH) { if (targetSizeLoc == LOC_UNRESOLVED) { diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/RwImageStoreExtractor.java b/src/main/java/net/coderbot/iris/pipeline/transform/RwImageStoreExtractor.java index ac5892461..c2a12abaa 100644 --- a/src/main/java/net/coderbot/iris/pipeline/transform/RwImageStoreExtractor.java +++ b/src/main/java/net/coderbot/iris/pipeline/transform/RwImageStoreExtractor.java @@ -7,6 +7,7 @@ import org.antlr.v4.runtime.CommonToken; import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.TerminalNode; +import org.embeddedt.embeddium.impl.render.shader.ShaderLoader; import org.taumc.glsl.ShaderParser; import org.taumc.glsl.Transformer; import org.taumc.glsl.grammar.GLSLLexer; @@ -25,23 +26,26 @@ import java.util.Set; public final class RwImageStoreExtractor { - public enum RwExtractMode { CHUNK, COMPOSITE_VSH, COMPOSITE_FSH } + public enum RwExtractMode { CHUNK, COMPOSITE_VSH, COMPOSITE_FSH, VERTEX_REPLAY } public static RwExtractMode parseSentinel(String computeSource) { if (computeSource == null) return null; if (computeSource.startsWith("// _vg_mode: chunk")) return RwExtractMode.CHUNK; if (computeSource.startsWith("// _vg_mode: composite-vsh")) return RwExtractMode.COMPOSITE_VSH; if (computeSource.startsWith("// _vg_mode: composite-fsh")) return RwExtractMode.COMPOSITE_FSH; + if (computeSource.startsWith("// _vg_mode: vertex-replay")) return RwExtractMode.VERTEX_REPLAY; return null; } + public record VertexInput(String name, String glslType, int location) {} + public record AttributeSlot(String name, String glslType, int byteOffset, String unpackExpr, boolean liveBranch) { public AttributeSlot(String name, String glslType, int byteOffset, String unpackExpr) { this(name, glslType, byteOffset, unpackExpr, true); } } - public record Result(String strippedSource, String computeSource, Set writtenImages, RwExtractMode mode) {} + public record Result(String strippedSource, String computeSource, Set writtenImages, RwExtractMode mode, boolean keepsImageAtomics) {} public record ImageDecl(String name, String glslType, String layoutFormat) {} @@ -53,6 +57,9 @@ public final class RwImageStoreExtractor { public static final int VG_VBUF_SSBO_BINDING = 9; public static final int VG_RANGES_SSBO_BINDING = 10; + public static final int VG_REPLAY_VERTEX_BUFFER_BINDING = 9; + public static final int VG_REPLAY_VERTEX_BUFFER_COUNT = 4; + public static final int VG_REPLAY_INDEX_BUFFER_BINDING = VG_REPLAY_VERTEX_BUFFER_BINDING + VG_REPLAY_VERTEX_BUFFER_COUNT; private static final int STRIDE_BYTES = 48; private static final int STRIDE_UINTS = STRIDE_BYTES / 4; @@ -73,6 +80,10 @@ public final class RwImageStoreExtractor { ); public static Result tryExtract(String source, PatchShaderType stage, String programName) { + return tryExtract(source, stage, programName, null); + } + + public static Result tryExtract(String source, PatchShaderType stage, String programName, RwExtractMode vertexMode) { if (source == null) return null; if (stage != PatchShaderType.VERTEX && stage != PatchShaderType.FRAGMENT) return null; @@ -94,17 +105,184 @@ public final class RwImageStoreExtractor { final Map chunkAttrAstTypes = (stage == PatchShaderType.VERTEX) ? collectChunkAttrAstTypes(decls) : Map.of(); final Set chunkAttrsUsed = (stage == PatchShaderType.VERTEX) ? collectChunkAttrsUsed(decls, root, source) : Set.of(); - final RwExtractMode mode = (stage == PatchShaderType.VERTEX) ? (chunkAttrsUsed.isEmpty() ? RwExtractMode.COMPOSITE_VSH : RwExtractMode.CHUNK) : RwExtractMode.COMPOSITE_FSH; + final RwExtractMode mode = (stage == PatchShaderType.VERTEX) + ? (vertexMode != null ? vertexMode : chunkAttrsUsed.isEmpty() ? RwExtractMode.COMPOSITE_VSH : RwExtractMode.CHUNK) + : RwExtractMode.COMPOSITE_FSH; final List attrs = (mode == RwExtractMode.CHUNK) ? collectVertexAttributes(chunkAttrsUsed, chunkAttrAstTypes) : List.of(); + final Set quadInputs = (mode == RwExtractMode.COMPOSITE_VSH) ? declaredQuadInputs(decls) : Set.of(); + final boolean definesFtransform = definesFunction(root, "iris_ftransform"); final String stripped = buildRasterOutputViaSourceSlicing(source, decls, exprs, writtenImages, declared, mode); final Transformer tc = new Transformer(root); - applyComputeMutations(tc, writtenImages, declared, chunkAttrsUsed, mode, nonWriteonlyImages); - final String compute = buildComputeOutput(root, mode, writtenImages, nonWriteonlyImages, declared, attrs); + applyComputeMutations(tc, writtenImages, declared, mode == RwExtractMode.COMPOSITE_VSH ? quadInputs : chunkAttrsUsed, mode, nonWriteonlyImages); + final String compute = buildComputeOutput(root, mode, writtenImages, nonWriteonlyImages, declared, attrs, quadInputs, definesFtransform, null); - return new Result(stripped, compute, writtenImages, mode); + return new Result(stripped, compute, writtenImages, mode, keepsImageAtomics(root, writtenImages)); + } + + public static Result tryExtractVertexReplay(String source) { + if (source == null) return null; + + final ShaderParser.ParsedShader parsed = ShaderParser.parseShader(source); + final GLSLParser.Translation_unitContext root = parsed.full(); + + final List decls = new ArrayList<>(); + final List exprs = new ArrayList<>(); + GlslAstHelpers.collectInto(root, + new List[] { decls, exprs }, + new Class[] { GLSLParser.Single_declarationContext.class, GLSLParser.Postfix_expressionContext.class }); + + final Map declared = collectImageDecls(decls); + final Set writtenImages = new LinkedHashSet<>(); + final Set nonWriteonlyImages = new LinkedHashSet<>(); + classifyImageCalls(exprs, declared, writtenImages, nonWriteonlyImages); + if (writtenImages.isEmpty()) return null; + + final String stripped = buildRasterOutputViaSourceSlicing(source, decls, exprs, writtenImages, declared, RwExtractMode.VERTEX_REPLAY); + final List inputs = collectVertexInputs(decls); + if (inputs == null) return new Result(stripped, null, writtenImages, RwExtractMode.VERTEX_REPLAY, false); + + final List readImages = readOnlyImages(exprs, declared, writtenImages); + final Set inputNames = new LinkedHashSet<>(); + for (VertexInput input : inputs) inputNames.add(input.name()); + final Transformer tc = new Transformer(root); + applyComputeMutations(tc, writtenImages, declared, inputNames, RwExtractMode.VERTEX_REPLAY, nonWriteonlyImages); + for (String name : readImages) tc.removeVariable(name); + final ReplayLayout layout = new ReplayLayout(inputs, readImages, replayHeader(inputs, writtenImages, readImages, collectSamplerNames(decls))); + final String compute = buildComputeOutput(root, RwExtractMode.VERTEX_REPLAY, writtenImages, nonWriteonlyImages, declared, List.of(), Set.of(), false, layout); + + return new Result(stripped, compute, writtenImages, RwExtractMode.VERTEX_REPLAY, keepsImageAtomics(root, writtenImages)); + } + + private record ReplayLayout(List inputs, List readImages, String header) {} + + private static final String REPLAY_INPUTS_HEADER = "// _vg_inputs:"; + private static final String REPLAY_IMAGES_HEADER = "// _vg_images:"; + private static final String REPLAY_READ_IMAGES_HEADER = "// _vg_read_images:"; + private static final String REPLAY_SAMPLERS_HEADER = "// _vg_samplers:"; + + private static String replayHeader(List inputs, Set writtenImages, List readImages, List samplers) { + final StringBuilder out = new StringBuilder(REPLAY_INPUTS_HEADER); + for (VertexInput input : inputs) out.append(' ').append(input.name()).append(':').append(input.glslType()).append(':').append(input.location()); + out.append('\n').append(REPLAY_IMAGES_HEADER); + for (String image : sortedImages(writtenImages)) out.append(' ').append(image); + out.append('\n').append(REPLAY_READ_IMAGES_HEADER); + for (String image : readImages) out.append(' ').append(image); + out.append('\n').append(REPLAY_SAMPLERS_HEADER); + for (String sampler : samplers) out.append(' ').append(sampler); + return out.append('\n').toString(); + } + + /** Images the shader reads but never writes, sorted; the replay binds them after the written ones. */ + private static List readOnlyImages(List exprs, Map declared, Set writtenImages) { + final Set read = new LinkedHashSet<>(); + for (var expr : exprs) { + if (GlslAstHelpers.extractCallName(expr) == null) continue; + final String firstArg = GlslAstHelpers.firstArgIdentifier(expr); + if (declared.containsKey(firstArg) && !writtenImages.contains(firstArg)) read.add(firstArg); + } + final List sorted = new ArrayList<>(read); + Collections.sort(sorted); + return sorted; + } + + /** The images a vertex-replay compute source only reads, in the order of their bindings after the written ones. */ + public static List parseVertexReplayReadImages(String computeSource) { + return headerTokens(computeSource, REPLAY_READ_IMAGES_HEADER); + } + + /** The samplers a vertex-replay compute source declares. */ + public static List parseVertexReplaySamplers(String computeSource) { + return headerTokens(computeSource, REPLAY_SAMPLERS_HEADER); + } + + /** The inputs of a vertex-replay compute source, in the order of its {@code _vg_attr} slots. */ + public static List parseVertexReplayInputs(String computeSource) { + final List out = new ArrayList<>(); + for (String token : headerTokens(computeSource, REPLAY_INPUTS_HEADER)) { + final String[] parts = token.split(":"); + if (parts.length != 3) continue; + out.add(new VertexInput(parts[0], parts[1], Integer.parseInt(parts[2]))); + } + return out; + } + + /** The images a vertex-replay compute source writes, in the order of their bindings. */ + public static List parseVertexReplayImages(String computeSource) { + return headerTokens(computeSource, REPLAY_IMAGES_HEADER); + } + + private static List headerTokens(String computeSource, String header) { + if (computeSource == null) return List.of(); + final int start = computeSource.indexOf(header); + if (start < 0) return List.of(); + final int end = computeSource.indexOf('\n', start); + final String line = computeSource.substring(start + header.length(), end < 0 ? computeSource.length() : end).trim(); + if (line.isEmpty()) return List.of(); + return List.of(line.split(" ")); + } + + private static final Set REPLAYABLE_INPUT_TYPES = Set.of( + "float", "vec2", "vec3", "vec4", "int", "ivec2", "ivec3", "ivec4", "uint", "uvec2", "uvec3", "uvec4"); + + /** Global {@code in}/{@code attribute} declarations, or null if one of them cannot be fetched per component. */ + private static List collectVertexInputs(List decls) { + final List out = new ArrayList<>(); + for (var decl : decls) { + final var fst = decl.fully_specified_type(); + if (fst == null || fst.type_qualifier() == null) continue; + if (GlslAstHelpers.enclosingOfType(decl, GLSLParser.Function_definitionContext.class) != null) continue; + boolean isInput = false; + int location = -1; + for (var sq : fst.type_qualifier().single_type_qualifier()) { + if (sq.storage_qualifier() != null) { + final String storage = sq.storage_qualifier().getText(); + if (storage.equals("in") || storage.equals("attribute")) isInput = true; + } + final var lq = sq.layout_qualifier(); + if (lq == null || lq.layout_qualifier_id_list() == null) continue; + for (var id : lq.layout_qualifier_id_list().layout_qualifier_id()) { + if (id.IDENTIFIER() == null || !"location".equals(id.IDENTIFIER().getText())) continue; + try { + location = Integer.parseInt(id.getText().substring(id.getText().indexOf('=') + 1).trim()); + } catch (NumberFormatException e) { + return null; + } + } + } + if (!isInput) continue; + final String type = fst.type_specifier().getText(); + if (!REPLAYABLE_INPUT_TYPES.contains(type)) return null; + final List declarators = new ArrayList<>(); + declarators.add(decl.typeless_declaration()); + if (decl.getParent() instanceof GLSLParser.Init_declarator_listContext list) declarators.addAll(list.typeless_declaration()); + for (var typeless : declarators) { + if (typeless == null || typeless.IDENTIFIER() == null || typeless.array_specifier() != null) return null; + out.add(new VertexInput(typeless.IDENTIFIER().getText(), type, location)); + } + } + return out; + } + + /** Global sampler uniforms, which the replay may read even after the graphics program stops reading them. */ + private static List collectSamplerNames(List decls) { + final List out = new ArrayList<>(); + for (var decl : decls) { + final var fst = decl.fully_specified_type(); + if (fst == null || fst.type_specifier() == null) continue; + if (GlslAstHelpers.enclosingOfType(decl, GLSLParser.Function_definitionContext.class) != null) continue; + final String type = fst.type_specifier().getText(); + if (!type.startsWith("sampler") && !type.startsWith("isampler") && !type.startsWith("usampler")) continue; + final List declarators = new ArrayList<>(); + declarators.add(decl.typeless_declaration()); + if (decl.getParent() instanceof GLSLParser.Init_declarator_listContext list) declarators.addAll(list.typeless_declaration()); + for (var typeless : declarators) { + if (typeless != null && typeless.IDENTIFIER() != null) out.add(typeless.IDENTIFIER().getText()); + } + } + return out; } private static Map collectImageDecls(List decls) { @@ -270,13 +448,22 @@ public final class RwImageStoreExtractor { private static void applyComputeMutations(Transformer tc, Set writtenImages, Map declared, Set chunkAttrsUsed, RwExtractMode mode, Set nonWriteonlyImages) { hoistNonConstGlobalInitializers(tc); renameMainToVgBody(tc); - rewriteImageAtomicsToNonAtomic(tc, declared, writtenImages, nonWriteonlyImages); + if (mode != RwExtractMode.VERTEX_REPLAY) rewriteImageAtomicsToNonAtomic(tc, declared, writtenImages, nonWriteonlyImages); for (String name : writtenImages) tc.removeVariable(name); - if (mode == RwExtractMode.CHUNK) { + if (mode == RwExtractMode.CHUNK || mode == RwExtractMode.VERTEX_REPLAY) { for (String name : chunkAttrsUsed) tc.removeVariable(name); + convertInterfaceBlocks(tc); + if (mode == RwExtractMode.VERTEX_REPLAY) { + stripInterfaceLayoutQualifiers(tc); + removeQualifierOnlyDeclarations(tc); + } stripStorageQualifiers(tc, Set.of("out", "varying")); } else if (mode == RwExtractMode.COMPOSITE_VSH) { + for (String name : chunkAttrsUsed) tc.removeVariable(name); tc.renameFunctionCall("ftransform", "iris_ftransform"); + convertInterfaceBlocks(tc); + stripInterfaceLayoutQualifiers(tc); + removeQualifierOnlyDeclarations(tc); stripStorageQualifiers(tc, Set.of("in", "attribute", "out", "varying")); } else if (mode == RwExtractMode.COMPOSITE_FSH) { stripFragOutputs(tc); @@ -311,6 +498,14 @@ public final class RwImageStoreExtractor { }); } + private static boolean keepsImageAtomics(GLSLParser.Translation_unitContext computeRoot, Set writtenImages) { + for (var expr : GlslAstHelpers.collectAll(computeRoot, GLSLParser.Postfix_expressionContext.class)) { + final String fname = GlslAstHelpers.extractCallName(expr); + if (fname != null && ATOMIC_CALLS.contains(fname) && writtenImages.contains(GlslAstHelpers.firstArgIdentifier(expr))) return true; + } + return false; + } + private static boolean isCallTopLevelStatement(GLSLParser.Postfix_expressionContext expr) { final GLSLParser.StatementContext stmt = GlslAstHelpers.enclosingOfType(expr, GLSLParser.StatementContext.class); if (stmt == null) return false; @@ -409,6 +604,7 @@ public final class RwImageStoreExtractor { t.mutateTree(tree -> { for (var sq : GlslAstHelpers.collectAll(tree, GLSLParser.Storage_qualifierContext.class)) { if (!kill.contains(sq.getText())) continue; + if (GlslAstHelpers.enclosingOfType(sq, GLSLParser.Parameter_declarationContext.class) != null) continue; blankLeadingToken(sq); } }); @@ -428,6 +624,91 @@ public final class RwImageStoreExtractor { private static final Set AUXILIARY_STORAGE_QUALIFIERS = Set.of("centroid", "sample", "patch"); + private static final Set INTERFACE_STORAGE_QUALIFIERS = Set.of("in", "out", "attribute", "varying"); + + private static void removeQualifierOnlyDeclarations(Transformer t) { + t.mutateTree(tree -> { + if (tree.children == null) return; + tree.children.removeIf(child -> child instanceof GLSLParser.External_declarationContext ed + && ed.declaration() != null && isQualifierOnly(ed.declaration())); + }); + } + + private static boolean isQualifierOnly(GLSLParser.DeclarationContext decl) { + if (decl.LEFT_BRACE() != null) return false; + if (decl.init_declarator_list() == null) return decl.type_qualifier() != null && qualifiesStageInterface(decl.type_qualifier()); + final var single = decl.init_declarator_list().single_declaration(); + return single != null && single.typeless_declaration() == null && decl.init_declarator_list().COMMA().isEmpty() + && single.fully_specified_type() != null && single.fully_specified_type().type_qualifier() != null + && qualifiesStageInterface(single.fully_specified_type().type_qualifier()); + } + + private static boolean qualifiesStageInterface(GLSLParser.Type_qualifierContext tq) { + for (var sq : tq.single_type_qualifier()) { + if (sq.invariant_qualifier() != null || sq.precise_qualifier() != null) return true; + if (sq.storage_qualifier() != null && INTERFACE_STORAGE_QUALIFIERS.contains(sq.storage_qualifier().getText())) return true; + } + return false; + } + + private static void convertInterfaceBlocks(Transformer t) { + t.mutateTree(tree -> { + if (tree.children == null) return; + tree.children.removeIf(child -> child instanceof GLSLParser.External_declarationContext ed + && isInterfaceBlock(ed.declaration()) && "gl_PerVertex".equals(ed.declaration().IDENTIFIER(0).getText())); + for (var child : tree.children) { + if (!(child instanceof GLSLParser.External_declarationContext ed) || !isInterfaceBlock(ed.declaration())) continue; + final GLSLParser.DeclarationContext decl = ed.declaration(); + final String blockName = decl.IDENTIFIER(0).getText(); + blankAllTerminals(decl.type_qualifier()); + if (decl.IDENTIFIER().size() > 1) { + setFirstTerminal(decl.type_qualifier(), "struct"); + setText(decl.RIGHT_BRACE(), "}; " + blockName); + } else { + setText(decl.IDENTIFIER(0), ""); + setText(decl.LEFT_BRACE(), ""); + setText(decl.RIGHT_BRACE(), ""); + setText(decl.SEMICOLON(), ""); + } + } + }); + } + + private static boolean isInterfaceBlock(GLSLParser.DeclarationContext decl) { + return decl != null && decl.LEFT_BRACE() != null && decl.type_qualifier() != null && !decl.IDENTIFIER().isEmpty() + && qualifiesStageInterface(decl.type_qualifier()); + } + + private static void setText(TerminalNode node, String text) { + if (node != null && node.getSymbol() instanceof CommonToken ct) ct.setText(text); + } + + private static void setFirstTerminal(ParseTree node, String text) { + if (node instanceof TerminalNode tn) { + setText(tn, text); + return; + } + if (node.getChildCount() > 0) setFirstTerminal(node.getChild(0), text); + } + + private static void stripInterfaceLayoutQualifiers(Transformer t) { + t.mutateTree(tree -> { + for (var tq : GlslAstHelpers.collectAll(tree, GLSLParser.Type_qualifierContext.class)) { + boolean isInterface = false; + for (var sq : tq.single_type_qualifier()) { + if (sq.storage_qualifier() != null && INTERFACE_STORAGE_QUALIFIERS.contains(sq.storage_qualifier().getText())) { + isInterface = true; + break; + } + } + if (!isInterface) continue; + for (var sq : tq.single_type_qualifier()) { + if (sq.layout_qualifier() != null) blankAllTerminals(sq.layout_qualifier()); + } + } + }); + } + private static void blankLeadingToken(ParserRuleContext ctx) { if (ctx.getChildCount() == 0) return; if (!(ctx.getChild(0) instanceof TerminalNode tn)) return; @@ -537,19 +818,42 @@ public final class RwImageStoreExtractor { }); } - private static String buildComputeOutput(GLSLParser.Translation_unitContext computeRoot, RwExtractMode mode, Set writtenImages, Set nonWriteonlyImages, Map declared, List attrs) { + private static final Set QUAD_INPUTS = Set.of("iris_Vertex", "iris_MultiTexCoord0"); + + private static Set declaredQuadInputs(List decls) { + final Set out = new LinkedHashSet<>(); + for (var decl : decls) { + if (decl.typeless_declaration() == null || decl.typeless_declaration().IDENTIFIER() == null) continue; + if (GlslAstHelpers.enclosingOfType(decl, GLSLParser.Function_definitionContext.class) != null) continue; + final String name = decl.typeless_declaration().IDENTIFIER().getText(); + if (QUAD_INPUTS.contains(name)) out.add(name); + } + return out; + } + + private static boolean definesFunction(GLSLParser.Translation_unitContext root, String name) { + for (var fdef : GlslAstHelpers.collectAll(root, GLSLParser.Function_definitionContext.class)) { + final var id = fdef.function_prototype().IDENTIFIER(); + if (id != null && name.equals(id.getText())) return true; + } + return false; + } + + private static String buildComputeOutput(GLSLParser.Translation_unitContext computeRoot, RwExtractMode mode, Set writtenImages, Set nonWriteonlyImages, Map declared, List attrs, Set quadInputs, boolean definesFtransform, ReplayLayout replay) { final StringBuilder out = new StringBuilder(8192); out.append("// _vg_mode: ").append(modeTag(mode)).append('\n'); + if (replay != null) out.append(replay.header()); out.append("#version 460 core\n"); switch (mode) { case CHUNK -> emitChunkPrelude(out, writtenImages, nonWriteonlyImages, declared, attrs); - case COMPOSITE_VSH -> emitCompositeVshPrelude(out, writtenImages, nonWriteonlyImages, declared); + case COMPOSITE_VSH -> emitCompositeVshPrelude(out, writtenImages, nonWriteonlyImages, declared, quadInputs, definesFtransform); case COMPOSITE_FSH -> emitCompositeFshPrelude(out, writtenImages, nonWriteonlyImages, declared); + case VERTEX_REPLAY -> emitVertexReplayPrelude(out, writtenImages, nonWriteonlyImages, declared, replay); } out.append('\n'); out.append(GlslTransformUtils.getFormattedShader(computeRoot, "")); out.append('\n'); - emitDispatchMain(out, mode, attrs); + emitDispatchMain(out, mode, attrs, replay != null ? replay.inputs() : List.of(), quadInputs); return out.toString(); } @@ -558,19 +862,26 @@ public final class RwImageStoreExtractor { case CHUNK -> "chunk"; case COMPOSITE_VSH -> "composite-vsh"; case COMPOSITE_FSH -> "composite-fsh"; + case VERTEX_REPLAY -> "vertex-replay"; }; } - private static void emitImageDecls(StringBuilder out, Set writtenImages, Set nonWriteonlyImages, Map declared) { + private static List sortedImages(Set writtenImages) { final List sorted = new ArrayList<>(writtenImages); Collections.sort(sorted); + return sorted; + } + + private static void emitImageDecls(StringBuilder out, Set writtenImages, Set nonWriteonlyImages, Map declared) { + final List sorted = sortedImages(writtenImages); final Map infos = activeCustomImages; for (int i = 0; i < sorted.size(); i++) { final String name = sorted.get(i); final ImageDecl d = declared.get(name); if (d == null) continue; final InternalTextureFormat fmt = (infos != null && infos.get(name) != null) ? infos.get(name).internalTextureFormat() : null; - final String layoutInner = (fmt != null) ? "binding = " + i + ", " + fmt.name().toLowerCase(Locale.ROOT) : "binding = " + i; + final String format = fmt != null ? fmt.name().toLowerCase(Locale.ROOT) : d.layoutFormat(); + final String layoutInner = (format != null) ? "binding = " + i + ", " + format : "binding = " + i; final String qualifier = nonWriteonlyImages.contains(name) ? "" : "writeonly "; out.append("layout(").append(layoutInner).append(") ").append(qualifier).append("uniform ").append(d.glslType()).append(' ').append(name).append(";\n"); } @@ -604,7 +915,7 @@ public final class RwImageStoreExtractor { out.append("void _vert_init() {}\n"); } - private static void emitCompositeVshPrelude(StringBuilder out, Set writtenImages, Set nonWriteonlyImages, Map declared) { + private static void emitCompositeVshPrelude(StringBuilder out, Set writtenImages, Set nonWriteonlyImages, Map declared, Set quadInputs, boolean definesFtransform) { out.append("layout(local_size_x = 64) in;\n\n"); emitImageDecls(out, writtenImages, nonWriteonlyImages, declared); out.append("\nconst vec4 _vg_quad[4] = vec4[4](\n"); @@ -614,10 +925,67 @@ public final class RwImageStoreExtractor { out.append(" vec4(0.0, 0.0, 0.0, 1.0));\n"); out.append("vec4 _vg_gl_vertex;\n"); out.append("int _vg_gl_vertex_id;\n"); + out.append("vec4 _vg_sink_pos;\n"); + out.append("float _vg_sink_psize;\n"); + out.append("float _vg_sink_clip[8];\n"); out.append("#define gl_Vertex _vg_gl_vertex\n"); out.append("#define gl_VertexID _vg_gl_vertex_id\n"); - out.append("uniform mat4 gl_ModelViewProjectionMatrix;\n"); - out.append("vec4 iris_ftransform() { return gl_ModelViewProjectionMatrix * gl_Vertex; }\n"); + out.append("#define gl_Position _vg_sink_pos\n"); + out.append("#define gl_PointSize _vg_sink_psize\n"); + out.append("#define gl_ClipDistance _vg_sink_clip\n"); + for (String input : quadInputs) out.append("vec4 ").append(input).append(";\n"); + if (!definesFtransform) { + out.append("uniform mat4 gl_ModelViewProjectionMatrix;\n"); + out.append("vec4 iris_ftransform() { return gl_ModelViewProjectionMatrix * gl_Vertex; }\n"); + } + } + + private static void emitVertexReplayPrelude(StringBuilder out, Set writtenImages, Set nonWriteonlyImages, Map declared, ReplayLayout replay) { + final List inputs = replay.inputs(); + out.append("layout(local_size_x = 64) in;\n\n"); + emitImageDecls(out, writtenImages, nonWriteonlyImages, declared); + final Map infos = activeCustomImages; + for (int i = 0; i < replay.readImages().size(); i++) { + final ImageDecl d = declared.get(replay.readImages().get(i)); + final InternalTextureFormat fmt = infos.get(d.name()) != null ? infos.get(d.name()).internalTextureFormat() : null; + final String format = fmt != null ? fmt.name().toLowerCase(Locale.ROOT) : d.layoutFormat(); + out.append("layout(binding = ").append(writtenImages.size() + i).append(format != null ? ", " + format : "").append(") readonly uniform ") + .append(d.glslType()).append(' ').append(d.name()).append(";\n"); + } + out.append('\n'); + for (int i = 0; i < VG_REPLAY_VERTEX_BUFFER_COUNT; i++) { + out.append("layout(std430, binding = ").append(VG_REPLAY_VERTEX_BUFFER_BINDING + i) + .append(") readonly buffer _VgVertices").append(i).append(" { uint d[]; } _vg_vertices").append(i).append(";\n"); + } + out.append("layout(std430, binding = ").append(VG_REPLAY_INDEX_BUFFER_BINDING).append(") readonly buffer _VgIndices { uint d[]; } _vg_indices;\n\n"); + final int slots = Math.max(1, inputs.size()); + out.append("uniform uvec4 _vg_attr[").append(slots).append("];\n"); + out.append("uniform vec4 _vg_attrDefault[").append(slots).append("];\n"); + out.append("uniform ivec4 _vg_draw;\n"); + out.append("uniform ivec3 _vg_index;\n"); + out.append("uniform uvec2 _vg_restart;\n"); + out.append("uniform int _vg_invocationBase;\n\n"); + out.append("vec4 _vg_sink_pos;\n"); + out.append("float _vg_sink_psize;\n"); + out.append("float _vg_sink_clip[8];\n"); + out.append("int _vg_vertex_id;\n"); + out.append("int _vg_instance_id;\n"); + out.append("#define gl_Position _vg_sink_pos\n"); + out.append("#define gl_PointSize _vg_sink_psize\n"); + out.append("#define gl_ClipDistance _vg_sink_clip\n"); + out.append("#define gl_VertexID _vg_vertex_id\n"); + out.append("#define gl_VertexIndex _vg_vertex_id\n"); + out.append("#define gl_InstanceID _vg_instance_id\n"); + out.append("#define gl_InstanceIndex _vg_instance_id\n"); + for (String suffix : new String[] { "", "ARB" }) { + out.append("#define gl_BaseVertex").append(suffix).append(" _vg_draw.w\n"); + out.append("#define gl_BaseInstance").append(suffix).append(" 0\n"); + out.append("#define gl_DrawID").append(suffix).append(" 0\n"); + } + out.append('\n'); + for (VertexInput input : inputs) out.append(input.glslType()).append(' ').append(input.name()).append(";\n"); + out.append('\n'); + out.append(ShaderLoader.getShaderSource("angelica:include/vertex_replay_fetch.glsl")); } private static void emitCompositeFshPrelude(StringBuilder out, Set writtenImages, Set nonWriteonlyImages, Map declared) { @@ -643,7 +1011,7 @@ public final class RwImageStoreExtractor { } } - private static void emitDispatchMain(StringBuilder out, RwExtractMode mode, List attrs) { + private static void emitDispatchMain(StringBuilder out, RwExtractMode mode, List attrs, List inputs, Set quadInputs) { out.append("void main() {\n"); switch (mode) { case CHUNK -> { @@ -665,6 +1033,7 @@ public final class RwImageStoreExtractor { out.append(" uint vid = gl_GlobalInvocationID.x;\n"); out.append(" if (vid >= 4u) return;\n"); out.append(" _vg_gl_vertex = _vg_quad[vid];\n"); + for (String input : quadInputs) out.append(" ").append(input).append(" = _vg_quad[vid];\n"); out.append(" _vg_gl_vertex_id = int(vid);\n"); out.append(" _vg_body();\n"); } @@ -674,10 +1043,37 @@ public final class RwImageStoreExtractor { out.append(" _vg_gl_fragcoord_raw = vec4(vec2(px) + 0.5, 0.0, 1.0);\n"); out.append(" _vg_body();\n"); } + case VERTEX_REPLAY -> { + out.append(" uint _vg_g = uint(_vg_invocationBase) + gl_GlobalInvocationID.x;\n"); + out.append(" uint _vg_count = uint(_vg_draw.y);\n"); + out.append(" if (_vg_count == 0u || _vg_g >= _vg_count * uint(_vg_draw.z)) return;\n"); + out.append(" uint _vg_v = _vg_g % _vg_count;\n"); + out.append(" uint _vg_i = _vg_g / _vg_count;\n"); + out.append(" uint _vg_vertex = uint(_vg_draw.x) + _vg_v;\n"); + out.append(" if (_vg_index.x != 0) {\n"); + out.append(" uint _vg_index_value = _vg_v < uint(_vg_index.z) ? _vg_read(4u, uint(_vg_index.y) + _vg_v * uint(_vg_index.x), uint(_vg_index.x)) : 0u;\n"); + out.append(" if (_vg_restart.x != 0u && _vg_index_value == _vg_restart.y) return;\n"); + out.append(" _vg_vertex = _vg_index_value + uint(_vg_draw.w);\n"); + out.append(" }\n"); + out.append(" _vg_vertex_id = int(_vg_vertex);\n"); + out.append(" _vg_instance_id = int(_vg_i);\n"); + for (int k = 0; k < inputs.size(); k++) { + final VertexInput input = inputs.get(k); + out.append(" ").append(input.name()).append(" = ").append(input.glslType()).append('(') + .append(fetchFunctionFor(input.glslType())).append('(').append(k).append("u, _vg_vertex, _vg_i));\n"); + } + out.append(" _vg_body();\n"); + } } out.append("}\n"); } + private static String fetchFunctionFor(String glslType) { + if (glslType.startsWith("u")) return "_vg_uint"; + if (glslType.startsWith("i")) return "_vg_int"; + return "_vg_float"; + } + private static List collectVertexAttributes(Set chunkAttrsUsed, Map chunkAttrAstTypes) { final List out = new ArrayList<>(); for (String name : chunkAttrsUsed) { diff --git a/src/main/java/net/coderbot/iris/pipeline/transform/ShaderTransformer.java b/src/main/java/net/coderbot/iris/pipeline/transform/ShaderTransformer.java index dc8d95986..78f5253e3 100644 --- a/src/main/java/net/coderbot/iris/pipeline/transform/ShaderTransformer.java +++ b/src/main/java/net/coderbot/iris/pipeline/transform/ShaderTransformer.java @@ -237,7 +237,8 @@ public class ShaderTransformer { final ShaderDiskCache.Key k = ShaderDiskCache.key("iris-transform").str(patch.name()); parameters.appendDiskKey(k); k.i(RenderSystem.getMaxGlslVersion()).b(RenderSystem.supportsSSBO()).b(RenderSystem.supportsImageLoadStore()) - .b(RenderSystem.isGLES()).b(BackendManager.RENDER_BACKEND.isSDLGPU()).i(maxSupportedHoistVersion); + .b(RenderSystem.isGLES()).b(BackendManager.RENDER_BACKEND.isSDLGPU()).b(BackendManager.RENDER_BACKEND.supportsComputeImageAtomics()) + .i(maxSupportedHoistVersion); if (enabledRequirements == null) { k.i(-1); } else { @@ -559,18 +560,23 @@ public class ShaderTransformer { artifactsOut.put(shaderType, new StageArtifact(treeHolder[0], formattedShader.length() - printedBody.length())); } } - if (BackendManager.RENDER_BACKEND.isSDLGPU()) extractRwImageStores(result, patchType, artifactsOut); + if (BackendManager.RENDER_BACKEND.isSDLGPU()) extractRwImageStores(result, patchType, artifactsOut, BackendManager.RENDER_BACKEND.supportsComputeImageAtomics()); watch.stop(); Iris.logger.info("[Load #{}] Transformed shader for {} in {}", Iris.getShaderPackLoadId(), patchType.name(), watch); return result; } - static void extractRwImageStores(EnumMap result, Patch patchType, EnumMap artifactsOut) { + static void extractRwImageStores(EnumMap result, Patch patchType, EnumMap artifactsOut, boolean computeImageAtomics) { if (result.containsKey(PatchShaderType.COMPUTE)) return; + if (patchType == Patch.ATTRIBUTES) { + extractVertexReplay(result, artifactsOut, computeImageAtomics); + return; + } final String vsh = result.get(PatchShaderType.VERTEX); final String fsh = result.get(PatchShaderType.FRAGMENT); - final RwImageStoreExtractor.Result vshResult = (vsh != null) ? RwImageStoreExtractor.tryExtract(vsh, PatchShaderType.VERTEX, patchType.name()) : null; + final RwImageStoreExtractor.RwExtractMode vertexMode = patchType == Patch.COMPOSITE ? RwImageStoreExtractor.RwExtractMode.COMPOSITE_VSH : null; + final RwImageStoreExtractor.Result vshResult = (vsh != null) ? RwImageStoreExtractor.tryExtract(vsh, PatchShaderType.VERTEX, patchType.name(), vertexMode) : null; final RwImageStoreExtractor.Result fshResult = (fsh != null) ? RwImageStoreExtractor.tryExtract(fsh, PatchShaderType.FRAGMENT, patchType.name()) : null; if (vshResult != null && fshResult != null) { @@ -581,14 +587,42 @@ public class ShaderTransformer { final PatchShaderType stage = vshResult != null ? PatchShaderType.VERTEX : PatchShaderType.FRAGMENT; result.put(stage, extracted.strippedSource()); if (artifactsOut != null) artifactsOut.remove(stage); - if (patchType == Patch.COMPOSITE || patchType == Patch.CELERITAS_TERRAIN) { + if (patchType != Patch.COMPOSITE && patchType != Patch.CELERITAS_TERRAIN) { + Iris.logger.info("[RwImageStoreExtractor] Dropped image writes from {} (written={})", patchType.name(), extracted.writtenImages()); + } else if (extracted.keepsImageAtomics() && !computeImageAtomics) { + Iris.logger.info("[RwImageStoreExtractor] Dropped image writes from {}, they need image atomics this backend cannot run (written={})", patchType.name(), extracted.writtenImages()); + } else { result.put(PatchShaderType.COMPUTE, extracted.computeSource()); Iris.logger.info("[RwImageStoreExtractor] Extracted compute pre-pass for {} (mode={}, written={})", patchType.name(), extracted.mode(), extracted.writtenImages()); - } else { - Iris.logger.info("[RwImageStoreExtractor] Dropped image writes from {} (written={})", patchType.name(), extracted.writtenImages()); } } + private static void extractVertexReplay(EnumMap result, EnumMap artifactsOut, boolean computeImageAtomics) { + final String fsh = result.get(PatchShaderType.FRAGMENT); + final RwImageStoreExtractor.Result fshResult = (fsh != null) ? RwImageStoreExtractor.tryExtract(fsh, PatchShaderType.FRAGMENT, Patch.ATTRIBUTES.name()) : null; + if (fshResult != null) { + result.put(PatchShaderType.FRAGMENT, fshResult.strippedSource()); + if (artifactsOut != null) artifactsOut.remove(PatchShaderType.FRAGMENT); + Iris.logger.info("[RwImageStoreExtractor] Dropped fragment image writes from ATTRIBUTES (written={})", fshResult.writtenImages()); + } + + final String vsh = result.get(PatchShaderType.VERTEX); + final RwImageStoreExtractor.Result vshResult = (vsh != null) ? RwImageStoreExtractor.tryExtractVertexReplay(vsh) : null; + if (vshResult == null) return; + result.put(PatchShaderType.VERTEX, vshResult.strippedSource()); + if (artifactsOut != null) artifactsOut.remove(PatchShaderType.VERTEX); + if (vshResult.computeSource() == null) { + Iris.logger.info("[RwImageStoreExtractor] Dropped vertex image writes from ATTRIBUTES, an input cannot be replayed (written={})", vshResult.writtenImages()); + return; + } + if (vshResult.keepsImageAtomics() && !computeImageAtomics) { + Iris.logger.info("[RwImageStoreExtractor] Dropped vertex image writes from ATTRIBUTES, they need image atomics this backend cannot run (written={})", vshResult.writtenImages()); + return; + } + result.put(PatchShaderType.COMPUTE, vshResult.computeSource()); + Iris.logger.info("[RwImageStoreExtractor] Extracted vertex replay for ATTRIBUTES (written={})", vshResult.writtenImages()); + } + private static void doTransform(Transformer transformer, Patch patchType, Parameters parameters, int versionInt) { switch (patchType) { case CELERITAS_TERRAIN: diff --git a/src/main/resources/assets/angelica/shaders/include/vertex_replay_fetch.glsl b/src/main/resources/assets/angelica/shaders/include/vertex_replay_fetch.glsl new file mode 100644 index 000000000..775d7b043 --- /dev/null +++ b/src/main/resources/assets/angelica/shaders/include/vertex_replay_fetch.glsl @@ -0,0 +1,103 @@ +// Taken mostly from mesa + +uint _vg_word(uint slot, uint i) { + switch (slot) { + case 0u: return _vg_vertices0.d[i]; + case 1u: return _vg_vertices1.d[i]; + case 2u: return _vg_vertices2.d[i]; + case 3u: return _vg_vertices3.d[i]; + default: return _vg_indices.d[i]; + } +} + +uint _vg_read(uint slot, uint at, uint size) { + uint shift = (at & 3u) * 8u; + uint value = _vg_word(slot, at >> 2) >> shift; + if (shift + size * 8u > 32u) value |= _vg_word(slot, (at >> 2) + 1u) << (32u - shift); + return size == 4u ? value : value & ((1u << (size * 8u)) - 1u); +} + +const uint _VG_COMPONENT_SIZE[12] = uint[12](4u, 2u, 1u, 1u, 2u, 2u, 4u, 4u, 4u, 4u, 4u, 4u); + +bool _vg_enabled(uint a) { + return (_vg_attr[a].x & 0x2000u) != 0u; +} + +uint _vg_element(uint a, uint vertex, uint instance) { + uvec4 desc = _vg_attr[a]; + uint divisor = desc.x >> 16; + uint element = divisor == 0u ? vertex : instance / divisor; + return element > desc.w ? 0u : element; +} + +uvec4 _vg_raw(uint a, uint element) { + uvec4 desc = _vg_attr[a]; + uint type = (desc.x >> 4) & 15u; + uint at = desc.y + element * desc.z; + bool none = (desc.x & 0x8000u) != 0u; + if (type >= 9u) { + uint packed = none ? 0u : _vg_read(desc.x & 7u, at, 4u); + if (type == 9u) return uvec4(ivec4(int(packed << 22), int(packed << 12), int(packed << 2), int(packed)) >> ivec4(22, 22, 22, 30)); + if (type == 10u) return (uvec4(packed) >> uvec4(0u, 10u, 20u, 30u)) & uvec4(1023u, 1023u, 1023u, 3u); + return uvec4(packed, 0u, 0u, 1u); + } + uint count = (desc.x >> 8) & 7u; + uint size = _VG_COMPONENT_SIZE[type]; + uvec4 raw = uvec4(0u, 0u, 0u, 1u); + for (uint c = 0u; c < count; c++) { + uint v = none ? 0u : _vg_read(desc.x & 7u, at + c * size, size); + if (type == 2u) v = uint(int(v << 24) >> 24); + else if (type == 4u) v = uint(int(v << 16) >> 16); + raw[c] = v; + } + return raw; +} + +vec4 _vg_float(uint a, uint vertex, uint instance) { + if (!_vg_enabled(a)) return _vg_attrDefault[a]; + uvec4 desc = _vg_attr[a]; + uint type = (desc.x >> 4) & 15u; + uint count = (desc.x >> 8) & 7u; + bool normalized = (desc.x & 0x800u) != 0u; + uvec4 raw = _vg_raw(a, _vg_element(a, vertex, instance)); + vec4 v = vec4(0.0, 0.0, 0.0, 1.0); + if (type == 9u) { + vec4 c = vec4(ivec4(raw)); + v = normalized ? max(c / vec4(511.0, 511.0, 511.0, 1.0), vec4(-1.0)) : c; + } else if (type == 10u) { + vec4 c = vec4(raw); + v = normalized ? c / vec4(1023.0, 1023.0, 1023.0, 3.0) : c; + } else if (type == 11u) { + uint p = raw.x; + v = vec4(unpackHalf2x16((p & 0x7ffu) << 4).x, unpackHalf2x16((p & 0x3ff800u) >> 7).x, + unpackHalf2x16((p & 0xffc00000u) >> 17).x, 1.0); + } else { + for (uint c = 0u; c < count; c++) { + uint r = raw[c]; + switch (type) { + case 0u: v[c] = uintBitsToFloat(r); break; + case 1u: v[c] = unpackHalf2x16(r).x; break; + case 2u: v[c] = normalized ? max(float(int(r)) / 127.0, -1.0) : float(int(r)); break; + case 3u: v[c] = normalized ? float(r) / 255.0 : float(r); break; + case 4u: v[c] = normalized ? max(float(int(r)) / 32767.0, -1.0) : float(int(r)); break; + case 5u: v[c] = normalized ? float(r) / 65535.0 : float(r); break; + case 6u: v[c] = normalized ? max(float(int(r)) / 2147483647.0, -1.0) : float(int(r)); break; + case 7u: v[c] = normalized ? float(r) / 4294967295.0 : float(r); break; + default: v[c] = float(int(r)) / 65536.0; break; + } + } + } + return (desc.x & 0x4000u) != 0u ? v.zyxw : v; +} + +ivec4 _vg_int(uint a, uint vertex, uint instance) { + if (!_vg_enabled(a)) return ivec4(_vg_attrDefault[a]); + if ((_vg_attr[a].x & 0x1000u) == 0u) return ivec4(_vg_float(a, vertex, instance)); + return ivec4(_vg_raw(a, _vg_element(a, vertex, instance))); +} + +uvec4 _vg_uint(uint a, uint vertex, uint instance) { + if (!_vg_enabled(a)) return uvec4(_vg_attrDefault[a]); + if ((_vg_attr[a].x & 0x1000u) == 0u) return uvec4(_vg_float(a, vertex, instance)); + return _vg_raw(a, _vg_element(a, vertex, instance)); +} diff --git a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderDragon.java b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderDragon.java index 2ff1e61f7..be119631b 100644 --- a/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderDragon.java +++ b/src/mixin/java/com/gtnewhorizons/angelica/mixins/early/shaders/MixinRenderDragon.java @@ -2,18 +2,13 @@ package com.gtnewhorizons.angelica.mixins.early.shaders; import com.gtnewhorizons.angelica.experimental.surround.Surround; import com.gtnewhorizons.angelica.glsm.GLStateManager; -import net.coderbot.iris.gbuffer_overrides.matching.SpecialCondition; -import net.coderbot.iris.layer.GbufferPrograms; -import net.coderbot.iris.shaderpack.materialmap.NamespacedId; -import net.coderbot.iris.uniforms.CapturedRenderingState; +import com.gtnewhorizons.angelica.api.EnderDragonBeams; import net.minecraft.client.renderer.entity.RenderDragon; import net.minecraft.entity.boss.EntityDragon; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.gen.Invoker; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; /** * Mixin allows devs to target the ender crystal beams and dragon death rays as separate entities. @@ -21,43 +16,24 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; */ @Mixin(RenderDragon.class) public abstract class MixinRenderDragon { - @Unique - private static final NamespacedId END_CRYSTAL_BEAM = new NamespacedId("minecraft", "end_crystal_beam"); - - @Unique - private static final NamespacedId DRAGON_DEATH_RAY = new NamespacedId("minecraft", "dragon_death_rays"); - - @Unique - private boolean angelica$beamScope; - @Unique private int angelica$depthPassReplay = 0; @Invoker("renderEquippedItems") protected abstract void angelica$invokeRenderEquippedItems(EntityDragon dragon, float partialTicks); - @Inject( + @Surround( + id = "crystalBeam", method = "doRender(Lnet/minecraft/entity/boss/EntityDragon;DDDFF)V", - at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glPushMatrix()V", ordinal = 0, shift = At.Shift.AFTER, remap = false) + at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/Tessellator;draw()I") ) - private void iris$setBeamEntityId(EntityDragon dragon, double x, double y, double z, float entityYaw, float partialTicks, CallbackInfo ci) { - // Only set the ID if the dragon is being healed by a crystal - if (dragon.healingEnderCrystal != null) { - CapturedRenderingState.INSTANCE.pushCurrentEntityAndItem(); - CapturedRenderingState.INSTANCE.setCurrentNamedEntity(END_CRYSTAL_BEAM); - angelica$beamScope = true; - } + private void iris$beginCrystalBeam() { + EnderDragonBeams.beginCrystalBeam(); } - @Inject( - method = "doRender(Lnet/minecraft/entity/boss/EntityDragon;DDDFF)V", - at = @At(value = "INVOKE", target = "Lorg/lwjgl/opengl/GL11;glPopMatrix()V", ordinal = 0, shift = At.Shift.BEFORE, remap = false) - ) - private void iris$restoreEntityId(EntityDragon dragon, double x, double y, double z, float entityYaw, float partialTicks, CallbackInfo ci) { - if (angelica$beamScope) { - CapturedRenderingState.INSTANCE.popCurrentEntityAndItem(); - angelica$beamScope = false; - } + @Surround.Finally("crystalBeam") + private void iris$endCrystalBeam() { + EnderDragonBeams.endCrystalBeam(); } /** @@ -72,9 +48,7 @@ public abstract class MixinRenderDragon { @Surround.Carry final boolean deathBeams = angelica$depthPassReplay == 0 && dragon.deathTicks > 0; if (deathBeams) { - GbufferPrograms.setupSpecialRenderCondition(SpecialCondition.LIGHTNING); - CapturedRenderingState.INSTANCE.pushCurrentEntityAndItem(); - CapturedRenderingState.INSTANCE.setCurrentNamedEntity(DRAGON_DEATH_RAY); + EnderDragonBeams.beginDeathRays(); angelica$depthPassReplay++; GLStateManager.glColorMask(false, false, false, false); @@ -114,8 +88,6 @@ public abstract class MixinRenderDragon { @Surround.Finally("deathBeams") private void angelica$endDeathBeamsLighting(@Surround.Carry boolean deathBeams) { - if (!deathBeams) return; - CapturedRenderingState.INSTANCE.popCurrentEntityAndItem(); - GbufferPrograms.teardownSpecialRenderCondition(); + if (deathBeams) EnderDragonBeams.endDeathRays(); } } diff --git a/src/test/java/net/coderbot/iris/pipeline/transform/CompositeVertexWriteExtractionTest.java b/src/test/java/net/coderbot/iris/pipeline/transform/CompositeVertexWriteExtractionTest.java new file mode 100644 index 000000000..d6dae71a2 --- /dev/null +++ b/src/test/java/net/coderbot/iris/pipeline/transform/CompositeVertexWriteExtractionTest.java @@ -0,0 +1,85 @@ +package net.coderbot.iris.pipeline.transform; + +import com.gtnewhorizons.angelica.glsm.RenderSystem; +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.EnumMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CompositeVertexWriteExtractionTest { + + // Shaped like Euphoria Patches' End prepare pass + static final String PREPARE_VERTEX = """ + #version 130 + #extension GL_ARB_shader_image_load_store : enable + uniform float frameTime; + layout(r32i) uniform iimage2D endcrystal_img; + varying vec2 texCoord; + void main() { + texCoord = gl_MultiTexCoord0.xy; + gl_Position = ftransform(); + vec4 position0 = ftransform(); + if (position0.x < 0.0 && position0.y > 0.0) { + for (int index = 0; index < 20; index++) { + int state = imageLoad(endcrystal_img, ivec2(index, 8)).r - max(1, int(10000.0 * frameTime)); + int previousWeight = imageAtomicCompSwap(endcrystal_img, ivec2(index, 8), 0, -1); + imageStore(endcrystal_img, ivec2(index, 5), ivec4(state + previousWeight)); + } + } + } + """; + + static final String PREPARE_FRAGMENT = """ + #version 130 + varying vec2 texCoord; + void main() { gl_FragData[0] = vec4(texCoord, 0.0, 1.0); } + """; + + @BeforeAll + static void initShaderTransformer() throws Exception { + Reflect.setStatic(RenderSystem.class, "maxGlslVersion", 460); + ShaderTransformer.init(); + } + + @BeforeEach + void clearCaches() { + TransformPatcher.clearCache(); + ShaderTransformer.clearCache(); + } + + @Test + void compositeVertexWritesRunOverTheQuadNotTerrain() { + final Map transformed = TransformPatcher.patchComposite(PREPARE_VERTEX, null, PREPARE_FRAGMENT); + final EnumMap result = new EnumMap<>(transformed); + result.remove(PatchShaderType.COMPUTE); + + ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, null, true); + + final String compute = result.get(PatchShaderType.COMPUTE); + assertEquals(RwImageStoreExtractor.RwExtractMode.COMPOSITE_VSH, RwImageStoreExtractor.parseSentinel(compute)); + assertTrue(compute.contains("iris_Vertex = _vg_quad[vid];"), compute); + assertTrue(compute.contains("iris_MultiTexCoord0 = _vg_quad[vid];"), compute); + assertFalse(compute.contains("location"), "a location on a former interface variable does not compile in compute"); + assertEquals(1, compute.split("vec4 iris_ftransform", -1).length - 1, "the transform's own iris_ftransform is the only one"); + assertTrue(compute.contains("#define gl_Position _vg_sink_pos"), compute); + } + + @Test + void keptAtomicDropsThePrePassWithoutBackendSupport() { + final EnumMap result = new EnumMap<>(TransformPatcher.patchComposite(PREPARE_VERTEX, null, PREPARE_FRAGMENT)); + result.remove(PatchShaderType.COMPUTE); + + // The CompSwap's result is used, so it stays a real atomic, which native Metal cannot bind + ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, null, false); + + assertFalse(result.containsKey(PatchShaderType.COMPUTE)); + assertFalse(result.get(PatchShaderType.VERTEX).contains("imageAtomic"), result.get(PatchShaderType.VERTEX)); + } +} diff --git a/src/test/java/net/coderbot/iris/pipeline/transform/ShaderTransformerTest.java b/src/test/java/net/coderbot/iris/pipeline/transform/ShaderTransformerTest.java index ce704c919..076dcdfa4 100644 --- a/src/test/java/net/coderbot/iris/pipeline/transform/ShaderTransformerTest.java +++ b/src/test/java/net/coderbot/iris/pipeline/transform/ShaderTransformerTest.java @@ -246,7 +246,7 @@ class ShaderTransformerTest { final EnumMap result = new EnumMap<>(out); final String fragmentBefore = result.get(PatchShaderType.FRAGMENT); - ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, artifacts); + ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, artifacts, true); assertNotNull(result.get(PatchShaderType.COMPUTE), "fixture must actually exercise the extraction"); assertFalse(fragmentBefore.equals(result.get(PatchShaderType.FRAGMENT)), "fixture must actually rewrite the fragment stage"); @@ -263,7 +263,7 @@ class ShaderTransformerTest { assertNotNull(out); final EnumMap result = new EnumMap<>(out); - ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, artifacts); + ShaderTransformer.extractRwImageStores(result, Patch.COMPOSITE, artifacts, true); assertFalse(artifacts.isEmpty(), "fixture must leave at least one artifact to check"); for (Map.Entry e : artifacts.entrySet()) { diff --git a/src/test/java/net/coderbot/iris/pipeline/transform/VertexReplayExtractionTest.java b/src/test/java/net/coderbot/iris/pipeline/transform/VertexReplayExtractionTest.java new file mode 100644 index 000000000..94a4a8105 --- /dev/null +++ b/src/test/java/net/coderbot/iris/pipeline/transform/VertexReplayExtractionTest.java @@ -0,0 +1,302 @@ +package net.coderbot.iris.pipeline.transform; + +import com.gtnewhorizons.angelica.glsm.RenderSystem; +import com.gtnewhorizons.angelica.glsm.ffp.InstancedAttribs; +import com.gtnewhorizons.angelica.glsm.ffp.Instancing; +import com.gtnewhorizons.angelica.glsm.testutil.Reflect; +import net.coderbot.iris.gbuffer_overrides.matching.InputAvailability; +import net.coderbot.iris.pipeline.transform.RwImageStoreExtractor.VertexInput; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class VertexReplayExtractionTest { + + // Shaped like Euphoria Patches' end crystal voxelization in its shadow program + private static final String CRYSTAL_VERTEX = """ + #version 130 + #extension GL_ARB_shader_image_load_store : enable + uniform int entityId; + uniform mat4 shadowModelViewInverse; + uniform mat4 shadowProjectionInverse; + attribute vec4 mc_Entity; + layout(r32i) uniform iimage2D endcrystal_img; + varying vec2 texCoord; + void UpdateEndCrystalMap(vec3 pos) { + int packedPos = int(pos.x + 512.5); + for (int k = 0; k < 20; k++) { + int registeredPos = imageAtomicCompSwap(endcrystal_img, ivec2(k, 0), 0, packedPos); + if (registeredPos == 0 || registeredPos == packedPos) { + imageAtomicAdd(endcrystal_img, ivec2(k, 1), int(10.0 * pos.x)); + break; + } + } + } + void main() { + texCoord = gl_MultiTexCoord0.xy; + vec3 normal = normalize(gl_NormalMatrix * gl_Normal); + vec4 position = shadowModelViewInverse * shadowProjectionInverse * ftransform(); + if (entityId == 50000 && abs(normal.y) > 0.5 && mc_Entity.x < 0.0) UpdateEndCrystalMap(position.xyz); + gl_Position = ftransform(); + } + """; + + private static final String FRAGMENT = """ + #version 130 + uniform sampler2D tex; + varying vec2 texCoord; + void main() { gl_FragData[0] = texture2D(tex, texCoord); } + """; + + private static final InputAvailability TEX_LM = InputAvailability.of(true, true); + + @BeforeAll + static void initShaderTransformer() throws Exception { + Reflect.setStatic(RenderSystem.class, "maxGlslVersion", 460); + ShaderTransformer.init(); + } + + @BeforeEach + void clearCaches() { + TransformPatcher.clearCache(); + ShaderTransformer.clearCache(); + } + + private static RwImageStoreExtractor.Result replayOf(Map transformed) { + final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay(transformed.get(PatchShaderType.VERTEX)); + assertNotNull(result); + assertNotNull(result.computeSource(), "every input of an Iris gbuffers vertex shader is replayable"); + return result; + } + + private static VertexInput input(List inputs, String name) { + return inputs.stream().filter(i -> i.name().equals(name)).findFirst() + .orElseThrow(() -> new AssertionError(name + " missing from " + inputs)); + } + + @Test + void replayKeepsWritesAtomicAndRasterDropsThem() { + final RwImageStoreExtractor.Result result = replayOf(TransformPatcher.patchAttributes(CRYSTAL_VERTEX, null, FRAGMENT, TEX_LM)); + + assertEquals(RwImageStoreExtractor.RwExtractMode.VERTEX_REPLAY, RwImageStoreExtractor.parseSentinel(result.computeSource())); + assertFalse(result.strippedSource().contains("imageAtomic"), result.strippedSource()); + assertFalse(result.strippedSource().contains("endcrystal_img"), result.strippedSource()); + // Many vertices of one crystal add into the same texels at once + assertTrue(result.computeSource().contains("imageAtomicAdd"), result.computeSource()); + assertFalse(result.computeSource().contains("_vg_prev"), result.computeSource()); + assertTrue(result.computeSource().contains("layout(binding = 0, r32i) uniform iimage2D endcrystal_img;"), result.computeSource()); + assertEquals(List.of("endcrystal_img"), RwImageStoreExtractor.parseVertexReplayImages(result.computeSource())); + } + + @Test + void replayFetchesEveryDeclaredInput() { + final RwImageStoreExtractor.Result result = replayOf(TransformPatcher.patchAttributes(CRYSTAL_VERTEX, null, FRAGMENT, TEX_LM)); + final List inputs = RwImageStoreExtractor.parseVertexReplayInputs(result.computeSource()); + + assertEquals("vec4", input(inputs, "mc_Entity").glslType()); + for (int k = 0; k < inputs.size(); k++) { + final VertexInput in = inputs.get(k); + assertTrue(result.computeSource().contains(in.name() + " = " + in.glslType() + "("), "no fetch for " + in.name()); + assertTrue(result.computeSource().contains("(" + k + "u, _vg_vertex, _vg_i)"), "input " + in.name() + " is not fetched from slot " + k); + } + } + + @Test + void instancedVariantReadsEntityIdFromItsInstanceAttribute() { + final RwImageStoreExtractor.Result result = replayOf(TransformPatcher.patchAttributesInstanced(CRYSTAL_VERTEX, null, null, null, FRAGMENT, TEX_LM, false, Instancing.TEMPLATE)); + final List inputs = RwImageStoreExtractor.parseVertexReplayInputs(result.computeSource()); + + final VertexInput entity = input(inputs, "iris_Entity"); + assertEquals(InstancedAttribs.LOC_ENTITY, entity.location()); + assertEquals("uvec4", entity.glslType()); + assertTrue(result.computeSource().contains("iris_Entity = uvec4(_vg_uint("), result.computeSource()); + assertFalse(result.computeSource().contains("location"), "a location on a former interface variable does not compile in compute"); + } + + @Test + void everyDeclaratorOfAnInputListIsFetched() { + final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay(""" + #version 460 core + in vec3 position, normal; + layout(r32ui) uniform uimage2D marks; + void main() { + imageStore(marks, ivec2(normal.xy), uvec4(1u)); + gl_Position = vec4(position, 1.0); + } + """); + assertNotNull(result.computeSource()); + final List inputs = RwImageStoreExtractor.parseVertexReplayInputs(result.computeSource()); + assertEquals(List.of("position", "normal"), inputs.stream().map(VertexInput::name).toList()); + assertFalse(result.computeSource().contains("in vec3"), "a stage input does not compile in compute\n" + result.computeSource()); + assertTrue(result.computeSource().contains("normal = vec3(_vg_float(1u, _vg_vertex, _vg_i));"), result.computeSource()); + } + + @Test + void samplerOnlyTheWriteReadsIsListedForThePipeline() { + final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay(""" + #version 460 core + in vec4 iris_Vertex; + uniform sampler2D noisetex; + layout(rgba8) uniform image2D marks; + void main() { + imageStore(marks, ivec2(0), textureLod(noisetex, iris_Vertex.xy, 0.0)); + gl_Position = iris_Vertex; + } + """); + assertFalse(result.strippedSource().contains("textureLod"), result.strippedSource()); + assertEquals(List.of("noisetex"), RwImageStoreExtractor.parseVertexReplaySamplers(result.computeSource())); + } + + @Test + void readOnlyImagesBindAfterTheWrittenOnes() { + final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay(""" + #version 460 core + in vec4 iris_Vertex; + layout(rgba8) uniform image2D source; + layout(rgba8) uniform image2D target; + void main() { + imageStore(target, ivec2(0), imageLoad(source, ivec2(iris_Vertex.xy))); + gl_Position = iris_Vertex; + } + """); + final String compute = result.computeSource(); + assertEquals(List.of("target"), RwImageStoreExtractor.parseVertexReplayImages(compute)); + assertEquals(List.of("source"), RwImageStoreExtractor.parseVertexReplayReadImages(compute)); + assertTrue(compute.contains("layout(binding = 0, rgba8) writeonly uniform image2D target;"), compute); + assertTrue(compute.contains("layout(binding = 1, rgba8) readonly uniform image2D source;"), compute); + assertEquals(1, compute.split("image2D source", -1).length - 1, compute); + } + + @Test + void outParametersOfHelpersStayOutParameters() { + final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay(""" + #version 460 core + in vec4 iris_Vertex; + out vec2 texCoord; + layout(r32f) uniform image2D marks; + void fill(out float value) { value = iris_Vertex.x; } + void main() { + float v = 0.0; + fill(v); + imageStore(marks, ivec2(0), vec4(v)); + texCoord = iris_Vertex.xy; + gl_Position = iris_Vertex; + } + """); + final String compute = result.computeSource().replaceAll("\\s+", " "); + assertTrue(compute.contains("void fill ( out float value )"), compute); + assertFalse(compute.contains("out vec2 texCoord"), compute); + } + + @Test + void outputBlocksBecomeGlobalsAndLayoutDefaultsStay() { + final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay(""" + #version 460 core + layout(std430) buffer; + in vec4 iris_Vertex; + out gl_PerVertex { vec4 gl_Position; }; + out Outputs { flat int id; vec2 uv; } v; + out Extra { float fade; }; + buffer Marks { uint counts[]; }; + layout(r32ui) uniform uimage2D marks; + void main() { + v.uv = iris_Vertex.xy; + fade = iris_Vertex.z; + imageStore(marks, ivec2(v.uv), uvec4(counts[0] + uint(fade))); + gl_Position = iris_Vertex; + } + """); + assertNotNull(result); + final String compute = result.computeSource().replaceAll("\\s+", " "); + assertTrue(compute.contains("layout ( std430 ) buffer ;"), compute); + assertFalse(compute.contains("gl_PerVertex"), compute); + assertTrue(compute.contains("struct Outputs { int id ; vec2 uv ; }; Outputs v ;"), compute); + assertTrue(compute.contains("float fade ;"), compute); + assertFalse(compute.contains("Extra"), compute); + } + + @Test + void primitiveRestartMarkersAreNotReplayedAsVertices() { + final String compute = replayOf(TransformPatcher.patchAttributes(CRYSTAL_VERTEX, null, FRAGMENT, TEX_LM)).computeSource(); + final int skip = compute.indexOf("if (_vg_restart.x != 0u && _vg_index_value == _vg_restart.y) return;"); + assertTrue(skip >= 0, compute); + assertTrue(skip < compute.indexOf("_vg_vertex = _vg_index_value + uint(_vg_draw.w);"), compute); + } + + @Test + void atomicReplayIsDroppedWithoutBackendSupport() { + final EnumMap atomic = new EnumMap<>(PatchShaderType.class); + atomic.put(PatchShaderType.VERTEX, TransformPatcher.patchAttributes(CRYSTAL_VERTEX, null, FRAGMENT, TEX_LM).get(PatchShaderType.VERTEX)); + ShaderTransformer.extractRwImageStores(atomic, Patch.ATTRIBUTES, null, false); + assertFalse(atomic.containsKey(PatchShaderType.COMPUTE)); + assertFalse(atomic.get(PatchShaderType.VERTEX).contains("imageAtomic"), atomic.get(PatchShaderType.VERTEX)); + + final EnumMap plain = new EnumMap<>(PatchShaderType.class); + plain.put(PatchShaderType.VERTEX, """ + #version 460 core + in vec4 iris_Vertex; + layout(r32ui) uniform uimage2D marks; + void main() { + imageStore(marks, ivec2(0), uvec4(1u)); + gl_Position = iris_Vertex; + } + """); + ShaderTransformer.extractRwImageStores(plain, Patch.ATTRIBUTES, null, false); + assertTrue(plain.containsKey(PatchShaderType.COMPUTE), "plain stores need no atomics, so the replay stays"); + } + + @Test + void matrixInputIsNotReplayed() { + final RwImageStoreExtractor.Result result = RwImageStoreExtractor.tryExtractVertexReplay(""" + #version 460 core + in mat4 instanceMatrix; + layout(r32ui) uniform uimage2D marks; + void main() { + imageStore(marks, ivec2(0), uvec4(1u)); + gl_Position = instanceMatrix[0]; + } + """); + assertNotNull(result); + assertNull(result.computeSource()); + assertFalse(result.strippedSource().contains("imageStore"), result.strippedSource()); + } + + @Test + void attributesProgramKeepsVertexReplayAndDropsFragmentWrites() { + final EnumMap result = new EnumMap<>(PatchShaderType.class); + result.put(PatchShaderType.VERTEX, """ + #version 460 core + in vec4 iris_Vertex; + layout(r32ui) uniform uimage2D marks; + void main() { + imageStore(marks, ivec2(0), uvec4(1u)); + gl_Position = iris_Vertex; + } + """); + result.put(PatchShaderType.FRAGMENT, """ + #version 460 core + layout(r32ui) uniform uimage2D marks; + out vec4 color; + void main() { + imageStore(marks, ivec2(1), uvec4(2u)); + color = vec4(1.0); + } + """); + + ShaderTransformer.extractRwImageStores(result, Patch.ATTRIBUTES, null, true); + + assertNotNull(result.get(PatchShaderType.COMPUTE)); + assertFalse(result.get(PatchShaderType.VERTEX).contains("imageStore")); + assertFalse(result.get(PatchShaderType.FRAGMENT).contains("imageStore")); + } +}