Skip to content

Commit

Permalink
add removable tree and forest system for supporting the out-of-graph …
Browse files Browse the repository at this point in the history
…fallback and for use in iris. Now that there's a fallback, we don't need to accept using invalid trees if there's no other trees available.

fix tree isValid test for wide trees.
rename the trees field on RSM since it's misleading (it's actually forests, and they're cull results)
  • Loading branch information
douira committed Dec 27, 2024
1 parent 56c38ad commit 48ff25a
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ public void setupTerrain(Camera camera,
}

private void processChunkEvents() {
this.renderSectionManager.beforeSectionUpdates();
var tracker = ChunkTrackerHolder.get(this.level);
tracker.forEachEvent(this.renderSectionManager::onChunkAdded, this.renderSectionManager::onChunkRemoved);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData;
import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.CameraMovement;
import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.trigger.SortTriggering;
import net.caffeinemc.mods.sodium.client.render.chunk.tree.RemovableMultiForest;
import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkMeshFormats;
import net.caffeinemc.mods.sodium.client.render.texture.SpriteUtil;
import net.caffeinemc.mods.sodium.client.render.util.RenderAsserts;
Expand Down Expand Up @@ -115,7 +116,8 @@ public class RenderSectionManager {
private final ObjectArrayList<AsyncRenderTask<?>> pendingTasks = new ObjectArrayList<>();
private SectionTree renderTree = null;
private TaskSectionTree globalTaskTree = null;
private final Map<CullType, SectionTree> trees = new EnumMap<>(CullType.class);
private final Map<CullType, SectionTree> cullResults = new EnumMap<>(CullType.class);
private final RemovableMultiForest renderableSectionTree;

private final AsyncCameraTimingControl cameraTimingControl = new AsyncCameraTimingControl();

Expand All @@ -134,6 +136,8 @@ public RenderSectionManager(ClientLevel level, int renderDistance, CommandList c

this.renderLists = SortedRenderLists.empty();
this.occlusionCuller = new OcclusionCuller(Long2ReferenceMaps.unmodifiable(this.sectionByPosition), this.level);

this.renderableSectionTree = new RemovableMultiForest(renderDistance);
}

public void updateCameraState(Vector3dc cameraPosition, Camera camera) {
Expand All @@ -148,8 +152,8 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato
if (this.averageFrameDuration == -1) {
this.averageFrameDuration = this.lastFrameDuration;
} else {
this.averageFrameDuration = (long)(this.lastFrameDuration * AVERAGE_FRAME_DURATION_FACTOR) +
(long)(this.averageFrameDuration * (1 - AVERAGE_FRAME_DURATION_FACTOR));
this.averageFrameDuration = (long) (this.lastFrameDuration * AVERAGE_FRAME_DURATION_FACTOR) +
(long) (this.averageFrameDuration * (1 - AVERAGE_FRAME_DURATION_FACTOR));
}
this.averageFrameDuration = Mth.clamp(this.averageFrameDuration, 1_000_100, 100_000_000);

Expand All @@ -170,7 +174,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato

// discard unusable present and pending frustum-tested trees
if (this.cameraChanged) {
this.trees.remove(CullType.FRUSTUM);
this.cullResults.remove(CullType.FRUSTUM);

this.pendingTasks.removeIf(task -> {
if (task instanceof CullTask<?> cullTask && cullTask.getCullType() == CullType.FRUSTUM) {
Expand All @@ -184,7 +188,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato
// remove all tasks that aren't in progress yet
this.pendingTasks.removeIf(AsyncRenderTask::cancelIfNotStarted);

this.unpackTaskResults(false);
this.unpackTaskResults(null);

this.scheduleAsyncWork(camera, viewport, spectator);

Expand Down Expand Up @@ -218,27 +222,28 @@ private void renderSync(Camera camera, Viewport viewport, boolean spectator) {

this.frustumTaskLists = tree.getPendingTaskLists();
this.globalTaskLists = null;
this.trees.put(CullType.FRUSTUM, tree);
this.cullResults.put(CullType.FRUSTUM, tree);
this.renderTree = tree;

this.renderLists = tree.createRenderLists(viewport);

// remove the other trees, they're very wrong by now
this.trees.remove(CullType.WIDE);
this.trees.remove(CullType.REGULAR);
this.cullResults.remove(CullType.WIDE);
this.cullResults.remove(CullType.REGULAR);

this.needsRenderListUpdate = false;
this.needsGraphUpdate = false;
this.cameraChanged = false;
}

private SectionTree unpackTaskResults(boolean wait) {
private SectionTree unpackTaskResults(Viewport waitingViewport) {
SectionTree latestTree = null;
CullType latestTreeCullType = null;

var it = this.pendingTasks.iterator();
while (it.hasNext()) {
var task = it.next();
if (!wait && !task.isDone()) {
if (waitingViewport == null && !task.isDone()) {
continue;
}
it.remove();
Expand All @@ -252,8 +257,9 @@ private SectionTree unpackTaskResults(boolean wait) {
// ensure no useless frustum tree is accepted
if (!this.cameraChanged) {
var tree = result.getTree();
this.trees.put(CullType.FRUSTUM, tree);
this.cullResults.put(CullType.FRUSTUM, tree);
latestTree = tree;
latestTreeCullType = CullType.FRUSTUM;

this.needsRenderListUpdate = true;
}
Expand All @@ -264,8 +270,10 @@ private SectionTree unpackTaskResults(boolean wait) {
this.globalTaskLists = result.getGlobalTaskLists();
this.frustumTaskLists = result.getFrustumTaskLists();
this.globalTaskTree = tree;
this.trees.put(globalCullTask.getCullType(), tree);
var cullType = globalCullTask.getCullType();
this.cullResults.put(cullType, tree);
latestTree = tree;
latestTreeCullType = cullType;

this.needsRenderListUpdate = true;
}
Expand All @@ -276,7 +284,13 @@ private SectionTree unpackTaskResults(boolean wait) {
}
}

return latestTree;
if (waitingViewport != null && latestTree != null) {
var searchDistance = this.getSearchDistanceForCullType(latestTreeCullType);
if (latestTree.isValidFor(waitingViewport, searchDistance)) {
return latestTree;
}
}
return null;
}

private static Thread makeAsyncCullThread(Runnable runnable) {
Expand All @@ -286,6 +300,11 @@ private static Thread makeAsyncCullThread(Runnable runnable) {
}

private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectator) {
// if the origin section doesn't exist, cull tasks won't produce any useful results
if (!this.occlusionCuller.graphOriginPresent(viewport)) {
return;
}

// submit tasks of types that are applicable and not yet running
AsyncRenderTask<?> currentRunningTask = null;
if (!this.pendingTasks.isEmpty()) {
Expand All @@ -296,7 +315,7 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat
var scheduleOrder = getScheduleOrder();

for (var type : scheduleOrder) {
var tree = this.trees.get(type);
var tree = this.cullResults.get(type);

// don't schedule frustum tasks if the camera just changed to prevent throwing them away constantly
// since they're going to be invalid by the time they're completed in the next frame
Expand Down Expand Up @@ -369,47 +388,59 @@ private void updateFrustumTaskList(Viewport viewport) {
}

private void processRenderListUpdate(Viewport viewport) {
// pick the narrowest up-to-date tree, if this tree is insufficiently up to date we would've switched to sync bfs earlier
// pick the narrowest valid tree. This tree is either up-to-date or the origin is out of the graph as otherwise sync bfs would have been triggered (in graph but moving rapidly)
SectionTree bestTree = null;
boolean bestTreeValid = false;
for (var type : NARROW_TO_WIDE) {
var tree = this.trees.get(type);
var tree = this.cullResults.get(type);
if (tree == null) {
continue;
}

// pick the most recent and most valid tree
float searchDistance = this.getSearchDistanceForCullType(type);
var treeIsValid = tree.isValidFor(viewport, searchDistance);
if (bestTree == null || tree.getFrame() > bestTree.getFrame() || !bestTreeValid && treeIsValid) {
if (!tree.isValidFor(viewport, searchDistance)) {
continue;
}
if (bestTree == null || tree.getFrame() > bestTree.getFrame()) {
bestTree = tree;
bestTreeValid = treeIsValid;
}
}

// wait for pending tasks if there's no current tree (first frames after initial load/reload)
// wait for pending tasks to maybe supply a valid tree if there's no current tree (first frames after initial load/reload)
if (bestTree == null) {
bestTree = this.unpackTaskResults(viewport);
}

// use out-of-graph fallback there's still no result because nothing was scheduled (missing origin section, empty world)
if (bestTree == null) {
bestTree = this.unpackTaskResults(true);
var searchDistance = this.getSearchDistance();
var visitor = new FallbackVisibleChunkCollector(viewport, searchDistance, this.sectionByPosition, this.regions, this.frame);

if (bestTree == null) {
throw new IllegalStateException("Unpacked tree was not valid but a tree is required to render.");
this.renderableSectionTree.prepareForTraversal();
this.renderableSectionTree.traverse(visitor, viewport, searchDistance);

this.renderLists = visitor.createRenderLists();
this.frustumTaskLists = visitor.getPendingTaskLists();
this.globalTaskLists = null;
this.renderTree = null;
} else {
var start = System.nanoTime();

var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame);
bestTree.traverse(visibleCollector, viewport, this.getSearchDistance());
this.renderLists = visibleCollector.createRenderLists();

var end = System.nanoTime();
var time = end - start;
timings.add(time);
if (timings.size() >= 500) {
var average = timings.longStream().average().orElse(0);
System.out.println("Render list generation took " + (average) / 1000 + "µs over " + timings.size() + " samples");
timings.clear();
}
}

var start = System.nanoTime();
var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame);
bestTree.traverse(visibleCollector, viewport, this.getSearchDistance());
this.renderLists = visibleCollector.createRenderLists();
var end = System.nanoTime();
var time = end - start;
timings.add(time);
if (timings.size() >= 500) {
var average = timings.longStream().average().orElse(0);
System.out.println("Render list generation took " + (average) / 1000 + "µs over " + timings.size() + " samples");
timings.clear();
this.renderTree = bestTree;
}

this.renderTree = bestTree;
}

public void markGraphDirty() {
Expand Down Expand Up @@ -457,6 +488,10 @@ private boolean shouldUseOcclusionCulling(Camera camera, boolean spectator) {
return useOcclusionCulling;
}

public void beforeSectionUpdates() {
this.renderableSectionTree.ensureCapacity(this.getRenderDistance());
}

public void onSectionAdded(int x, int y, int z) {
long key = SectionPos.asLong(x, y, z);

Expand All @@ -477,6 +512,7 @@ public void onSectionAdded(int x, int y, int z) {
if (section.hasOnlyAir()) {
this.updateSectionInfo(renderSection, BuiltSectionInfo.EMPTY);
} else {
this.renderableSectionTree.add(renderSection);
renderSection.setPendingUpdate(ChunkUpdateType.INITIAL_BUILD, this.lastFrameAtTime);
}

Expand All @@ -494,6 +530,8 @@ public void onSectionRemoved(int x, int y, int z) {
return;
}

this.renderableSectionTree.remove(x, y, z);

if (section.getTranslucentData() != null) {
this.sortTriggering.removeSection(section.getTranslucentData(), sectionPos);
}
Expand Down Expand Up @@ -550,6 +588,7 @@ public void tickVisibleRenders() {
}

public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) {
// TODO: this isn't actually frustum tested? Should it be? Is the original method we're replacing here frustum-tested?
return this.renderTree == null || this.renderTree.isBoxVisible(x1, y1, z1, x2, y2, z2);
}

Expand Down Expand Up @@ -617,6 +656,12 @@ private boolean processChunkBuildResults(ArrayList<BuilderTaskOutput> results) {
}

private boolean updateSectionInfo(RenderSection render, BuiltSectionInfo info) {
if (info == null || (info.flags & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) {
this.renderableSectionTree.remove(render);
} else {
this.renderableSectionTree.add(render);
}

var infoChanged = render.setInfo(info);

if (info == null || ArrayUtils.isEmpty(info.globalBlockEntities)) {
Expand Down Expand Up @@ -1049,7 +1094,7 @@ public Collection<String> getDebugStrings() {
list.add(String.format("Transfer Queue: %s", this.regions.getStagingBuffer().toString()));

list.add(String.format("Chunk Builder: Schd=%02d | Busy=%02d (%04d%%) | Total=%02d",
this.builder.getScheduledJobCount(), this.builder.getBusyThreadCount(), (int)(this.builder.getBusyFraction(this.lastFrameDuration) * 100), this.builder.getTotalThreadCount())
this.builder.getScheduledJobCount(), this.builder.getBusyThreadCount(), (int) (this.builder.getBusyFraction(this.lastFrameDuration) * 100), this.builder.getTotalThreadCount())
);

list.add(String.format("Tasks: N0=%03d | N1=%03d | Def=%03d, Recv=%03d",
Expand Down Expand Up @@ -1086,7 +1131,7 @@ public String getChunksDebugString() {
private String getCullTypeName() {
CullType renderTreeCullType = null;
for (var type : CullType.values()) {
if (this.trees.get(type) == this.renderTree) {
if (this.cullResults.get(type) == this.renderTree) {
renderTreeCullType = type;
break;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package net.caffeinemc.mods.sodium.client.render.chunk.lists;

import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection;
import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager;
import net.caffeinemc.mods.sodium.client.render.viewport.Viewport;

public class FallbackVisibleChunkCollector extends FrustumTaskCollector {
private final VisibleChunkCollectorAsync renderListCollector;

public FallbackVisibleChunkCollector(Viewport viewport, float buildDistance, Long2ReferenceMap<RenderSection> sectionByPosition, RenderRegionManager regions, int frame) {
super(viewport, buildDistance, sectionByPosition);
this.renderListCollector = new VisibleChunkCollectorAsync(regions, frame);
}

public SortedRenderLists createRenderLists() {
return this.renderListCollector.createRenderLists();
}

@Override
public void visit(int x, int y, int z) {
super.visit(x, y, z);
this.renderListCollector.visit(x, y, z);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,14 @@ private void addNearbySections(GraphOcclusionVisitor visitor, Viewport viewport)
}
}

public boolean graphOriginPresent(Viewport viewport) {
var origin = viewport.getChunkCoord();
var originY = origin.getY();
return originY < this.level.getMinSectionY() ||
originY > this.level.getMaxSectionY() ||
this.sections.get(viewport.getChunkCoord().asLong()) != null;
}

private void init(WriteQueue<RenderSection> queue)
{
var origin = this.viewport.getChunkCoord();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,6 @@
import net.minecraft.core.SectionPos;
import net.minecraft.world.level.Level;

/*
* - make another tree similar to this one that is used to track invalidation cubes in the bfs to make it possible to reuse some of its results (?)
* - make another tree that that is filled with all bfs-visited sections to do ray-cast culling during traversal. This is fast if we can just check for certain bits in the tree instead of stepping through many sections. If the top node is 1, that means a ray might be able to get through, traverse further in that case. If it's 0, that means it's definitely blocked since we haven't visited sections that it might go through, but since bfs goes outwards, no such sections will be added later. Delete this auxiliary tree after traversal. Would need to check the projection of the entire section to the camera (potentially irregular hexagonal frustum, or just check each of the at most six visible corners.) Do a single traversal where each time the node is checked against all participating rays/visibility shapes. Alternatively, check a cylinder that encompasses the section's elongation towards the camera plane. (would just require some distance checks, maybe faster?)
* - are incremental bfs updates possible or useful? Since bfs order doesn't matter with the render list being generated from the tree, that might reduce the load on the async cull thread. (essentially just bfs but with the queue initialized to the set of changed sections.) Problem: might result in more sections being visible than intended, since sections aren't removed when another bfs is run starting from updated sections.
*/
public class SectionTree extends PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor {
private final TraversableForest tree;

Expand Down Expand Up @@ -43,9 +38,9 @@ public int getFrame() {

public boolean isValidFor(Viewport viewport, float searchDistance) {
var cameraPos = viewport.getChunkCoord();
return this.cameraX >> 4 == cameraPos.getX() &&
this.cameraY >> 4 == cameraPos.getY() &&
this.cameraZ >> 4 == cameraPos.getZ() &&
return Math.abs((this.cameraX >> 4) - cameraPos.getX()) <= this.bfsWidth &&
Math.abs((this.cameraY >> 4) - cameraPos.getY()) <= this.bfsWidth &&
Math.abs((this.cameraZ >> 4) - cameraPos.getZ()) <= this.bfsWidth &&
this.buildDistance >= searchDistance;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package net.caffeinemc.mods.sodium.client.render.chunk.tree;

public interface RemovableForest extends TraversableForest {
void remove(int x, int y, int z);
}
Loading

0 comments on commit 48ff25a

Please sign in to comment.