Skip to content

Commit

Permalink
Sort render lists for regions and sections after traversal (#2780)
Browse files Browse the repository at this point in the history
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 #2266
  • Loading branch information
douira authored Nov 24, 2024
1 parent 5e7c8ac commit 03aa768
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ public RenderSection(RenderRegion region, int chunkX, int chunkY, int chunkZ) {
this.chunkY = chunkY;
this.chunkZ = chunkZ;

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ private void createTerrainRenderList(Camera camera, Viewport viewport, int frame

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

this.renderLists = visitor.createRenderLists();
this.renderLists = visitor.createRenderLists(viewport);
this.taskLists = visitor.getRebuildLists();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package net.caffeinemc.mods.sodium.client.render.chunk.lists;

import net.caffeinemc.mods.sodium.client.render.chunk.LocalSectionIndex;
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags;
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform;
import net.caffeinemc.mods.sodium.client.util.iterator.ByteArrayIterator;
import net.caffeinemc.mods.sodium.client.util.iterator.ByteIterator;
import net.caffeinemc.mods.sodium.client.util.iterator.ReversibleByteArrayIterator;
import net.caffeinemc.mods.sodium.client.util.iterator.ByteArrayIterator;
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
import net.minecraft.util.Mth;
import org.jetbrains.annotations.Nullable;

public class ChunkRenderList {
Expand Down Expand Up @@ -37,6 +40,39 @@ public void reset(int frame) {
this.lastVisibleFrame = frame;
}

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

public void sortSections(CameraTransform transform, int[] sortItems) {
var cameraX = Mth.clamp((transform.intX >> 4) - this.region.getChunkX(), 0, RenderRegion.REGION_WIDTH - 1);
var cameraY = Mth.clamp((transform.intY >> 4) - this.region.getChunkY(), 0, RenderRegion.REGION_HEIGHT - 1);
var cameraZ = Mth.clamp((transform.intZ >> 4) - this.region.getChunkZ(), 0, RenderRegion.REGION_LENGTH - 1);

int[] histogram = new int[SORTING_HISTOGRAM_SIZE];

for (int i = 0; i < this.sectionsWithGeometryCount; i++) {
var index = this.sectionsWithGeometry[i] & 0xFF; // makes sure the byte -> int conversion is unsigned
var x = Math.abs(LocalSectionIndex.unpackX(index) - cameraX);
var y = Math.abs(LocalSectionIndex.unpackY(index) - cameraY);
var z = Math.abs(LocalSectionIndex.unpackZ(index) - cameraZ);

var distance = x + y + z;
histogram[distance]++;
sortItems[i] = distance << 8 | index;
}

// prefix sum to calculate indexes
for (int i = 1; i < SORTING_HISTOGRAM_SIZE; i++) {
histogram[i] += histogram[i - 1];
}

for (int i = 0; i < this.sectionsWithGeometryCount; i++) {
var item = sortItems[i];
var distance = item >>> 8;
this.sectionsWithGeometry[--histogram[distance]] = (byte) item;
}
}

public void add(RenderSection render) {
if (this.size >= RenderRegion.REGION_SIZE) {
throw new ArrayIndexOutOfBoundsException("Render list is full");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package net.caffeinemc.mods.sodium.client.render.chunk.lists;

import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
import net.caffeinemc.mods.sodium.client.util.iterator.ReversibleObjectArrayIterator;
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;

/**
* Stores one render list of sections per region, sorted by the order in which
Expand All @@ -27,44 +25,4 @@ public ReversibleObjectArrayIterator<ChunkRenderList> iterator(boolean reverse)
public static SortedRenderLists empty() {
return EMPTY;
}

public static class Builder {
private final ObjectArrayList<ChunkRenderList> lists = new ObjectArrayList<>();
private final int frame;

public Builder(int frame) {
this.frame = frame;
}

public void add(RenderSection section) {
RenderRegion region = section.getRegion();
ChunkRenderList list = region.getRenderList();

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

this.lists.add(list);
}

// Only add the section to the render list if it actually contains render objects
if (section.getFlags() != 0) {
list.add(section);
}
}

public SortedRenderLists build() {
var filtered = new ObjectArrayList<ChunkRenderList>(this.lists.size());

// Filter any empty render lists
for (var list : this.lists) {
if (list.size() > 0) {
filtered.add(list);
}
}

return new SortedRenderLists(filtered);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package net.caffeinemc.mods.sodium.client.render.chunk.lists;

import it.unimi.dsi.fastutil.ints.IntArrays;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateType;
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller;
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion;
import net.caffeinemc.mods.sodium.client.render.viewport.Viewport;

import java.util.*;

Expand All @@ -30,22 +32,22 @@ public VisibleChunkCollector(int frame) {
}

@Override
public void visit(RenderSection section, boolean visible) {
RenderRegion region = section.getRegion();
ChunkRenderList renderList = region.getRenderList();
public void visit(RenderSection section) {
// only process section (and associated render list) if it has content that needs rendering
if (section.getFlags() != 0) {
RenderRegion region = section.getRegion();
ChunkRenderList renderList = region.getRenderList();

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

this.sortedRenderLists.add(renderList);
}
this.sortedRenderLists.add(renderList);
}

if (visible && section.getFlags() != 0) {
renderList.add(section);
}

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

Expand All @@ -61,8 +63,42 @@ private void addToRebuildLists(RenderSection section) {
}
}

public SortedRenderLists createRenderLists() {
return new SortedRenderLists(this.sortedRenderLists);
private static int[] sortItems = new int[RenderRegion.REGION_SIZE];

public SortedRenderLists createRenderLists(Viewport viewport) {
// sort the regions by distance to fix rare region ordering bugs
var transform = viewport.getTransform();
var cameraX = transform.intX >> (4 + RenderRegion.REGION_WIDTH_SH);
var cameraY = transform.intY >> (4 + RenderRegion.REGION_HEIGHT_SH);
var cameraZ = transform.intZ >> (4 + RenderRegion.REGION_LENGTH_SH);
var size = this.sortedRenderLists.size();

if (sortItems.length < size) {
sortItems = new int[size];
}

for (var i = 0; i < size; i++) {
var region = this.sortedRenderLists.get(i).getRegion();
var x = Math.abs(region.getX() - cameraX);
var y = Math.abs(region.getY() - cameraY);
var z = Math.abs(region.getZ() - cameraZ);
sortItems[i] = (x + y + z) << 16 | i;
}

IntArrays.unstableSort(sortItems, 0, size);

var sorted = new ObjectArrayList<ChunkRenderList>(size);
for (var i = 0; i < size; i++) {
var key = sortItems[i];
var renderList = this.sortedRenderLists.get(key & 0xFFFF);
sorted.add(renderList);
}

for (var list : sorted) {
list.sortSections(transform, sortItems);
}

return new SortedRenderLists(sorted);
}

public Map<ChunkUpdateType, ArrayDeque<RenderSection>> getRebuildLists() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@ private static void processQueue(Visitor visitor,
RenderSection section;

while ((section = readQueue.dequeue()) != null) {
boolean visible = isSectionVisible(section, viewport, searchDistance);
visitor.visit(section, visible);

if (!visible) {
if (!isSectionVisible(section, viewport, searchDistance)) {
continue;
}

visitor.visit(section);

int connections;

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

visitor.visit(section, true);
visitor.visit(section);

int outgoing;

Expand Down Expand Up @@ -335,6 +334,6 @@ private RenderSection getRenderSection(int x, int y, int z) {
}

public interface Visitor {
void visit(RenderSection section, boolean visible);
void visit(RenderSection section);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ public class RenderRegion {
public static final int REGION_HEIGHT = 4;
public static final int REGION_LENGTH = 8;

private static final int REGION_WIDTH_M = RenderRegion.REGION_WIDTH - 1;
private static final int REGION_HEIGHT_M = RenderRegion.REGION_HEIGHT - 1;
private static final int REGION_LENGTH_M = RenderRegion.REGION_LENGTH - 1;
public static final int REGION_WIDTH_M = RenderRegion.REGION_WIDTH - 1;
public static final int REGION_HEIGHT_M = RenderRegion.REGION_HEIGHT - 1;
public static final int REGION_LENGTH_M = RenderRegion.REGION_LENGTH - 1;

protected static final int REGION_WIDTH_SH = Integer.bitCount(REGION_WIDTH_M);
protected static final int REGION_HEIGHT_SH = Integer.bitCount(REGION_HEIGHT_M);
protected static final int REGION_LENGTH_SH = Integer.bitCount(REGION_LENGTH_M);
public static final int REGION_WIDTH_SH = Integer.bitCount(REGION_WIDTH_M);
public static final int REGION_HEIGHT_SH = Integer.bitCount(REGION_HEIGHT_M);
public static final int REGION_LENGTH_SH = Integer.bitCount(REGION_LENGTH_M);

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

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

public int getX() {
return this.x;
}

public int getY() {
return this.y;
}

public int getZ() {
return this.z;
}

public int getChunkX() {
return this.x << REGION_WIDTH_SH;
}
Expand Down

0 comments on commit 03aa768

Please sign in to comment.