Skip to content

Commit b3a4c69

Browse files
committed
Implementation of bitmap-based trees for rendering something even when the camera is outside the graph (CaffeineMC#2887 merge item 2)
1 parent 81cc775 commit b3a4c69

30 files changed

+1198
-138
lines changed

common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@ public void setupTerrain(Camera camera,
253253
}
254254

255255
private void processChunkEvents() {
256+
this.renderSectionManager.beforeSectionUpdates();
256257
var tracker = ChunkTrackerHolder.get(this.level);
257258
tracker.forEachEvent(this.renderSectionManager::onChunkAdded, this.renderSectionManager::onChunkRemoved);
258259
}

common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,14 @@ public class RenderSectionFlags {
55
public static final int HAS_BLOCK_ENTITIES = 1;
66
public static final int HAS_ANIMATED_SPRITES = 2;
77

8+
public static final int MASK_HAS_BLOCK_GEOMETRY = 1 << HAS_BLOCK_GEOMETRY;
9+
public static final int MASK_HAS_BLOCK_ENTITIES = 1 << HAS_BLOCK_ENTITIES;
10+
public static final int MASK_HAS_ANIMATED_SPRITES = 1 << HAS_ANIMATED_SPRITES;
11+
public static final int MASK_NEEDS_RENDER = MASK_HAS_BLOCK_GEOMETRY | MASK_HAS_BLOCK_ENTITIES | MASK_HAS_ANIMATED_SPRITES;
12+
813
public static final int NONE = 0;
14+
15+
public static boolean needsRender(int flags) {
16+
return (flags & MASK_NEEDS_RENDER) != 0;
17+
}
918
}

common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@
2121
import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderSortingTask;
2222
import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask;
2323
import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo;
24-
import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderList;
25-
import net.caffeinemc.mods.sodium.client.render.chunk.lists.SortedRenderLists;
26-
import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollector;
24+
import net.caffeinemc.mods.sodium.client.render.chunk.lists.*;
2725
import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection;
2826
import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller;
2927
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
@@ -35,6 +33,7 @@
3533
import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData;
3634
import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.CameraMovement;
3735
import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.SortTriggering;
36+
import net.caffeinemc.mods.sodium.client.render.chunk.tree.RemovableMultiForest;
3837
import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkMeshFormats;
3938
import net.caffeinemc.mods.sodium.client.render.util.RenderAsserts;
4039
import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform;
@@ -110,6 +109,8 @@ public class RenderSectionManager {
110109

111110
private @Nullable Vector3dc cameraPosition;
112111

112+
private final RemovableMultiForest renderableSectionTree;
113+
113114
public RenderSectionManager(ClientLevel level, int renderDistance, CommandList commandList) {
114115
this.chunkRenderer = new DefaultChunkRenderer(RenderDevice.INSTANCE, ChunkMeshFormats.COMPACT);
115116

@@ -127,6 +128,8 @@ public RenderSectionManager(ClientLevel level, int renderDistance, CommandList c
127128
this.renderLists = SortedRenderLists.empty();
128129
this.occlusionCuller = new OcclusionCuller(Long2ReferenceMaps.unmodifiable(this.sectionByPosition), this.level);
129130

131+
this.renderableSectionTree = new RemovableMultiForest(renderDistance);
132+
130133
this.taskLists = new EnumMap<>(TaskQueueType.class);
131134

132135
for (var type : TaskQueueType.values()) {
@@ -164,12 +167,28 @@ private void createTerrainRenderList(Camera camera, Viewport viewport, FogParame
164167
final var searchDistance = this.getSearchDistance(fogParameters);
165168
final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator);
166169

167-
var visitor = new VisibleChunkCollector(frame, SodiumClientMod.options().performance.chunkBuildDeferMode.getImportantRebuildQueueType());
170+
RenderListProvider renderListProvider;
171+
var importantRebuildQueueType = SodiumClientMod.options().performance.chunkBuildDeferMode.getImportantRebuildQueueType();
172+
if (this.isOutOfGraph(viewport.getChunkCoord())) {
173+
var visitor = new TreeSectionCollector(frame, importantRebuildQueueType, this.sectionByPosition);
174+
this.renderableSectionTree.prepareForTraversal();
175+
this.renderableSectionTree.traverse(visitor, viewport, searchDistance);
176+
177+
renderListProvider = visitor;
178+
} else {
179+
var visitor = new OcclusionSectionCollector(frame, importantRebuildQueueType);
180+
this.occlusionCuller.findVisible(visitor, viewport, searchDistance, useOcclusionCulling, frame);
181+
182+
renderListProvider = visitor;
183+
}
168184

169-
this.occlusionCuller.findVisible(visitor, viewport, searchDistance, useOcclusionCulling, frame);
185+
this.renderLists = renderListProvider.createRenderLists(viewport);
186+
this.taskLists = renderListProvider.getTaskLists();
187+
}
170188

171-
this.renderLists = visitor.createRenderLists(viewport);
172-
this.taskLists = visitor.getRebuildLists();
189+
private boolean isOutOfGraph(SectionPos pos) {
190+
var sectionY = pos.getY();
191+
return this.level.getMinSectionY() <= sectionY && sectionY <= this.level.getMaxSectionY() && !this.sectionByPosition.containsKey(pos.asLong());
173192
}
174193

175194
private float getSearchDistance(FogParameters fogParameters) {
@@ -197,6 +216,10 @@ private boolean shouldUseOcclusionCulling(Camera camera, boolean spectator) {
197216
return useOcclusionCulling;
198217
}
199218

219+
public void beforeSectionUpdates() {
220+
this.renderableSectionTree.ensureCapacity(this.getRenderDistance());
221+
}
222+
200223
private void resetRenderLists() {
201224
this.renderLists = SortedRenderLists.empty();
202225

@@ -225,13 +248,14 @@ public void onSectionAdded(int x, int y, int z) {
225248
if (section.hasOnlyAir()) {
226249
this.updateSectionInfo(renderSection, BuiltSectionInfo.EMPTY);
227250
} else {
251+
this.renderableSectionTree.add(renderSection);
228252
renderSection.setPendingUpdate(ChunkUpdateTypes.INITIAL_BUILD, this.lastFrameAtTime);
229253
}
230254

231255
this.connectNeighborNodes(renderSection);
232256

233257
// force update to schedule build task
234-
this.needsGraphUpdate = true;
258+
this.markGraphDirty();
235259
}
236260

237261
public void onSectionRemoved(int x, int y, int z) {
@@ -242,6 +266,8 @@ public void onSectionRemoved(int x, int y, int z) {
242266
return;
243267
}
244268

269+
this.renderableSectionTree.remove(x, y, z);
270+
245271
if (section.getTranslucentData() != null) {
246272
this.sortTriggering.removeSection(section.getTranslucentData(), sectionPos);
247273
}
@@ -258,7 +284,7 @@ public void onSectionRemoved(int x, int y, int z) {
258284
section.delete();
259285

260286
// force update to remove section from render lists
261-
this.needsGraphUpdate = true;
287+
this.markGraphDirty();
262288
}
263289

264290
public void renderLayer(ChunkRenderMatrices matrices, TerrainRenderPass pass, double x, double y, double z) {
@@ -375,6 +401,12 @@ private boolean processChunkBuildResults(ArrayList<BuilderTaskOutput> results) {
375401
}
376402

377403
private boolean updateSectionInfo(RenderSection render, BuiltSectionInfo info) {
404+
if (info == null || !RenderSectionFlags.needsRender(info.flags)) {
405+
this.renderableSectionTree.remove(render);
406+
} else {
407+
this.renderableSectionTree.add(render);
408+
}
409+
378410
var infoChanged = render.setInfo(info);
379411

380412
if (info == null || ArrayUtils.isEmpty(info.globalBlockEntities)) {

common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class ChunkRenderList {
2424
private int lastRelativeCameraSectionX;
2525
private int lastRelativeCameraSectionY;
2626
private int lastRelativeCameraSectionZ;
27+
private boolean addedSectionsAreSorted = false;
2728

2829
private final byte[] sectionsWithSprites = new byte[RenderRegion.REGION_SIZE];
2930
private int sectionsWithSpritesCount = 0;
@@ -39,7 +40,7 @@ public ChunkRenderList(RenderRegion region) {
3940
this.region = region;
4041
}
4142

42-
public void reset(int frame) {
43+
public void reset(int frame, boolean addedSectionsAreSorted) {
4344
this.prevSectionsWithGeometryCount = this.sectionsWithGeometryCount;
4445
Arrays.fill(this.sectionsWithGeometryMap, 0L);
4546

@@ -49,12 +50,13 @@ public void reset(int frame) {
4950

5051
this.size = 0;
5152
this.lastVisibleFrame = frame;
53+
this.addedSectionsAreSorted = addedSectionsAreSorted;
5254
}
5355

5456
// clamping the relative camera position to the region bounds means there can only be very few different distances
5557
private static final int SORTING_HISTOGRAM_SIZE = RenderRegion.REGION_WIDTH + RenderRegion.REGION_HEIGHT + RenderRegion.REGION_LENGTH - 2;
5658

57-
public void prepareForRender(SectionPos cameraPos, int[] sortItems) {
59+
public void prepareForRender(SectionPos cameraPos, SortItemsProvider sortItemsProvider) {
5860
// The relative coordinates are clamped to one section larger than the region bounds to also capture cache invalidation that happens
5961
// when the camera moves from outside the region to inside the region (when seen on all axes independently).
6062
// This type of cache invalidation stems from different facings of sections being rendered if the camera is aligned with them on an axis.
@@ -78,16 +80,20 @@ public void prepareForRender(SectionPos cameraPos, int[] sortItems) {
7880
this.lastRelativeCameraSectionY = relativeCameraSectionY;
7981
this.lastRelativeCameraSectionZ = relativeCameraSectionZ;
8082

81-
this.sortSections(relativeCameraSectionX, relativeCameraSectionY, relativeCameraSectionZ, sortItems);
83+
// only sort sections if necessary, read directly from bitmap instead of no sorting is required
84+
if (!this.addedSectionsAreSorted) {
85+
this.sortSections(relativeCameraSectionX, relativeCameraSectionY, relativeCameraSectionZ, sortItemsProvider);
86+
}
8287
}
8388
}
8489

85-
public void sortSections(int relativeCameraSectionX, int relativeCameraSectionY, int relativeCameraSectionZ, int[] sortItems) {
90+
private void sortSections(int relativeCameraSectionX, int relativeCameraSectionY, int relativeCameraSectionZ, SortItemsProvider sortItemsProvider) {
8691
relativeCameraSectionX = Mth.clamp(relativeCameraSectionX, 0, RenderRegion.REGION_WIDTH - 1);
8792
relativeCameraSectionY = Mth.clamp(relativeCameraSectionY, 0, RenderRegion.REGION_HEIGHT - 1);
8893
relativeCameraSectionZ = Mth.clamp(relativeCameraSectionZ, 0, RenderRegion.REGION_LENGTH - 1);
8994

9095
int[] histogram = new int[SORTING_HISTOGRAM_SIZE];
96+
var sortItems = sortItemsProvider.ensureSortItemsOfLength(this.sectionsWithGeometryCount);
9197

9298
this.sectionsWithGeometryCount = 0;
9399
for (int mapIndex = 0; mapIndex < this.sectionsWithGeometryMap.length; mapIndex++) {
@@ -132,6 +138,9 @@ public void add(RenderSection render) {
132138

133139
if (((flags >>> RenderSectionFlags.HAS_BLOCK_GEOMETRY) & 1) == 1) {
134140
this.sectionsWithGeometryMap[index >> 6] |= 1L << (index & 0b111111);
141+
if (this.addedSectionsAreSorted) {
142+
this.sectionsWithGeometry[this.sectionsWithGeometryCount] = (byte) index;
143+
}
135144
this.sectionsWithGeometryCount++;
136145
}
137146

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package net.caffeinemc.mods.sodium.client.render.chunk.lists;
2+
3+
public interface CoordinateSectionVisitor {
4+
void visit(int x, int y, int z);
5+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package net.caffeinemc.mods.sodium.client.render.chunk.lists;
2+
3+
import net.caffeinemc.mods.sodium.client.render.chunk.TaskQueueType;
4+
5+
/**
6+
* The occlusion section collector is passed to the occlusion graph search culler to
7+
* collect the visible chunks.
8+
*/
9+
public class OcclusionSectionCollector extends SectionCollector {
10+
public OcclusionSectionCollector(int frame, TaskQueueType importantRebuildQueueType) {
11+
super(frame, importantRebuildQueueType);
12+
}
13+
14+
@Override
15+
public boolean orderIsSorted() {
16+
return false;
17+
}
18+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package net.caffeinemc.mods.sodium.client.render.chunk.lists;
2+
3+
import it.unimi.dsi.fastutil.ints.IntArrays;
4+
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
5+
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
6+
import net.caffeinemc.mods.sodium.client.render.chunk.TaskQueueType;
7+
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
8+
import net.caffeinemc.mods.sodium.client.render.viewport.Viewport;
9+
10+
import java.util.ArrayDeque;
11+
import java.util.Map;
12+
13+
public interface RenderListProvider extends SortItemsProvider {
14+
ObjectArrayList<ChunkRenderList> getUnsortedRenderLists();
15+
16+
Map<TaskQueueType, ArrayDeque<RenderSection>> getTaskLists();
17+
18+
boolean orderIsSorted();
19+
20+
default SortedRenderLists createRenderLists(Viewport viewport) {
21+
var sectionPos = viewport.getChunkCoord();
22+
var unsorted = this.getUnsortedRenderLists();
23+
24+
// sort the regions by distance to fix rare region ordering problems.
25+
// regions need to be sorted even when the order is generated by a correctly traversed section tree
26+
// but sections don't need to be sorted within regions
27+
var cameraX = sectionPos.getX() >> RenderRegion.REGION_WIDTH_SH;
28+
var cameraY = sectionPos.getY() >> RenderRegion.REGION_HEIGHT_SH;
29+
var cameraZ = sectionPos.getZ() >> RenderRegion.REGION_LENGTH_SH;
30+
31+
var size = unsorted.size();
32+
var sortItems = this.ensureSortItemsOfLength(size);
33+
34+
for (var i = 0; i < size; i++) {
35+
var region = unsorted.get(i).getRegion();
36+
var x = Math.abs(region.getX() - cameraX);
37+
var y = Math.abs(region.getY() - cameraY);
38+
var z = Math.abs(region.getZ() - cameraZ);
39+
sortItems[i] = (x + y + z) << 16 | i;
40+
}
41+
42+
IntArrays.unstableSort(sortItems, 0, size);
43+
44+
var sorted = new ObjectArrayList<ChunkRenderList>(size);
45+
for (var i = 0; i < size; i++) {
46+
var key = sortItems[i];
47+
var renderList = unsorted.get(key & 0xFFFF);
48+
sorted.add(renderList);
49+
}
50+
51+
for (var list : sorted) {
52+
list.prepareForRender(sectionPos, this);
53+
}
54+
55+
return new SortedRenderLists(sorted);
56+
}
57+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package net.caffeinemc.mods.sodium.client.render.chunk.lists;
2+
3+
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
4+
5+
public interface RenderSectionVisitor {
6+
void visit(RenderSection section);
7+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package net.caffeinemc.mods.sodium.client.render.chunk.lists;
2+
3+
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
4+
import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateTypes;
5+
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
6+
import net.caffeinemc.mods.sodium.client.render.chunk.TaskQueueType;
7+
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
8+
9+
import java.util.ArrayDeque;
10+
import java.util.EnumMap;
11+
import java.util.Map;
12+
import java.util.Queue;
13+
14+
public abstract class SectionCollector implements RenderListProvider, RenderSectionVisitor {
15+
private final int frame;
16+
private final TaskQueueType importantRebuildQueueType;
17+
private final ObjectArrayList<ChunkRenderList> renderLists;
18+
private final EnumMap<TaskQueueType, ArrayDeque<RenderSection>> sortedTaskLists;
19+
20+
private static int[] sortItems = new int[RenderRegion.REGION_SIZE];
21+
22+
public SectionCollector(int frame, TaskQueueType importantRebuildQueueType) {
23+
this.frame = frame;
24+
this.importantRebuildQueueType = importantRebuildQueueType;
25+
26+
this.renderLists = new ObjectArrayList<>();
27+
this.sortedTaskLists = new EnumMap<>(TaskQueueType.class);
28+
29+
for (var type : TaskQueueType.values()) {
30+
this.sortedTaskLists.put(type, new ArrayDeque<>());
31+
}
32+
}
33+
34+
@Override
35+
public void visit(RenderSection section) {
36+
// only process section (and associated render list) if it has content that needs rendering
37+
if (section.getFlags() != 0) {
38+
RenderRegion region = section.getRegion();
39+
ChunkRenderList renderList = region.getRenderList();
40+
41+
if (renderList.getLastVisibleFrame() != this.frame) {
42+
renderList.reset(this.frame, this.orderIsSorted());
43+
44+
this.renderLists.add(renderList);
45+
}
46+
47+
renderList.add(section);
48+
}
49+
50+
// always add to rebuild lists though, because it might just not be built yet
51+
var pendingUpdate = section.getPendingUpdate();
52+
53+
if (pendingUpdate != 0 && section.getTaskCancellationToken() == null) {
54+
var queueType = ChunkUpdateTypes.getQueueType(pendingUpdate, this.importantRebuildQueueType);
55+
Queue<RenderSection> queue = this.sortedTaskLists.get(queueType);
56+
57+
if (queue.size() < queueType.queueSizeLimit()) {
58+
queue.add(section);
59+
}
60+
}
61+
}
62+
63+
@Override
64+
public ObjectArrayList<ChunkRenderList> getUnsortedRenderLists() {
65+
return this.renderLists;
66+
}
67+
68+
@Override
69+
public Map<TaskQueueType, ArrayDeque<RenderSection>> getTaskLists() {
70+
return this.sortedTaskLists;
71+
}
72+
73+
@Override
74+
public void setCachedSortItems(int[] sortItems) {
75+
SectionCollector.sortItems = sortItems;
76+
}
77+
78+
@Override
79+
public int[] getCachedSortItems() {
80+
return SectionCollector.sortItems;
81+
}
82+
}

0 commit comments

Comments
 (0)