SDL Vertex Replay + Dragon API (#2224)

This commit is contained in:
Eclipse-Sol authored and GitHub committed 2026-10-05 20:23:36 -07:00
1 parent 3389b952d0
commit 5ed1c92590
25 files changed
+2035 -101

No files matched your search

@@ -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);
@@ -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) {
}
@@ -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
@@ -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();
@@ -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);
}
}
}
@@ -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<String> 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<String> names) {
final IntArrayList units = new IntArrayList();
final List<IntSupplier> sources = new ArrayList<>();
final IntArrayList objects = new IntArrayList();
final IntArrayList ownTextureIndices = new IntArrayList();
final List<String> ownTextureNames = new ArrayList<>();
final List<String> 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<ContextState> state;
private final ArrayList<Capture> 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<String> warned = new HashSet<>();
public VertexWriteReplayer(ShaderManager shaderManager, ResourceManager resourceManager, PipelineApplier pipelineApplier, ComputeBinder computeBinder, Supplier<ContextState> 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);
}
}
@@ -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) {
@@ -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();
}
}
@@ -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 {
@@ -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);
@@ -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;
@@ -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);
}
@@ -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();
}
}
@@ -0,0 +1,4 @@
package com.gtnewhorizons.angelica.api;
public interface EyePassRenderer {
}
@@ -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) {
@@ -181,6 +181,7 @@ public class DeferredWorldRenderingPipeline implements WorldRenderingPipeline, R
// Custom images and SSBOs
private final Set<GlImage> 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<ImmutableSet<Integer>> 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;
@@ -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<ComputeProgram> twins = new ArrayList<>();
private final IntArrayList attachedPrograms = new IntArrayList();
void attach(String name, Program program, Map<PatchShaderType, String> transformed, BiConsumer<SamplerHolder, ImageHolder> 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<String> writtenImages = RwImageStoreExtractor.parseVertexReplayImages(source);
final List<String> images = new ArrayList<>(writtenImages);
images.addAll(RwImageStoreExtractor.parseVertexReplayReadImages(source));
final List<String> 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<VertexInput> 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<String> imageNames;
final IntSupplier[] images;
final List<String> samplerNames;
final int[] samplerUnits;
final IntSupplier[] samplerTextures;
final int[] samplerObjects;
ResourceCollector(List<String> imageNames, List<String> 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;
}
}
}
@@ -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) {
@@ -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<String> writtenImages, RwExtractMode mode) {}
public record Result(String strippedSource, String computeSource, Set<String> 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<String, String> chunkAttrAstTypes = (stage == PatchShaderType.VERTEX) ? collectChunkAttrAstTypes(decls) : Map.of();
final Set<String> 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<AttributeSlot> attrs = (mode == RwExtractMode.CHUNK) ? collectVertexAttributes(chunkAttrsUsed, chunkAttrAstTypes) : List.of();
final Set<String> 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<GLSLParser.Single_declarationContext> decls = new ArrayList<>();
final List<GLSLParser.Postfix_expressionContext> exprs = new ArrayList<>();
GlslAstHelpers.collectInto(root,
new List[] { decls, exprs },
new Class<?>[] { GLSLParser.Single_declarationContext.class, GLSLParser.Postfix_expressionContext.class });
final Map<String, ImageDecl> declared = collectImageDecls(decls);
final Set<String> writtenImages = new LinkedHashSet<>();
final Set<String> 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<VertexInput> inputs = collectVertexInputs(decls);
if (inputs == null) return new Result(stripped, null, writtenImages, RwExtractMode.VERTEX_REPLAY, false);
final List<String> readImages = readOnlyImages(exprs, declared, writtenImages);
final Set<String> 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<VertexInput> inputs, List<String> 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<VertexInput> inputs, Set<String> writtenImages, List<String> readImages, List<String> 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<String> readOnlyImages(List<GLSLParser.Postfix_expressionContext> exprs, Map<String, ImageDecl> declared, Set<String> writtenImages) {
final Set<String> 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<String> 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<String> parseVertexReplayReadImages(String computeSource) {
return headerTokens(computeSource, REPLAY_READ_IMAGES_HEADER);
}
/** The samplers a vertex-replay compute source declares. */
public static List<String> 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<VertexInput> parseVertexReplayInputs(String computeSource) {
final List<VertexInput> 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<String> parseVertexReplayImages(String computeSource) {
return headerTokens(computeSource, REPLAY_IMAGES_HEADER);
}
private static List<String> 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<String> 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<VertexInput> collectVertexInputs(List<GLSLParser.Single_declarationContext> decls) {
final List<VertexInput> 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<GLSLParser.Typeless_declarationContext> 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<String> collectSamplerNames(List<GLSLParser.Single_declarationContext> decls) {
final List<String> 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<GLSLParser.Typeless_declarationContext> 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<String, ImageDecl> collectImageDecls(List<GLSLParser.Single_declarationContext> decls) {
@@ -270,13 +448,22 @@ public final class RwImageStoreExtractor {
private static void applyComputeMutations(Transformer tc, Set<String> writtenImages, Map<String, ImageDecl> declared, Set<String> chunkAttrsUsed, RwExtractMode mode, Set<String> 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<String> 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<String> AUXILIARY_STORAGE_QUALIFIERS = Set.of("centroid", "sample", "patch");
private static final Set<String> 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<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared, List<AttributeSlot> attrs) {
private static final Set<String> QUAD_INPUTS = Set.of("iris_Vertex", "iris_MultiTexCoord0");
private static Set<String> declaredQuadInputs(List<GLSLParser.Single_declarationContext> decls) {
final Set<String> 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<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared, List<AttributeSlot> attrs, Set<String> 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<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared) {
private static List<String> sortedImages(Set<String> writtenImages) {
final List<String> sorted = new ArrayList<>(writtenImages);
Collections.sort(sorted);
return sorted;
}
private static void emitImageDecls(StringBuilder out, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared) {
final List<String> sorted = sortedImages(writtenImages);
final Map<String, ImageInformation> 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<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared) {
private static void emitCompositeVshPrelude(StringBuilder out, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared, Set<String> 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<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared, ReplayLayout replay) {
final List<VertexInput> inputs = replay.inputs();
out.append("layout(local_size_x = 64) in;\n\n");
emitImageDecls(out, writtenImages, nonWriteonlyImages, declared);
final Map<String, ImageInformation> 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<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared) {
@@ -643,7 +1011,7 @@ public final class RwImageStoreExtractor {
}
}
private static void emitDispatchMain(StringBuilder out, RwExtractMode mode, List<AttributeSlot> attrs) {
private static void emitDispatchMain(StringBuilder out, RwExtractMode mode, List<AttributeSlot> attrs, List<VertexInput> inputs, Set<String> 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<AttributeSlot> collectVertexAttributes(Set<String> chunkAttrsUsed, Map<String, String> chunkAttrAstTypes) {
final List<AttributeSlot> out = new ArrayList<>();
for (String name : chunkAttrsUsed) {
@@ -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<PatchShaderType, String> result, Patch patchType, EnumMap<PatchShaderType, StageArtifact> artifactsOut) {
static void extractRwImageStores(EnumMap<PatchShaderType, String> result, Patch patchType, EnumMap<PatchShaderType, StageArtifact> 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<PatchShaderType, String> result, EnumMap<PatchShaderType, StageArtifact> 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:
@@ -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));
}
@@ -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();
}
}
@@ -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<PatchShaderType, String> transformed = TransformPatcher.patchComposite(PREPARE_VERTEX, null, PREPARE_FRAGMENT);
final EnumMap<PatchShaderType, String> 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<PatchShaderType, String> 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));
}
}
@@ -246,7 +246,7 @@ class ShaderTransformerTest {
final EnumMap<PatchShaderType, String> 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<PatchShaderType, String> 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<PatchShaderType, ShaderTransformer.StageArtifact> e : artifacts.entrySet()) {
@@ -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<PatchShaderType, String> 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<VertexInput> 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<VertexInput> 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<VertexInput> 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<VertexInput> 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<PatchShaderType, String> 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<PatchShaderType, String> 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<PatchShaderType, String> 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"));
}
}