Skip to content

Commit 773a163

Browse files
douiraThatMG393
authored andcommitted
Sort render lists for regions and sections after traversal (CaffeineMC#2780)
Render sections and regions are sorted after the graph traversal is performed. This decouples their ordering from the graph, which isn't entirely correct for draw call sorting. Fixes CaffeineMC#2266
1 parent 3a0296d commit 773a163

File tree

7 files changed

+113
-72
lines changed

7 files changed

+113
-72
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ public RenderSection(RenderRegion region, int chunkX, int chunkY, int chunkZ) {
6969
this.chunkY = chunkY;
7070
this.chunkZ = chunkZ;
7171

72-
int rX = this.getChunkX() & (RenderRegion.REGION_WIDTH - 1);
73-
int rY = this.getChunkY() & (RenderRegion.REGION_HEIGHT - 1);
74-
int rZ = this.getChunkZ() & (RenderRegion.REGION_LENGTH - 1);
72+
int rX = this.getChunkX() & RenderRegion.REGION_WIDTH_M;
73+
int rY = this.getChunkY() & RenderRegion.REGION_HEIGHT_M;
74+
int rZ = this.getChunkZ() & RenderRegion.REGION_LENGTH_M;
7575

7676
this.sectionIndex = LocalSectionIndex.pack(rX, rY, rZ);
7777

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ private void createTerrainRenderList(Camera camera, Viewport viewport, int frame
146146

147147
this.occlusionCuller.findVisible(visitor, viewport, searchDistance, useOcclusionCulling, frame);
148148

149-
this.renderLists = visitor.createRenderLists();
149+
this.renderLists = visitor.createRenderLists(viewport);
150150
this.taskLists = visitor.getRebuildLists();
151151
}
152152

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

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package net.caffeinemc.mods.sodium.client.render.chunk.lists;
22

3+
import net.caffeinemc.mods.sodium.client.render.chunk.LocalSectionIndex;
34
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
45
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags;
6+
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
7+
import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform;
8+
import net.caffeinemc.mods.sodium.client.util.iterator.ByteArrayIterator;
59
import net.caffeinemc.mods.sodium.client.util.iterator.ByteIterator;
610
import net.caffeinemc.mods.sodium.client.util.iterator.ReversibleByteArrayIterator;
7-
import net.caffeinemc.mods.sodium.client.util.iterator.ByteArrayIterator;
8-
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
11+
import net.minecraft.util.Mth;
912
import org.jetbrains.annotations.Nullable;
1013

1114
public class ChunkRenderList {
@@ -37,6 +40,39 @@ public void reset(int frame) {
3740
this.lastVisibleFrame = frame;
3841
}
3942

43+
// clamping the relative camera position to the region bounds means there can only be very few different distances
44+
private static final int SORTING_HISTOGRAM_SIZE = RenderRegion.REGION_WIDTH + RenderRegion.REGION_HEIGHT + RenderRegion.REGION_LENGTH - 2;
45+
46+
public void sortSections(CameraTransform transform, int[] sortItems) {
47+
var cameraX = Mth.clamp((transform.intX >> 4) - this.region.getChunkX(), 0, RenderRegion.REGION_WIDTH - 1);
48+
var cameraY = Mth.clamp((transform.intY >> 4) - this.region.getChunkY(), 0, RenderRegion.REGION_HEIGHT - 1);
49+
var cameraZ = Mth.clamp((transform.intZ >> 4) - this.region.getChunkZ(), 0, RenderRegion.REGION_LENGTH - 1);
50+
51+
int[] histogram = new int[SORTING_HISTOGRAM_SIZE];
52+
53+
for (int i = 0; i < this.sectionsWithGeometryCount; i++) {
54+
var index = this.sectionsWithGeometry[i] & 0xFF; // makes sure the byte -> int conversion is unsigned
55+
var x = Math.abs(LocalSectionIndex.unpackX(index) - cameraX);
56+
var y = Math.abs(LocalSectionIndex.unpackY(index) - cameraY);
57+
var z = Math.abs(LocalSectionIndex.unpackZ(index) - cameraZ);
58+
59+
var distance = x + y + z;
60+
histogram[distance]++;
61+
sortItems[i] = distance << 8 | index;
62+
}
63+
64+
// prefix sum to calculate indexes
65+
for (int i = 1; i < SORTING_HISTOGRAM_SIZE; i++) {
66+
histogram[i] += histogram[i - 1];
67+
}
68+
69+
for (int i = 0; i < this.sectionsWithGeometryCount; i++) {
70+
var item = sortItems[i];
71+
var distance = item >>> 8;
72+
this.sectionsWithGeometry[--histogram[distance]] = (byte) item;
73+
}
74+
}
75+
4076
public void add(RenderSection render) {
4177
if (this.size >= RenderRegion.REGION_SIZE) {
4278
throw new ArrayIndexOutOfBoundsException("Render list is full");
Lines changed: 0 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package net.caffeinemc.mods.sodium.client.render.chunk.lists;
22

33
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
4-
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
54
import net.caffeinemc.mods.sodium.client.util.iterator.ReversibleObjectArrayIterator;
6-
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
75

86
/**
97
* Stores one render list of sections per region, sorted by the order in which
@@ -27,44 +25,4 @@ public ReversibleObjectArrayIterator<ChunkRenderList> iterator(boolean reverse)
2725
public static SortedRenderLists empty() {
2826
return EMPTY;
2927
}
30-
31-
public static class Builder {
32-
private final ObjectArrayList<ChunkRenderList> lists = new ObjectArrayList<>();
33-
private final int frame;
34-
35-
public Builder(int frame) {
36-
this.frame = frame;
37-
}
38-
39-
public void add(RenderSection section) {
40-
RenderRegion region = section.getRegion();
41-
ChunkRenderList list = region.getRenderList();
42-
43-
// Even if a section does not have render objects, we must ensure the render list is initialized and put
44-
// into the sorted queue of lists, so that we maintain the correct order of draw calls.
45-
if (list.getLastVisibleFrame() != this.frame) {
46-
list.reset(this.frame);
47-
48-
this.lists.add(list);
49-
}
50-
51-
// Only add the section to the render list if it actually contains render objects
52-
if (section.getFlags() != 0) {
53-
list.add(section);
54-
}
55-
}
56-
57-
public SortedRenderLists build() {
58-
var filtered = new ObjectArrayList<ChunkRenderList>(this.lists.size());
59-
60-
// Filter any empty render lists
61-
for (var list : this.lists) {
62-
if (list.size() > 0) {
63-
filtered.add(list);
64-
}
65-
}
66-
67-
return new SortedRenderLists(filtered);
68-
}
69-
}
7028
}

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

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package net.caffeinemc.mods.sodium.client.render.chunk.lists;
22

3+
import it.unimi.dsi.fastutil.ints.IntArrays;
34
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
45
import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateType;
56
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
67
import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller;
78
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
9+
import net.caffeinemc.mods.sodium.client.render.viewport.Viewport;
810

911
import java.util.*;
1012

@@ -30,22 +32,22 @@ public VisibleChunkCollector(int frame) {
3032
}
3133

3234
@Override
33-
public void visit(RenderSection section, boolean visible) {
34-
RenderRegion region = section.getRegion();
35-
ChunkRenderList renderList = region.getRenderList();
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();
3640

37-
// Even if a section does not have render objects, we must ensure the render list is initialized and put
38-
// into the sorted queue of lists, so that we maintain the correct order of draw calls.
39-
if (renderList.getLastVisibleFrame() != this.frame) {
40-
renderList.reset(this.frame);
41+
if (renderList.getLastVisibleFrame() != this.frame) {
42+
renderList.reset(this.frame);
4143

42-
this.sortedRenderLists.add(renderList);
43-
}
44+
this.sortedRenderLists.add(renderList);
45+
}
4446

45-
if (visible && section.getFlags() != 0) {
4647
renderList.add(section);
4748
}
4849

50+
// always add to rebuild lists though, because it might just not be built yet
4951
this.addToRebuildLists(section);
5052
}
5153

@@ -61,8 +63,42 @@ private void addToRebuildLists(RenderSection section) {
6163
}
6264
}
6365

64-
public SortedRenderLists createRenderLists() {
65-
return new SortedRenderLists(this.sortedRenderLists);
66+
private static int[] sortItems = new int[RenderRegion.REGION_SIZE];
67+
68+
public SortedRenderLists createRenderLists(Viewport viewport) {
69+
// sort the regions by distance to fix rare region ordering bugs
70+
var transform = viewport.getTransform();
71+
var cameraX = transform.intX >> (4 + RenderRegion.REGION_WIDTH_SH);
72+
var cameraY = transform.intY >> (4 + RenderRegion.REGION_HEIGHT_SH);
73+
var cameraZ = transform.intZ >> (4 + RenderRegion.REGION_LENGTH_SH);
74+
var size = this.sortedRenderLists.size();
75+
76+
if (sortItems.length < size) {
77+
sortItems = new int[size];
78+
}
79+
80+
for (var i = 0; i < size; i++) {
81+
var region = this.sortedRenderLists.get(i).getRegion();
82+
var x = Math.abs(region.getX() - cameraX);
83+
var y = Math.abs(region.getY() - cameraY);
84+
var z = Math.abs(region.getZ() - cameraZ);
85+
sortItems[i] = (x + y + z) << 16 | i;
86+
}
87+
88+
IntArrays.unstableSort(sortItems, 0, size);
89+
90+
var sorted = new ObjectArrayList<ChunkRenderList>(size);
91+
for (var i = 0; i < size; i++) {
92+
var key = sortItems[i];
93+
var renderList = this.sortedRenderLists.get(key & 0xFFFF);
94+
sorted.add(renderList);
95+
}
96+
97+
for (var list : sorted) {
98+
list.sortSections(transform, sortItems);
99+
}
100+
101+
return new SortedRenderLists(sorted);
66102
}
67103

68104
public Map<ChunkUpdateType, ArrayDeque<RenderSection>> getRebuildLists() {

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,12 @@ private static void processQueue(Visitor visitor,
5050
RenderSection section;
5151

5252
while ((section = readQueue.dequeue()) != null) {
53-
boolean visible = isSectionVisible(section, viewport, searchDistance);
54-
visitor.visit(section, visible);
55-
56-
if (!visible) {
53+
if (!isSectionVisible(section, viewport, searchDistance)) {
5754
continue;
5855
}
5956

57+
visitor.visit(section);
58+
6059
int connections;
6160

6261
{
@@ -249,7 +248,7 @@ private void initWithinWorld(Visitor visitor, WriteQueue<RenderSection> queue, V
249248
section.setLastVisibleFrame(frame);
250249
section.setIncomingDirections(GraphDirectionSet.NONE);
251250

252-
visitor.visit(section, true);
251+
visitor.visit(section);
253252

254253
int outgoing;
255254

@@ -335,6 +334,6 @@ private RenderSection getRenderSection(int x, int y, int z) {
335334
}
336335

337336
public interface Visitor {
338-
void visit(RenderSection section, boolean visible);
337+
void visit(RenderSection section);
339338
}
340339
}

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

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ public class RenderRegion {
2424
public static final int REGION_HEIGHT = 4;
2525
public static final int REGION_LENGTH = 8;
2626

27-
private static final int REGION_WIDTH_M = RenderRegion.REGION_WIDTH - 1;
28-
private static final int REGION_HEIGHT_M = RenderRegion.REGION_HEIGHT - 1;
29-
private static final int REGION_LENGTH_M = RenderRegion.REGION_LENGTH - 1;
27+
public static final int REGION_WIDTH_M = RenderRegion.REGION_WIDTH - 1;
28+
public static final int REGION_HEIGHT_M = RenderRegion.REGION_HEIGHT - 1;
29+
public static final int REGION_LENGTH_M = RenderRegion.REGION_LENGTH - 1;
3030

31-
protected static final int REGION_WIDTH_SH = Integer.bitCount(REGION_WIDTH_M);
32-
protected static final int REGION_HEIGHT_SH = Integer.bitCount(REGION_HEIGHT_M);
33-
protected static final int REGION_LENGTH_SH = Integer.bitCount(REGION_LENGTH_M);
31+
public static final int REGION_WIDTH_SH = Integer.bitCount(REGION_WIDTH_M);
32+
public static final int REGION_HEIGHT_SH = Integer.bitCount(REGION_HEIGHT_M);
33+
public static final int REGION_LENGTH_SH = Integer.bitCount(REGION_LENGTH_M);
3434

3535
public static final int REGION_SIZE = REGION_WIDTH * REGION_HEIGHT * REGION_LENGTH;
3636

@@ -64,6 +64,18 @@ public static long key(int x, int y, int z) {
6464
return SectionPos.asLong(x, y, z);
6565
}
6666

67+
public int getX() {
68+
return this.x;
69+
}
70+
71+
public int getY() {
72+
return this.y;
73+
}
74+
75+
public int getZ() {
76+
return this.z;
77+
}
78+
6779
public int getChunkX() {
6880
return this.x << REGION_WIDTH_SH;
6981
}

0 commit comments

Comments
 (0)