Fix voxeliation performance under SDL GPU (#2188)

This commit is contained in:
Jason Mitchell authored and GitHub committed 2026-10-02 19:46:17 -07:00
1 parent db7d328481
commit 80e5b544c0
15 files changed
+415 -216

No files matched your search

@@ -268,9 +268,8 @@ public abstract class RenderBackend {
public String getTransferDebugInfo() { return null; }
public boolean bindVoxelizationRegion(int ssboBinding, long openPass, float x, float y, float z) { return false; }
public long beginVoxelizationBatch(int ssboBinding) { return 0L; }
public void voxelizeRange(long pass, int vertexOffset, int vertexCount) {}
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 abstract void enable(int cap);
@@ -4338,19 +4338,11 @@ public class SDLGPURenderBackend extends RenderBackend {
return frameManager.getFrameNumber();
}
private int voxLocStart = -1;
private int voxLocCount = -1;
@Override public boolean bindVoxelizationRegion(int ssboBinding, long openPass, float x, float y, float z) {
if (ssboBinding < 0 || ssboBinding >= ContextState.MAX_INDEXED_BUFFERS) return false;
final ContextState st = s();
if (st.boundProgram == 0) return false;
if (st.boundSsboByIndex[ssboBinding] == 0) return false;
final int loc = shaderManager.getUniformLocation(st.boundProgram, "u_RegionOffset");
if (loc >= 0) GLStateManager.glUniform3f(loc, x, y, z);
if (openPass != 0) voxelizationDispatcher.rebindVertexBuffer(st, openPass);
return true;
}
private int voxLocRangeBase = -1;
private int voxLocRangeCount = -1;
private int voxLocVertexTotal = -1;
private int voxLocInvocationBase = -1;
private int voxLocRegionOffset = -1;
/** Reflected at link time, so it names exactly the images the compute writes rather than everything bound. */
public String[] getComputeWrittenImageNames(int programId) {
@@ -4365,13 +4357,19 @@ public class SDLGPURenderBackend extends RenderBackend {
final ContextState st = s();
final int program = st.boundProgram;
if (program == 0 || st.boundSsboByIndex[ssboBinding] == 0) return 0;
voxLocStart = shaderManager.getUniformLocation(program, "_vg_startVertex");
voxLocCount = shaderManager.getUniformLocation(program, "_vg_vertexCount");
voxLocRangeBase = shaderManager.getUniformLocation(program, "_vg_rangeBase");
voxLocRangeCount = shaderManager.getUniformLocation(program, "_vg_rangeCount");
voxLocVertexTotal = shaderManager.getUniformLocation(program, "_vg_vertexTotal");
voxLocInvocationBase = shaderManager.getUniformLocation(program, "_vg_invocationBase");
voxLocRegionOffset = shaderManager.getUniformLocation(program, "u_RegionOffset");
return voxelizationDispatcher.beginBatch(st);
}
@Override public void voxelizeRange(long pass, int vertexOffset, int vertexCount) {
voxelizationDispatcher.dispatchRange(pass, voxLocStart, voxLocCount, vertexOffset, vertexCount, s());
@Override public void voxelizeRegion(long pass, boolean rebindVertexBuffer, float x, float y, float z, int rangeBase, int rangeCount, int vertexTotal) {
final ContextState st = s();
if (voxLocRegionOffset >= 0) GLStateManager.glUniform3f(voxLocRegionOffset, x, y, z);
if (rebindVertexBuffer) voxelizationDispatcher.rebindVertexBuffer(st, pass);
voxelizationDispatcher.dispatchRegion(pass, voxLocRangeBase, voxLocRangeCount, voxLocVertexTotal, voxLocInvocationBase, rangeBase, rangeCount, vertexTotal, st);
}
@Override public void endVoxelizationBatch(long pass) {
@@ -4,6 +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 final ComputeDispatchSink computeBinder;
@@ -20,19 +21,19 @@ public final class VoxelizationDispatcher {
computeBinder.rebindRoStorageBuffers(st, pass);
}
public void dispatchRange(long pass, int locStart, int locCount, int vertexOffset, int vertexCount, ContextState st) {
if (pass == 0 || vertexCount <= 0) return;
if (locStart >= 0) GLStateManager.glUniform1i(locStart, vertexOffset);
if (locCount >= 0) GLStateManager.glUniform1i(locCount, vertexCount);
computeBinder.pushPendingComputeUniforms(st);
final int groups = (vertexCount + WORKGROUP_SIZE - 1) / WORKGROUP_SIZE;
computeBinder.dispatchInBatch(pass, groups, 1, 1);
public void dispatchRegion(long pass, int locBase, int locCount, int locTotal, int locInvBase, int rangeBase, int rangeCount, int vertexTotal, ContextState st) {
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);
}
}
public void endBatch(long pass) {
if (pass == 0) return;
computeBinder.endBatchedComputeDispatch(pass);
}
public static int workgroupSize() { return WORKGROUP_SIZE; }
}
@@ -8,7 +8,6 @@ import java.util.List;
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 VoxelizationDispatcherTest {
@@ -42,25 +41,29 @@ class VoxelizationDispatcherTest {
assertFalse(sink.lastCycleRwBuffers, "voxelization must open the batched pass with cycle=false on rw SSBO bindings");
}
@Test
void dispatchRange_pushesUniformsBeforeEachDispatch() {
final RecordingSink sink = new RecordingSink();
final VoxelizationDispatcher d = new VoxelizationDispatcher(sink);
final ContextState st = new ContextState();
d.dispatchRange(PASS_HANDLE, -1, -1, 0, 64, st);
d.dispatchRange(PASS_HANDLE, -1, -1, 64, 64, st);
assertEquals(2, sink.dispatches.size());
assertTrue(sink.uniformPushCount >= sink.dispatches.size(), "pushPendingComputeUniforms must run at least once per dispatchInBatch so per-range uniforms land; pushes=" + sink.uniformPushCount + " dispatches=" + sink.dispatches.size());
private static void region(VoxelizationDispatcher d, int rangeCount, int vertexTotal, ContextState st) {
d.dispatchRegion(PASS_HANDLE, -1, -1, -1, -1, 0, rangeCount, vertexTotal, st);
}
@Test
void dispatchRange_roundsGroupsUpToWorkgroupSize() {
void dispatchRegion_pushesUniformsBeforeEachDispatch() {
final RecordingSink sink = new RecordingSink();
final VoxelizationDispatcher d = new VoxelizationDispatcher(sink);
final ContextState st = new ContextState();
d.dispatchRange(PASS_HANDLE, -1, -1, 0, 1, st);
d.dispatchRange(PASS_HANDLE, -1, -1, 0, 64, st);
d.dispatchRange(PASS_HANDLE, -1, -1, 0, 65, st);
region(d, 1, 64, st);
region(d, 3, 200, st);
assertEquals(2, sink.dispatches.size());
assertEquals(sink.dispatches.size(), sink.uniformPushCount);
}
@Test
void dispatchRegion_roundsGroupsUpToWorkgroupSize() {
final RecordingSink sink = new RecordingSink();
final VoxelizationDispatcher d = new VoxelizationDispatcher(sink);
final ContextState st = new ContextState();
region(d, 1, 1, st);
region(d, 1, 64, st);
region(d, 1, 65, st);
assertEquals(1, sink.dispatches.get(0)[0]);
assertEquals(1, sink.dispatches.get(1)[0]);
assertEquals(2, sink.dispatches.get(2)[0]);
@@ -71,24 +74,14 @@ class VoxelizationDispatcherTest {
}
@Test
void dispatchRange_ignoresEmptyRangeAndNullPass() {
void dispatchRegion_splitsAboveMaxGroups() {
final RecordingSink sink = new RecordingSink();
final VoxelizationDispatcher d = new VoxelizationDispatcher(sink);
final ContextState st = new ContextState();
d.dispatchRange(PASS_HANDLE, -1, -1, 0, 0, st);
d.dispatchRange(0, -1, -1, 0, 64, st);
assertEquals(0, sink.dispatches.size());
assertEquals(0, sink.uniformPushCount);
}
@Test
void endBatch_ignoresNullPass() {
final RecordingSink sink = new RecordingSink();
final VoxelizationDispatcher d = new VoxelizationDispatcher(sink);
d.endBatch(0);
assertEquals(0, sink.endCount);
d.endBatch(PASS_HANDLE);
assertEquals(1, sink.endCount);
region(d, 1, VoxelizationDispatcher.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(1, sink.dispatches.get(1)[0]);
assertEquals(2, sink.uniformPushCount);
}
@Test
@@ -99,9 +92,4 @@ class VoxelizationDispatcherTest {
assertEquals(1, sink.rebindCount, "a region change inside one encoder must rebind the vertex buffer");
assertEquals(0, sink.beginCount, "rebinding must not open a second pass");
}
@Test
void constants_matchExpectations() {
assertEquals(64, VoxelizationDispatcher.workgroupSize());
}
}
@@ -9,18 +9,13 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.lwjgl.opengl.GL43;
import static com.gtnewhorizons.angelica.glsm.backend.BackendManager.RENDER_BACKEND;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("glsm-sdl")
@ExtendWith(GlsmSdlFrameExtension.class)
class GlsmSdlVoxelRegionBindTest {
private static final int BOUND_SLOT = 5;
private static final int EMPTY_SLOT = 6;
private static final int UNLINKED_PROGRAM = Integer.MAX_VALUE;
@BeforeAll
static void boot() {
@@ -49,33 +44,6 @@ class GlsmSdlVoxelRegionBindTest {
}
}
@Test
void regionHookRefusesWithoutProgramOrBoundSlotAndNeverRebinds() {
final ContextState st = SdlTestRig.contextState();
final int savedProgram = st.boundProgram;
final int buf = GLStateManager.glGenBuffers();
try {
unbind(EMPTY_SLOT);
GLStateManager.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, BOUND_SLOT, buf);
st.boundProgram = 0;
assertFalse(RENDER_BACKEND.bindVoxelizationRegion(BOUND_SLOT, 0L, 0f, 0f, 0f), "no program bound");
assertEquals(buf, st.boundSsboByIndex[BOUND_SLOT], "refusal must leave the slot alone");
st.boundProgram = UNLINKED_PROGRAM;
assertFalse(RENDER_BACKEND.bindVoxelizationRegion(EMPTY_SLOT, 0L, 0f, 0f, 0f), "slot has no buffer");
assertEquals(0, st.boundSsboByIndex[EMPTY_SLOT], "the hook must not bind a buffer itself");
assertTrue(RENDER_BACKEND.bindVoxelizationRegion(BOUND_SLOT, 0L, 1f, 2f, 3f), "a GLSM-bound slot with a program must be accepted, or the refusals prove nothing");
assertEquals(buf, st.boundSsboByIndex[BOUND_SLOT]);
assertEquals(buf, glsmGenericSsbo(), "GLSM and SDL must still agree after the hook");
} finally {
st.boundProgram = savedProgram;
unbind(BOUND_SLOT);
GLStateManager.glDeleteBuffers(buf);
}
}
@Test
void deletingTheRegionBufferClearsTheGlsmGenericBinding() {
final int buf = GLStateManager.glGenBuffers();
@@ -225,12 +225,22 @@ class MslCrossCompileTest {
layout(local_size_x = 64) in;
layout(binding = 0, r32i) uniform iimage2D endcrystal_img;
layout(binding = 1, r16ui) writeonly uniform uimage3D voxel_img;
uniform int _vg_startVertex;
uniform int _vg_vertexCount;
uniform int _vg_rangeBase;
uniform int _vg_rangeCount;
uniform int _vg_vertexTotal;
uniform int _vg_invocationBase;
layout(std430, binding = 9) readonly buffer _VgVbuf { uint data[]; } _vg_vbuf;
layout(std430, binding = 10) readonly buffer _VgRanges { uvec2 r[]; } _vg_ranges;
void main() {
int id = _vg_startVertex + int(gl_GlobalInvocationID.x);
if (id >= _vg_startVertex + _vg_vertexCount) return;
uint _vg_g = uint(_vg_invocationBase) + gl_GlobalInvocationID.x;
if (_vg_g >= uint(_vg_vertexTotal)) return;
int _vg_lo = _vg_rangeBase;
int _vg_hi = _vg_rangeBase + _vg_rangeCount - 1;
while (_vg_lo < _vg_hi) {
int _vg_mid = (_vg_lo + _vg_hi + 1) >> 1;
if (_vg_ranges.r[_vg_mid].y <= _vg_g) _vg_lo = _vg_mid; else _vg_hi = _vg_mid - 1;
}
int id = int(_vg_ranges.r[_vg_lo].x + (_vg_g - _vg_ranges.r[_vg_lo].y));
imageAtomicAdd(endcrystal_img, ivec2(id, 0), 1);
imageStore(voxel_img, ivec3(0), uvec4(uint(id), 0u, 0u, 0u));
}
@@ -301,9 +311,12 @@ class MslCrossCompileTest {
#version 460 core
layout(local_size_x = 64) in;
layout(binding = 0, r32i) uniform iimage2D endcrystal_img;
uniform int _vg_startVertex;
uniform int _vg_vertexCount;
uniform int _vg_rangeBase;
uniform int _vg_rangeCount;
uniform int _vg_vertexTotal;
uniform int _vg_invocationBase;
layout(std430, binding = 9) readonly buffer _VgVbuf { uint data[]; } _vg_vbuf;
layout(std430, binding = 10) readonly buffer _VgRanges { uvec2 r[]; } _vg_ranges;
void main() {
{ int _vg_prev = imageLoad(endcrystal_img, ivec2(0, 0)).x;
imageStore(endcrystal_img, ivec2(0, 0), ivec4(_vg_prev + 1, 0, 0, 0)); }
@@ -51,7 +51,7 @@ public final class TracyFramePlots {
private static final long P_TERRAIN_DRAW_COMMANDS = Tracy.plotHandle("terrain.drawCommands");
private static final long P_TERRAIN_REGIONS_DRAWN = Tracy.plotHandle("terrain.regionsDrawn");
private static final long P_VOX_ENCODERS = Tracy.plotHandle("voxelization.encoders");
private static final long P_VOX_DISPATCHES = Tracy.plotHandle("voxelization.dispatches");
private static final long P_VOX_RANGES = Tracy.plotHandle("voxelization.ranges");
private static final long P_VOX_REGIONS = Tracy.plotHandle("voxelization.regions");
private static final long P_DRAW_BATCH_REBUILDS = Tracy.plotHandle("draw.batchRebuilds");
private static final PlotDelta P_DRAW_INDEX_BUFFER_GROWTHS = new PlotDelta("draw.indexBufferGrowths");
@@ -201,7 +201,7 @@ public final class TracyFramePlots {
Tracy.plotInt(P_TERRAIN_REGIONS_DRAWN, TerrainDrawStats.takeRegionsDrawn());
Tracy.plotInt(P_DRAW_BATCH_REBUILDS, TerrainDrawStats.takeBatchRebuilds());
Tracy.plotInt(P_VOX_ENCODERS, SdlShadowVoxelizationSink.takeEncoders());
Tracy.plotInt(P_VOX_DISPATCHES, SdlShadowVoxelizationSink.takeDispatches());
Tracy.plotInt(P_VOX_RANGES, SdlShadowVoxelizationSink.takeRanges());
Tracy.plotInt(P_VOX_REGIONS, SdlShadowVoxelizationSink.takeRegions());
Tracy.plotInt(P_CULL_REGIONS_DRAWN, TerrainDrawStats.takeCullRegionsDrawn());
Tracy.plotInt(P_CULL_MAX_REGION_COMMANDS, TerrainDrawStats.takeCullMaxRegionCommands());
@@ -73,10 +73,10 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
private int packTerrainSamplerAnisotropy = -1;
private boolean terrainSamplerBound;
private final GpuTerrainCuller culler;
private final SdlShadowVoxelizationSink shadowVoxelSink = new SdlShadowVoxelizationSink();
private final ReusableCachedBatch gpuBatch = new ReusableCachedBatch();
private static final ShadowVoxelizer shadowVoxelizer = new ShadowVoxelizer();
private static final SdlShadowVoxelizationSink shadowVoxelSink = new SdlShadowVoxelizationSink();
private static int loggedVoxelizationSkips;
private static MultiDrawMode installedBatchMode;
@@ -277,6 +277,7 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
if (culler != null) {
culler.delete();
}
shadowVoxelSink.delete();
unbindTerrainSampler();
RenderSystem.destroySampler(terrainSampler);
@@ -423,7 +424,9 @@ class AngelicaChunkRenderer extends DefaultChunkRenderer {
deferred.prepareShadowVoxelizationCompute(matrices.modelView());
try {
shadowVoxelizer.walkPass(renderLists, renderPass, renderPass.vertexType().getVertexFormat(), camera, occlusionCamera, useBlockFaceCulling(), shadowVoxelSink);
shadowVoxelSink.flush();
} finally {
shadowVoxelSink.discard();
if (prevProgram != 0) GLStateManager.glUseProgram(prevProgram);
}
}
@@ -1,6 +1,7 @@
package com.gtnewhorizons.angelica.rendering.voxelization;
import com.gtnewhorizons.angelica.glsm.GLStateManager;
import com.gtnewhorizons.angelica.glsm.RenderSystem;
import com.gtnewhorizons.angelica.glsm.backend.BackendManager;
import com.gtnewhorizons.angelica.glsm.backend.RenderBackend;
import net.coderbot.iris.pipeline.transform.RwImageStoreExtractor;
@@ -9,24 +10,25 @@ import org.embeddedt.embeddium.impl.gl.buffer.GlBuffer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.embeddium.impl.render.chunk.region.RenderRegion;
import org.lwjgl.opengl.GL15;
import org.lwjgl.opengl.GL43;
import java.nio.ByteBuffer;
public final class SdlShadowVoxelizationSink implements ShadowVoxelizer.Sink {
private static final Logger LOG = LogManager.getLogger("Angelica");
private static final int SSBO_BINDING = RwImageStoreExtractor.VG_VBUF_SSBO_BINDING;
private static final int VBUF_BINDING = RwImageStoreExtractor.VG_VBUF_SSBO_BINDING;
private static final int RANGES_BINDING = RwImageStoreExtractor.VG_RANGES_SSBO_BINDING;
private static int encodersThisFrame;
private static int dispatchesThisFrame;
private static int regionsThisFrame;
private static int rangesThisFrame;
private boolean warnedBindFailure;
private final VoxelRangeTable table = new VoxelRangeTable();
private int rangesBuf;
private long rangesCapacityBytes;
private boolean warnedBeginFailure;
private long pass;
private int pendingVertexBuffer;
private float pendingX;
private float pendingY;
private float pendingZ;
private static RenderBackend sdl() {
return BackendManager.RENDER_BACKEND;
@@ -38,55 +40,68 @@ public final class SdlShadowVoxelizationSink implements ShadowVoxelizer.Sink {
if (resources == null) return false;
final GlBuffer vertexBuffer = resources.getVertexBuffer();
if (vertexBuffer == null || vertexBuffer.handle() == 0) return false;
pendingVertexBuffer = vertexBuffer.handle();
pendingX = offsetX;
pendingY = offsetY;
pendingZ = offsetZ;
table.beginRegion(vertexBuffer.handle(), offsetX, offsetY, offsetZ);
regionsThisFrame++;
return true;
}
@Override
public boolean range(int vertexOffset, int vertexCount) {
if (pendingVertexBuffer != 0) {
GLStateManager.glBindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, SSBO_BINDING, pendingVertexBuffer);
if (!sdl().bindVoxelizationRegion(SSBO_BINDING, pass, pendingX, pendingY, pendingZ)) {
if (!warnedBindFailure) {
warnedBindFailure = true;
LOG.warn("shadow voxelization: could not bind region vertex buffer {} at SSBO slot {}", pendingVertexBuffer, SSBO_BINDING);
}
return false;
}
pendingVertexBuffer = 0;
}
if (pass == 0) {
pass = sdl().beginVoxelizationBatch(SSBO_BINDING);
if (pass != 0) encodersThisFrame++;
if (pass == 0) {
if (!warnedBeginFailure) {
warnedBeginFailure = true;
LOG.warn("shadow voxelization: compute encoder refused; no voxel writes this pass");
}
return false;
}
}
sdl().voxelizeRange(pass, vertexOffset, vertexCount);
dispatchesThisFrame++;
return true;
public void range(int vertexOffset, int vertexCount) {
table.addRange(vertexOffset, vertexCount);
}
public static int takeEncoders() { final int n = encodersThisFrame; encodersThisFrame = 0; return n; }
public static int takeDispatches() { final int n = dispatchesThisFrame; dispatchesThisFrame = 0; return n; }
public static int takeRegions() { final int n = regionsThisFrame; regionsThisFrame = 0; return n; }
@Override
public void finish() {
if (pass != 0) {
sdl().endVoxelizationBatch(pass);
pass = 0;
public static int takeRanges() { final int n = rangesThisFrame; rangesThisFrame = 0; return n; }
public void flush() {
final int regions = table.regionCount();
if (regions == 0) return;
upload();
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, VBUF_BINDING, table.regionVbuf(0));
final long pass = sdl().beginVoxelizationBatch(VBUF_BINDING);
if (pass == 0) {
if (!warnedBeginFailure) {
warnedBeginFailure = true;
LOG.warn("shadow voxelization: compute encoder refused; no voxel writes this pass");
}
return;
}
pendingVertexBuffer = 0;
encodersThisFrame++;
rangesThisFrame += table.entryCount();
for (int i = 0; i < regions; i++) {
if (i > 0) RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, VBUF_BINDING, table.regionVbuf(i));
sdl().voxelizeRegion(pass, i > 0, table.regionX(i), table.regionY(i), table.regionZ(i), table.regionRangeBase(i), table.regionRangeCount(i), table.regionVertexTotal(i));
}
sdl().endVoxelizationBatch(pass);
}
private void upload() {
final ByteBuffer data = table.entries();
final int bytes = data.remaining();
if (rangesBuf == 0) rangesBuf = GLStateManager.glGenBuffers();
RenderSystem.bindBufferBase(GL43.GL_SHADER_STORAGE_BUFFER, RANGES_BINDING, rangesBuf);
if (rangesCapacityBytes < bytes) {
long cap = Math.max(rangesCapacityBytes, 4096L);
while (cap < bytes) cap <<= 1;
rangesCapacityBytes = cap;
GLStateManager.glBufferData(GL43.GL_SHADER_STORAGE_BUFFER, rangesCapacityBytes, GL15.GL_STREAM_DRAW);
}
GLStateManager.glBufferSubData(GL43.GL_SHADER_STORAGE_BUFFER, 0L, data);
}
public void discard() {
table.clear();
}
public void delete() {
if (rangesBuf != 0) {
GLStateManager.glDeleteBuffers(rangesBuf);
rangesBuf = 0;
rangesCapacityBytes = 0;
}
table.free();
}
}
@@ -19,27 +19,20 @@ public final class ShadowVoxelizer {
/** @return false to skip this region */
boolean region(RenderRegion region, GlVertexFormat format, float offsetX, float offsetY, float offsetZ);
/** @return false to skip the rest of this region; later regions are still attempted */
boolean range(int vertexOffset, int vertexCount);
void finish();
void range(int vertexOffset, int vertexCount);
}
public void walkPass(ChunkRenderListIterable renderLists, TerrainRenderPass renderPass, GlVertexFormat format, CameraTransform camera, CameraTransform occlusionCamera, boolean useBlockFaceCulling, Sink sink) {
try {
final boolean reverse = renderPass.isReverseOrder();
final int n = renderLists.getNumRegions();
if (renderPass.isSorted()) {
for (int i = 0; i < n; i++) {
walkRegionFull(renderLists.getRegion(reverse ? n - 1 - i : i), renderPass, format, camera, sink);
}
} else {
for (int i = 0; i < n; i++) {
walkRegionCompact(renderLists.getRegion(reverse ? n - 1 - i : i), renderPass, format, camera, occlusionCamera, useBlockFaceCulling, sink);
}
final boolean reverse = renderPass.isReverseOrder();
final int n = renderLists.getNumRegions();
if (renderPass.isSorted()) {
for (int i = 0; i < n; i++) {
walkRegionFull(renderLists.getRegion(reverse ? n - 1 - i : i), renderPass, format, camera, sink);
}
} else {
for (int i = 0; i < n; i++) {
walkRegionCompact(renderLists.getRegion(reverse ? n - 1 - i : i), renderPass, format, camera, occlusionCamera, useBlockFaceCulling, sink);
}
} finally {
sink.finish();
}
}
@@ -85,7 +78,7 @@ public final class ShadowVoxelizer {
if (!bindRegion(region, format, camera, sink)) return;
bound = true;
}
if (!sink.range(startVertex, vertexCount)) return;
sink.range(startVertex, vertexCount);
}
}
}
@@ -122,7 +115,7 @@ public final class ShadowVoxelizer {
if (!bindRegion(region, format, camera, sink)) return;
bound = true;
}
if (!sink.range(startVertex, vertexCount)) return;
sink.range(startVertex, vertexCount);
}
}
}
@@ -0,0 +1,125 @@
package com.gtnewhorizons.angelica.rendering.voxelization;
import java.nio.ByteBuffer;
import java.util.Arrays;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAlloc;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memFree;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memPutInt;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memRealloc;
public final class VoxelRangeTable {
private static final int ENTRY_BYTES = 8;
private ByteBuffer entries;
private long entriesAddr;
private int entryCount;
private int runEnd;
private int regionCount;
private int[] regVbuf = new int[16];
private float[] regX = new float[16];
private float[] regY = new float[16];
private float[] regZ = new float[16];
private int[] regBase = new int[16];
private int[] regCount = new int[16];
private int[] regTotal = new int[16];
public void beginRegion(int vbufGlId, float x, float y, float z) {
final int g = regionCount++;
if (g == regVbuf.length) {
final int n = g << 1;
regVbuf = Arrays.copyOf(regVbuf, n);
regX = Arrays.copyOf(regX, n);
regY = Arrays.copyOf(regY, n);
regZ = Arrays.copyOf(regZ, n);
regBase = Arrays.copyOf(regBase, n);
regCount = Arrays.copyOf(regCount, n);
regTotal = Arrays.copyOf(regTotal, n);
}
regVbuf[g] = vbufGlId;
regX[g] = x;
regY[g] = y;
regZ[g] = z;
regBase[g] = entryCount;
regCount[g] = 0;
regTotal[g] = 0;
}
public void addRange(int vertexStart, int vertexCount) {
final int g = regionCount - 1;
if (regCount[g] > 0 && vertexStart == runEnd) {
regTotal[g] += vertexCount;
runEnd += vertexCount;
return;
}
if (entries == null) {
entries = memAlloc(4096);
entriesAddr = memAddress0(entries);
} else if ((entryCount + 1) * ENTRY_BYTES > entries.capacity()) {
entries = memRealloc(entries, entries.capacity() << 1);
entriesAddr = memAddress0(entries);
}
final long p = entriesAddr + (long) entryCount * ENTRY_BYTES;
memPutInt(p, vertexStart);
memPutInt(p + 4, regTotal[g]);
entryCount++;
regCount[g]++;
regTotal[g] += vertexCount;
runEnd = vertexStart + vertexCount;
}
public ByteBuffer entries() {
entries.position(0).limit(entryCount * ENTRY_BYTES);
return entries;
}
public int entryCount() {
return entryCount;
}
public int regionCount() {
return regionCount;
}
public int regionVbuf(int i) {
return regVbuf[i];
}
public float regionX(int i) {
return regX[i];
}
public float regionY(int i) {
return regY[i];
}
public float regionZ(int i) {
return regZ[i];
}
public int regionRangeBase(int i) {
return regBase[i];
}
public int regionRangeCount(int i) {
return regCount[i];
}
public int regionVertexTotal(int i) {
return regTotal[i];
}
public void clear() {
regionCount = 0;
entryCount = 0;
}
public void free() {
if (entries != null) {
memFree(entries);
entries = null;
}
}
}
@@ -52,6 +52,7 @@ public final class RwImageStoreExtractor {
private RwImageStoreExtractor() {}
public static final int VG_VBUF_SSBO_BINDING = 9;
public static final int VG_RANGES_SSBO_BINDING = 10;
private static final int STRIDE_BYTES = 48;
private static final int STRIDE_UINTS = STRIDE_BYTES / 4;
@@ -561,9 +562,12 @@ public final class RwImageStoreExtractor {
private static void emitChunkPrelude(StringBuilder out, Set<String> writtenImages, Set<String> nonWriteonlyImages, Map<String, ImageDecl> declared, List<AttributeSlot> attrs) {
out.append("layout(local_size_x = 64) in;\n\n");
emitImageDecls(out, writtenImages, nonWriteonlyImages, declared);
out.append("\nuniform int _vg_startVertex;\n");
out.append("uniform int _vg_vertexCount;\n\n");
out.append("layout(std430, binding = ").append(VG_VBUF_SSBO_BINDING).append(") readonly buffer _VgVbuf { uint data[]; } _vg_vbuf;\n\n");
out.append("\nuniform int _vg_rangeBase;\n");
out.append("uniform int _vg_rangeCount;\n");
out.append("uniform int _vg_vertexTotal;\n");
out.append("uniform int _vg_invocationBase;\n\n");
out.append("layout(std430, binding = ").append(VG_VBUF_SSBO_BINDING).append(") readonly buffer _VgVbuf { uint data[]; } _vg_vbuf;\n");
out.append("layout(std430, binding = ").append(VG_RANGES_SSBO_BINDING).append(") readonly buffer _VgRanges { uvec2 r[]; } _vg_ranges;\n\n");
out.append("vec4 _vg_sink_pos;\n");
out.append("float _vg_sink_psize;\n");
out.append("int _vg_id_global;\n");
@@ -626,8 +630,15 @@ public final class RwImageStoreExtractor {
out.append("void main() {\n");
switch (mode) {
case CHUNK -> {
out.append(" int id = _vg_startVertex + int(gl_GlobalInvocationID.x);\n");
out.append(" if (id >= _vg_startVertex + _vg_vertexCount) return;\n");
out.append(" uint _vg_g = uint(_vg_invocationBase) + gl_GlobalInvocationID.x;\n");
out.append(" if (_vg_g >= uint(_vg_vertexTotal)) return;\n");
out.append(" int _vg_lo = _vg_rangeBase;\n");
out.append(" int _vg_hi = _vg_rangeBase + _vg_rangeCount - 1;\n");
out.append(" while (_vg_lo < _vg_hi) {\n");
out.append(" int _vg_mid = (_vg_lo + _vg_hi + 1) >> 1;\n");
out.append(" if (_vg_ranges.r[_vg_mid].y <= _vg_g) _vg_lo = _vg_mid; else _vg_hi = _vg_mid - 1;\n");
out.append(" }\n");
out.append(" int id = int(_vg_ranges.r[_vg_lo].x + (_vg_g - _vg_ranges.r[_vg_lo].y));\n");
out.append(" _vg_id_global = id;\n");
out.append(" _vg_unpack(uint(id));\n");
emitVertexDecode(out, attrs);
@@ -36,9 +36,7 @@ class ShadowVoxelizerTest {
private static class RecordingSink implements ShadowVoxelizer.Sink {
final List<float[]> regions = new ArrayList<>();
final List<int[]> ranges = new ArrayList<>();
int finishes;
boolean refuseRegion;
boolean refuseRange;
@Override
public boolean region(RenderRegion region, GlVertexFormat format, float x, float y, float z) {
@@ -48,15 +46,8 @@ class ShadowVoxelizerTest {
}
@Override
public boolean range(int vertexOffset, int vertexCount) {
if (refuseRange) return false;
public void range(int vertexOffset, int vertexCount) {
ranges.add(new int[]{vertexOffset, vertexCount});
return true;
}
@Override
public void finish() {
finishes++;
}
}
@@ -300,7 +291,6 @@ class ShadowVoxelizerTest {
assertTrue(sink.regions.isEmpty(), "an empty region must not open an encoder");
assertTrue(sink.ranges.isEmpty());
assertEquals(1, sink.finishes);
}
@Test
@@ -311,7 +301,6 @@ class ShadowVoxelizerTest {
final RecordingSink sink = walk(lists(renderList(region, 0)), renderPass, new CameraTransform(0, 0, 0), false);
assertTrue(sink.ranges.isEmpty());
assertEquals(1, sink.finishes);
}
@Test
@@ -334,33 +323,6 @@ class ShadowVoxelizerTest {
new CameraTransform(0, 0, 0), new CameraTransform(0, 0, 0), false, sink);
assertTrue(sink.ranges.isEmpty(), "a region that cannot be bound emits nothing");
assertEquals(1, sink.finishes, "the encoder is still closed exactly once");
}
@Test
void refusedRangeSkipsOnlyThatRegion() {
final TerrainRenderPass renderPass = pass("solid");
final RenderRegion regionA = region(0, 0, 0);
final RenderRegion regionB = region(1, 0, 0);
final long heapA = heap();
final long heapB = heap();
final SectionRenderDataStorage storageA = storage(heapA);
final SectionRenderDataStorage storageB = storage(heapB);
writeSection(heapA, 0, 0, 4, 0, 4, 0, 0, 0, 0);
writeSection(heapB, 0, 0, 4, 0, 0, 0, 0, 0, 0);
attachStorage(regionA, renderPass, storageA);
attachStorage(regionB, renderPass, storageB);
final RecordingSink sink = new RecordingSink() {
@Override public boolean range(int vertexOffset, int vertexCount) {
return regions.size() != 1 && super.range(vertexOffset, vertexCount);
}
};
new ShadowVoxelizer().walkPass(lists(renderList(regionA, 0), renderList(regionB, 0)), renderPass, null, new CameraTransform(0, 0, 0), new CameraTransform(0, 0, 0), false, sink);
assertEquals(2, sink.regions.size(), "the second region is still attempted after the first is refused");
assertEquals(1, sink.ranges.size(), "only the second region's range lands");
assertEquals(1, sink.finishes);
}
@Test
@@ -0,0 +1,117 @@
package com.gtnewhorizons.angelica.rendering.voxelization;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memAddress0;
import static com.gtnewhorizon.gtnhlib.bytebuf.MemoryUtilities.memGetInt;
import static org.junit.jupiter.api.Assertions.assertEquals;
class VoxelRangeTableTest {
private final VoxelRangeTable t = new VoxelRangeTable();
@AfterEach
void free() {
t.free();
}
private int entry(int i) {
return memGetInt(memAddress0(t.entries()) + i * 4L);
}
@Test
void singleRegionPrefixes() {
t.beginRegion(7, 1f, 2f, 3f);
t.addRange(100, 10);
t.addRange(200, 20);
t.addRange(50, 5);
assertEquals(1, t.regionCount());
assertEquals(7, t.regionVbuf(0));
assertEquals(1f, t.regionX(0));
assertEquals(2f, t.regionY(0));
assertEquals(3f, t.regionZ(0));
assertEquals(0, t.regionRangeBase(0));
assertEquals(3, t.regionRangeCount(0));
assertEquals(35, t.regionVertexTotal(0));
assertEquals(3, t.entryCount());
assertEquals(24, t.entries().remaining());
assertEquals(100, entry(0));
assertEquals(0, entry(1));
assertEquals(200, entry(2));
assertEquals(10, entry(3));
assertEquals(50, entry(4));
assertEquals(30, entry(5));
}
@Test
void regionsAppendInOrderAndNeverMergeAcross() {
t.beginRegion(1, 0f, 0f, 0f);
t.addRange(0, 4);
t.addRange(40, 6);
t.beginRegion(2, 5f, 0f, 0f);
t.addRange(46, 8);
t.addRange(2000, 2);
assertEquals(2, t.regionCount());
assertEquals(1, t.regionVbuf(0));
assertEquals(0f, t.regionX(0));
assertEquals(0, t.regionRangeBase(0));
assertEquals(2, t.regionRangeCount(0));
assertEquals(10, t.regionVertexTotal(0));
assertEquals(2, t.regionVbuf(1));
assertEquals(5f, t.regionX(1));
assertEquals(2, t.regionRangeBase(1));
assertEquals(2, t.regionRangeCount(1));
assertEquals(10, t.regionVertexTotal(1));
assertEquals(4, t.entryCount());
assertEquals(0, entry(0));
assertEquals(0, entry(1));
assertEquals(40, entry(2));
assertEquals(4, entry(3));
assertEquals(46, entry(4));
assertEquals(0, entry(5));
assertEquals(2000, entry(6));
assertEquals(8, entry(7));
}
@Test
void contiguousRangesMerge() {
t.beginRegion(3, 0f, 0f, 0f);
t.addRange(100, 10);
t.addRange(110, 5);
t.addRange(200, 3);
t.addRange(300, 2);
assertEquals(3, t.regionRangeCount(0));
assertEquals(20, t.regionVertexTotal(0));
assertEquals(3, t.entryCount());
assertEquals(100, entry(0));
assertEquals(0, entry(1));
assertEquals(200, entry(2));
assertEquals(15, entry(3));
assertEquals(300, entry(4));
assertEquals(18, entry(5));
}
@Test
void growsAndClearsForReuse() {
for (int i = 0; i < 300; i++) {
t.beginRegion(i, 0f, 0f, 0f);
t.addRange(i * 100, 3);
t.addRange(i * 100 + 50, 3);
}
assertEquals(300, t.regionCount());
assertEquals(600, t.entryCount());
assertEquals(4800, t.entries().remaining());
assertEquals(299 * 100 + 50, entry(1198));
assertEquals(3, entry(1199));
assertEquals(598, t.regionRangeBase(299));
t.clear();
t.beginRegion(5, 1f, 1f, 1f);
t.addRange(9, 2);
assertEquals(1, t.regionCount());
assertEquals(1, t.entryCount());
assertEquals(0, t.regionRangeBase(0));
assertEquals(2, t.regionVertexTotal(0));
assertEquals(9, entry(0));
}
}
@@ -79,8 +79,14 @@ class RwImageStoreExtractorTest {
@Test void compute_hasUniformsAndSsboBinding() {
final var result = RwImageStoreExtractor.tryExtract(MINIMAL_VOX_VSH, PatchShaderType.VERTEX, "shadow");
final String s = result.computeSource();
assertTrue(s.contains("uniform int _vg_startVertex;"));
assertTrue(s.contains("uniform int _vg_vertexCount;"));
assertTrue(s.contains("uniform int _vg_rangeBase;"));
assertTrue(s.contains("uniform int _vg_rangeCount;"));
assertTrue(s.contains("uniform int _vg_vertexTotal;"));
assertTrue(s.contains("uniform int _vg_invocationBase;"));
assertTrue(s.contains("layout(std430, binding = 10) readonly buffer _VgRanges { uvec2 r[]; } _vg_ranges;"));
assertTrue(s.contains("int id = int(_vg_ranges.r[_vg_lo].x + (_vg_g - _vg_ranges.r[_vg_lo].y));"));
assertFalse(s.contains("_vg_startVertex"));
assertFalse(s.contains("_vg_vertexCount"));
assertTrue(s.contains("readonly buffer _VgVbuf"));
assertTrue(s.contains("layout(std430, binding = " + RwImageStoreExtractor.VG_VBUF_SSBO_BINDING + ") readonly buffer _VgVbuf"), "VBO SSBO must bind at the dedicated voxelization slot (not 0..8 which shaderpacks claim via buffer.N)");
}