From caca14a8f9ffcdf06b88b7e0f2bc5dfab3e4b74f Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 29 Aug 2024 05:51:31 +0200 Subject: [PATCH 01/81] documentation cleanup --- .../mods/sodium/client/util/collections/BitArray.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java index f018b18e6c..ed7b08edd4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/collections/BitArray.java @@ -5,7 +5,7 @@ import java.util.Arrays; /** - * Originally authored here: https://github.com/CaffeineMC/sodium/blob/ddfb9f21a54bfb30aa876678204371e94d8001db/src/main/java/net/caffeinemc/sodium/util/collections/BitArray.java + * Originally authored here * @author burgerindividual */ public class BitArray { From 9393180821be8955f783bef81584c505873aae9c Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 29 Aug 2024 05:52:35 +0200 Subject: [PATCH 02/81] do frustum cull using a linear octree, WIP (BFS isn't actually decoupled yet, it just does the tree frustum test right after each bfs for testing purposes atm) --- .../LinearOctreeSectionCollector.java | 194 ++++++++++++++++++ .../chunk/occlusion/OcclusionCuller.java | 28 ++- .../chunk/occlusion/TreeSectionCollector.java | 15 ++ 3 files changed, 226 insertions(+), 11 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/TreeSectionCollector.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java new file mode 100644 index 0000000000..8f7a42b49f --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java @@ -0,0 +1,194 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; + +class LinearOctreeSectionCollector extends TreeSectionCollector { + private final Long2ReferenceMap sections; + final Tree mainTree; + Tree secondaryTree; + final int baseOffsetX, baseOffsetY, baseOffsetZ; + + LinearOctreeSectionCollector(Long2ReferenceMap sections, Viewport viewport, float searchDistance) { + this.sections = sections; + + var transform = viewport.getTransform(); + int offsetDistance = Mth.floor(searchDistance / 16.0f); + this.baseOffsetX = (transform.intX >> 4) - offsetDistance - 1; + this.baseOffsetY = (transform.intY >> 4) - offsetDistance - 1; + this.baseOffsetZ = (transform.intZ >> 4) - offsetDistance - 1; + + this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + } + + @Override + void add(RenderSection section) { + int x = section.getChunkX(); + int y = section.getChunkY(); + int z = section.getChunkZ(); + + if (this.mainTree.add(x, y, z)) { + if (this.secondaryTree == null) { + // offset diagonally to fully encompass the required area + this.secondaryTree = new Tree(this.baseOffsetX + 4, this.baseOffsetY, this.baseOffsetZ + 4); + } + if (this.secondaryTree.add(x, y, z)) { + throw new IllegalStateException("Failed to add section to secondary tree"); + } + } + } + + @Override + void traverseVisible(OcclusionCuller.Visitor visitor, Viewport viewport) { + this.mainTree.traverse(visitor, viewport); + if (this.secondaryTree != null) { + this.secondaryTree.traverse(visitor, viewport); + } + } + + private class Tree { + private final long[] tree = new long[64 * 64]; + private final long[] treeReduced = new long[64]; + private long treeDoubleReduced = 0L; + final int offsetX, offsetY, offsetZ; + + Tree(int offsetX, int offsetY, int offsetZ) { + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + } + + boolean add(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (x > 63 || y > 63 || z > 63 || x < 0 || y < 0 || z < 0) { + return true; + } + + var bitIndex = interleave6x3(x, y, z); + int reducedBitIndex = bitIndex >> 6; + int doubleReducedBitIndex = bitIndex >> 12; + this.tree[reducedBitIndex] |= 1L << (bitIndex & 0b111111); + this.treeReduced[doubleReducedBitIndex] |= 1L << (reducedBitIndex & 0b111111); + this.treeDoubleReduced |= 1L << doubleReducedBitIndex; + + return false; + } + + + private static int interleave6x3(int x, int y, int z) { + return interleave6(x) | interleave6(y) << 1 | interleave6(z) << 2; + } + + private static int interleave6(int n) { + n &= 0b000000000000111111; + n = (n | n << 8) & 0b000011000000001111; + n = (n | n << 4) & 0b000011000011000011; + n = (n | n << 2) & 0b001001001001001001; + return n; + } + + private static int deinterleave6(int n) { + n &= 0b001001001001001001; + n = (n | n >> 2) & 0b000011000011000011; + n = (n | n >> 4) & 0b000011000000001111; + n = (n | n >> 8) & 0b000000000000111111; + return n; + } + + void traverse(OcclusionCuller.Visitor visitor, Viewport viewport) { + this.traverse(visitor, viewport, 0, 5); + } + + void traverse(OcclusionCuller.Visitor visitor, Viewport viewport, int nodeOrigin, int level) { + int childDim = 1 << (level + 3); // * 16 / 2 + + if (level <= 1) { + // check using the full bitmap + int bitStep = 1 << (level * 3); + long mask = (1L << bitStep) - 1; + int startBit = nodeOrigin & 0b111111; + int endBit = startBit + (bitStep << 3); + int childOriginBase = nodeOrigin & 0b111111_111111_000000; + long map = this.tree[nodeOrigin >> 6]; + + if (level == 0) { + for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { + if ((map & (mask << bitIndex)) != 0) { + int sectionOrigin = childOriginBase | bitIndex; + int x = deinterleave6(sectionOrigin) + this.offsetX; + int y = deinterleave6(sectionOrigin >> 1) + this.offsetY; + int z = deinterleave6(sectionOrigin >> 2) + this.offsetZ; + if (testNode(viewport, x, y, z, childDim)) { + // TODO: profile if it's faster to do a hashmap lookup to get the region + // and then use the region's array to get the render section + // TODO: also profile if it's worth it to store an array of all render sections + // for each node to accelerate "fully inside frustum" situations, + // otherwise also optimize "fully inside" situations with a different traversal type + // NOTE: such an array would need to be traversed in the correct front-to-back order + // for this the sections should be in it in x, y, z order and then front-to-back iteration is easy + var section = LinearOctreeSectionCollector.this.sections.get(SectionPos.asLong(x, y, z)); + visitor.visit(section, true); + } + } + } + } else { + for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { + if ((map & (mask << bitIndex)) != 0) { + this.testChild(visitor, viewport, childOriginBase | bitIndex, childDim, level); + } + } + } + } else if (level <= 3) { + // check using the single reduced bitmap + int bitStep = 1 << (level * 3 - 6); + long mask = (1L << bitStep) - 1; + int startBit = (nodeOrigin >> 6) & 0b111111; + int endBit = startBit + (bitStep << 3); + int childOriginBase = nodeOrigin & 0b111111_000000_000000; + long map = this.treeReduced[nodeOrigin >> 12]; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { + if ((map & (mask << bitIndex)) != 0) { + this.testChild(visitor, viewport, childOriginBase | (bitIndex << 6), childDim, level); + } + } + } else { + // check using the double reduced bitmap + int bitStep = 1 << (level * 3 - 12); + long mask = (1L << bitStep) - 1; + int startBit = nodeOrigin >> 12; + int endBit = startBit + (bitStep << 3); + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { + if ((this.treeDoubleReduced & (mask << bitIndex)) != 0) { + this.testChild(visitor, viewport, bitIndex << 12, childDim, level); + } + } + } + } + + void testChild(OcclusionCuller.Visitor visitor, Viewport viewport, int childOrigin, int childDim, int level) { + int x = deinterleave6(childOrigin) + this.offsetX; + int y = deinterleave6(childOrigin >> 1) + this.offsetY; + int z = deinterleave6(childOrigin >> 2) + this.offsetZ; + if (testNode(viewport, x, y, z, childDim)) { + this.traverse(visitor, viewport, childOrigin, level - 1); + } + } + + static boolean testNode(Viewport viewport, int x, int y, int z, int childDim) { + return viewport.isBoxVisible( + (x << 4) + childDim, + (y << 4) + childDim, + (z << 4) + childDim, + childDim + OcclusionCuller.CHUNK_SECTION_SIZE, + childDim + OcclusionCuller.CHUNK_SECTION_SIZE, + childDim + OcclusionCuller.CHUNK_SECTION_SIZE); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index a6550ff8ca..20ce139835 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -18,6 +18,12 @@ public class OcclusionCuller { private final DoubleBufferedQueue queue = new DoubleBufferedQueue<>(); + // The bounding box of a chunk section must be large enough to contain all possible geometry within it. Block models + // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon + // to deal with floating point imprecision during a frustum check (see GH#2132). + static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; + static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + 1.0f /* maximum model extent */ + 0.125f /* epsilon */; + public OcclusionCuller(Long2ReferenceMap sections, Level level) { this.sections = sections; this.level = level; @@ -32,13 +38,16 @@ public void findVisible(Visitor visitor, final var queues = this.queue; queues.reset(); - this.init(visitor, queues.write(), viewport, searchDistance, useOcclusionCulling, frame); + var collector = new LinearOctreeSectionCollector(this.sections, viewport, searchDistance); + this.init(collector, queues.write(), viewport, searchDistance, useOcclusionCulling, frame); while (queues.flip()) { - processQueue(visitor, viewport, searchDistance, useOcclusionCulling, frame, queues.read(), queues.write()); + processQueue(collector, viewport, searchDistance, useOcclusionCulling, frame, queues.read(), queues.write()); } this.addNearbySections(visitor, viewport, searchDistance, frame); + + collector.traverseVisible(visitor, viewport); } private static void processQueue(Visitor visitor, @@ -111,7 +120,8 @@ private static long getAngleVisibilityMask(Viewport viewport, RenderSection sect } private static boolean isSectionVisible(RenderSection section, Viewport viewport, float maxDistance) { - return isWithinRenderDistance(viewport.getTransform(), section, maxDistance) && isWithinFrustum(viewport, section); + // TODO: fix + return isWithinRenderDistance(viewport.getTransform(), section, maxDistance); // && isWithinFrustum(viewport, section); } private static void visitNeighbors(final WriteQueue queue, RenderSection section, int outgoing, int frame) { @@ -207,12 +217,6 @@ private static int nearestToZero(int min, int max) { return clamped; } - // The bounding box of a chunk section must be large enough to contain all possible geometry within it. Block models - // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon - // to deal with floating point imprecision during a frustum check (see GH#2132). - private static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; - private static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + 1.0f /* maximum model extent */ + 0.125f /* epsilon */; - public static boolean isWithinFrustum(Viewport viewport, RenderSection section) { return viewport.isBoxVisible(section.getCenterX(), section.getCenterY(), section.getCenterZ(), CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE); @@ -220,7 +224,7 @@ public static boolean isWithinFrustum(Viewport viewport, RenderSection section) // this bigger chunk section size is only used for frustum-testing nearby sections with large models private static final float CHUNK_SECTION_SIZE_NEARBY = CHUNK_SECTION_RADIUS + 2.0f /* bigger model extent */ + 0.125f /* epsilon */; - + public static boolean isWithinNearbySectionFrustum(Viewport viewport, RenderSection section) { return viewport.isBoxVisible(section.getCenterX(), section.getCenterY(), section.getCenterZ(), CHUNK_SECTION_SIZE_NEARBY, CHUNK_SECTION_SIZE_NEARBY, CHUNK_SECTION_SIZE_NEARBY); @@ -365,7 +369,9 @@ private void initOutsideWorldHeight(WriteQueue queue, private void tryVisitNode(WriteQueue queue, int x, int y, int z, int direction, int frame, Viewport viewport) { RenderSection section = this.getRenderSection(x, y, z); - if (section == null || !isWithinFrustum(viewport, section)) { + // TODO: fix + // if (section == null || !isWithinFrustum(viewport, section)) { + if (section == null) { return; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/TreeSectionCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/TreeSectionCollector.java new file mode 100644 index 0000000000..a95a750c76 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/TreeSectionCollector.java @@ -0,0 +1,15 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public abstract class TreeSectionCollector implements OcclusionCuller.Visitor { + abstract void add(RenderSection section); + + abstract void traverseVisible(OcclusionCuller.Visitor visitor, Viewport viewport); + + @Override + public void visit(RenderSection section, boolean visible) { + this.add(section); + } +} From 5ced0f9876ff82c7e8271225ab99248bdccbfb3d Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 29 Aug 2024 20:04:00 +0200 Subject: [PATCH 03/81] use faster deinterleave with one fewer steps --- .../chunk/occlusion/LinearOctreeSectionCollector.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java index 8f7a42b49f..2f95e57999 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java @@ -85,7 +85,7 @@ private static int interleave6x3(int x, int y, int z) { } private static int interleave6(int n) { - n &= 0b000000000000111111; + n &= 0b000000000000111111; n = (n | n << 8) & 0b000011000000001111; n = (n | n << 4) & 0b000011000011000011; n = (n | n << 2) & 0b001001001001001001; @@ -93,10 +93,9 @@ private static int interleave6(int n) { } private static int deinterleave6(int n) { - n &= 0b001001001001001001; - n = (n | n >> 2) & 0b000011000011000011; - n = (n | n >> 4) & 0b000011000000001111; - n = (n | n >> 8) & 0b000000000000111111; + n &= 0b001001001001001001; + n = (n | n >> 2) & 0b000011000011000011; + n = (n | n >> 4 | n >> 8) & 0b000000000000111111; return n; } From 6fd8bd0f64f5bea282b216d08f98d3b24e7f6d17 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 29 Aug 2024 21:31:30 +0200 Subject: [PATCH 04/81] implement front-to-back ordering during tree traversal --- .../LinearOctreeSectionCollector.java | 95 +++++++++++++------ 1 file changed, 65 insertions(+), 30 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java index 2f95e57999..145a0e0989 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java @@ -12,14 +12,20 @@ class LinearOctreeSectionCollector extends TreeSectionCollector { Tree secondaryTree; final int baseOffsetX, baseOffsetY, baseOffsetZ; + OcclusionCuller.Visitor visitor; + Viewport viewport; + LinearOctreeSectionCollector(Long2ReferenceMap sections, Viewport viewport, float searchDistance) { this.sections = sections; + // offset is shifted by 1 to encompass all sections towards the negative + // + 1 to block position approximate fractional part of camera position (correct?) + // TODO: is this the correct way of calculating the minimum possible section index? var transform = viewport.getTransform(); - int offsetDistance = Mth.floor(searchDistance / 16.0f); - this.baseOffsetX = (transform.intX >> 4) - offsetDistance - 1; - this.baseOffsetY = (transform.intY >> 4) - offsetDistance - 1; - this.baseOffsetZ = (transform.intZ >> 4) - offsetDistance - 1; + int offsetDistance = Mth.floor(searchDistance / 16.0f) + 1; + this.baseOffsetX = ((transform.intX + 1) >> 4) - offsetDistance; + this.baseOffsetY = ((transform.intY + 1) >> 4) - offsetDistance; + this.baseOffsetZ = ((transform.intZ + 1) >> 4) - offsetDistance; this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); } @@ -33,20 +39,26 @@ void add(RenderSection section) { if (this.mainTree.add(x, y, z)) { if (this.secondaryTree == null) { // offset diagonally to fully encompass the required area - this.secondaryTree = new Tree(this.baseOffsetX + 4, this.baseOffsetY, this.baseOffsetZ + 4); + this.secondaryTree = new Tree(this.baseOffsetX + 4, this.baseOffsetY, this.baseOffsetZ + 4); } if (this.secondaryTree.add(x, y, z)) { - throw new IllegalStateException("Failed to add section to secondary tree"); + throw new IllegalStateException("Failed to add section to trees"); } } } @Override void traverseVisible(OcclusionCuller.Visitor visitor, Viewport viewport) { - this.mainTree.traverse(visitor, viewport); + this.visitor = visitor; + this.viewport = viewport; + + this.mainTree.traverse(viewport); if (this.secondaryTree != null) { - this.secondaryTree.traverse(visitor, viewport); + this.secondaryTree.traverse(viewport); } + + this.visitor = null; + this.viewport = null; } private class Tree { @@ -54,6 +66,7 @@ private class Tree { private final long[] treeReduced = new long[64]; private long treeDoubleReduced = 0L; final int offsetX, offsetY, offsetZ; + int cameraOffsetX, cameraOffsetY, cameraOffsetZ; Tree(int offsetX, int offsetY, int offsetZ) { this.offsetX = offsetX; @@ -99,12 +112,24 @@ private static int deinterleave6(int n) { return n; } - void traverse(OcclusionCuller.Visitor visitor, Viewport viewport) { - this.traverse(visitor, viewport, 0, 5); + void traverse(Viewport viewport) { + var transform = viewport.getTransform(); + + // + 1 to block position approximate fractional part of camera position + // + 1 to section position to compensate for shifted global offset + this.cameraOffsetX = ((transform.intX + 1) >> 4) - this.offsetX + 1; + this.cameraOffsetY = ((transform.intY + 1) >> 4) - this.offsetY + 1; + this.cameraOffsetZ = ((transform.intZ + 1) >> 4) - this.offsetZ + 1; + + this.traverse(0, 0, 0, 0, 5); } - void traverse(OcclusionCuller.Visitor visitor, Viewport viewport, int nodeOrigin, int level) { - int childDim = 1 << (level + 3); // * 16 / 2 + void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level) { + int childHalfDim = 1 << (level + 3); // * 16 / 2 + int orderModulator = getNodeTraversalModulator(nodeX, nodeY, nodeZ, childHalfDim >> 3); + if ((level & 1) == 1) { + orderModulator <<= 3; + } if (level <= 1) { // check using the full bitmap @@ -117,12 +142,13 @@ void traverse(OcclusionCuller.Visitor visitor, Viewport viewport, int nodeOrigin if (level == 0) { for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { - if ((map & (mask << bitIndex)) != 0) { - int sectionOrigin = childOriginBase | bitIndex; + int childIndex = bitIndex ^ orderModulator; + if ((map & (mask << childIndex)) != 0) { + int sectionOrigin = childOriginBase | childIndex; int x = deinterleave6(sectionOrigin) + this.offsetX; int y = deinterleave6(sectionOrigin >> 1) + this.offsetY; int z = deinterleave6(sectionOrigin >> 2) + this.offsetZ; - if (testNode(viewport, x, y, z, childDim)) { + if (testNode(x, y, z, childHalfDim)) { // TODO: profile if it's faster to do a hashmap lookup to get the region // and then use the region's array to get the render section // TODO: also profile if it's worth it to store an array of all render sections @@ -131,14 +157,15 @@ void traverse(OcclusionCuller.Visitor visitor, Viewport viewport, int nodeOrigin // NOTE: such an array would need to be traversed in the correct front-to-back order // for this the sections should be in it in x, y, z order and then front-to-back iteration is easy var section = LinearOctreeSectionCollector.this.sections.get(SectionPos.asLong(x, y, z)); - visitor.visit(section, true); + LinearOctreeSectionCollector.this.visitor.visit(section, true); } } } } else { for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { - if ((map & (mask << bitIndex)) != 0) { - this.testChild(visitor, viewport, childOriginBase | bitIndex, childDim, level); + int childIndex = bitIndex ^ orderModulator; + if ((map & (mask << childIndex)) != 0) { + this.testChild(childOriginBase | childIndex, childHalfDim, level); } } } @@ -152,8 +179,9 @@ void traverse(OcclusionCuller.Visitor visitor, Viewport viewport, int nodeOrigin long map = this.treeReduced[nodeOrigin >> 12]; for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { - if ((map & (mask << bitIndex)) != 0) { - this.testChild(visitor, viewport, childOriginBase | (bitIndex << 6), childDim, level); + int childIndex = bitIndex ^ orderModulator; + if ((map & (mask << childIndex)) != 0) { + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level); } } } else { @@ -164,24 +192,25 @@ void traverse(OcclusionCuller.Visitor visitor, Viewport viewport, int nodeOrigin int endBit = startBit + (bitStep << 3); for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { - if ((this.treeDoubleReduced & (mask << bitIndex)) != 0) { - this.testChild(visitor, viewport, bitIndex << 12, childDim, level); + int childIndex = bitIndex ^ orderModulator; + if ((this.treeDoubleReduced & (mask << childIndex)) != 0) { + this.testChild(childIndex << 12, childHalfDim, level); } } } } - void testChild(OcclusionCuller.Visitor visitor, Viewport viewport, int childOrigin, int childDim, int level) { - int x = deinterleave6(childOrigin) + this.offsetX; - int y = deinterleave6(childOrigin >> 1) + this.offsetY; - int z = deinterleave6(childOrigin >> 2) + this.offsetZ; - if (testNode(viewport, x, y, z, childDim)) { - this.traverse(visitor, viewport, childOrigin, level - 1); + void testChild(int childOrigin, int childDim, int level) { + int x = deinterleave6(childOrigin); + int y = deinterleave6(childOrigin >> 1); + int z = deinterleave6(childOrigin >> 2); + if (testNode(x + this.offsetX, y + this.offsetY, z + this.offsetZ, childDim)) { + this.traverse(x, y, z, childOrigin, level - 1); } } - static boolean testNode(Viewport viewport, int x, int y, int z, int childDim) { - return viewport.isBoxVisible( + boolean testNode(int x, int y, int z, int childDim) { + return LinearOctreeSectionCollector.this.viewport.isBoxVisible( (x << 4) + childDim, (y << 4) + childDim, (z << 4) + childDim, @@ -189,5 +218,11 @@ static boolean testNode(Viewport viewport, int x, int y, int z, int childDim) { childDim + OcclusionCuller.CHUNK_SECTION_SIZE, childDim + OcclusionCuller.CHUNK_SECTION_SIZE); } + + int getNodeTraversalModulator(int x, int y, int z, int childSections) { + return (x + childSections - this.cameraOffsetX) >>> 31 + | ((y + childSections - this.cameraOffsetY) >>> 31) << 1 + | ((z + childSections - this.cameraOffsetZ) >>> 31) << 2; + } } } From 83aca131216d2b6a2a50ad4ae22b858cb1aba850 Mon Sep 17 00:00:00 2001 From: douira Date: Mon, 25 Nov 2024 19:55:52 +0100 Subject: [PATCH 05/81] refactor plumbing between RSM and the chunk renderer to avoid touching RenderSection objects, improve tree render list generation performance --- .../client/render/SodiumWorldRenderer.java | 19 +-- .../client/render/chunk/RenderSection.java | 57 +------ .../render/chunk/RenderSectionFlags.java | 6 + .../render/chunk/RenderSectionManager.java | 137 ++++++++++------- .../render/chunk/data/BuiltSectionInfo.java | 6 +- .../render/chunk/lists/ChunkRenderList.java | 11 +- .../chunk/lists/PendingTaskCollector.java | 38 +++++ .../chunk/lists/VisibleChunkCollector.java | 59 +++----- ...ollector.java => LinearSectionOctree.java} | 140 +++++++++++------- .../chunk/occlusion/OcclusionCuller.java | 125 ++++++++-------- .../chunk/occlusion/TreeSectionCollector.java | 15 -- .../render/chunk/region/RenderRegion.java | 58 ++++++++ .../chunk/region/RenderRegionManager.java | 8 +- .../client/render/viewport/Viewport.java | 16 ++ .../render/viewport/frustum/Frustum.java | 2 + .../viewport/frustum/SimpleFrustum.java | 5 + 16 files changed, 414 insertions(+), 288 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java rename common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/{LinearOctreeSectionCollector.java => LinearSectionOctree.java} (57%) delete mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/TreeSectionCollector.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index c2e72fb2e4..c197d4a382 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -208,7 +208,7 @@ public void setupTerrain(Camera camera, this.lastCameraYaw = yaw; if (cameraLocationChanged || cameraAngleChanged || cameraProjectionChanged) { - this.renderSectionManager.markGraphDirty(); + this.renderSectionManager.markFrustumDirty(); } this.lastFogDistance = fogDistance; @@ -225,10 +225,10 @@ public void setupTerrain(Camera camera, int maxChunkUpdates = updateChunksImmediately ? this.renderDistance : 1; for (int i = 0; i < maxChunkUpdates; i++) { - if (this.renderSectionManager.needsUpdate()) { + if (this.renderSectionManager.needsAnyUpdate()) { profiler.popPush("chunk_render_lists"); - this.renderSectionManager.update(camera, viewport, spectator); + this.renderSectionManager.updateRenderLists(camera, viewport, spectator); } profiler.popPush("chunk_update"); @@ -240,7 +240,7 @@ public void setupTerrain(Camera camera, this.renderSectionManager.uploadChunks(); - if (!this.renderSectionManager.needsUpdate()) { + if (!this.renderSectionManager.needsAnyUpdate()) { break; } } @@ -345,9 +345,8 @@ private void renderBlockEntities(PoseStack matrices, while (renderSectionIterator.hasNext()) { var renderSectionId = renderSectionIterator.nextByteAsInt(); - var renderSection = renderRegion.getSection(renderSectionId); - var blockEntities = renderSection.getCulledBlockEntities(); + var blockEntities = renderRegion.getCulledBlockEntities(renderSectionId); if (blockEntities == null) { continue; @@ -372,7 +371,7 @@ private void renderGlobalBlockEntities(PoseStack matrices, LocalPlayer player, LocalBooleanRef isGlowing) { for (var renderSection : this.renderSectionManager.getSectionsWithGlobalEntities()) { - var blockEntities = renderSection.getGlobalBlockEntities(); + var blockEntities = renderSection.getRegion().getGlobalBlockEntities(renderSection.getSectionIndex()); if (blockEntities == null) { continue; @@ -446,9 +445,7 @@ public void iterateVisibleBlockEntities(Consumer blockEntityConsume while (renderSectionIterator.hasNext()) { var renderSectionId = renderSectionIterator.nextByteAsInt(); - var renderSection = renderRegion.getSection(renderSectionId); - - var blockEntities = renderSection.getCulledBlockEntities(); + var blockEntities = renderRegion.getCulledBlockEntities(renderSectionId); if (blockEntities == null) { continue; @@ -461,7 +458,7 @@ public void iterateVisibleBlockEntities(Consumer blockEntityConsume } for (var renderSection : this.renderSectionManager.getSectionsWithGlobalEntities()) { - var blockEntities = renderSection.getGlobalBlockEntities(); + var blockEntities = renderSection.getRegion().getGlobalBlockEntities(renderSection.getSectionIndex()); if (blockEntities == null) { continue; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java index 9aa933474c..2d6d3f22ac 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java @@ -43,11 +43,6 @@ public class RenderSection { // Rendering State - private boolean built = false; // merge with the flags? - private int flags = RenderSectionFlags.NONE; - private BlockEntity @Nullable[] globalBlockEntities; - private BlockEntity @Nullable[] culledBlockEntities; - private TextureAtlasSprite @Nullable[] animatedSprites; @Nullable private TranslucentData translucentData; @@ -72,7 +67,6 @@ public RenderSection(RenderRegion region, int chunkX, int chunkY, int chunkZ) { 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); this.region = region; @@ -148,32 +142,22 @@ public boolean setInfo(@Nullable BuiltSectionInfo info) { } private boolean setRenderState(@NotNull BuiltSectionInfo info) { - var prevBuilt = this.built; - var prevFlags = this.flags; + var prevFlags = this.region.getSectionFlags(this.sectionIndex); var prevVisibilityData = this.visibilityData; - this.built = true; - this.flags = info.flags; + this.region.setSectionRenderState(this.sectionIndex, info); this.visibilityData = info.visibilityData; - this.globalBlockEntities = info.globalBlockEntities; - this.culledBlockEntities = info.culledBlockEntities; - this.animatedSprites = info.animatedSprites; - // the section is marked as having received graph-relevant changes if it's build state, flags, or connectedness has changed. // the entities and sprites don't need to be checked since whether they exist is encoded in the flags. - return !prevBuilt || prevFlags != this.flags || prevVisibilityData != this.visibilityData; + return prevFlags != this.region.getSectionFlags(this.sectionIndex) || prevVisibilityData != this.visibilityData; } private boolean clearRenderState() { - var wasBuilt = this.built; + var wasBuilt = this.isBuilt(); - this.built = false; - this.flags = RenderSectionFlags.NONE; + this.region.clearSectionRenderState(this.sectionIndex); this.visibilityData = VisibilityEncoding.NULL; - this.globalBlockEntities = null; - this.culledBlockEntities = null; - this.animatedSprites = null; // changes to data if it moves from built to not built don't matter, so only build state changes matter return wasBuilt; @@ -272,7 +256,7 @@ public String toString() { } public boolean isBuilt() { - return this.built; + return (this.region.getSectionFlags(this.sectionIndex) & RenderSectionFlags.MASK_IS_BUILT) != 0; } public int getSectionIndex() { @@ -303,13 +287,6 @@ public void setIncomingDirections(int directions) { this.incomingDirections = directions; } - /** - * Returns a bitfield containing the {@link RenderSectionFlags} for this built section. - */ - public int getFlags() { - return this.flags; - } - /** * Returns the occlusion culling data which determines this chunk's connectedness on the visibility graph. */ @@ -317,28 +294,6 @@ public long getVisibilityData() { return this.visibilityData; } - /** - * Returns the collection of animated sprites contained by this rendered chunk section. - */ - public TextureAtlasSprite @Nullable[] getAnimatedSprites() { - return this.animatedSprites; - } - - /** - * Returns the collection of block entities contained by this rendered chunk. - */ - public BlockEntity @Nullable[] getCulledBlockEntities() { - return this.culledBlockEntities; - } - - /** - * Returns the collection of block entities contained by this rendered chunk, which are not part of its culling - * volume. These entities should always be rendered regardless of the render being visible in the frustum. - */ - public BlockEntity @Nullable[] getGlobalBlockEntities() { - return this.globalBlockEntities; - } - public @Nullable CancellationToken getTaskCancellationToken() { return this.taskCancellationToken; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java index cb19504069..eccf7eda38 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java @@ -4,6 +4,12 @@ public class RenderSectionFlags { public static final int HAS_BLOCK_GEOMETRY = 0; public static final int HAS_BLOCK_ENTITIES = 1; public static final int HAS_ANIMATED_SPRITES = 2; + public static final int IS_BUILT = 3; + + public static final int MASK_HAS_BLOCK_GEOMETRY = 1 << HAS_BLOCK_GEOMETRY; + public static final int MASK_HAS_BLOCK_ENTITIES = 1 << HAS_BLOCK_ENTITIES; + public static final int MASK_HAS_ANIMATED_SPRITES = 1 << HAS_ANIMATED_SPRITES; + public static final int MASK_IS_BUILT = 1 << IS_BUILT; public static final int NONE = 0; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index d91c5496ab..562c0117aa 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -4,10 +4,7 @@ import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceMaps; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -import it.unimi.dsi.fastutil.objects.Reference2ReferenceLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; -import it.unimi.dsi.fastutil.objects.ReferenceSet; -import it.unimi.dsi.fastutil.objects.ReferenceSets; +import it.unimi.dsi.fastutil.objects.*; import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.gl.device.RenderDevice; @@ -15,8 +12,8 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkSortOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderSortingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; @@ -25,6 +22,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.lists.SortedRenderLists; import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollector; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.LinearSectionOctree; 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.chunk.region.RenderRegionManager; @@ -61,6 +59,9 @@ import java.util.*; import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class RenderSectionManager { private final ChunkBuilder builder; @@ -90,22 +91,29 @@ public class RenderSectionManager { private SortedRenderLists renderLists; @NotNull - private Map> taskLists; + private Map> taskLists; private int lastUpdatedFrame; - private boolean needsGraphUpdate; + private enum UpdateType { + NONE, + GRAPH, + FRUSTUM + } + private UpdateType needsUpdate = UpdateType.NONE; private @Nullable BlockPos cameraBlockPos; private @Nullable Vector3dc cameraPosition; + private final ExecutorService cullExecutor = Executors.newSingleThreadExecutor(); + private LinearSectionOctree currentTree = null; + public RenderSectionManager(ClientLevel level, int renderDistance, CommandList commandList) { this.chunkRenderer = new DefaultChunkRenderer(RenderDevice.INSTANCE, ChunkMeshFormats.COMPACT); this.level = level; this.builder = new ChunkBuilder(level, ChunkMeshFormats.COMPACT); - this.needsGraphUpdate = true; this.renderDistance = renderDistance; this.sortTriggering = new SortTriggering(); @@ -119,7 +127,7 @@ public RenderSectionManager(ClientLevel level, int renderDistance, CommandList c this.taskLists = new EnumMap<>(ChunkUpdateType.class); for (var type : ChunkUpdateType.values()) { - this.taskLists.put(type, new ArrayDeque<>()); + this.taskLists.put(type, new ObjectArrayFIFOQueue<>()); } } @@ -128,26 +136,44 @@ public void updateCameraState(Vector3dc cameraPosition, Camera camera) { this.cameraPosition = cameraPosition; } - public void update(Camera camera, Viewport viewport, boolean spectator) { + public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator) { this.lastUpdatedFrame += 1; - this.createTerrainRenderList(camera, viewport, this.lastUpdatedFrame, spectator); - - this.needsGraphUpdate = false; - } - - private void createTerrainRenderList(Camera camera, Viewport viewport, int frame, boolean spectator) { this.resetRenderLists(); final var searchDistance = this.getSearchDistance(); final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); - var visitor = new VisibleChunkCollector(frame); + // preliminary implementation: re-do cull job on camera movement, always use the previous result + // TODO: preempt camera movement or make BFS work with movement (widen outgoing direction impl) + + // generate a section tree result with the occlusion culler if there currently is none + if (this.needsGraphUpdate() || this.currentTree == null || !this.currentTree.isAcceptableFor(viewport)) { + try { + this.currentTree = this.cullExecutor.submit(() -> { + var start = System.nanoTime(); + var tree = new LinearSectionOctree(viewport, searchDistance); + this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, this.lastUpdatedFrame); + var end = System.nanoTime(); + System.out.println("Graph search with tree build took " + (end - start) / 1000 + "µs"); + return tree; + }).get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Failed to cull chunks", e); + } + this.taskLists = this.currentTree.getRebuildLists(); + } - this.occlusionCuller.findVisible(visitor, viewport, searchDistance, useOcclusionCulling, frame); + // there must a result there now, use it to generate render lists for the frame + var visibleCollector = new VisibleChunkCollector(this.regions, this.lastUpdatedFrame); + var start = System.nanoTime(); + this.currentTree.traverseVisible(visibleCollector, viewport); + var end = System.nanoTime(); + System.out.println("Tree read took " + (end - start) / 1000 + "µs"); - this.renderLists = visitor.createRenderLists(viewport); - this.taskLists = visitor.getRebuildLists(); + this.renderLists = visibleCollector.createRenderLists(); + + this.needsUpdate = UpdateType.NONE; } private float getSearchDistance() { @@ -167,8 +193,7 @@ private boolean shouldUseOcclusionCulling(Camera camera, boolean spectator) { BlockPos origin = camera.getBlockPosition(); if (spectator && this.level.getBlockState(origin) - .isSolidRender()) - { + .isSolidRender()) { useOcclusionCulling = false; } else { useOcclusionCulling = Minecraft.getInstance().smartCull; @@ -210,7 +235,7 @@ public void onSectionAdded(int x, int y, int z) { this.connectNeighborNodes(renderSection); // force update to schedule build task - this.needsGraphUpdate = true; + this.markGraphDirty(); } public void onSectionRemoved(int x, int y, int z) { @@ -237,7 +262,7 @@ public void onSectionRemoved(int x, int y, int z) { section.delete(); // force update to remove section from render lists - this.needsGraphUpdate = true; + this.markGraphDirty(); } public void renderLayer(ChunkRenderMatrices matrices, TerrainRenderPass pass, double x, double y, double z) { @@ -263,13 +288,7 @@ public void tickVisibleRenders() { } while (iterator.hasNext()) { - var section = region.getSection(iterator.nextByteAsInt()); - - if (section == null) { - continue; - } - - var sprites = section.getAnimatedSprites(); + var sprites = region.getAnimatedSprites(iterator.nextByteAsInt()); if (sprites == null) { continue; @@ -303,7 +322,9 @@ public void uploadChunks() { // (sort results never change the graph) // generally there's no sort results without a camera movement, which would also trigger // a graph update, but it can sometimes happen because of async task execution - this.needsGraphUpdate |= this.processChunkBuildResults(results); + if (this.processChunkBuildResults(results)) { + this.markGraphDirty(); + } for (var result : results) { result.destroy(); @@ -409,9 +430,9 @@ public void updateChunks(boolean updateImmediately) { } else { var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); var deferredCollector = new ChunkJobCollector( - this.builder.getHighEffortSchedulingBudget(), - this.builder.getLowEffortSchedulingBudget(), - this.buildResults::add); + this.builder.getHighEffortSchedulingBudget(), + this.builder.getLowEffortSchedulingBudget(), + this.buildResults::add); // if zero frame delay is allowed, submit important sorts with the current frame blocking collector. // otherwise submit with the collector that the next frame is blocking on. @@ -431,26 +452,26 @@ public void updateChunks(boolean updateImmediately) { } private void submitSectionTasks( - ChunkJobCollector importantCollector, - ChunkJobCollector semiImportantCollector, - ChunkJobCollector deferredCollector) { - this.submitSectionTasks(importantCollector, ChunkUpdateType.IMPORTANT_SORT, true); - this.submitSectionTasks(semiImportantCollector, ChunkUpdateType.IMPORTANT_REBUILD, true); + ChunkJobCollector importantCollector, + ChunkJobCollector semiImportantCollector, + ChunkJobCollector deferredCollector) { + this.submitSectionTasks(importantCollector, ChunkUpdateType.IMPORTANT_SORT, true); + this.submitSectionTasks(semiImportantCollector, ChunkUpdateType.IMPORTANT_REBUILD, true); - // since the sort tasks are run last, the effort category can be ignored and - // simply fills up the remaining budget. Splitting effort categories is still - // important to prevent high effort tasks from using up the entire budget if it - // happens to divide evenly. - this.submitSectionTasks(deferredCollector, ChunkUpdateType.REBUILD, false); - this.submitSectionTasks(deferredCollector, ChunkUpdateType.INITIAL_BUILD, false); - this.submitSectionTasks(deferredCollector, ChunkUpdateType.SORT, true); + // since the sort tasks are run last, the effort category can be ignored and + // simply fills up the remaining budget. Splitting effort categories is still + // important to prevent high effort tasks from using up the entire budget if it + // happens to divide evenly. + this.submitSectionTasks(deferredCollector, ChunkUpdateType.REBUILD, false); + this.submitSectionTasks(deferredCollector, ChunkUpdateType.INITIAL_BUILD, false); + this.submitSectionTasks(deferredCollector, ChunkUpdateType.SORT, true); } private void submitSectionTasks(ChunkJobCollector collector, ChunkUpdateType type, boolean ignoreEffortCategory) { var queue = this.taskLists.get(type); while (!queue.isEmpty() && collector.hasBudgetFor(type.getTaskEffort(), ignoreEffortCategory)) { - RenderSection section = queue.remove(); + RenderSection section = queue.dequeue(); if (section.isDisposed()) { continue; @@ -525,11 +546,21 @@ public void processGFNIMovement(CameraMovement movement) { } public void markGraphDirty() { - this.needsGraphUpdate = true; + this.needsUpdate = UpdateType.GRAPH; + } + + public void markFrustumDirty() { + if (this.needsUpdate == UpdateType.NONE) { + this.needsUpdate = UpdateType.FRUSTUM; + } } - public boolean needsUpdate() { - return this.needsGraphUpdate; + public boolean needsAnyUpdate() { + return this.needsUpdate != UpdateType.NONE; + } + + public boolean needsGraphUpdate() { + return this.needsUpdate == UpdateType.GRAPH; } public ChunkBuilder getBuilder() { @@ -539,6 +570,8 @@ public ChunkBuilder getBuilder() { public void destroy() { this.builder.shutdown(); // stop all the workers, and cancel any tasks + this.cullExecutor.shutdownNow(); + for (var result : this.collectChunkBuildResults()) { result.destroy(); // delete resources for any pending tasks (including those that were cancelled) } @@ -611,7 +644,7 @@ public void scheduleRebuild(int x, int y, int z, boolean important) { section.setPendingUpdate(pendingUpdate); // force update to schedule rebuild task on this section - this.needsGraphUpdate = true; + this.markGraphDirty(); } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java index 25bf883a4d..bc3342a9f9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/data/BuiltSectionInfo.java @@ -41,15 +41,15 @@ private BuiltSectionInfo(@NotNull Collection blockRenderPasse int flags = 0; if (!blockRenderPasses.isEmpty()) { - flags |= 1 << RenderSectionFlags.HAS_BLOCK_GEOMETRY; + flags |= RenderSectionFlags.MASK_HAS_BLOCK_GEOMETRY; } if (!culledBlockEntities.isEmpty()) { - flags |= 1 << RenderSectionFlags.HAS_BLOCK_ENTITIES; + flags |= RenderSectionFlags.MASK_HAS_BLOCK_ENTITIES; } if (!animatedSprites.isEmpty()) { - flags |= 1 << RenderSectionFlags.HAS_ANIMATED_SPRITES; + flags |= RenderSectionFlags.MASK_HAS_ANIMATED_SPRITES; } this.flags = flags; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java index e45638b8ad..ea212ae495 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/ChunkRenderList.java @@ -73,23 +73,22 @@ public void sortSections(SectionPos cameraPos, int[] sortItems) { } } - public void add(RenderSection render) { + public void add(int localSectionIndex) { if (this.size >= RenderRegion.REGION_SIZE) { throw new ArrayIndexOutOfBoundsException("Render list is full"); } this.size++; - int index = render.getSectionIndex(); - int flags = render.getFlags(); + int flags = this.region.getSectionFlags(localSectionIndex); - this.sectionsWithGeometry[this.sectionsWithGeometryCount] = (byte) index; + this.sectionsWithGeometry[this.sectionsWithGeometryCount] = (byte) localSectionIndex; this.sectionsWithGeometryCount += (flags >>> RenderSectionFlags.HAS_BLOCK_GEOMETRY) & 1; - this.sectionsWithSprites[this.sectionsWithSpritesCount] = (byte) index; + this.sectionsWithSprites[this.sectionsWithSpritesCount] = (byte) localSectionIndex; this.sectionsWithSpritesCount += (flags >>> RenderSectionFlags.HAS_ANIMATED_SPRITES) & 1; - this.sectionsWithEntities[this.sectionsWithEntitiesCount] = (byte) index; + this.sectionsWithEntities[this.sectionsWithEntitiesCount] = (byte) localSectionIndex; this.sectionsWithEntitiesCount += (flags >>> RenderSectionFlags.HAS_BLOCK_ENTITIES) & 1; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java new file mode 100644 index 0000000000..bacb826234 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -0,0 +1,38 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue; +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 java.util.EnumMap; +import java.util.Map; + +public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { + private final EnumMap> sortedRebuildLists; + + public PendingTaskCollector() { + this.sortedRebuildLists = new EnumMap<>(ChunkUpdateType.class); + + for (var type : ChunkUpdateType.values()) { + this.sortedRebuildLists.put(type, new ObjectArrayFIFOQueue<>()); + } + } + + @Override + public void visit(RenderSection section) { + ChunkUpdateType type = section.getPendingUpdate(); + + if (type != null && section.getTaskCancellationToken() == null) { + ObjectArrayFIFOQueue queue = this.sortedRebuildLists.get(type); + + if (queue.size() < type.getMaximumQueueSize()) { + queue.enqueue(section); + } + } + } + + public Map> getRebuildLists() { + return this.sortedRebuildLists; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java index 89cf22cf78..349283e43a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java @@ -1,11 +1,12 @@ package net.caffeinemc.mods.sodium.client.render.chunk.lists; import it.unimi.dsi.fastutil.ints.IntArrays; +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; 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.LocalSectionIndex; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.LinearSectionOctree; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import java.util.ArrayDeque; @@ -17,58 +18,44 @@ * The visible chunk collector is passed to the occlusion graph search culler to * collect the visible chunks. */ -public class VisibleChunkCollector implements OcclusionCuller.Visitor { +public class VisibleChunkCollector implements LinearSectionOctree.VisibleSectionVisitor { private final ObjectArrayList sortedRenderLists; - private final EnumMap> sortedRebuildLists; + + private final RenderRegionManager regions; private final int frame; - public VisibleChunkCollector(int frame) { + public VisibleChunkCollector(RenderRegionManager regions, int frame) { + this.regions = regions; this.frame = frame; this.sortedRenderLists = new ObjectArrayList<>(); - this.sortedRebuildLists = new EnumMap<>(ChunkUpdateType.class); - - for (var type : ChunkUpdateType.values()) { - this.sortedRebuildLists.put(type, new ArrayDeque<>()); - } } @Override - 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(); + public void visit(int x, int y, int z) { + var region = this.regions.getForChunk(x, y, z); + int rX = x & (RenderRegion.REGION_WIDTH - 1); + int rY = y & (RenderRegion.REGION_HEIGHT - 1); + int rZ = z & (RenderRegion.REGION_LENGTH - 1); + var sectionIndex = LocalSectionIndex.pack(rX, rY, rZ); + + ChunkRenderList renderList = region.getRenderList(); if (renderList.getLastVisibleFrame() != this.frame) { - renderList.reset(this.frame); + renderList.reset(this.frame); this.sortedRenderLists.add(renderList); } - renderList.add(section); - } - - // always add to rebuild lists though, because it might just not be built yet - this.addToRebuildLists(section); - } - - private void addToRebuildLists(RenderSection section) { - ChunkUpdateType type = section.getPendingUpdate(); - - if (type != null && section.getTaskCancellationToken() == null) { - Queue queue = this.sortedRebuildLists.get(type); - - if (queue.size() < type.getMaximumQueueSize()) { - queue.add(section); - } + if (region.getSectionFlags(sectionIndex) != 0) { + renderList.add(sectionIndex); } } private static int[] sortItems = new int[RenderRegion.REGION_SIZE]; - public SortedRenderLists createRenderLists(Viewport viewport) { + public SortedRenderLists createSortedRenderLists(Viewport viewport) { // sort the regions by distance to fix rare region ordering bugs var sectionPos = viewport.getChunkCoord(); var cameraX = sectionPos.getX() >> RenderRegion.REGION_WIDTH_SH; @@ -104,7 +91,7 @@ public SortedRenderLists createRenderLists(Viewport viewport) { return new SortedRenderLists(sorted); } - public Map> getRebuildLists() { - return this.sortedRebuildLists; + public SortedRenderLists createRenderLists() { + return new SortedRenderLists(this.sortedRenderLists); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java similarity index 57% rename from common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java rename to common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java index 145a0e0989..dd979f5b8a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearOctreeSectionCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java @@ -1,37 +1,65 @@ package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; -import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; -import net.minecraft.core.SectionPos; import net.minecraft.util.Mth; +import org.joml.FrustumIntersection; -class LinearOctreeSectionCollector extends TreeSectionCollector { - private final Long2ReferenceMap sections; +/** + * TODO: do distance test here? what happens when the camera moves but the bfs doesn't know that? expand the distance limit? + * ideas to prevent one frame of wrong display when BFS is recalculated but not ready yet: + * - preemptively do the bfs from the next section the camera is going to be in, and maybe pad the render distance by how far the player can move before we need to recalculate. (if there's padding, then I guess the distance check would need to also be put in the traversal's test) + * - a more experimental idea would be to allow the BFS to go both left and right (as it currently does in sections that are aligned with the origin section) in the sections aligned with the origin section's neighbors. This would mean we can safely use the bfs result in all neighbors, but could slightly increase the number of false positives (which is a problem already...) + */ +public class LinearSectionOctree extends PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { final Tree mainTree; Tree secondaryTree; final int baseOffsetX, baseOffsetY, baseOffsetZ; + final int buildSectionCenterX, buildSectionCenterY, buildSectionCenterZ; - OcclusionCuller.Visitor visitor; + VisibleSectionVisitor visitor; Viewport viewport; - LinearOctreeSectionCollector(Long2ReferenceMap sections, Viewport viewport, float searchDistance) { - this.sections = sections; + // offset is shifted by 1 to encompass all sections towards the negative + // TODO: is this the correct way of calculating the minimum possible section index? + private static final int TREE_OFFSET = 1; + private static final int REUSE_MAX_DISTANCE = 8; + + public interface VisibleSectionVisitor { + void visit(int x, int y, int z); + } + + public LinearSectionOctree(Viewport viewport, float searchDistance) { - // offset is shifted by 1 to encompass all sections towards the negative - // + 1 to block position approximate fractional part of camera position (correct?) - // TODO: is this the correct way of calculating the minimum possible section index? var transform = viewport.getTransform(); - int offsetDistance = Mth.floor(searchDistance / 16.0f) + 1; - this.baseOffsetX = ((transform.intX + 1) >> 4) - offsetDistance; - this.baseOffsetY = ((transform.intY + 1) >> 4) - offsetDistance; - this.baseOffsetZ = ((transform.intZ + 1) >> 4) - offsetDistance; + int offsetDistance = Mth.floor(searchDistance / 16.0f) + TREE_OFFSET; + this.buildSectionCenterX = (transform.intX & ~0b1111) + 8; + this.buildSectionCenterY = (transform.intY & ~0b1111) + 8; + this.buildSectionCenterZ = (transform.intZ & ~0b1111) + 8; + this.baseOffsetX = (transform.intX >> 4) - offsetDistance; + this.baseOffsetY = (transform.intY >> 4) - offsetDistance; + this.baseOffsetZ = (transform.intZ >> 4) - offsetDistance; this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); } + public boolean isAcceptableFor(Viewport viewport) { + var transform = viewport.getTransform(); + return Math.abs(transform.intX - this.buildSectionCenterX) <= REUSE_MAX_DISTANCE + && Math.abs(transform.intY - this.buildSectionCenterY) <= REUSE_MAX_DISTANCE + && Math.abs(transform.intZ - this.buildSectionCenterZ) <= REUSE_MAX_DISTANCE; + } + @Override - void add(RenderSection section) { + public boolean isWithinFrustum(Viewport viewport, RenderSection section) { + return true; + } + + @Override + public void visit(RenderSection section) { + super.visit(section); + int x = section.getChunkX(); int y = section.getChunkY(); int z = section.getChunkZ(); @@ -47,8 +75,7 @@ void add(RenderSection section) { } } - @Override - void traverseVisible(OcclusionCuller.Visitor visitor, Viewport viewport) { + public void traverseVisible(VisibleSectionVisitor visitor, Viewport viewport) { this.visitor = visitor; this.viewport = viewport; @@ -65,8 +92,9 @@ private class Tree { private final long[] tree = new long[64 * 64]; private final long[] treeReduced = new long[64]; private long treeDoubleReduced = 0L; - final int offsetX, offsetY, offsetZ; - int cameraOffsetX, cameraOffsetY, cameraOffsetZ; + private final int offsetX, offsetY, offsetZ; + + private int cameraOffsetX, cameraOffsetY, cameraOffsetZ; Tree(int offsetX, int offsetY, int offsetZ) { this.offsetX = offsetX; @@ -92,13 +120,12 @@ boolean add(int x, int y, int z) { return false; } - private static int interleave6x3(int x, int y, int z) { return interleave6(x) | interleave6(y) << 1 | interleave6(z) << 2; } private static int interleave6(int n) { - n &= 0b000000000000111111; + n &= 0b000000000000111111; n = (n | n << 8) & 0b000011000000001111; n = (n | n << 4) & 0b000011000011000011; n = (n | n << 2) & 0b001001001001001001; @@ -106,8 +133,8 @@ private static int interleave6(int n) { } private static int deinterleave6(int n) { - n &= 0b001001001001001001; - n = (n | n >> 2) & 0b000011000011000011; + n &= 0b001001001001001001; + n = (n | n >> 2) & 0b000011000011000011; n = (n | n >> 4 | n >> 8) & 0b000000000000111111; return n; } @@ -115,18 +142,17 @@ private static int deinterleave6(int n) { void traverse(Viewport viewport) { var transform = viewport.getTransform(); - // + 1 to block position approximate fractional part of camera position // + 1 to section position to compensate for shifted global offset - this.cameraOffsetX = ((transform.intX + 1) >> 4) - this.offsetX + 1; - this.cameraOffsetY = ((transform.intY + 1) >> 4) - this.offsetY + 1; - this.cameraOffsetZ = ((transform.intZ + 1) >> 4) - this.offsetZ + 1; + this.cameraOffsetX = (transform.intX >> 4) - this.offsetX + 1; + this.cameraOffsetY = (transform.intY >> 4) - this.offsetY + 1; + this.cameraOffsetZ = (transform.intZ >> 4) - this.offsetZ + 1; - this.traverse(0, 0, 0, 0, 5); + this.traverse(0, 0, 0, 0, 5, false); } - void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level) { + void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, boolean inside) { int childHalfDim = 1 << (level + 3); // * 16 / 2 - int orderModulator = getNodeTraversalModulator(nodeX, nodeY, nodeZ, childHalfDim >> 3); + int orderModulator = getChildOrderModulator(nodeX, nodeY, nodeZ, childHalfDim >> 3); if ((level & 1) == 1) { orderModulator <<= 3; } @@ -148,16 +174,9 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level) { int x = deinterleave6(sectionOrigin) + this.offsetX; int y = deinterleave6(sectionOrigin >> 1) + this.offsetY; int z = deinterleave6(sectionOrigin >> 2) + this.offsetZ; - if (testNode(x, y, z, childHalfDim)) { - // TODO: profile if it's faster to do a hashmap lookup to get the region - // and then use the region's array to get the render section - // TODO: also profile if it's worth it to store an array of all render sections - // for each node to accelerate "fully inside frustum" situations, - // otherwise also optimize "fully inside" situations with a different traversal type - // NOTE: such an array would need to be traversed in the correct front-to-back order - // for this the sections should be in it in x, y, z order and then front-to-back iteration is easy - var section = LinearOctreeSectionCollector.this.sections.get(SectionPos.asLong(x, y, z)); - LinearOctreeSectionCollector.this.visitor.visit(section, true); + + if (inside || testNode(x, y, z, childHalfDim)) { + LinearSectionOctree.this.visitor.visit(x, y, z); } } } @@ -165,7 +184,7 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level) { for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { int childIndex = bitIndex ^ orderModulator; if ((map & (mask << childIndex)) != 0) { - this.testChild(childOriginBase | childIndex, childHalfDim, level); + this.testChild(childOriginBase | childIndex, childHalfDim, level, inside); } } } @@ -181,7 +200,7 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level) { for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { int childIndex = bitIndex ^ orderModulator; if ((map & (mask << childIndex)) != 0) { - this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level); + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); } } } else { @@ -194,23 +213,31 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level) { for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { int childIndex = bitIndex ^ orderModulator; if ((this.treeDoubleReduced & (mask << childIndex)) != 0) { - this.testChild(childIndex << 12, childHalfDim, level); + this.testChild(childIndex << 12, childHalfDim, level, inside); } } } } - void testChild(int childOrigin, int childDim, int level) { + void testChild(int childOrigin, int childDim, int level, boolean inside) { int x = deinterleave6(childOrigin); int y = deinterleave6(childOrigin >> 1); int z = deinterleave6(childOrigin >> 2); - if (testNode(x + this.offsetX, y + this.offsetY, z + this.offsetZ, childDim)) { - this.traverse(x, y, z, childOrigin, level - 1); + + int result = 0; + int cacheWriteTarget = -1; + if (!inside) { + result = intersectNode(x + this.offsetX, y + this.offsetY, z + this.offsetZ, childDim); + inside = result == FrustumIntersection.INSIDE; + } + + if (inside || result == FrustumIntersection.INTERSECT) { + this.traverse(x, y, z, childOrigin, level - 1, inside); } } boolean testNode(int x, int y, int z, int childDim) { - return LinearOctreeSectionCollector.this.viewport.isBoxVisible( + return LinearSectionOctree.this.viewport.isBoxVisible( (x << 4) + childDim, (y << 4) + childDim, (z << 4) + childDim, @@ -219,10 +246,21 @@ boolean testNode(int x, int y, int z, int childDim) { childDim + OcclusionCuller.CHUNK_SECTION_SIZE); } - int getNodeTraversalModulator(int x, int y, int z, int childSections) { - return (x + childSections - this.cameraOffsetX) >>> 31 - | ((y + childSections - this.cameraOffsetY) >>> 31) << 1 - | ((z + childSections - this.cameraOffsetZ) >>> 31) << 2; + int intersectNode(int x, int y, int z, int childDim) { + return LinearSectionOctree.this.viewport.getBoxIntersection( + (x << 4) + childDim, + (y << 4) + childDim, + (z << 4) + childDim, + childDim + OcclusionCuller.CHUNK_SECTION_SIZE, + childDim + OcclusionCuller.CHUNK_SECTION_SIZE, + childDim + OcclusionCuller.CHUNK_SECTION_SIZE); + } + + int getChildOrderModulator(int x, int y, int z, int childSectionDim) { + return (x + childSectionDim - this.cameraOffsetX) >>> 31 + | ((y + childSectionDim - this.cameraOffsetY) >>> 31) << 1 + | ((z + childSectionDim - this.cameraOffsetZ) >>> 31) << 2; } } + } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 20ce139835..65a752f9b6 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -24,12 +24,36 @@ public class OcclusionCuller { static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + 1.0f /* maximum model extent */ + 0.125f /* epsilon */; + public interface GraphOcclusionVisitor { + void visit(RenderSection section); + + default boolean isWithinFrustum(Viewport viewport, RenderSection section) { + return viewport.isBoxVisible(section.getCenterX(), section.getCenterY(), section.getCenterZ(), + CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE); + } + + default int getOutwardDirections(SectionPos origin, RenderSection section) { + int planes = 0; + + planes |= section.getChunkX() <= origin.getX() ? 1 << GraphDirection.WEST : 0; + planes |= section.getChunkX() >= origin.getX() ? 1 << GraphDirection.EAST : 0; + + planes |= section.getChunkY() <= origin.getY() ? 1 << GraphDirection.DOWN : 0; + planes |= section.getChunkY() >= origin.getY() ? 1 << GraphDirection.UP : 0; + + planes |= section.getChunkZ() <= origin.getZ() ? 1 << GraphDirection.NORTH : 0; + planes |= section.getChunkZ() >= origin.getZ() ? 1 << GraphDirection.SOUTH : 0; + + return planes; + } + } + public OcclusionCuller(Long2ReferenceMap sections, Level level) { this.sections = sections; this.level = level; } - public void findVisible(Visitor visitor, + public void findVisible(GraphOcclusionVisitor visitor, Viewport viewport, float searchDistance, boolean useOcclusionCulling, @@ -38,11 +62,10 @@ public void findVisible(Visitor visitor, final var queues = this.queue; queues.reset(); - var collector = new LinearOctreeSectionCollector(this.sections, viewport, searchDistance); - this.init(collector, queues.write(), viewport, searchDistance, useOcclusionCulling, frame); + this.init(visitor, queues.write(), viewport, searchDistance, useOcclusionCulling, frame); while (queues.flip()) { - processQueue(collector, viewport, searchDistance, useOcclusionCulling, frame, queues.read(), queues.write()); + processQueue(visitor, viewport, searchDistance, useOcclusionCulling, frame, queues.read(), queues.write()); } this.addNearbySections(visitor, viewport, searchDistance, frame); @@ -50,7 +73,7 @@ public void findVisible(Visitor visitor, collector.traverseVisible(visitor, viewport); } - private static void processQueue(Visitor visitor, + private static void processQueue(GraphOcclusionVisitor visitor, Viewport viewport, float searchDistance, boolean useOcclusionCulling, @@ -60,8 +83,11 @@ private static void processQueue(Visitor visitor, { RenderSection section; + // TODO: move visibility test into visit neighbor method? while ((section = readQueue.dequeue()) != null) { - if (!isSectionVisible(section, viewport, searchDistance)) { + boolean sectionVisible = isWithinRenderDistance(viewport.getTransform(), section, searchDistance) && + visitor.isWithinFrustum(viewport, section); + if (!sectionVisible) { continue; } @@ -88,7 +114,7 @@ private static void processQueue(Visitor visitor, // We can only traverse *outwards* from the center of the graph search, so mask off any invalid // directions. - connections &= getOutwardDirections(viewport.getChunkCoord(), section); + connections &= visitor.getOutwardDirections(viewport.getChunkCoord(), section); } visitNeighbors(writeQueue, section, connections, frame); @@ -119,9 +145,21 @@ private static long getAngleVisibilityMask(Viewport viewport, RenderSection sect return ~angleOcclusionMask; } - private static boolean isSectionVisible(RenderSection section, Viewport viewport, float maxDistance) { - // TODO: fix - return isWithinRenderDistance(viewport.getTransform(), section, maxDistance); // && isWithinFrustum(viewport, section); + private static boolean isWithinRenderDistance(CameraTransform camera, RenderSection section, float maxDistance) { + // origin point of the chunk's bounding box (in view space) + int ox = section.getOriginX() - camera.intX; + int oy = section.getOriginY() - camera.intY; + int oz = section.getOriginZ() - camera.intZ; + + // coordinates of the point to compare (in view space) + // this is the closest point within the bounding box to the center (0, 0, 0) + float dx = nearestToZero(ox, ox + 16) - camera.fracX; + float dy = nearestToZero(oy, oy + 16) - camera.fracY; + float dz = nearestToZero(oz, oz + 16) - camera.fracZ; + + // vanilla's "cylindrical fog" algorithm + // max(length(distance.xz), abs(distance.y)) + return (((dx * dx) + (dz * dz)) < (maxDistance * maxDistance)) && (Math.abs(dy) < maxDistance); } private static void visitNeighbors(final WriteQueue queue, RenderSection section, int outgoing, int frame) { @@ -176,38 +214,6 @@ private static void visitNode(final WriteQueue queue, @NotNull Re render.addIncomingDirections(incoming); } - private static int getOutwardDirections(SectionPos origin, RenderSection section) { - int planes = 0; - - planes |= section.getChunkX() <= origin.getX() ? 1 << GraphDirection.WEST : 0; - planes |= section.getChunkX() >= origin.getX() ? 1 << GraphDirection.EAST : 0; - - planes |= section.getChunkY() <= origin.getY() ? 1 << GraphDirection.DOWN : 0; - planes |= section.getChunkY() >= origin.getY() ? 1 << GraphDirection.UP : 0; - - planes |= section.getChunkZ() <= origin.getZ() ? 1 << GraphDirection.NORTH : 0; - planes |= section.getChunkZ() >= origin.getZ() ? 1 << GraphDirection.SOUTH : 0; - - return planes; - } - - private static boolean isWithinRenderDistance(CameraTransform camera, RenderSection section, float maxDistance) { - // origin point of the chunk's bounding box (in view space) - int ox = section.getOriginX() - camera.intX; - int oy = section.getOriginY() - camera.intY; - int oz = section.getOriginZ() - camera.intZ; - - // coordinates of the point to compare (in view space) - // this is the closest point within the bounding box to the center (0, 0, 0) - float dx = nearestToZero(ox, ox + 16) - camera.fracX; - float dy = nearestToZero(oy, oy + 16) - camera.fracY; - float dz = nearestToZero(oz, oz + 16) - camera.fracZ; - - // vanilla's "cylindrical fog" algorithm - // max(length(distance.xz), abs(distance.y)) - return (((dx * dx) + (dz * dz)) < (maxDistance * maxDistance)) && (Math.abs(dy) < maxDistance); - } - @SuppressWarnings("ManualMinMaxCalculation") // we know what we are doing. private static int nearestToZero(int min, int max) { // this compiles to slightly better code than Math.min(Math.max(0, min), max) @@ -262,7 +268,7 @@ private void addNearbySections(Visitor visitor, Viewport viewport, float searchD } } - private void init(Visitor visitor, + private void init(GraphOcclusionVisitor visitor, WriteQueue queue, Viewport viewport, float searchDistance, @@ -273,18 +279,18 @@ private void init(Visitor visitor, if (origin.getY() < this.level.getMinSectionY()) { // below the level - this.initOutsideWorldHeight(queue, viewport, searchDistance, frame, + this.initOutsideWorldHeight(visitor, queue, viewport, searchDistance, frame, this.level.getMinSectionY(), GraphDirection.DOWN); } else if (origin.getY() > this.level.getMaxSectionY()) { // above the level - this.initOutsideWorldHeight(queue, viewport, searchDistance, frame, + this.initOutsideWorldHeight(visitor, queue, viewport, searchDistance, frame, this.level.getMaxSectionY(), GraphDirection.UP); } else { this.initWithinWorld(visitor, queue, viewport, useOcclusionCulling, frame); } } - private void initWithinWorld(Visitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, int frame) { + private void initWithinWorld(GraphOcclusionVisitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, int frame) { var origin = viewport.getChunkCoord(); var section = this.getRenderSection(origin.getX(), origin.getY(), origin.getZ()); @@ -314,7 +320,8 @@ private void initWithinWorld(Visitor visitor, WriteQueue queue, V // Enqueues sections that are inside the viewport using diamond spiral iteration to avoid sorting and ensure a // consistent order. Innermost layers are enqueued first. Within each layer, iteration starts at the northernmost // section and proceeds counterclockwise (N->W->S->E). - private void initOutsideWorldHeight(WriteQueue queue, + private void initOutsideWorldHeight(GraphOcclusionVisitor visitor, + WriteQueue queue, Viewport viewport, float searchDistance, int frame, @@ -325,18 +332,18 @@ private void initOutsideWorldHeight(WriteQueue queue, var radius = Mth.floor(searchDistance / 16.0f); // Layer 0 - this.tryVisitNode(queue, origin.getX(), height, origin.getZ(), direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX(), height, origin.getZ(), direction, frame, viewport); // Complete layers, excluding layer 0 for (int layer = 1; layer <= radius; layer++) { for (int z = -layer; z < layer; z++) { int x = Math.abs(z) - layer; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); } for (int z = layer; z > -layer; z--) { int x = layer - Math.abs(z); - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); } } @@ -346,32 +353,30 @@ private void initOutsideWorldHeight(WriteQueue queue, for (int z = -radius; z <= -l; z++) { int x = -z - layer; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); } for (int z = l; z <= radius; z++) { int x = z - layer; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); } for (int z = radius; z >= l; z--) { int x = layer - z; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); } for (int z = -l; z >= -radius; z--) { int x = layer + z; - this.tryVisitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); } } } - private void tryVisitNode(WriteQueue queue, int x, int y, int z, int direction, int frame, Viewport viewport) { + private void tryInitNode(GraphOcclusionVisitor visitor, WriteQueue queue, int x, int y, int z, int direction, int frame, Viewport viewport) { RenderSection section = this.getRenderSection(x, y, z); - // TODO: fix - // if (section == null || !isWithinFrustum(viewport, section)) { - if (section == null) { + if (section == null || !visitor.isWithinFrustum(viewport, section)) { return; } @@ -381,8 +386,4 @@ private void tryVisitNode(WriteQueue queue, int x, int y, int z, private RenderSection getRenderSection(int x, int y, int z) { return this.sections.get(SectionPos.asLong(x, y, z)); } - - public interface Visitor { - void visit(RenderSection section); - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/TreeSectionCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/TreeSectionCollector.java deleted file mode 100644 index a95a750c76..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/TreeSectionCollector.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; - -import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; -import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; - -public abstract class TreeSectionCollector implements OcclusionCuller.Visitor { - abstract void add(RenderSection section); - - abstract void traverseVisible(OcclusionCuller.Visitor visitor, Viewport viewport); - - @Override - public void visit(RenderSection section, boolean visible) { - this.add(section); - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java index 29ca64b226..88cdddbc50 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java @@ -7,14 +7,19 @@ import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.gl.tessellation.GlTessellation; 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.data.BuiltSectionInfo; import net.caffeinemc.mods.sodium.client.render.chunk.data.SectionRenderDataStorage; import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderList; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.DefaultTerrainRenderPasses; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; import net.caffeinemc.mods.sodium.client.render.chunk.vertex.format.ChunkMeshFormats; import net.caffeinemc.mods.sodium.client.util.MathUtil; +import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.SectionPos; +import net.minecraft.world.level.block.entity.BlockEntity; import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.Map; @@ -46,6 +51,10 @@ public class RenderRegion { private final ChunkRenderList renderList; private final RenderSection[] sections = new RenderSection[RenderRegion.REGION_SIZE]; + private final byte[] sectionFlags = new byte[RenderRegion.REGION_SIZE]; + private final BlockEntity[] @Nullable [] globalBlockEntities = new BlockEntity[RenderRegion.REGION_SIZE][]; + private final BlockEntity[] @Nullable [] culledBlockEntities = new BlockEntity[RenderRegion.REGION_SIZE][]; + private final TextureAtlasSprite[] @Nullable [] animatedSprites = new TextureAtlasSprite[RenderRegion.REGION_SIZE][]; private int sectionCount; private final Map sectionRenderData = new Reference2ReferenceOpenHashMap<>(); @@ -165,6 +174,55 @@ public void addSection(RenderSection section) { this.sectionCount++; } + public void setSectionRenderState(int id, BuiltSectionInfo info) { + this.sectionFlags[id] = (byte) (info.flags | RenderSectionFlags.MASK_IS_BUILT); + this.globalBlockEntities[id] = info.globalBlockEntities; + this.culledBlockEntities[id] = info.culledBlockEntities; + this.animatedSprites[id] = info.animatedSprites; + } + + public void clearSectionRenderState(int id) { + this.sectionFlags[id] = RenderSectionFlags.NONE; + this.globalBlockEntities[id] = null; + this.culledBlockEntities[id] = null; + this.animatedSprites[id] = null; + } + + public int getSectionFlags(int id) { + return this.sectionFlags[id]; + } + + /** + * Returns the collection of block entities contained by this rendered chunk, which are not part of its culling + * volume. These entities should always be rendered regardless of the render being visible in the frustum. + * + * @param id The section index + * @return The collection of block entities + */ + public BlockEntity[] getGlobalBlockEntities(int id) { + return this.globalBlockEntities[id]; + } + + /** + * Returns the collection of block entities contained by this rendered chunk. + * + * @param id The section index + * @return The collection of block entities + */ + public BlockEntity[] getCulledBlockEntities(int id) { + return this.culledBlockEntities[id]; + } + + /** + * Returns the collection of animated sprites contained by this rendered chunk section. + * + * @param id The section index + * @return The collection of animated sprites + */ + public TextureAtlasSprite[] getAnimatedSprites(int id) { + return this.animatedSprites[id]; + } + public void removeSection(RenderSection section) { var sectionIndex = section.getSectionIndex(); var prev = this.sections[sectionIndex]; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java index 2b4c704256..52112696ee 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java @@ -25,7 +25,7 @@ import java.util.*; public class RenderRegionManager { - private final Long2ReferenceOpenHashMap regions = new Long2ReferenceOpenHashMap<>(); + public final Long2ReferenceOpenHashMap regions = new Long2ReferenceOpenHashMap<>(); private final StagingBuffer stagingBuffer; @@ -188,6 +188,12 @@ public RenderRegion createForChunk(int chunkX, int chunkY, int chunkZ) { chunkZ >> RenderRegion.REGION_LENGTH_SH); } + public RenderRegion getForChunk(int chunkX, int chunkY, int chunkZ) { + return this.regions.get(RenderRegion.key(chunkX >> RenderRegion.REGION_WIDTH_SH, + chunkY >> RenderRegion.REGION_HEIGHT_SH, + chunkZ >> RenderRegion.REGION_LENGTH_SH)); + } + @NotNull private RenderRegion create(int x, int y, int z) { var key = RenderRegion.key(x, y, z); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java index be9521ebad..80ff1b6c44 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java @@ -41,6 +41,22 @@ public boolean isBoxVisible(int intOriginX, int intOriginY, int intOriginZ, floa ); } + public int getBoxIntersection(int intOriginX, int intOriginY, int intOriginZ, float floatSizeX, float floatSizeY, float floatSizeZ) { + float floatOriginX = (intOriginX - this.transform.intX) - this.transform.fracX; + float floatOriginY = (intOriginY - this.transform.intY) - this.transform.fracY; + float floatOriginZ = (intOriginZ - this.transform.intZ) - this.transform.fracZ; + + return this.frustum.intersectAab( + floatOriginX - floatSizeX, + floatOriginY - floatSizeY, + floatOriginZ - floatSizeZ, + + floatOriginX + floatSizeX, + floatOriginY + floatSizeY, + floatOriginZ + floatSizeZ + ); + } + public CameraTransform getTransform() { return this.transform; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java index 3ec8d16aa6..c7495555dd 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/Frustum.java @@ -2,4 +2,6 @@ public interface Frustum { boolean testAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ); + + int intersectAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java index 88ff3b7738..2ada093971 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/frustum/SimpleFrustum.java @@ -13,4 +13,9 @@ public SimpleFrustum(FrustumIntersection frustumIntersection) { public boolean testAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { return this.frustum.testAab(minX, minY, minZ, maxX, maxY, maxZ); } + + @Override + public int intersectAab(float minX, float minY, float minZ, float maxX, float maxY, float maxZ) { + return this.frustum.intersectAab(minX, minY, minZ, maxX, maxY, maxZ); + } } From 24eadd64a74c03ea9f5a28e97faa0e1abe04cdef Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 4 Sep 2024 05:49:43 +0200 Subject: [PATCH 06/81] filter out sections that don't render anything before adding them to the tree, this greatly improves performance --- .../render/chunk/RenderSectionFlags.java | 1 + .../chunk/lists/VisibleChunkCollector.java | 12 ++++---- .../chunk/occlusion/LinearSectionOctree.java | 30 +++++++++++-------- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java index eccf7eda38..bc973ccf3d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java @@ -10,6 +10,7 @@ public class RenderSectionFlags { public static final int MASK_HAS_BLOCK_ENTITIES = 1 << HAS_BLOCK_ENTITIES; public static final int MASK_HAS_ANIMATED_SPRITES = 1 << HAS_ANIMATED_SPRITES; public static final int MASK_IS_BUILT = 1 << IS_BUILT; + public static final int MASK_NEEDS_RENDER = MASK_HAS_BLOCK_GEOMETRY | MASK_HAS_ANIMATED_SPRITES | MASK_HAS_BLOCK_ENTITIES; public static final int NONE = 0; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java index 349283e43a..f8675a9eee 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java @@ -1,7 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.lists; import it.unimi.dsi.fastutil.ints.IntArrays; -import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import net.caffeinemc.mods.sodium.client.render.chunk.LocalSectionIndex; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.LinearSectionOctree; @@ -42,15 +41,14 @@ public void visit(int x, int y, int z) { ChunkRenderList renderList = region.getRenderList(); - if (renderList.getLastVisibleFrame() != this.frame) { + if (renderList.getLastVisibleFrame() != this.frame) { renderList.reset(this.frame); - this.sortedRenderLists.add(renderList); - } - - if (region.getSectionFlags(sectionIndex) != 0) { - renderList.add(sectionIndex); + this.sortedRenderLists.add(renderList); } + + // flags don't need to be checked here since only sections with contents (RenderSectionFlags.MASK_NEEDS_RENDER) are added to the octree + renderList.add(sectionIndex); } private static int[] sortItems = new int[RenderRegion.REGION_SIZE]; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java index dd979f5b8a..e967bdf205 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java @@ -1,6 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; 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.lists.PendingTaskCollector; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import net.minecraft.util.Mth; @@ -60,6 +61,11 @@ public boolean isWithinFrustum(Viewport viewport, RenderSection section) { public void visit(RenderSection section) { super.visit(section); + // discard invisible or sections that don't need to be rendered + if (!visible || (section.getRegion().getSectionFlags(section.getSectionIndex()) & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) { + return; + } + int x = section.getChunkX(); int y = section.getChunkY(); int z = section.getChunkZ(); @@ -159,17 +165,16 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, boolea if (level <= 1) { // check using the full bitmap - int bitStep = 1 << (level * 3); - long mask = (1L << bitStep) - 1; - int startBit = nodeOrigin & 0b111111; - int endBit = startBit + (bitStep << 3); int childOriginBase = nodeOrigin & 0b111111_111111_000000; long map = this.tree[nodeOrigin >> 6]; if (level == 0) { - for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { + int startBit = nodeOrigin & 0b111111; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { int childIndex = bitIndex ^ orderModulator; - if ((map & (mask << childIndex)) != 0) { + if ((map & (1L << childIndex)) != 0) { int sectionOrigin = childOriginBase | childIndex; int x = deinterleave6(sectionOrigin) + this.offsetX; int y = deinterleave6(sectionOrigin >> 1) + this.offsetY; @@ -181,9 +186,9 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, boolea } } } else { - for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { int childIndex = bitIndex ^ orderModulator; - if ((map & (mask << childIndex)) != 0) { + if ((map & (0xFFL << childIndex)) != 0) { this.testChild(childOriginBase | childIndex, childHalfDim, level, inside); } } @@ -224,14 +229,15 @@ void testChild(int childOrigin, int childDim, int level, boolean inside) { int y = deinterleave6(childOrigin >> 1); int z = deinterleave6(childOrigin >> 2); - int result = 0; - int cacheWriteTarget = -1; + boolean intersection = false; if (!inside) { - result = intersectNode(x + this.offsetX, y + this.offsetY, z + this.offsetZ, childDim); + // TODO: actually measure time to generate a render list on dev and compare + var result = intersectNode(x + this.offsetX, y + this.offsetY, z + this.offsetZ, childDim); inside = result == FrustumIntersection.INSIDE; + intersection = result == FrustumIntersection.INTERSECT; } - if (inside || result == FrustumIntersection.INTERSECT) { + if (inside || intersection) { this.traverse(x, y, z, childOrigin, level - 1, inside); } } From 12be3dde3ceaa85cdc0504d585e4021e05c5a1a2 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 4 Sep 2024 05:49:59 +0200 Subject: [PATCH 07/81] change tree read timing to average 100 samples --- .../client/render/chunk/RenderSectionManager.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 562c0117aa..b5e0d5b57e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -1,6 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk; import com.mojang.blaze3d.systems.RenderSystem; +import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceMaps; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; @@ -136,6 +137,8 @@ public void updateCameraState(Vector3dc cameraPosition, Camera camera) { this.cameraPosition = cameraPosition; } + private final IntArrayList traversalSamples = new IntArrayList(); + public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator) { this.lastUpdatedFrame += 1; @@ -169,7 +172,16 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato var start = System.nanoTime(); this.currentTree.traverseVisible(visibleCollector, viewport); var end = System.nanoTime(); - System.out.println("Tree read took " + (end - start) / 1000 + "µs"); + if (this.traversalSamples.size() == 100) { + long sum = 0; + for (int i = 0; i < this.traversalSamples.size(); i++) { + sum += this.traversalSamples.getInt(i); + } + System.out.println("Tree traversal took " + sum / this.traversalSamples.size() + "µs on average over " + this.traversalSamples.size() + " samples"); + this.traversalSamples.clear(); + } else { + this.traversalSamples.add((int) (end - start) / 1000); + } this.renderLists = visibleCollector.createRenderLists(); From 805864fe5644961a6a93c6fb5bf969195597a053 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 4 Sep 2024 05:55:06 +0200 Subject: [PATCH 08/81] doesn't need to be public anymore --- .../sodium/client/render/chunk/region/RenderRegionManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java index 52112696ee..f4ca4127fb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegionManager.java @@ -25,7 +25,7 @@ import java.util.*; public class RenderRegionManager { - public final Long2ReferenceOpenHashMap regions = new Long2ReferenceOpenHashMap<>(); + private final Long2ReferenceOpenHashMap regions = new Long2ReferenceOpenHashMap<>(); private final StagingBuffer stagingBuffer; From 27e3f37db335890fe938ac8c5d4d5306b6e7ef89 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 4 Sep 2024 19:48:18 +0200 Subject: [PATCH 09/81] more reasonable flag mask ordering --- .../mods/sodium/client/render/chunk/RenderSectionFlags.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java index bc973ccf3d..489289e6b2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java @@ -10,7 +10,7 @@ public class RenderSectionFlags { public static final int MASK_HAS_BLOCK_ENTITIES = 1 << HAS_BLOCK_ENTITIES; public static final int MASK_HAS_ANIMATED_SPRITES = 1 << HAS_ANIMATED_SPRITES; public static final int MASK_IS_BUILT = 1 << IS_BUILT; - public static final int MASK_NEEDS_RENDER = MASK_HAS_BLOCK_GEOMETRY | MASK_HAS_ANIMATED_SPRITES | MASK_HAS_BLOCK_ENTITIES; + public static final int MASK_NEEDS_RENDER = MASK_HAS_BLOCK_GEOMETRY | MASK_HAS_BLOCK_ENTITIES | MASK_HAS_ANIMATED_SPRITES; public static final int NONE = 0; } From 0a2d2e1612769d54bdb89ea7f2e0ebf60db04160 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 4 Sep 2024 20:05:00 +0200 Subject: [PATCH 10/81] take 2000 samples for measurement --- .../mods/sodium/client/render/chunk/RenderSectionManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index b5e0d5b57e..26cdc40c45 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -172,7 +172,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato var start = System.nanoTime(); this.currentTree.traverseVisible(visibleCollector, viewport); var end = System.nanoTime(); - if (this.traversalSamples.size() == 100) { + if (this.traversalSamples.size() == 2000) { long sum = 0; for (int i = 0; i < this.traversalSamples.size(); i++) { sum += this.traversalSamples.getInt(i); From f8cecd0694a50e5c47d4eeb44059b18a5dd74b69 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 4 Sep 2024 20:46:38 +0200 Subject: [PATCH 11/81] fix entity culling --- .../client/render/SodiumWorldRenderer.java | 20 +------- .../render/chunk/RenderSectionManager.java | 14 +++-- .../chunk/occlusion/LinearSectionOctree.java | 51 +++++++++++++++++++ .../client/render/viewport/Viewport.java | 12 +++++ 4 files changed, 70 insertions(+), 27 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index c197d4a382..2e8827fb35 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -506,25 +506,7 @@ public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y return true; } - int minX = SectionPos.posToSectionCoord(x1 - 0.5D); - int minY = SectionPos.posToSectionCoord(y1 - 0.5D); - int minZ = SectionPos.posToSectionCoord(z1 - 0.5D); - - int maxX = SectionPos.posToSectionCoord(x2 + 0.5D); - int maxY = SectionPos.posToSectionCoord(y2 + 0.5D); - int maxZ = SectionPos.posToSectionCoord(z2 + 0.5D); - - for (int x = minX; x <= maxX; x++) { - for (int z = minZ; z <= maxZ; z++) { - for (int y = minY; y <= maxY; y++) { - if (this.renderSectionManager.isSectionVisible(x, y, z)) { - return true; - } - } - } - } - - return false; + return this.renderSectionManager.isBoxVisible(x1, y1, z1, x2, y2, z2); } public String getChunksDebugString() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 26cdc40c45..85c85e23d8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -108,6 +108,7 @@ private enum UpdateType { private final ExecutorService cullExecutor = Executors.newSingleThreadExecutor(); private LinearSectionOctree currentTree = null; + private Viewport currentViewport = null; public RenderSectionManager(ClientLevel level, int renderDistance, CommandList commandList) { this.chunkRenderer = new DefaultChunkRenderer(RenderDevice.INSTANCE, ChunkMeshFormats.COMPACT); @@ -169,9 +170,12 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato // there must a result there now, use it to generate render lists for the frame var visibleCollector = new VisibleChunkCollector(this.regions, this.lastUpdatedFrame); + this.currentViewport = viewport; + var start = System.nanoTime(); this.currentTree.traverseVisible(visibleCollector, viewport); var end = System.nanoTime(); + if (this.traversalSamples.size() == 2000) { long sum = 0; for (int i = 0; i < this.traversalSamples.size(); i++) { @@ -313,14 +317,8 @@ public void tickVisibleRenders() { } } - public boolean isSectionVisible(int x, int y, int z) { - RenderSection render = this.getRenderSection(x, y, z); - - if (render == null) { - return false; - } - - return render.getLastVisibleFrame() == this.lastUpdatedFrame; + public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { + return this.currentTree == null || this.currentTree.isBoxVisible(this.currentViewport, x1, y1, z1, x2, y2, z2); } public void uploadChunks() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java index e967bdf205..e64bee6b36 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java @@ -4,6 +4,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.core.SectionPos; import net.minecraft.util.Mth; import org.joml.FrustumIntersection; @@ -81,6 +82,38 @@ public void visit(RenderSection section) { } } + public boolean isBoxVisible(Viewport viewport, double x1, double y1, double z1, double x2, double y2, double z2) { + if (!viewport.isBoxVisible(x1, y1, z1, x2, y2, z2)) { + return false; + } + + // check if there's a section at any part of the box + int minX = SectionPos.posToSectionCoord(x1 - 0.5D); + int minY = SectionPos.posToSectionCoord(y1 - 0.5D); + int minZ = SectionPos.posToSectionCoord(z1 - 0.5D); + + int maxX = SectionPos.posToSectionCoord(x2 + 0.5D); + int maxY = SectionPos.posToSectionCoord(y2 + 0.5D); + int maxZ = SectionPos.posToSectionCoord(z2 + 0.5D); + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = minY; y <= maxY; y++) { + if (this.isSectionPresent(x, y, z)) { + return true; + } + } + } + } + + return false; + } + + private boolean isSectionPresent(int x, int y, int z) { + return this.mainTree.isSectionPresent(x, y, z) || + (this.secondaryTree != null && this.secondaryTree.isSectionPresent(x, y, z)); + } + public void traverseVisible(VisibleSectionVisitor visitor, Viewport viewport) { this.visitor = visitor; this.viewport = viewport; @@ -145,6 +178,24 @@ private static int deinterleave6(int n) { return n; } + boolean isSectionPresent(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (x > 63 || y > 63 || z > 63 || x < 0 || y < 0 || z < 0) { + return false; + } + + var bitIndex = interleave6x3(x, y, z); + int doubleReducedBitIndex = bitIndex >> 12; + if ((this.treeDoubleReduced & (1L << doubleReducedBitIndex)) == 0) { + return false; + } + + int reducedBitIndex = bitIndex >> 6; + return (this.tree[reducedBitIndex] & (1L << (bitIndex & 0b111111))) != 0; + } + void traverse(Viewport viewport) { var transform = viewport.getTransform(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java index 80ff1b6c44..3cdeac8dc0 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java @@ -41,6 +41,18 @@ public boolean isBoxVisible(int intOriginX, int intOriginY, int intOriginZ, floa ); } + public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { + return this.frustum.testAab( + (float)((x1 - this.transform.intX) - this.transform.fracX), + (float)((y1 - this.transform.intY) - this.transform.fracY), + (float)((z1 - this.transform.intZ) - this.transform.fracZ), + + (float)((x2 - this.transform.intX) - this.transform.fracX), + (float)((y2 - this.transform.intY) - this.transform.fracY), + (float)((z2 - this.transform.intZ) - this.transform.fracZ) + ); + } + public int getBoxIntersection(int intOriginX, int intOriginY, int intOriginZ, float floatSizeX, float floatSizeY, float floatSizeZ) { float floatOriginX = (intOriginX - this.transform.intX) - this.transform.fracX; float floatOriginY = (intOriginY - this.transform.intY) - this.transform.fracY; From a4c259520f3a6ba587cc68ab3eebe565f60638d2 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Sep 2024 01:11:00 +0200 Subject: [PATCH 12/81] split node iteration into separate loops, no change in performance, clearer code --- .../chunk/occlusion/LinearSectionOctree.java | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java index e64bee6b36..ecee360992 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java @@ -245,31 +245,44 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, boolea } } } else if (level <= 3) { - // check using the single reduced bitmap - int bitStep = 1 << (level * 3 - 6); - long mask = (1L << bitStep) - 1; - int startBit = (nodeOrigin >> 6) & 0b111111; - int endBit = startBit + (bitStep << 3); int childOriginBase = nodeOrigin & 0b111111_000000_000000; long map = this.treeReduced[nodeOrigin >> 12]; - for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { - int childIndex = bitIndex ^ orderModulator; - if ((map & (mask << childIndex)) != 0) { - this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); + if (level == 2) { + int startBit = (nodeOrigin >> 6) & 0b111111; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex ++) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (1L << childIndex)) != 0) { + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (0xFFL << childIndex)) != 0) { + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); + } } } } else { - // check using the double reduced bitmap - int bitStep = 1 << (level * 3 - 12); - long mask = (1L << bitStep) - 1; - int startBit = nodeOrigin >> 12; - int endBit = startBit + (bitStep << 3); - - for (int bitIndex = startBit; bitIndex < endBit; bitIndex += bitStep) { - int childIndex = bitIndex ^ orderModulator; - if ((this.treeDoubleReduced & (mask << childIndex)) != 0) { - this.testChild(childIndex << 12, childHalfDim, level, inside); + if (level == 4) { + int startBit = nodeOrigin >> 12; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex ++) { + int childIndex = bitIndex ^ orderModulator; + if ((this.treeDoubleReduced & (1L << childIndex)) != 0) { + this.testChild(childIndex << 12, childHalfDim, level, inside); + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((this.treeDoubleReduced & (0xFFL << childIndex)) != 0) { + this.testChild(childIndex << 12, childHalfDim, level, inside); + } } } } From 3bcc1ee8daa936b027fa51e791215d5db82f43bb Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Sep 2024 05:38:22 +0200 Subject: [PATCH 13/81] use section coordinates for tree compatibility test --- .../chunk/occlusion/LinearSectionOctree.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java index ecee360992..ffba37f505 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java @@ -18,7 +18,7 @@ public class LinearSectionOctree extends PendingTaskCollector implements Occlusi final Tree mainTree; Tree secondaryTree; final int baseOffsetX, baseOffsetY, baseOffsetZ; - final int buildSectionCenterX, buildSectionCenterY, buildSectionCenterZ; + final int buildSectionX, buildSectionY, buildSectionZ; VisibleSectionVisitor visitor; Viewport viewport; @@ -26,7 +26,7 @@ public class LinearSectionOctree extends PendingTaskCollector implements Occlusi // offset is shifted by 1 to encompass all sections towards the negative // TODO: is this the correct way of calculating the minimum possible section index? private static final int TREE_OFFSET = 1; - private static final int REUSE_MAX_DISTANCE = 8; + private static final int REUSE_MAX_SECTION_DISTANCE = 0; public interface VisibleSectionVisitor { void visit(int x, int y, int z); @@ -36,21 +36,21 @@ public LinearSectionOctree(Viewport viewport, float searchDistance) { var transform = viewport.getTransform(); int offsetDistance = Mth.floor(searchDistance / 16.0f) + TREE_OFFSET; - this.buildSectionCenterX = (transform.intX & ~0b1111) + 8; - this.buildSectionCenterY = (transform.intY & ~0b1111) + 8; - this.buildSectionCenterZ = (transform.intZ & ~0b1111) + 8; - this.baseOffsetX = (transform.intX >> 4) - offsetDistance; - this.baseOffsetY = (transform.intY >> 4) - offsetDistance; - this.baseOffsetZ = (transform.intZ >> 4) - offsetDistance; + this.buildSectionX = transform.intX >> 4; + this.buildSectionY = transform.intY >> 4; + this.buildSectionZ = transform.intZ >> 4; + this.baseOffsetX = this.buildSectionX - offsetDistance; + this.baseOffsetY = this.buildSectionY - offsetDistance; + this.baseOffsetZ = this.buildSectionZ - offsetDistance; this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); } public boolean isAcceptableFor(Viewport viewport) { var transform = viewport.getTransform(); - return Math.abs(transform.intX - this.buildSectionCenterX) <= REUSE_MAX_DISTANCE - && Math.abs(transform.intY - this.buildSectionCenterY) <= REUSE_MAX_DISTANCE - && Math.abs(transform.intZ - this.buildSectionCenterZ) <= REUSE_MAX_DISTANCE; + return Math.abs((transform.intX >> 4) - this.buildSectionX) <= REUSE_MAX_SECTION_DISTANCE + && Math.abs((transform.intY >> 4) - this.buildSectionY) <= REUSE_MAX_SECTION_DISTANCE + && Math.abs((transform.intZ >> 4) - this.buildSectionZ) <= REUSE_MAX_SECTION_DISTANCE; } @Override From 86b1c92347101f843a85d7ad24d8d248adbd8261 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Sep 2024 05:39:44 +0200 Subject: [PATCH 14/81] remove unnecessary frustum test from entity culling --- .../sodium/client/render/SodiumWorldRenderer.java | 12 +++++++----- .../client/render/chunk/RenderSectionManager.java | 2 +- .../render/chunk/occlusion/LinearSectionOctree.java | 6 +----- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index 2e8827fb35..c08aa37b33 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -36,7 +36,6 @@ import net.minecraft.client.renderer.entity.state.EntityRenderState; import net.minecraft.client.resources.model.ModelBakery; import net.minecraft.core.BlockPos; -import net.minecraft.core.SectionPos; import net.minecraft.server.level.BlockDestructionProgress; import net.minecraft.util.Mth; import net.minecraft.util.profiling.Profiler; @@ -208,7 +207,7 @@ public void setupTerrain(Camera camera, this.lastCameraYaw = yaw; if (cameraLocationChanged || cameraAngleChanged || cameraProjectionChanged) { - this.renderSectionManager.markFrustumDirty(); + this.renderSectionManager.markRenderListDirty(); } this.lastFogDistance = fogDistance; @@ -471,10 +470,13 @@ public void iterateVisibleBlockEntities(Consumer blockEntityConsume } // the volume of a section multiplied by the number of sections to be checked at most - private static final double MAX_ENTITY_CHECK_VOLUME = 16 * 16 * 16 * 15; + private static final double MAX_ENTITY_CHECK_VOLUME = 16 * 16 * 16 * 50; /** - * Returns whether or not the entity intersects with any visible chunks in the graph. + * Returns whether the entity intersects with any visible chunks in the graph. + * + * Note that this method assumes the entity is within the frustum. It does not perform a frustum check. + * * @return True if the entity is visible, otherwise false */ public boolean isEntityVisible(EntityRenderer renderer, T entity) { @@ -492,7 +494,7 @@ public boolean isEntityVisible(E // bail on very large entities to avoid checking many sections double entityVolume = (bb.maxX - bb.minX) * (bb.maxY - bb.minY) * (bb.maxZ - bb.minZ); if (entityVolume > MAX_ENTITY_CHECK_VOLUME) { - // TODO: do a frustum check instead, even large entities aren't visible if they're outside the frustum + // large entities are only frustum tested, their sections are not checked for visibility return true; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 85c85e23d8..c929a10f31 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -318,7 +318,7 @@ public void tickVisibleRenders() { } public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { - return this.currentTree == null || this.currentTree.isBoxVisible(this.currentViewport, x1, y1, z1, x2, y2, z2); + return this.currentTree == null || this.currentTree.isBoxVisible(x1, y1, z1, x2, y2, z2); } public void uploadChunks() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java index ffba37f505..6a49e454c9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java @@ -82,11 +82,7 @@ public void visit(RenderSection section) { } } - public boolean isBoxVisible(Viewport viewport, double x1, double y1, double z1, double x2, double y2, double z2) { - if (!viewport.isBoxVisible(x1, y1, z1, x2, y2, z2)) { - return false; - } - + public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { // check if there's a section at any part of the box int minX = SectionPos.posToSectionCoord(x1 - 0.5D); int minY = SectionPos.posToSectionCoord(y1 - 0.5D); From bec6a5e38b6209c317eaf8109ba3bae658c7f5fe Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 5 Sep 2024 05:41:12 +0200 Subject: [PATCH 15/81] refactor update state management in RSM, add option to fall back to sync bfs if there is very fast movement --- .../render/chunk/RenderSectionManager.java | 166 +++++++++++------- ...r.java => VisibleChunkCollectorAsync.java} | 13 +- .../lists/VisibleChunkCollectorSync.java | 49 ++++++ .../occlusion/AsyncCameraTimingControl.java | 39 ++++ .../chunk/occlusion/LinearSectionOctree.java | 2 +- 5 files changed, 202 insertions(+), 67 deletions(-) rename common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/{VisibleChunkCollector.java => VisibleChunkCollectorAsync.java} (86%) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index c929a10f31..c8c7af5ddc 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -21,7 +21,9 @@ import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderList; import net.caffeinemc.mods.sodium.client.render.chunk.lists.SortedRenderLists; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollectorAsync; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollectorSync; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.AsyncCameraTimingControl; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.LinearSectionOctree; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; @@ -59,10 +61,7 @@ import org.joml.Vector3dc; import java.util.*; -import java.util.concurrent.ConcurrentLinkedDeque; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.*; public class RenderSectionManager { private final ChunkBuilder builder; @@ -96,19 +95,18 @@ public class RenderSectionManager { private int lastUpdatedFrame; - private enum UpdateType { - NONE, - GRAPH, - FRUSTUM - } - private UpdateType needsUpdate = UpdateType.NONE; + private boolean needsGraphUpdate = true; + private boolean needsRenderListUpdate = true; private @Nullable BlockPos cameraBlockPos; private @Nullable Vector3dc cameraPosition; private final ExecutorService cullExecutor = Executors.newSingleThreadExecutor(); + private Future pendingTree = null; private LinearSectionOctree currentTree = null; - private Viewport currentViewport = null; + private boolean treeIsFrustumTested = false; + + private final AsyncCameraTimingControl cameraTimingControl = new AsyncCameraTimingControl(); public RenderSectionManager(ClientLevel level, int renderDistance, CommandList commandList) { this.chunkRenderer = new DefaultChunkRenderer(RenderDevice.INSTANCE, ChunkMeshFormats.COMPACT); @@ -140,56 +138,97 @@ public void updateCameraState(Vector3dc cameraPosition, Camera camera) { private final IntArrayList traversalSamples = new IntArrayList(); + private void unpackPendingTree() { + try { + this.currentTree = this.pendingTree.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Failed to do graph search occlusion culling", e); + } + + this.taskLists = this.currentTree.getRebuildLists(); + this.pendingTree = null; + + this.needsGraphUpdate = false; + this.treeIsFrustumTested = false; + this.needsRenderListUpdate = true; + } + public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator) { this.lastUpdatedFrame += 1; - this.resetRenderLists(); + // TODO: preempt camera movement or make BFS work with movement (widen outgoing direction impl) - final var searchDistance = this.getSearchDistance(); - final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); + if (this.pendingTree != null && this.pendingTree.isDone()) { + this.unpackPendingTree(); + } - // preliminary implementation: re-do cull job on camera movement, always use the previous result - // TODO: preempt camera movement or make BFS work with movement (widen outgoing direction impl) + var treeCompatible = this.currentTree == null || !this.currentTree.isCompatibleWith(viewport); + if (this.cameraTimingControl.getShouldRenderSync(camera) && treeCompatible) { + // switch to sync rendering if the camera moved too much + final var searchDistance = this.getSearchDistance(); + final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); + + var start = System.nanoTime(); + this.currentTree = new LinearSectionOctree(viewport, searchDistance); + var visibleCollector = new VisibleChunkCollectorSync(this.currentTree, this.lastUpdatedFrame); + this.occlusionCuller.findVisible(visibleCollector, viewport, searchDistance, useOcclusionCulling, this.lastUpdatedFrame); + var end = System.nanoTime(); + + System.out.println("Sync graph search took " + (end - start) / 1000 + "µs"); + + this.renderLists = visibleCollector.createRenderLists(); + this.needsRenderListUpdate = false; + this.needsGraphUpdate = false; + + // make sure the bfs is re-done before just using this tree with an incompatible frustum + this.treeIsFrustumTested = true; + + return; + } // generate a section tree result with the occlusion culler if there currently is none - if (this.needsGraphUpdate() || this.currentTree == null || !this.currentTree.isAcceptableFor(viewport)) { - try { - this.currentTree = this.cullExecutor.submit(() -> { - var start = System.nanoTime(); - var tree = new LinearSectionOctree(viewport, searchDistance); - this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, this.lastUpdatedFrame); - var end = System.nanoTime(); - System.out.println("Graph search with tree build took " + (end - start) / 1000 + "µs"); - return tree; - }).get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException("Failed to cull chunks", e); + if ((this.needsGraphUpdate() || treeCompatible || this.treeIsFrustumTested) && this.pendingTree == null) { + final var searchDistance = this.getSearchDistance(); + final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); + + this.pendingTree = this.cullExecutor.submit(() -> { +// Thread.sleep(100); + var start = System.nanoTime(); + var tree = new LinearSectionOctree(viewport, searchDistance); + this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, this.lastUpdatedFrame); + var end = System.nanoTime(); + System.out.println("Graph search with tree build took " + (end - start) / 1000 + "µs"); + return tree; + }); + } + + if (this.needsRenderListUpdate()) { + // wait if there's no current tree (first frames) + if (this.currentTree == null) { + this.unpackPendingTree(); } - this.taskLists = this.currentTree.getRebuildLists(); - } - // there must a result there now, use it to generate render lists for the frame - var visibleCollector = new VisibleChunkCollector(this.regions, this.lastUpdatedFrame); - this.currentViewport = viewport; + var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.lastUpdatedFrame); - var start = System.nanoTime(); - this.currentTree.traverseVisible(visibleCollector, viewport); - var end = System.nanoTime(); + var start = System.nanoTime(); + this.currentTree.traverseVisible(visibleCollector, viewport); + var end = System.nanoTime(); - if (this.traversalSamples.size() == 2000) { - long sum = 0; - for (int i = 0; i < this.traversalSamples.size(); i++) { - sum += this.traversalSamples.getInt(i); + if (this.traversalSamples.size() == 2000) { + long sum = 0; + for (int i = 0; i < this.traversalSamples.size(); i++) { + sum += this.traversalSamples.getInt(i); + } + System.out.println("Tree traversal took " + sum / this.traversalSamples.size() + "µs on average over " + this.traversalSamples.size() + " samples"); + this.traversalSamples.clear(); + } else { + this.traversalSamples.add((int) (end - start) / 1000); } - System.out.println("Tree traversal took " + sum / this.traversalSamples.size() + "µs on average over " + this.traversalSamples.size() + " samples"); - this.traversalSamples.clear(); - } else { - this.traversalSamples.add((int) (end - start) / 1000); - } - this.renderLists = visibleCollector.createRenderLists(); + this.renderLists = visibleCollector.createRenderLists(); - this.needsUpdate = UpdateType.NONE; + this.needsRenderListUpdate = false; + } } private float getSearchDistance() { @@ -217,14 +256,6 @@ private boolean shouldUseOcclusionCulling(Camera camera, boolean spectator) { return useOcclusionCulling; } - private void resetRenderLists() { - this.renderLists = SortedRenderLists.empty(); - - for (var list : this.taskLists.values()) { - list.clear(); - } - } - public void onSectionAdded(int x, int y, int z) { long key = SectionPos.asLong(x, y, z); @@ -556,21 +587,23 @@ public void processGFNIMovement(CameraMovement movement) { } public void markGraphDirty() { - this.needsUpdate = UpdateType.GRAPH; + this.needsGraphUpdate = true; } - public void markFrustumDirty() { - if (this.needsUpdate == UpdateType.NONE) { - this.needsUpdate = UpdateType.FRUSTUM; - } + public void markRenderListDirty() { + this.needsRenderListUpdate = true; } public boolean needsAnyUpdate() { - return this.needsUpdate != UpdateType.NONE; + return this.needsGraphUpdate || this.needsRenderListUpdate || (this.pendingTree != null && this.pendingTree.isDone()); } public boolean needsGraphUpdate() { - return this.needsUpdate == UpdateType.GRAPH; + return this.needsGraphUpdate; + } + + public boolean needsRenderListUpdate() { + return this.needsRenderListUpdate; } public ChunkBuilder getBuilder() { @@ -591,7 +624,12 @@ public void destroy() { } this.sectionsWithGlobalEntities.clear(); - this.resetRenderLists(); + + this.renderLists = SortedRenderLists.empty(); + + for (var list : this.taskLists.values()) { + list.clear(); + } try (CommandList commandList = RenderDevice.INSTANCE.createCommandList()) { this.regions.delete(commandList); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java similarity index 86% rename from common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java rename to common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java index f8675a9eee..0f6a25fd19 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java @@ -3,7 +3,10 @@ import it.unimi.dsi.fastutil.ints.IntArrays; import it.unimi.dsi.fastutil.objects.ObjectArrayList; 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.occlusion.LinearSectionOctree; +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.chunk.region.RenderRegionManager; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; @@ -17,14 +20,14 @@ * The visible chunk collector is passed to the occlusion graph search culler to * collect the visible chunks. */ -public class VisibleChunkCollector implements LinearSectionOctree.VisibleSectionVisitor { +public class VisibleChunkCollectorAsync implements LinearSectionOctree.VisibleSectionVisitor { private final ObjectArrayList sortedRenderLists; private final RenderRegionManager regions; private final int frame; - public VisibleChunkCollector(RenderRegionManager regions, int frame) { + public VisibleChunkCollectorAsync(RenderRegionManager regions, int frame) { this.regions = regions; this.frame = frame; @@ -34,6 +37,12 @@ public VisibleChunkCollector(RenderRegionManager regions, int frame) { @Override public void visit(int x, int y, int z) { var region = this.regions.getForChunk(x, y, z); + + // since this is async, the region might have been removed in the meantime + if (region == null) { + return; + } + int rX = x & (RenderRegion.REGION_WIDTH - 1); int rY = y & (RenderRegion.REGION_HEIGHT - 1); int rZ = z & (RenderRegion.REGION_LENGTH - 1); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java new file mode 100644 index 0000000000..1bb9fbbc8a --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java @@ -0,0 +1,49 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +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.occlusion.LinearSectionOctree; +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.chunk.region.RenderRegionManager; + +public class VisibleChunkCollectorSync implements OcclusionCuller.GraphOcclusionVisitor { + private final ObjectArrayList sortedRenderLists; + + private final LinearSectionOctree tree; + private final int frame; + + public VisibleChunkCollectorSync(LinearSectionOctree tree, int frame) { + this.tree = tree; + this.frame = frame; + + this.sortedRenderLists = new ObjectArrayList<>(); + } + + @Override + public void visit(RenderSection section, boolean visible) { + this.tree.visit(section, visible); + + 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); + + this.sortedRenderLists.add(renderList); + } + + var index = section.getSectionIndex(); + if (visible && (region.getSectionFlags(index) & RenderSectionFlags.MASK_NEEDS_RENDER) != 0) { + renderList.add(index); + } + } + + public SortedRenderLists createRenderLists() { + return new SortedRenderLists(this.sortedRenderLists); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java new file mode 100644 index 0000000000..de2bc610d3 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java @@ -0,0 +1,39 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; + +import net.minecraft.client.Camera; +import net.minecraft.world.phys.Vec3; + +public class AsyncCameraTimingControl { + private static final double ENTER_SYNC_STEP_THRESHOLD = 16; + private static final double EXIT_SYNC_STEP_THRESHOLD = 10; + + private Vec3 previousPosition; + private boolean isSyncRendering = false; + + public boolean getShouldRenderSync(Camera camera) { + var cameraPosition = camera.getPosition(); + + if (this.previousPosition == null) { + this.previousPosition = cameraPosition; + return true; + } + + // if the camera moved too much, use sync rendering until it stops + var distance = Math.max( + Math.abs(cameraPosition.x - this.previousPosition.x), + Math.max( + Math.abs(cameraPosition.y - this.previousPosition.y), + Math.abs(cameraPosition.z - this.previousPosition.z) + ) + ); + if (this.isSyncRendering && distance <= EXIT_SYNC_STEP_THRESHOLD) { + this.isSyncRendering = false; + } else if (!this.isSyncRendering && distance >= ENTER_SYNC_STEP_THRESHOLD) { + this.isSyncRendering = true; + } + + this.previousPosition = cameraPosition; + + return this.isSyncRendering; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java index 6a49e454c9..1a4aee9c9c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java @@ -46,7 +46,7 @@ public LinearSectionOctree(Viewport viewport, float searchDistance) { this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); } - public boolean isAcceptableFor(Viewport viewport) { + public boolean isCompatibleWith(Viewport viewport) { var transform = viewport.getTransform(); return Math.abs((transform.intX >> 4) - this.buildSectionX) <= REUSE_MAX_SECTION_DISTANCE && Math.abs((transform.intY >> 4) - this.buildSectionY) <= REUSE_MAX_SECTION_DISTANCE From cb7e306b82d26d1c3b374ae438ccc0eb7dfc6859 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 6 Sep 2024 06:14:02 +0200 Subject: [PATCH 16/81] add better async bfs and tree management. It now calculates multiple trees of varying accuracy to present as few sections as possible while not generating any errors when the camera is in motion. --- .../client/render/SodiumWorldRenderer.java | 9 +- .../render/chunk/RenderSectionManager.java | 205 ++++++++++-------- .../occlusion/AsyncCameraTimingControl.java | 4 +- .../render/chunk/occlusion/CullType.java | 18 ++ .../chunk/occlusion/LinearSectionOctree.java | 50 +++-- 5 files changed, 180 insertions(+), 106 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index c08aa37b33..22dcc1d669 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -222,13 +222,10 @@ public void setupTerrain(Camera camera, } int maxChunkUpdates = updateChunksImmediately ? this.renderDistance : 1; - for (int i = 0; i < maxChunkUpdates; i++) { - if (this.renderSectionManager.needsAnyUpdate()) { - profiler.popPush("chunk_render_lists"); + profiler.popPush("chunk_render_lists"); - this.renderSectionManager.updateRenderLists(camera, viewport, spectator); - } + this.renderSectionManager.updateRenderLists(camera, viewport, spectator, updateChunksImmediately); profiler.popPush("chunk_update"); @@ -239,7 +236,7 @@ public void setupTerrain(Camera camera, this.renderSectionManager.uploadChunks(); - if (!this.renderSectionManager.needsAnyUpdate()) { + if (!this.renderSectionManager.needsGraphUpdate()) { break; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index c8c7af5ddc..8a2d21a09a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -1,7 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk; import com.mojang.blaze3d.systems.RenderSystem; -import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceMaps; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; @@ -23,10 +22,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.lists.SortedRenderLists; import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollectorAsync; import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollectorSync; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.AsyncCameraTimingControl; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.LinearSectionOctree; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.*; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; @@ -93,7 +89,8 @@ public class RenderSectionManager { @NotNull private Map> taskLists; - private int lastUpdatedFrame; + private int frame; + private int lastGraphDirtyFrame; private boolean needsGraphUpdate = true; private boolean needsRenderListUpdate = true; @@ -102,9 +99,11 @@ public class RenderSectionManager { private @Nullable Vector3dc cameraPosition; private final ExecutorService cullExecutor = Executors.newSingleThreadExecutor(); + private CullType pendingCullType = null; private Future pendingTree = null; - private LinearSectionOctree currentTree = null; - private boolean treeIsFrustumTested = false; + private LinearSectionOctree latestUpdatedTree = null; + private LinearSectionOctree renderTree = null; + private final EnumMap trees = new EnumMap<>(CullType.class); private final AsyncCameraTimingControl cameraTimingControl = new AsyncCameraTimingControl(); @@ -136,101 +135,157 @@ public void updateCameraState(Vector3dc cameraPosition, Camera camera) { this.cameraPosition = cameraPosition; } - private final IntArrayList traversalSamples = new IntArrayList(); - private void unpackPendingTree() { + if (this.pendingTree == null) { + throw new IllegalStateException("No pending tree to unpack"); + } + + LinearSectionOctree tree; try { - this.currentTree = this.pendingTree.get(); + tree = this.pendingTree.get(); } catch (InterruptedException | ExecutionException e) { throw new RuntimeException("Failed to do graph search occlusion culling", e); } - this.taskLists = this.currentTree.getRebuildLists(); + // TODO: improve task system by reading back the render lists and applying a more reasonable priority to sections than just whether they're visible or not. We also want currently out-of-frustum sections to eventually get built, since otherwise the world is missing when the player turns around. + // TODO: another problem with async bfs is that since the bfs is slower, it leads a slower iterate/load cycle since new sections only get discovered if the bfs traverses into them, which is only possible after building the section and generating its visibility data. + this.taskLists = tree.getRebuildLists(); this.pendingTree = null; + this.pendingCullType = null; - this.needsGraphUpdate = false; - this.treeIsFrustumTested = false; - this.needsRenderListUpdate = true; - } + // use tree if it can be used (frustum tested bfs results can't be used if the frustum has changed) + if (!(this.needsRenderListUpdate() && tree.getCullType() == CullType.FRUSTUM)) { + this.trees.put(tree.getCullType(), tree); + this.latestUpdatedTree = tree; - public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator) { - this.lastUpdatedFrame += 1; + this.needsGraphUpdate = false; + this.needsRenderListUpdate = true; + } + } - // TODO: preempt camera movement or make BFS work with movement (widen outgoing direction impl) + public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator, boolean updateImmediately) { + this.frame += 1; - if (this.pendingTree != null && this.pendingTree.isDone()) { - this.unpackPendingTree(); - } + // TODO: with more work it might be good to submit multiple async bfs tasks at once (so that more than one cull type's tree can be built each frame). However, this introduces the need for a lot more complicated decisions. What to do when a task is pending but will be invalid by the time it's completed? Which of multiple results should be used for rebuild task scheduling? Should pending tasks be replaced with more up to date tasks if they're running (or not running)? - var treeCompatible = this.currentTree == null || !this.currentTree.isCompatibleWith(viewport); - if (this.cameraTimingControl.getShouldRenderSync(camera) && treeCompatible) { + // when updating immediately (flawless frame), just do sync bfs continuously. + // if we're not updating immediately, the camera timing control should receive the new camera position each time. + if ((updateImmediately || this.cameraTimingControl.getShouldRenderSync(camera)) && + (this.needsGraphUpdate() || this.needsRenderListUpdate())) { // switch to sync rendering if the camera moved too much final var searchDistance = this.getSearchDistance(); final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); - var start = System.nanoTime(); - this.currentTree = new LinearSectionOctree(viewport, searchDistance); - var visibleCollector = new VisibleChunkCollectorSync(this.currentTree, this.lastUpdatedFrame); - this.occlusionCuller.findVisible(visibleCollector, viewport, searchDistance, useOcclusionCulling, this.lastUpdatedFrame); - var end = System.nanoTime(); + var tree = new LinearSectionOctree(viewport, searchDistance, this.frame, CullType.FRUSTUM); + var visibleCollector = new VisibleChunkCollectorSync(tree, this.frame); + this.occlusionCuller.findVisible(visibleCollector, viewport, searchDistance, useOcclusionCulling, this.frame); - System.out.println("Sync graph search took " + (end - start) / 1000 + "µs"); + this.trees.put(CullType.FRUSTUM, tree); + this.renderTree = tree; this.renderLists = visibleCollector.createRenderLists(); this.needsRenderListUpdate = false; this.needsGraphUpdate = false; - // make sure the bfs is re-done before just using this tree with an incompatible frustum - this.treeIsFrustumTested = true; - return; } - // generate a section tree result with the occlusion culler if there currently is none - if ((this.needsGraphUpdate() || treeCompatible || this.treeIsFrustumTested) && this.pendingTree == null) { - final var searchDistance = this.getSearchDistance(); - final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); - - this.pendingTree = this.cullExecutor.submit(() -> { -// Thread.sleep(100); - var start = System.nanoTime(); - var tree = new LinearSectionOctree(viewport, searchDistance); - this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, this.lastUpdatedFrame); - var end = System.nanoTime(); - System.out.println("Graph search with tree build took " + (end - start) / 1000 + "µs"); - return tree; - }); + if (this.needsGraphUpdate()) { + this.lastGraphDirtyFrame = this.frame; + this.needsGraphUpdate = false; } if (this.needsRenderListUpdate()) { - // wait if there's no current tree (first frames) - if (this.currentTree == null) { - this.unpackPendingTree(); + this.trees.remove(CullType.FRUSTUM); + + // discard unusable tree + if (this.pendingTree != null && !this.pendingTree.isDone() && this.pendingCullType == CullType.FRUSTUM) { + this.pendingTree.cancel(true); + this.pendingTree = null; + this.pendingCullType = null; } + } - var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.lastUpdatedFrame); + // unpack the pending tree any time there is one + if (this.pendingTree != null && this.pendingTree.isDone()) { + this.unpackPendingTree(); + } - var start = System.nanoTime(); - this.currentTree.traverseVisible(visibleCollector, viewport); - var end = System.nanoTime(); + // if we're not currently working on a tree, check if there's any more work that needs to be done + if (this.pendingTree == null) { + // regardless of whether the graph has been marked as dirty, working on the widest tree that hasn't yet been updated to match the current graph dirty frame is the best option. Since the graph dirty frame is updated if the graph has been marked as dirty, this also results in the whole cascade of trees being reset when the graph is marked as dirty. + + CullType workOnType = null; + for (var type : CullType.WIDE_TO_NARROW) { + var tree = this.trees.get(type); + if (tree == null) { + workOnType = type; + break; + } else { + var treeUpdateFrame = tree.getUpdateFrame(); + if (treeUpdateFrame < this.lastGraphDirtyFrame) { + workOnType = type; + break; + } + } + } + + if (workOnType != null) { + final var searchDistance = this.getSearchDistance(); + final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); + final var localType = workOnType; + + this.pendingCullType = localType; + this.pendingTree = this.cullExecutor.submit(() -> { + var tree = new LinearSectionOctree(viewport, searchDistance, this.frame, localType); + this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, this.frame); + return tree; + }); + } + } - if (this.traversalSamples.size() == 2000) { - long sum = 0; - for (int i = 0; i < this.traversalSamples.size(); i++) { - sum += this.traversalSamples.getInt(i); + if (this.needsRenderListUpdate()) { + // pick the narrowest up-to-date tree, if this tree is insufficiently up to date we would've switched to sync bfs earlier + LinearSectionOctree bestTree = null; + for (var type : CullType.NARROW_TO_WIDE) { + var tree = this.trees.get(type); + if (tree != null && (bestTree == null || tree.getUpdateFrame() > bestTree.getUpdateFrame())) { + bestTree = tree; } - System.out.println("Tree traversal took " + sum / this.traversalSamples.size() + "µs on average over " + this.traversalSamples.size() + " samples"); - this.traversalSamples.clear(); - } else { - this.traversalSamples.add((int) (end - start) / 1000); } + this.needsRenderListUpdate = false; + + // wait if there's no current tree (first frames) + if (bestTree == null) { + this.unpackPendingTree(); + bestTree = this.latestUpdatedTree; + } + + var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame); + bestTree.traverseVisible(visibleCollector, viewport); this.renderLists = visibleCollector.createRenderLists(); - this.needsRenderListUpdate = false; + this.renderTree = bestTree; } } + public void markGraphDirty() { + this.needsGraphUpdate = true; + } + + public void markRenderListDirty() { + this.needsRenderListUpdate = true; + } + + public boolean needsGraphUpdate() { + return this.needsGraphUpdate; + } + + public boolean needsRenderListUpdate() { + return this.needsRenderListUpdate; + } + private float getSearchDistance() { float distance; @@ -349,7 +404,7 @@ public void tickVisibleRenders() { } public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { - return this.currentTree == null || this.currentTree.isBoxVisible(x1, y1, z1, x2, y2, z2); + return this.renderTree == null || this.renderTree.isBoxVisible(x1, y1, z1, x2, y2, z2); } public void uploadChunks() { @@ -524,7 +579,7 @@ private void submitSectionTasks(ChunkJobCollector collector, ChunkUpdateType typ continue; } - int frame = this.lastUpdatedFrame; + int frame = this.frame; ChunkBuilderTask task; if (type == ChunkUpdateType.SORT || type == ChunkUpdateType.IMPORTANT_SORT) { task = this.createSortTask(section, frame); @@ -586,26 +641,6 @@ public void processGFNIMovement(CameraMovement movement) { this.sortTriggering.triggerSections(this::scheduleSort, movement); } - public void markGraphDirty() { - this.needsGraphUpdate = true; - } - - public void markRenderListDirty() { - this.needsRenderListUpdate = true; - } - - public boolean needsAnyUpdate() { - return this.needsGraphUpdate || this.needsRenderListUpdate || (this.pendingTree != null && this.pendingTree.isDone()); - } - - public boolean needsGraphUpdate() { - return this.needsGraphUpdate; - } - - public boolean needsRenderListUpdate() { - return this.needsRenderListUpdate; - } - public ChunkBuilder getBuilder() { return this.builder; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java index de2bc610d3..5d34e9a4f5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/AsyncCameraTimingControl.java @@ -4,8 +4,8 @@ import net.minecraft.world.phys.Vec3; public class AsyncCameraTimingControl { - private static final double ENTER_SYNC_STEP_THRESHOLD = 16; - private static final double EXIT_SYNC_STEP_THRESHOLD = 10; + private static final double ENTER_SYNC_STEP_THRESHOLD = 32; + private static final double EXIT_SYNC_STEP_THRESHOLD = 20; private Vec3 previousPosition; private boolean isSyncRendering = false; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java new file mode 100644 index 0000000000..5f266616c9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java @@ -0,0 +1,18 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; + +public enum CullType { + WIDE(1, false), + REGULAR(0, false), + FRUSTUM(0, true); + + public final int bfsWidth; + public final boolean isFrustumTested; + + public static final CullType[] WIDE_TO_NARROW = values(); + public static final CullType[] NARROW_TO_WIDE = {FRUSTUM, REGULAR, WIDE}; + + CullType(int bfsWidth, boolean isFrustumTested) { + this.bfsWidth = bfsWidth; + this.isFrustumTested = isFrustumTested; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java index 1a4aee9c9c..9239ee937c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java @@ -13,26 +13,35 @@ * ideas to prevent one frame of wrong display when BFS is recalculated but not ready yet: * - preemptively do the bfs from the next section the camera is going to be in, and maybe pad the render distance by how far the player can move before we need to recalculate. (if there's padding, then I guess the distance check would need to also be put in the traversal's test) * - a more experimental idea would be to allow the BFS to go both left and right (as it currently does in sections that are aligned with the origin section) in the sections aligned with the origin section's neighbors. This would mean we can safely use the bfs result in all neighbors, but could slightly increase the number of false positives (which is a problem already...) + * - 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 (?) */ public class LinearSectionOctree extends PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { + // offset is shifted by 1 to encompass all sections towards the negative + // TODO: is this the correct way of calculating the minimum possible section index? + private static final int TREE_OFFSET = 1; + final Tree mainTree; Tree secondaryTree; final int baseOffsetX, baseOffsetY, baseOffsetZ; final int buildSectionX, buildSectionY, buildSectionZ; - VisibleSectionVisitor visitor; - Viewport viewport; + private final int updateFrame; + private final CullType cullType; + private final int bfsWidth; + private final boolean isFrustumTested; - // offset is shifted by 1 to encompass all sections towards the negative - // TODO: is this the correct way of calculating the minimum possible section index? - private static final int TREE_OFFSET = 1; - private static final int REUSE_MAX_SECTION_DISTANCE = 0; + private VisibleSectionVisitor visitor; + private Viewport viewport; public interface VisibleSectionVisitor { void visit(int x, int y, int z); } - public LinearSectionOctree(Viewport viewport, float searchDistance) { + public LinearSectionOctree(Viewport viewport, float searchDistance, int updateFrame, CullType cullType) { + this.updateFrame = updateFrame; + this.cullType = cullType; + this.bfsWidth = cullType.bfsWidth; + this.isFrustumTested = cullType.isFrustumTested; var transform = viewport.getTransform(); int offsetDistance = Mth.floor(searchDistance / 16.0f) + TREE_OFFSET; @@ -46,16 +55,31 @@ public LinearSectionOctree(Viewport viewport, float searchDistance) { this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); } - public boolean isCompatibleWith(Viewport viewport) { - var transform = viewport.getTransform(); - return Math.abs((transform.intX >> 4) - this.buildSectionX) <= REUSE_MAX_SECTION_DISTANCE - && Math.abs((transform.intY >> 4) - this.buildSectionY) <= REUSE_MAX_SECTION_DISTANCE - && Math.abs((transform.intZ >> 4) - this.buildSectionZ) <= REUSE_MAX_SECTION_DISTANCE; + public int getUpdateFrame() { + return this.updateFrame; + } + + public CullType getCullType() { + return this.cullType; } @Override public boolean isWithinFrustum(Viewport viewport, RenderSection section) { - return true; + return !this.isFrustumTested || super.isWithinFrustum(viewport, section); + } + + @Override + public int getOutwardDirections(SectionPos origin, RenderSection section) { + int planes = 0; + + planes |= section.getChunkX() <= origin.getX() + this.bfsWidth ? 1 << GraphDirection.WEST : 0; + planes |= section.getChunkX() >= origin.getX() - this.bfsWidth ? 1 << GraphDirection.EAST : 0; + planes |= section.getChunkY() <= origin.getY() + this.bfsWidth ? 1 << GraphDirection.DOWN : 0; + planes |= section.getChunkY() >= origin.getY() - this.bfsWidth ? 1 << GraphDirection.UP : 0; + planes |= section.getChunkZ() <= origin.getZ() + this.bfsWidth ? 1 << GraphDirection.NORTH : 0; + planes |= section.getChunkZ() >= origin.getZ() - this.bfsWidth ? 1 << GraphDirection.SOUTH : 0; + + return planes; } @Override From 3ffa22efc56ee0bad64e1bd7ecfd38ac632d8fe1 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 6 Sep 2024 15:24:05 +0200 Subject: [PATCH 17/81] prevent some NPEs that happen because of concurrency with loading and unloading of chunks --- .../sodium/client/render/chunk/RenderSectionManager.java | 7 +++++++ .../client/render/chunk/occlusion/OcclusionCuller.java | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 8a2d21a09a..475232b3bb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -176,6 +176,13 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato final var searchDistance = this.getSearchDistance(); final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); + // cancel running task to prevent parallel bfs which will cause race conditions + if (this.pendingTree != null) { + this.pendingTree.cancel(true); + this.pendingTree = null; + this.pendingCullType = null; + } + var tree = new LinearSectionOctree(viewport, searchDistance, this.frame, CullType.FRUSTUM); var visibleCollector = new VisibleChunkCollectorSync(tree, this.frame); this.occlusionCuller.findVisible(visibleCollector, viewport, searchDistance, useOcclusionCulling, this.frame); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 65a752f9b6..5a0c126e0f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -201,7 +201,12 @@ private static void visitNeighbors(final WriteQueue queue, Render } } - private static void visitNode(final WriteQueue queue, @NotNull RenderSection render, int incoming, int frame) { + private static void visitNode(final WriteQueue queue, RenderSection render, int incoming, int frame) { + // isn't usually null, but can be null if the bfs is happening during loading or unloading of chunks + if (render == null) { + return; + } + if (render.getLastVisibleFrame() != frame) { // This is the first time we are visiting this section during the given frame, so we must // reset the state. From 453c9bbcd3930486e28d894e232eff2319bfe25f Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 6 Sep 2024 15:24:51 +0200 Subject: [PATCH 18/81] comment --- .../mods/sodium/client/render/chunk/RenderSectionManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 475232b3bb..1d2b9a7dd3 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -176,7 +176,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato final var searchDistance = this.getSearchDistance(); final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); - // cancel running task to prevent parallel bfs which will cause race conditions + // cancel running task to prevent two parallel calls to bfs which will cause race conditions if (this.pendingTree != null) { this.pendingTree.cancel(true); this.pendingTree = null; From f376ca788812b0cb52ad12b7a6ef7635b9c0e4a5 Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 7 Sep 2024 00:24:21 +0200 Subject: [PATCH 19/81] increase tasks per thread per frame to 3, this seems to usually be ok --- .../client/render/chunk/compile/executor/ChunkBuilder.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java index e251f27e89..933b493863 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java @@ -33,8 +33,9 @@ public class ChunkBuilder { */ public static final int HIGH_EFFORT = 10; public static final int LOW_EFFORT = 1; - public static final int EFFORT_PER_THREAD_PER_FRAME = HIGH_EFFORT + LOW_EFFORT; - private static final float HIGH_EFFORT_BUDGET_FACTOR = (float)HIGH_EFFORT / EFFORT_PER_THREAD_PER_FRAME; + public static final int EFFORT_UNIT = HIGH_EFFORT + LOW_EFFORT; + public static final int EFFORT_PER_THREAD_PER_FRAME = 3 * EFFORT_UNIT; + private static final float HIGH_EFFORT_BUDGET_FACTOR = (float)HIGH_EFFORT / EFFORT_UNIT; static final Logger LOGGER = LogManager.getLogger("ChunkBuilder"); From 1b1f453488c6cbc2940ccab3097532690b29b807 Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 7 Sep 2024 01:02:29 +0200 Subject: [PATCH 20/81] do render distance and fog culling in the tree traversal to prevent issues when the distance changes (for example under water) --- .../render/chunk/RenderSectionManager.java | 27 +-- .../chunk/occlusion/LinearSectionOctree.java | 186 ++++++++++++++---- .../chunk/occlusion/OcclusionCuller.java | 6 +- .../client/render/viewport/Viewport.java | 32 ++- 4 files changed, 177 insertions(+), 74 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 1d2b9a7dd3..6bc20c4f77 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -176,7 +176,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato final var searchDistance = this.getSearchDistance(); final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); - // cancel running task to prevent two parallel calls to bfs which will cause race conditions + // cancel running task to prevent two bfs running at the same time, which will cause race conditions if (this.pendingTree != null) { this.pendingTree.cancel(true); this.pendingTree = null; @@ -190,6 +190,10 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato this.trees.put(CullType.FRUSTUM, tree); this.renderTree = tree; + // remove the other trees, they're very wrong by now + this.trees.remove(CullType.WIDE); + this.trees.remove(CullType.REGULAR); + this.renderLists = visibleCollector.createRenderLists(); this.needsRenderListUpdate = false; this.needsGraphUpdate = false; @@ -203,9 +207,8 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato } if (this.needsRenderListUpdate()) { + // discard unusable present and pending trees this.trees.remove(CullType.FRUSTUM); - - // discard unusable tree if (this.pendingTree != null && !this.pendingTree.isDone() && this.pendingCullType == CullType.FRUSTUM) { this.pendingTree.cancel(true); this.pendingTree = null; @@ -218,7 +221,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato this.unpackPendingTree(); } - // if we're not currently working on a tree, check if there's any more work that needs to be done + // check if more work needs to be done if none is currently being done if (this.pendingTree == null) { // regardless of whether the graph has been marked as dirty, working on the widest tree that hasn't yet been updated to match the current graph dirty frame is the best option. Since the graph dirty frame is updated if the graph has been marked as dirty, this also results in the whole cascade of trees being reset when the graph is marked as dirty. @@ -229,7 +232,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato workOnType = type; break; } else { - var treeUpdateFrame = tree.getUpdateFrame(); + var treeUpdateFrame = tree.getFrame(); if (treeUpdateFrame < this.lastGraphDirtyFrame) { workOnType = type; break; @@ -238,14 +241,14 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato } if (workOnType != null) { - final var searchDistance = this.getSearchDistance(); - final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); final var localType = workOnType; + final var localRenderDistance = this.getRenderDistance(); + final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); this.pendingCullType = localType; this.pendingTree = this.cullExecutor.submit(() -> { - var tree = new LinearSectionOctree(viewport, searchDistance, this.frame, localType); - this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, this.frame); + var tree = new LinearSectionOctree(viewport, localRenderDistance, this.frame, localType); + this.occlusionCuller.findVisible(tree, viewport, localRenderDistance, useOcclusionCulling, this.frame); return tree; }); } @@ -256,21 +259,21 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato LinearSectionOctree bestTree = null; for (var type : CullType.NARROW_TO_WIDE) { var tree = this.trees.get(type); - if (tree != null && (bestTree == null || tree.getUpdateFrame() > bestTree.getUpdateFrame())) { + if (tree != null && (bestTree == null || tree.getFrame() > bestTree.getFrame())) { bestTree = tree; } } this.needsRenderListUpdate = false; - // wait if there's no current tree (first frames) + // wait if there's no current tree (first frames after initial load/reload) if (bestTree == null) { this.unpackPendingTree(); bestTree = this.latestUpdatedTree; } var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame); - bestTree.traverseVisible(visibleCollector, viewport); + bestTree.traverseVisible(visibleCollector, viewport, this.getSearchDistance()); this.renderLists = visibleCollector.createRenderLists(); this.renderTree = bestTree; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java index 9239ee937c..2856e1f97b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java @@ -25,26 +25,29 @@ public class LinearSectionOctree extends PendingTaskCollector implements Occlusi final int baseOffsetX, baseOffsetY, baseOffsetZ; final int buildSectionX, buildSectionY, buildSectionZ; - private final int updateFrame; - private final CullType cullType; private final int bfsWidth; private final boolean isFrustumTested; + private final float buildDistance; + private final int frame; + private final CullType cullType; private VisibleSectionVisitor visitor; private Viewport viewport; + private float distanceLimit; public interface VisibleSectionVisitor { void visit(int x, int y, int z); } - public LinearSectionOctree(Viewport viewport, float searchDistance, int updateFrame, CullType cullType) { - this.updateFrame = updateFrame; - this.cullType = cullType; + public LinearSectionOctree(Viewport viewport, float buildDistance, int frame, CullType cullType) { this.bfsWidth = cullType.bfsWidth; this.isFrustumTested = cullType.isFrustumTested; + this.buildDistance = buildDistance; + this.frame = frame; + this.cullType = cullType; var transform = viewport.getTransform(); - int offsetDistance = Mth.floor(searchDistance / 16.0f) + TREE_OFFSET; + int offsetDistance = Mth.floor(buildDistance / 16.0f) + TREE_OFFSET; this.buildSectionX = transform.intX >> 4; this.buildSectionY = transform.intY >> 4; this.buildSectionZ = transform.intZ >> 4; @@ -55,14 +58,14 @@ public LinearSectionOctree(Viewport viewport, float searchDistance, int updateFr this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); } - public int getUpdateFrame() { - return this.updateFrame; - } - public CullType getCullType() { return this.cullType; } + public int getFrame() { + return this.frame; + } + @Override public boolean isWithinFrustum(Viewport viewport, RenderSection section) { return !this.isFrustumTested || super.isWithinFrustum(viewport, section); @@ -134,9 +137,14 @@ private boolean isSectionPresent(int x, int y, int z) { (this.secondaryTree != null && this.secondaryTree.isSectionPresent(x, y, z)); } - public void traverseVisible(VisibleSectionVisitor visitor, Viewport viewport) { + private boolean isDistanceLimitActive() { + return LinearSectionOctree.this.distanceLimit < LinearSectionOctree.this.buildDistance; + } + + public void traverseVisible(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { this.visitor = visitor; this.viewport = viewport; + this.distanceLimit = distanceLimit; this.mainTree.traverse(viewport); if (this.secondaryTree != null) { @@ -155,6 +163,10 @@ private class Tree { private int cameraOffsetX, cameraOffsetY, cameraOffsetZ; + private static final int INSIDE_FRUSTUM = 0b01; + private static final int INSIDE_DISTANCE = 0b10; + private static final int FULLY_INSIDE = 0b11; + Tree(int offsetX, int offsetY, int offsetZ) { this.offsetX = offsetX; this.offsetY = offsetY; @@ -224,10 +236,11 @@ void traverse(Viewport viewport) { this.cameraOffsetY = (transform.intY >> 4) - this.offsetY + 1; this.cameraOffsetZ = (transform.intZ >> 4) - this.offsetZ + 1; - this.traverse(0, 0, 0, 0, 5, false); + var initialInside = LinearSectionOctree.this.isDistanceLimitActive() ? 0 : INSIDE_DISTANCE; + this.traverse(0, 0, 0, 0, 5, initialInside); } - void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, boolean inside) { + void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, int inside) { int childHalfDim = 1 << (level + 3); // * 16 / 2 int orderModulator = getChildOrderModulator(nodeX, nodeY, nodeZ, childHalfDim >> 3); if ((level & 1) == 1) { @@ -251,7 +264,7 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, boolea int y = deinterleave6(sectionOrigin >> 1) + this.offsetY; int z = deinterleave6(sectionOrigin >> 2) + this.offsetZ; - if (inside || testNode(x, y, z, childHalfDim)) { + if (inside == FULLY_INSIDE || testLeafNode(x, y, z, inside)) { LinearSectionOctree.this.visitor.visit(x, y, z); } } @@ -272,7 +285,7 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, boolea int startBit = (nodeOrigin >> 6) & 0b111111; int endBit = startBit + 8; - for (int bitIndex = startBit; bitIndex < endBit; bitIndex ++) { + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { int childIndex = bitIndex ^ orderModulator; if ((map & (1L << childIndex)) != 0) { this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); @@ -291,7 +304,7 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, boolea int startBit = nodeOrigin >> 12; int endBit = startBit + 8; - for (int bitIndex = startBit; bitIndex < endBit; bitIndex ++) { + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { int childIndex = bitIndex ^ orderModulator; if ((this.treeDoubleReduced & (1L << childIndex)) != 0) { this.testChild(childIndex << 12, childHalfDim, level, inside); @@ -308,42 +321,134 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, boolea } } - void testChild(int childOrigin, int childDim, int level, boolean inside) { + void testChild(int childOrigin, int childHalfDim, int level, int inside) { + // calculate section coordinates in tree-space int x = deinterleave6(childOrigin); int y = deinterleave6(childOrigin >> 1); int z = deinterleave6(childOrigin >> 2); - boolean intersection = false; - if (!inside) { - // TODO: actually measure time to generate a render list on dev and compare - var result = intersectNode(x + this.offsetX, y + this.offsetY, z + this.offsetZ, childDim); - inside = result == FrustumIntersection.INSIDE; - intersection = result == FrustumIntersection.INTERSECT; + // immediately traverse if fully inside + if (inside == FULLY_INSIDE) { + this.traverse(x, y, z, childOrigin, level - 1, inside); + return; + } + + // convert to world-space section origin in blocks, then to camera space + var transform = LinearSectionOctree.this.viewport.getTransform(); + x = ((x + this.offsetX) << 4) - transform.intX; + y = ((y + this.offsetY) << 4) - transform.intY; + z = ((z + this.offsetZ) << 4) - transform.intZ; + + boolean visible = true; + + if ((inside & INSIDE_FRUSTUM) == 0) { + var intersectionResult = LinearSectionOctree.this.viewport.getBoxIntersectionDirect( + (x + childHalfDim) - transform.fracX, + (y + childHalfDim) - transform.fracY, + (z + childHalfDim) - transform.fracZ, + childHalfDim + OcclusionCuller.CHUNK_SECTION_MARGIN); + if (intersectionResult == FrustumIntersection.INSIDE) { + inside |= INSIDE_FRUSTUM; + } else { + visible = intersectionResult == FrustumIntersection.INTERSECT; + } + } + + if ((inside & INSIDE_DISTANCE) == 0) { + // calculate the point of the node closest to the camera + int childFullDim = childHalfDim << 1; + float dx = nearestToZero(x, x + childFullDim) - transform.fracX; + float dy = nearestToZero(y, y + childFullDim) - transform.fracY; + float dz = nearestToZero(z, z + childFullDim) - transform.fracZ; + + // check if closest point inside the cylinder + visible = cylindricalDistanceTest(dx, dy, dz, LinearSectionOctree.this.distanceLimit); + if (visible) { + // if the farthest point is also visible, the node is fully inside + dx = farthestFromZero(x, x + childFullDim) - transform.fracX; + dy = farthestFromZero(y, y + childFullDim) - transform.fracY; + dz = farthestFromZero(z, z + childFullDim) - transform.fracZ; + + if (cylindricalDistanceTest(dx, dy, dz, LinearSectionOctree.this.distanceLimit)) { + inside |= INSIDE_DISTANCE; + } + } } - if (inside || intersection) { + if (visible) { this.traverse(x, y, z, childOrigin, level - 1, inside); } } - boolean testNode(int x, int y, int z, int childDim) { - return LinearSectionOctree.this.viewport.isBoxVisible( - (x << 4) + childDim, - (y << 4) + childDim, - (z << 4) + childDim, - childDim + OcclusionCuller.CHUNK_SECTION_SIZE, - childDim + OcclusionCuller.CHUNK_SECTION_SIZE, - childDim + OcclusionCuller.CHUNK_SECTION_SIZE); + boolean testLeafNode(int x, int y, int z, int inside) { + // input coordinates are section coordinates in world-space + + var transform = LinearSectionOctree.this.viewport.getTransform(); + + // convert to blocks and move into integer camera space + x = (x << 4) - transform.intX; + y = (y << 4) - transform.intY; + z = (z << 4) - transform.intZ; + + // test frustum if not already inside frustum + if ((inside & INSIDE_FRUSTUM) == 0 && !LinearSectionOctree.this.viewport.isBoxVisibleDirect( + (x + 8) - transform.fracX, + (y + 8) - transform.fracY, + (z + 8) - transform.fracZ, + OcclusionCuller.CHUNK_SECTION_RADIUS)) { + return false; + } + + // test distance if not already inside distance + if ((inside & INSIDE_DISTANCE) == 0) { + // coordinates of the point to compare (in view space) + // this is the closest point within the bounding box to the center (0, 0, 0) + float dx = nearestToZero(x, x + 16) - transform.fracX; + float dy = nearestToZero(y, y + 16) - transform.fracY; + float dz = nearestToZero(z, z + 16) - transform.fracZ; + + return cylindricalDistanceTest(dx, dy, dz, LinearSectionOctree.this.distanceLimit); + } + + return true; } - int intersectNode(int x, int y, int z, int childDim) { - return LinearSectionOctree.this.viewport.getBoxIntersection( - (x << 4) + childDim, - (y << 4) + childDim, - (z << 4) + childDim, - childDim + OcclusionCuller.CHUNK_SECTION_SIZE, - childDim + OcclusionCuller.CHUNK_SECTION_SIZE, - childDim + OcclusionCuller.CHUNK_SECTION_SIZE); + static boolean cylindricalDistanceTest(float dx, float dy, float dz, float distanceLimit) { + // vanilla's "cylindrical fog" algorithm + // max(length(distance.xz), abs(distance.y)) + return (((dx * dx) + (dz * dz)) < (distanceLimit * distanceLimit)) && + (Math.abs(dy) < distanceLimit); + } + + @SuppressWarnings("ManualMinMaxCalculation") // we know what we are doing. + private static int nearestToZero(int min, int max) { + // this compiles to slightly better code than Math.min(Math.max(0, min), max) + int clamped = 0; + if (min > 0) { + clamped = min; + } + if (max < 0) { + clamped = max; + } + return clamped; + } + + private static int farthestFromZero(int min, int max) { + int clamped = 0; + if (min > 0) { + clamped = max; + } + if (max < 0) { + clamped = min; + } + if (clamped == 0) { + if (Math.abs(min) > Math.abs(max)) { + clamped = min; + } else { + clamped = max; + } + } + return clamped; } int getChildOrderModulator(int x, int y, int z, int childSectionDim) { @@ -352,5 +457,4 @@ int getChildOrderModulator(int x, int y, int z, int childSectionDim) { | ((z + childSectionDim - this.cameraOffsetZ) >>> 31) << 2; } } - } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 5a0c126e0f..4b5282dfc6 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -10,7 +10,6 @@ import net.minecraft.core.SectionPos; import net.minecraft.util.Mth; import net.minecraft.world.level.Level; -import org.jetbrains.annotations.NotNull; public class OcclusionCuller { private final Long2ReferenceMap sections; @@ -22,14 +21,15 @@ public class OcclusionCuller { // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon // to deal with floating point imprecision during a frustum check (see GH#2132). static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; - static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + 1.0f /* maximum model extent */ + 0.125f /* epsilon */; + static final float CHUNK_SECTION_MARGIN = 1.0f /* maximum model extent */ + 0.125f /* epsilon */; + static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + CHUNK_SECTION_MARGIN; public interface GraphOcclusionVisitor { void visit(RenderSection section); default boolean isWithinFrustum(Viewport viewport, RenderSection section) { return viewport.isBoxVisible(section.getCenterX(), section.getCenterY(), section.getCenterZ(), - CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE, CHUNK_SECTION_SIZE); + CHUNK_SECTION_RADIUS, CHUNK_SECTION_RADIUS, CHUNK_SECTION_RADIUS); } default int getOutwardDirections(SectionPos origin, RenderSection section) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java index 3cdeac8dc0..999310be3c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/viewport/Viewport.java @@ -41,31 +41,27 @@ public boolean isBoxVisible(int intOriginX, int intOriginY, int intOriginZ, floa ); } - public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { + public boolean isBoxVisibleDirect(float floatOriginX, float floatOriginY, float floatOriginZ, float floatSize) { return this.frustum.testAab( - (float)((x1 - this.transform.intX) - this.transform.fracX), - (float)((y1 - this.transform.intY) - this.transform.fracY), - (float)((z1 - this.transform.intZ) - this.transform.fracZ), + floatOriginX - floatSize, + floatOriginY - floatSize, + floatOriginZ - floatSize, - (float)((x2 - this.transform.intX) - this.transform.fracX), - (float)((y2 - this.transform.intY) - this.transform.fracY), - (float)((z2 - this.transform.intZ) - this.transform.fracZ) + floatOriginX + floatSize, + floatOriginY + floatSize, + floatOriginZ + floatSize ); } - public int getBoxIntersection(int intOriginX, int intOriginY, int intOriginZ, float floatSizeX, float floatSizeY, float floatSizeZ) { - float floatOriginX = (intOriginX - this.transform.intX) - this.transform.fracX; - float floatOriginY = (intOriginY - this.transform.intY) - this.transform.fracY; - float floatOriginZ = (intOriginZ - this.transform.intZ) - this.transform.fracZ; - + public int getBoxIntersectionDirect(float floatOriginX, float floatOriginY, float floatOriginZ, float floatSize) { return this.frustum.intersectAab( - floatOriginX - floatSizeX, - floatOriginY - floatSizeY, - floatOriginZ - floatSizeZ, + floatOriginX - floatSize, + floatOriginY - floatSize, + floatOriginZ - floatSize, - floatOriginX + floatSizeX, - floatOriginY + floatSizeY, - floatOriginZ + floatSizeZ + floatOriginX + floatSize, + floatOriginY + floatSize, + floatOriginZ + floatSize ); } From c296bd723dcd147901b82e38205ddac452f73304 Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 7 Sep 2024 04:25:50 +0200 Subject: [PATCH 21/81] don't do frustum bfs if the frame was dirty to prevent unnecessary work --- .../client/render/SodiumWorldRenderer.java | 2 +- .../render/chunk/RenderSectionManager.java | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index 22dcc1d669..bd27b04df1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -236,7 +236,7 @@ public void setupTerrain(Camera camera, this.renderSectionManager.uploadChunks(); - if (!this.renderSectionManager.needsGraphUpdate()) { + if (!this.renderSectionManager.needsUpdate()) { break; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 6bc20c4f77..ebdea1482e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -154,7 +154,7 @@ private void unpackPendingTree() { this.pendingCullType = null; // use tree if it can be used (frustum tested bfs results can't be used if the frustum has changed) - if (!(this.needsRenderListUpdate() && tree.getCullType() == CullType.FRUSTUM)) { + if (!(this.needsRenderListUpdate && tree.getCullType() == CullType.FRUSTUM)) { this.trees.put(tree.getCullType(), tree); this.latestUpdatedTree = tree; @@ -171,7 +171,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato // when updating immediately (flawless frame), just do sync bfs continuously. // if we're not updating immediately, the camera timing control should receive the new camera position each time. if ((updateImmediately || this.cameraTimingControl.getShouldRenderSync(camera)) && - (this.needsGraphUpdate() || this.needsRenderListUpdate())) { + (this.needsGraphUpdate || this.needsRenderListUpdate)) { // switch to sync rendering if the camera moved too much final var searchDistance = this.getSearchDistance(); final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); @@ -201,12 +201,12 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato return; } - if (this.needsGraphUpdate()) { + if (this.needsGraphUpdate) { this.lastGraphDirtyFrame = this.frame; this.needsGraphUpdate = false; } - if (this.needsRenderListUpdate()) { + if (this.needsRenderListUpdate) { // discard unusable present and pending trees this.trees.remove(CullType.FRUSTUM); if (this.pendingTree != null && !this.pendingTree.isDone() && this.pendingCullType == CullType.FRUSTUM) { @@ -228,6 +228,13 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato CullType workOnType = null; for (var type : CullType.WIDE_TO_NARROW) { var tree = this.trees.get(type); + + // don't schedule frustum-culled bfs until there hasn't been a dirty render list. + // otherwise a bfs is done each frame that gets thrown away each time. + if (type == CullType.FRUSTUM && this.needsRenderListUpdate) { + continue; + } + if (tree == null) { workOnType = type; break; @@ -254,7 +261,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato } } - if (this.needsRenderListUpdate()) { + if (this.needsRenderListUpdate) { // pick the narrowest up-to-date tree, if this tree is insufficiently up to date we would've switched to sync bfs earlier LinearSectionOctree bestTree = null; for (var type : CullType.NARROW_TO_WIDE) { @@ -288,14 +295,10 @@ public void markRenderListDirty() { this.needsRenderListUpdate = true; } - public boolean needsGraphUpdate() { + public boolean needsUpdate() { return this.needsGraphUpdate; } - public boolean needsRenderListUpdate() { - return this.needsRenderListUpdate; - } - private float getSearchDistance() { float distance; From 1fd9d2382c135624f45d8466255d37e49dcac0bf Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 13 Sep 2024 05:34:49 +0200 Subject: [PATCH 22/81] write a new async culling task and result system - rebuild tasks are scheduled in a queue and pruned each frame - async culling tasks and results are classes in their own package - chunk rebuild tasks are prioritized based on their distance to the camera, their type, how long the task has been pending, and whether the section is currently visible (in the frustum) - generally cleaned up the update method in RSM --- .../client/render/chunk/ChunkUpdateType.java | 34 +- .../sodium/client/render/chunk/DeferMode.java | 5 + .../client/render/chunk/RenderSection.java | 46 +- .../render/chunk/RenderSectionManager.java | 436 +++++++++++------- .../render/chunk/async/AsyncRenderTask.java | 59 +++ .../client/render/chunk/async/CullTask.java | 22 + .../render/chunk/async/FrustumCullResult.java | 7 + .../render/chunk/async/FrustumCullTask.java | 38 ++ .../async/FrustumTaskCollectionTask.java | 28 ++ .../chunk/async/FrustumTaskListsResult.java | 7 + .../render/chunk/async/GlobalCullResult.java | 10 + .../render/chunk/async/GlobalCullTask.java | 55 +++ .../chunk/compile/executor/ChunkBuilder.java | 4 +- .../compile/executor/ChunkJobCollector.java | 41 +- .../chunk/lists/FrustumTaskCollector.java | 28 ++ .../chunk/lists/PendingTaskCollector.java | 165 ++++++- .../render/chunk/lists/TaskSectionTree.java | 43 ++ .../lists/VisibleChunkCollectorAsync.java | 7 +- .../lists/VisibleChunkCollectorSync.java | 20 +- .../chunk/occlusion/OcclusionCuller.java | 66 +-- ...earSectionOctree.java => SectionTree.java} | 131 +++--- .../translucent_sorting/SortBehavior.java | 14 +- 22 files changed, 895 insertions(+), 371 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullResult.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FrustumTaskCollector.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java rename common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/{LinearSectionOctree.java => SectionTree.java} (78%) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java index 2cd7859716..f3a9197920 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java @@ -1,20 +1,18 @@ package net.caffeinemc.mods.sodium.client.render.chunk; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; - public enum ChunkUpdateType { - SORT(Integer.MAX_VALUE, ChunkBuilder.LOW_EFFORT), - INITIAL_BUILD(128, ChunkBuilder.HIGH_EFFORT), - REBUILD(Integer.MAX_VALUE, ChunkBuilder.HIGH_EFFORT), - IMPORTANT_REBUILD(Integer.MAX_VALUE, ChunkBuilder.HIGH_EFFORT), - IMPORTANT_SORT(Integer.MAX_VALUE, ChunkBuilder.LOW_EFFORT); - - private final int maximumQueueSize; - private final int taskEffort; - - ChunkUpdateType(int maximumQueueSize, int taskEffort) { - this.maximumQueueSize = maximumQueueSize; - this.taskEffort = taskEffort; + SORT(DeferMode.ALWAYS, 2), + INITIAL_BUILD(DeferMode.ALWAYS, 0), + REBUILD(DeferMode.ALWAYS, 1), + IMPORTANT_REBUILD(DeferMode.ONE_FRAME, 1), + IMPORTANT_SORT(DeferMode.ZERO_FRAMES, 2); + + private final DeferMode deferMode; + private final float priorityValue; + + ChunkUpdateType(DeferMode deferMode, float priorityValue) { + this.deferMode = deferMode; + this.priorityValue = priorityValue; } public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, ChunkUpdateType next) { @@ -29,15 +27,15 @@ public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, Chunk return null; } - public int getMaximumQueueSize() { - return this.maximumQueueSize; + public DeferMode getDeferMode() { + return this.deferMode; } public boolean isImportant() { return this == IMPORTANT_REBUILD || this == IMPORTANT_SORT; } - public int getTaskEffort() { - return this.taskEffort; + public float getPriorityValue() { + return this.priorityValue; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java new file mode 100644 index 0000000000..5201b5aa8c --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java @@ -0,0 +1,5 @@ +package net.caffeinemc.mods.sodium.client.render.chunk; + +public enum DeferMode { + ALWAYS, ONE_FRAME, ZERO_FRAMES +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java index 2d6d3f22ac..d995af3487 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java @@ -7,13 +7,32 @@ import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.TranslucentData; import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; -import net.minecraft.client.renderer.texture.TextureAtlasSprite; import net.minecraft.core.BlockPos; import net.minecraft.core.SectionPos; -import net.minecraft.world.level.block.entity.BlockEntity; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +// TODO: idea for topic-related section data storage: have an array of encoded data as longs (or ints) and enter or remove sections from it as they get loaded and unloaded. Keep track of empty spots in this array by building a single-linked free-list, using a bit to see if there's actually still data there or if it's free now. Newly loaded sections get the first available free entry and the location of this entry is stored in the section object for reference. The index of the next free entry is stored in a "head" pointer, and when a section gets removed it needs to "mend" the hole created by the removal by pointing its entry to the head and the head to it. We could also store more data in multiple longs by just treating multiple as one "entry". It could regularly re-organize the data to compact it and move nearby sections to nearby positions in the array (z order curve). This structure should be generic so various types of section data can be stored in it. +// problem: how does it initialize the occlusion culling? does it need to use the position-to-section hashmap to get the section objects and then extract the indexes? might be ok since it's just the init + +/* "struct" layout for occlusion culling: +- 1 bit for entry management (is present or not), if zero, the rest of the first 8 bytes is the next free entry index +- 24 bits = 3 bytes for the section position (x, y, z with each 0-255) +- 36 bits for the visibility data (6 bits per direction, 6 directions) +- 6 bits for adjacent mask +- 6 bits for incoming directions +- 144 bits = 18 bytes = 6 * 3 bytes for the adjacent sections (up to 6 directions, 3 bytes for section index) +- 32 bits for the last visible frame + */ + +/* "struct" layout for task management: +- 1 bit for entry management (entry deleted if disposed) +- 1 bit for if there's a running task +- 5 bits for the pending update type (ChunkUpdateType) +- 24 bits for the pending time since start in milliseconds +- 24 bits = 3 bytes for the section position (x, y, z with each 0-255), + */ + /** * The render state object for a chunk section. This contains all the graphics state for each render pass along with * data about the render in the chunk visibility graph. @@ -30,7 +49,7 @@ public class RenderSection { private long visibilityData = VisibilityEncoding.NULL; private int incomingDirections; - private int lastVisibleFrame = -1; + private int lastVisibleSearchToken = -1; private int adjacentMask; public RenderSection @@ -41,7 +60,6 @@ public class RenderSection { adjacentWest, adjacentEast; - // Rendering State @Nullable private TranslucentData translucentData; @@ -52,6 +70,7 @@ public class RenderSection { @Nullable private ChunkUpdateType pendingUpdateType; + private long pendingUpdateSince; private int lastUploadFrame = -1; private int lastSubmittedFrame = -1; @@ -267,12 +286,12 @@ public RenderRegion getRegion() { return this.region; } - public void setLastVisibleFrame(int frame) { - this.lastVisibleFrame = frame; + public void setLastVisibleSearchToken(int frame) { + this.lastVisibleSearchToken = frame; } - public int getLastVisibleFrame() { - return this.lastVisibleFrame; + public int getLastVisibleSearchToken() { + return this.lastVisibleSearchToken; } public int getIncomingDirections() { @@ -306,8 +325,17 @@ public void setTaskCancellationToken(@Nullable CancellationToken token) { return this.pendingUpdateType; } - public void setPendingUpdate(@Nullable ChunkUpdateType type) { + public long getPendingUpdateSince() { + return this.pendingUpdateSince; + } + + public void setPendingUpdate(ChunkUpdateType type, long now) { this.pendingUpdateType = type; + this.pendingUpdateSince = now; + } + + public void clearPendingUpdate() { + this.pendingUpdateType = null; } public void prepareTrigger(boolean isDirectTrigger) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index ebdea1482e..109ba0e4a2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -4,10 +4,12 @@ import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.longs.Long2ReferenceMaps; import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; import it.unimi.dsi.fastutil.objects.*; import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.gl.device.CommandList; import net.caffeinemc.mods.sodium.client.gl.device.RenderDevice; +import net.caffeinemc.mods.sodium.client.render.chunk.async.*; import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkSortOutput; @@ -18,15 +20,11 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderSortingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.ChunkRenderList; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.SortedRenderLists; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollectorAsync; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.VisibleChunkCollectorSync; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.*; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.*; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; import net.caffeinemc.mods.sodium.client.render.chunk.terrain.TerrainRenderPass; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior.DeferMode; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior.PriorityMode; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicTopoData; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.NoData; @@ -60,6 +58,9 @@ import java.util.concurrent.*; public class RenderSectionManager { + private static final float NEARBY_REBUILD_DISTANCE = Mth.square(16.0f); + private static final float NEARBY_SORT_DISTANCE = Mth.square(25.0f); + private final ChunkBuilder builder; private final RenderRegionManager regions; @@ -86,11 +87,12 @@ public class RenderSectionManager { @NotNull private SortedRenderLists renderLists; - @NotNull - private Map> taskLists; + private PendingTaskCollector.TaskListCollection frustumTaskLists; + private PendingTaskCollector.TaskListCollection globalTaskLists; private int frame; private int lastGraphDirtyFrame; + private long lastFrameAtTime = System.nanoTime(); private boolean needsGraphUpdate = true; private boolean needsRenderListUpdate = true; @@ -98,12 +100,15 @@ public class RenderSectionManager { private @Nullable BlockPos cameraBlockPos; private @Nullable Vector3dc cameraPosition; - private final ExecutorService cullExecutor = Executors.newSingleThreadExecutor(); - private CullType pendingCullType = null; - private Future pendingTree = null; - private LinearSectionOctree latestUpdatedTree = null; - private LinearSectionOctree renderTree = null; - private final EnumMap trees = new EnumMap<>(CullType.class); + private final ExecutorService asyncCullExecutor = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable); + thread.setName("Sodium Async Cull Thread"); + return thread; + }); + private final ObjectArrayList> pendingTasks = new ObjectArrayList<>(); + private SectionTree renderTree = null; + private TaskSectionTree globalTaskTree = null; + private final Map trees = new EnumMap<>(CullType.class); private final AsyncCameraTimingControl cameraTimingControl = new AsyncCameraTimingControl(); @@ -122,12 +127,6 @@ public RenderSectionManager(ClientLevel level, int renderDistance, CommandList c this.renderLists = SortedRenderLists.empty(); this.occlusionCuller = new OcclusionCuller(Long2ReferenceMaps.unmodifiable(this.sectionByPosition), this.level); - - this.taskLists = new EnumMap<>(ChunkUpdateType.class); - - for (var type : ChunkUpdateType.values()) { - this.taskLists.put(type, new ObjectArrayFIFOQueue<>()); - } } public void updateCameraState(Vector3dc cameraPosition, Camera camera) { @@ -135,69 +134,20 @@ public void updateCameraState(Vector3dc cameraPosition, Camera camera) { this.cameraPosition = cameraPosition; } - private void unpackPendingTree() { - if (this.pendingTree == null) { - throw new IllegalStateException("No pending tree to unpack"); - } - - LinearSectionOctree tree; - try { - tree = this.pendingTree.get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException("Failed to do graph search occlusion culling", e); - } - - // TODO: improve task system by reading back the render lists and applying a more reasonable priority to sections than just whether they're visible or not. We also want currently out-of-frustum sections to eventually get built, since otherwise the world is missing when the player turns around. - // TODO: another problem with async bfs is that since the bfs is slower, it leads a slower iterate/load cycle since new sections only get discovered if the bfs traverses into them, which is only possible after building the section and generating its visibility data. - this.taskLists = tree.getRebuildLists(); - this.pendingTree = null; - this.pendingCullType = null; + // TODO: schedule only as many tasks as fit within the frame time (measure how long the last frame took and how long the tasks took, though both of these will change over time) - // use tree if it can be used (frustum tested bfs results can't be used if the frustum has changed) - if (!(this.needsRenderListUpdate && tree.getCullType() == CullType.FRUSTUM)) { - this.trees.put(tree.getCullType(), tree); - this.latestUpdatedTree = tree; + // TODO idea: increase and decrease chunk builder thread budget based on if the upload buffer was filled if the entire budget was used up. if the fallback way of uploading buffers is used, just doing 3 * the budget actually slows down frames while things are getting uploaded. For this it should limit how much (or how often?) things are uploaded. In the case of the mapped upload, just making sure we don't exceed its size is probably enough. - this.needsGraphUpdate = false; - this.needsRenderListUpdate = true; - } - } + // TODO: use narrow-to-wide order for scheduling tasks if we can expect the player not to move much public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator, boolean updateImmediately) { this.frame += 1; + this.lastFrameAtTime = System.nanoTime(); - // TODO: with more work it might be good to submit multiple async bfs tasks at once (so that more than one cull type's tree can be built each frame). However, this introduces the need for a lot more complicated decisions. What to do when a task is pending but will be invalid by the time it's completed? Which of multiple results should be used for rebuild task scheduling? Should pending tasks be replaced with more up to date tasks if they're running (or not running)? - - // when updating immediately (flawless frame), just do sync bfs continuously. - // if we're not updating immediately, the camera timing control should receive the new camera position each time. - if ((updateImmediately || this.cameraTimingControl.getShouldRenderSync(camera)) && - (this.needsGraphUpdate || this.needsRenderListUpdate)) { - // switch to sync rendering if the camera moved too much - final var searchDistance = this.getSearchDistance(); - final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); - - // cancel running task to prevent two bfs running at the same time, which will cause race conditions - if (this.pendingTree != null) { - this.pendingTree.cancel(true); - this.pendingTree = null; - this.pendingCullType = null; - } - - var tree = new LinearSectionOctree(viewport, searchDistance, this.frame, CullType.FRUSTUM); - var visibleCollector = new VisibleChunkCollectorSync(tree, this.frame); - this.occlusionCuller.findVisible(visibleCollector, viewport, searchDistance, useOcclusionCulling, this.frame); - - this.trees.put(CullType.FRUSTUM, tree); - this.renderTree = tree; - - // remove the other trees, they're very wrong by now - this.trees.remove(CullType.WIDE); - this.trees.remove(CullType.REGULAR); - - this.renderLists = visibleCollector.createRenderLists(); - this.needsRenderListUpdate = false; - this.needsGraphUpdate = false; - + // do sync bfs based on update immediately (flawless frames) or if the camera moved too much + var shouldRenderSync = this.cameraTimingControl.getShouldRenderSync(camera); + if ((updateImmediately || shouldRenderSync) && (this.needsGraphUpdate || this.needsRenderListUpdate)) { + renderSync(camera, viewport, spectator); return; } @@ -206,85 +156,198 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato this.needsGraphUpdate = false; } + // discard unusable present and pending frustum-tested trees if (this.needsRenderListUpdate) { - // discard unusable present and pending trees this.trees.remove(CullType.FRUSTUM); - if (this.pendingTree != null && !this.pendingTree.isDone() && this.pendingCullType == CullType.FRUSTUM) { - this.pendingTree.cancel(true); - this.pendingTree = null; - this.pendingCullType = null; + + this.pendingTasks.removeIf(task -> { + if (task instanceof CullTask cullTask && cullTask.getCullType() == CullType.FRUSTUM) { + cullTask.cancelImmediately(); + return true; + } + return false; + }); + } + + // remove all tasks that aren't in progress yet + this.pendingTasks.removeIf(task -> { + if (!task.hasStarted()) { + task.cancelImmediately(); + return true; } + return false; + }); + + this.unpackTaskResults(false); + + this.scheduleAsyncWork(camera, viewport, spectator); + + if (this.needsRenderListUpdate) { + processRenderListUpdate(viewport); } - // unpack the pending tree any time there is one - if (this.pendingTree != null && this.pendingTree.isDone()) { - this.unpackPendingTree(); + var frameEnd = System.nanoTime(); +// System.out.println("Frame " + this.frame + " took " + (frameEnd - this.lastFrameAtTime) / 1000 + "µs"); + } + + private void renderSync(Camera camera, Viewport viewport, boolean spectator) { + final var searchDistance = this.getSearchDistance(); + final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); + + // cancel running tasks to prevent two bfs running at the same time, which will cause race conditions + for (var task : this.pendingTasks) { + task.cancelImmediately(); } + this.pendingTasks.clear(); - // check if more work needs to be done if none is currently being done - if (this.pendingTree == null) { - // regardless of whether the graph has been marked as dirty, working on the widest tree that hasn't yet been updated to match the current graph dirty frame is the best option. Since the graph dirty frame is updated if the graph has been marked as dirty, this also results in the whole cascade of trees being reset when the graph is marked as dirty. + var tree = new VisibleChunkCollectorSync(viewport, searchDistance, this.frame, CullType.FRUSTUM); + this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, this.frame); - CullType workOnType = null; - for (var type : CullType.WIDE_TO_NARROW) { - var tree = this.trees.get(type); + this.frustumTaskLists = tree.getPendingTaskLists(); + this.globalTaskLists = null; + this.trees.put(CullType.FRUSTUM, tree); + this.renderTree = tree; - // don't schedule frustum-culled bfs until there hasn't been a dirty render list. - // otherwise a bfs is done each frame that gets thrown away each time. - if (type == CullType.FRUSTUM && this.needsRenderListUpdate) { - continue; - } + // remove the other trees, they're very wrong by now + this.trees.remove(CullType.WIDE); + this.trees.remove(CullType.REGULAR); - if (tree == null) { - workOnType = type; - break; - } else { - var treeUpdateFrame = tree.getFrame(); - if (treeUpdateFrame < this.lastGraphDirtyFrame) { - workOnType = type; - break; + this.renderLists = tree.createRenderLists(); + this.needsRenderListUpdate = false; + this.needsGraphUpdate = false; + } + + private SectionTree unpackTaskResults(boolean wait) { + SectionTree latestTree = null; + + var it = this.pendingTasks.iterator(); + while (it.hasNext()) { + var task = it.next(); + if (!wait && !task.isDone()) { + continue; + } + it.remove(); + + // unpack the task and its result based on its type + switch (task) { + case FrustumCullTask frustumCullTask -> { + var result = frustumCullTask.getResult(); + this.frustumTaskLists = result.getFrustumTaskLists(); + + // ensure no useless frustum tree is accepted + if (!this.needsRenderListUpdate) { + var tree = result.getTree(); + this.trees.put(CullType.FRUSTUM, tree); + latestTree = tree; + + this.needsRenderListUpdate = true; } } + case GlobalCullTask globalCullTask -> { + var result = globalCullTask.getResult(); + var tree = result.getTaskTree(); + this.globalTaskLists = result.getGlobalTaskLists(); + this.frustumTaskLists = result.getFrustumTaskLists(); + this.globalTaskTree = tree; + this.trees.put(globalCullTask.getCullType(), tree); + latestTree = tree; + + this.needsRenderListUpdate = true; + } + case FrustumTaskCollectionTask collectionTask -> + this.frustumTaskLists = collectionTask.getResult().getFrustumTaskLists(); + default -> { + } + } + } + + return latestTree; + } + + private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectator) { + // submit tasks of types that are applicable and not yet running + AsyncRenderTask currentRunningTask = null; + if (!this.pendingTasks.isEmpty()) { + currentRunningTask = this.pendingTasks.getFirst(); + } + + var transform = viewport.getTransform(); + var cameraSectionX = transform.intX >> 4; + var cameraSectionY = transform.intY >> 4; + var cameraSectionZ = transform.intZ >> 4; + for (var type : CullType.WIDE_TO_NARROW) { + var tree = this.trees.get(type); + + // don't schedule frustum-culled bfs until there hasn't been a dirty render list. + // otherwise a bfs is done each frame that always gets thrown away. + if (type == CullType.FRUSTUM && this.needsRenderListUpdate) { + continue; } - if (workOnType != null) { - final var localType = workOnType; - final var localRenderDistance = this.getRenderDistance(); - final var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); - - this.pendingCullType = localType; - this.pendingTree = this.cullExecutor.submit(() -> { - var tree = new LinearSectionOctree(viewport, localRenderDistance, this.frame, localType); - this.occlusionCuller.findVisible(tree, viewport, localRenderDistance, useOcclusionCulling, this.frame); - return tree; - }); + if ((tree == null || tree.getFrame() < this.lastGraphDirtyFrame || + !tree.isValidFor(cameraSectionX, cameraSectionY, cameraSectionZ)) && ( + currentRunningTask == null || + currentRunningTask instanceof CullTask cullTask && cullTask.getCullType() != type || + currentRunningTask.getFrame() < this.lastGraphDirtyFrame)) { + var searchDistance = this.getSearchDistance(); + var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); + + var task = switch (type) { + case WIDE, REGULAR -> + new GlobalCullTask(this.occlusionCuller, viewport, searchDistance, useOcclusionCulling, this.frame, this.sectionByPosition, type); + case FRUSTUM -> + new FrustumCullTask(this.occlusionCuller, viewport, searchDistance, useOcclusionCulling, this.frame); + }; + task.submitTo(this.asyncCullExecutor); + this.pendingTasks.add(task); } } + } - if (this.needsRenderListUpdate) { - // pick the narrowest up-to-date tree, if this tree is insufficiently up to date we would've switched to sync bfs earlier - LinearSectionOctree bestTree = null; - for (var type : CullType.NARROW_TO_WIDE) { - var tree = this.trees.get(type); - if (tree != null && (bestTree == null || tree.getFrame() > bestTree.getFrame())) { - bestTree = tree; + private void processRenderListUpdate(Viewport viewport) { + // schedule generating a frustum task list if there's no frustum tree task running + if (this.globalTaskTree != null) { + var frustumTaskListPending = false; + for (var task : this.pendingTasks) { + if (task instanceof CullTask cullTask && cullTask.getCullType() == CullType.FRUSTUM || + task instanceof FrustumTaskCollectionTask) { + frustumTaskListPending = true; + break; } } + if (!frustumTaskListPending) { + var searchDistance = this.getSearchDistance(); + var task = new FrustumTaskCollectionTask(viewport, searchDistance, this.frame, this.sectionByPosition, this.globalTaskTree); + task.submitTo(this.asyncCullExecutor); + this.pendingTasks.add(task); + } + } - this.needsRenderListUpdate = false; + // pick the narrowest up-to-date tree, if this tree is insufficiently up to date we would've switched to sync bfs earlier + SectionTree bestTree = null; + for (var type : CullType.NARROW_TO_WIDE) { + var tree = this.trees.get(type); + if (tree != null && (bestTree == null || tree.getFrame() > bestTree.getFrame())) { + bestTree = tree; + } + } + + this.needsRenderListUpdate = false; + + // wait if there's no current tree (first frames after initial load/reload) + if (bestTree == null) { + bestTree = this.unpackTaskResults(true); - // wait if there's no current tree (first frames after initial load/reload) if (bestTree == null) { - this.unpackPendingTree(); - bestTree = this.latestUpdatedTree; + throw new IllegalStateException("Unpacked tree was not valid but a tree is required to render."); } + } - var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame); - bestTree.traverseVisible(visibleCollector, viewport, this.getSearchDistance()); - this.renderLists = visibleCollector.createRenderLists(); + var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame); + bestTree.traverseVisible(visibleCollector, viewport, this.getSearchDistance()); + this.renderLists = visibleCollector.createRenderLists(); - this.renderTree = bestTree; - } + this.renderTree = bestTree; } public void markGraphDirty() { @@ -344,7 +407,7 @@ public void onSectionAdded(int x, int y, int z) { if (section.hasOnlyAir()) { this.updateSectionInfo(renderSection, BuiltSectionInfo.EMPTY); } else { - renderSection.setPendingUpdate(ChunkUpdateType.INITIAL_BUILD); + renderSection.setPendingUpdate(ChunkUpdateType.INITIAL_BUILD, this.lastFrameAtTime); } this.connectNeighborNodes(renderSection); @@ -539,8 +602,7 @@ public void updateChunks(boolean updateImmediately) { } else { var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); var deferredCollector = new ChunkJobCollector( - this.builder.getHighEffortSchedulingBudget(), - this.builder.getLowEffortSchedulingBudget(), + this.builder.getTotalRemainingBudget(), this.buildResults::add); // if zero frame delay is allowed, submit important sorts with the current frame blocking collector. @@ -564,31 +626,78 @@ private void submitSectionTasks( ChunkJobCollector importantCollector, ChunkJobCollector semiImportantCollector, ChunkJobCollector deferredCollector) { - this.submitSectionTasks(importantCollector, ChunkUpdateType.IMPORTANT_SORT, true); - this.submitSectionTasks(semiImportantCollector, ChunkUpdateType.IMPORTANT_REBUILD, true); + for (var deferMode : DeferMode.values()) { + var collector = switch (deferMode) { + case ZERO_FRAMES -> importantCollector; + case ONE_FRAME -> semiImportantCollector; + case ALWAYS -> deferredCollector; + }; - // since the sort tasks are run last, the effort category can be ignored and - // simply fills up the remaining budget. Splitting effort categories is still - // important to prevent high effort tasks from using up the entire budget if it - // happens to divide evenly. - this.submitSectionTasks(deferredCollector, ChunkUpdateType.REBUILD, false); - this.submitSectionTasks(deferredCollector, ChunkUpdateType.INITIAL_BUILD, false); - this.submitSectionTasks(deferredCollector, ChunkUpdateType.SORT, true); + submitSectionTasks(collector, deferMode); + } } - private void submitSectionTasks(ChunkJobCollector collector, ChunkUpdateType type, boolean ignoreEffortCategory) { - var queue = this.taskLists.get(type); + private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode) { + LongHeapPriorityQueue frustumQueue = null; + LongHeapPriorityQueue globalQueue = null; + float frustumPriorityBias = 0; + float globalPriorityBias = 0; + if (this.frustumTaskLists != null) { + frustumQueue = this.frustumTaskLists.pendingTasks.get(deferMode); + } + if (frustumQueue != null) { + frustumPriorityBias = this.frustumTaskLists.getCollectorPriorityBias(this.lastFrameAtTime); + } else { + frustumQueue = new LongHeapPriorityQueue(); + } + + if (this.globalTaskLists != null) { + globalQueue = this.globalTaskLists.pendingTasks.get(deferMode); + } + if (globalQueue != null) { + globalPriorityBias = this.globalTaskLists.getCollectorPriorityBias(this.lastFrameAtTime); + } else { + globalQueue = new LongHeapPriorityQueue(); + } + + float frustumPriority = Float.POSITIVE_INFINITY; + float globalPriority = Float.POSITIVE_INFINITY; + long frustumItem = 0; + long globalItem = 0; + + while ((!frustumQueue.isEmpty() || !globalQueue.isEmpty()) && collector.hasBudgetRemaining()) { + // get the first item from the non-empty queues and see which one has higher priority. + // if the priority is not infinity, then the item priority was fetched the last iteration and doesn't need updating. + if (!frustumQueue.isEmpty() && Float.isInfinite(frustumPriority)) { + frustumItem = frustumQueue.firstLong(); + frustumPriority = PendingTaskCollector.decodePriority(frustumItem) + frustumPriorityBias; + } + if (!globalQueue.isEmpty() && Float.isInfinite(globalPriority)) { + globalItem = globalQueue.firstLong(); + globalPriority = PendingTaskCollector.decodePriority(globalItem) + globalPriorityBias; + } + + RenderSection section; + if (frustumPriority < globalPriority) { + frustumQueue.dequeueLong(); + frustumPriority = Float.POSITIVE_INFINITY; + + section = this.frustumTaskLists.decodeAndFetchSection(this.sectionByPosition, frustumItem); + } else { + globalQueue.dequeueLong(); + globalPriority = Float.POSITIVE_INFINITY; - while (!queue.isEmpty() && collector.hasBudgetFor(type.getTaskEffort(), ignoreEffortCategory)) { - RenderSection section = queue.dequeue(); + section = this.globalTaskLists.decodeAndFetchSection(this.sectionByPosition, globalItem); + } - if (section.isDisposed()) { + if (section == null || section.isDisposed()) { continue; } - // stop if the section is in this list but doesn't have this update type - var pendingUpdate = section.getPendingUpdate(); - if (pendingUpdate != null && pendingUpdate != type) { + // stop if the section doesn't need an update anymore + // TODO: check for section having a running task? + var type = section.getPendingUpdate(); + if (type == null) { continue; } @@ -632,7 +741,7 @@ private void submitSectionTasks(ChunkJobCollector collector, ChunkUpdateType typ } section.setLastSubmittedFrame(frame); - section.setPendingUpdate(null); + section.clearPendingUpdate(); } } @@ -661,7 +770,7 @@ public ChunkBuilder getBuilder() { public void destroy() { this.builder.shutdown(); // stop all the workers, and cancel any tasks - this.cullExecutor.shutdownNow(); + this.asyncCullExecutor.shutdownNow(); for (var result : this.collectChunkBuildResults()) { result.destroy(); // delete resources for any pending tasks (including those that were cancelled) @@ -675,10 +784,6 @@ public void destroy() { this.renderLists = SortedRenderLists.empty(); - for (var list : this.taskLists.values()) { - list.clear(); - } - try (CommandList commandList = RenderDevice.INSTANCE.createCommandList()) { this.regions.delete(commandList); this.chunkRenderer.delete(commandList); @@ -713,7 +818,7 @@ public void scheduleSort(long sectionPos, boolean isDirectTrigger) { } pendingUpdate = ChunkUpdateType.getPromotionUpdateType(section.getPendingUpdate(), pendingUpdate); if (pendingUpdate != null) { - section.setPendingUpdate(pendingUpdate); + section.setPendingUpdate(pendingUpdate, this.lastFrameAtTime); section.prepareTrigger(isDirectTrigger); } } @@ -737,7 +842,7 @@ public void scheduleRebuild(int x, int y, int z, boolean important) { pendingUpdate = ChunkUpdateType.getPromotionUpdateType(section.getPendingUpdate(), pendingUpdate); if (pendingUpdate != null) { - section.setPendingUpdate(pendingUpdate); + section.setPendingUpdate(pendingUpdate, this.lastFrameAtTime); // force update to schedule rebuild task on this section this.markGraphDirty(); @@ -745,9 +850,6 @@ public void scheduleRebuild(int x, int y, int z, boolean important) { } } - private static final float NEARBY_REBUILD_DISTANCE = Mth.square(16.0f); - private static final float NEARBY_SORT_DISTANCE = Mth.square(25.0f); - private boolean shouldPrioritizeTask(RenderSection section, float distance) { return this.cameraBlockPos != null && section.getSquaredDistance(this.cameraBlockPos) < distance; } @@ -832,12 +934,12 @@ public Collection getDebugStrings() { this.builder.getScheduledJobCount(), this.builder.getScheduledEffort(), this.builder.getBusyThreadCount(), this.builder.getTotalThreadCount()) ); - list.add(String.format("Chunk Queues: U=%02d (P0=%03d | P1=%03d | P2=%03d)", - this.buildResults.size(), - this.taskLists.get(ChunkUpdateType.IMPORTANT_REBUILD).size() + this.taskLists.get(ChunkUpdateType.IMPORTANT_SORT).size(), - this.taskLists.get(ChunkUpdateType.REBUILD).size() + this.taskLists.get(ChunkUpdateType.SORT).size(), - this.taskLists.get(ChunkUpdateType.INITIAL_BUILD).size()) - ); +// list.add(String.format("Chunk Queues: U=%02d (P0=%03d | P1=%03d | P2=%03d)", +// this.buildResults.size(), +// this.frustumTaskLists.get(ChunkUpdateType.IMPORTANT_REBUILD).size() + this.frustumTaskLists.get(ChunkUpdateType.IMPORTANT_SORT).size(), +// this.frustumTaskLists.get(ChunkUpdateType.REBUILD).size() + this.frustumTaskLists.get(ChunkUpdateType.SORT).size(), +// this.frustumTaskLists.get(ChunkUpdateType.INITIAL_BUILD).size()) +// ); this.sortTriggering.addDebugStrings(list); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java new file mode 100644 index 0000000000..03e50bde79 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java @@ -0,0 +1,59 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +public abstract class AsyncRenderTask implements Callable { + protected final Viewport viewport; + protected final float buildDistance; + protected final int frame; + + private Future future; + private volatile boolean started; + + protected AsyncRenderTask(Viewport viewport, float buildDistance, int frame) { + this.viewport = viewport; + this.buildDistance = buildDistance; + this.frame = frame; + } + + public void submitTo(ExecutorService executor) { + this.future = executor.submit(this); + } + + public boolean isDone() { + return this.future.isDone(); + } + + public boolean hasStarted() { + return this.started; + } + + public int getFrame() { + return this.frame; + } + + public void cancelImmediately() { + this.future.cancel(true); + } + + public T getResult() { + try { + return this.future.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Failed to get result of render task", e); + } + } + + @Override + public T call() throws Exception { + this.started = true; + return this.runTask(); + } + + protected abstract T runTask(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java new file mode 100644 index 0000000000..fc3b7807e7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java @@ -0,0 +1,22 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public abstract class CullTask extends AsyncRenderTask { + protected final OcclusionCuller occlusionCuller; + protected final boolean useOcclusionCulling; + + protected CullTask(Viewport viewport, float buildDistance, int frame, OcclusionCuller occlusionCuller, boolean useOcclusionCulling) { + super(viewport, buildDistance, frame); + this.occlusionCuller = occlusionCuller; + this.useOcclusionCulling = useOcclusionCulling; + } + + public abstract CullType getCullType(); + + protected int getOcclusionToken() { + return (this.getCullType().ordinal() << 28) ^ this.frame; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullResult.java new file mode 100644 index 0000000000..6715c2efdb --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullResult.java @@ -0,0 +1,7 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; + +public interface FrustumCullResult extends FrustumTaskListsResult { + SectionTree getTree(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java new file mode 100644 index 0000000000..516c8b978e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -0,0 +1,38 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public class FrustumCullTask extends CullTask { + public FrustumCullTask(OcclusionCuller occlusionCuller, Viewport viewport, float buildDistance, boolean useOcclusionCulling, int frame) { + super(viewport, buildDistance, frame, occlusionCuller, useOcclusionCulling); + } + + @Override + public FrustumCullResult runTask() { + var tree = new SectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM); + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this.frame); + + var frustumTaskLists = tree.getPendingTaskLists(); + + return new FrustumCullResult() { + @Override + public SectionTree getTree() { + return tree; + } + + @Override + public PendingTaskCollector.TaskListCollection getFrustumTaskLists() { + return frustumTaskLists; + } + }; + } + + @Override + public CullType getCullType() { + return CullType.FRUSTUM; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java new file mode 100644 index 0000000000..cea89a17c4 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java @@ -0,0 +1,28 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.FrustumTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public class FrustumTaskCollectionTask extends AsyncRenderTask { + private final Long2ReferenceMap sectionByPosition; + private final TaskSectionTree globalTaskTree; + + public FrustumTaskCollectionTask(Viewport viewport, float buildDistance, int frame, Long2ReferenceMap sectionByPosition, TaskSectionTree globalTaskTree) { + super(viewport, buildDistance, frame); + this.sectionByPosition = sectionByPosition; + this.globalTaskTree = globalTaskTree; + } + + @Override + public FrustumTaskListsResult runTask() { + var collector = new FrustumTaskCollector(this.viewport, this.buildDistance, this.sectionByPosition); + this.globalTaskTree.traverseVisiblePendingTasks(collector, this.viewport, this.buildDistance); + + var frustumTaskLists = collector.getPendingTaskLists(); + return () -> frustumTaskLists; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java new file mode 100644 index 0000000000..874d48ee4e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java @@ -0,0 +1,7 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; + +public interface FrustumTaskListsResult { + PendingTaskCollector.TaskListCollection getFrustumTaskLists(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java new file mode 100644 index 0000000000..74d1f3496b --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java @@ -0,0 +1,10 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; + +public interface GlobalCullResult extends FrustumTaskListsResult { + TaskSectionTree getTaskTree(); + + PendingTaskCollector.TaskListCollection getGlobalTaskLists(); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java new file mode 100644 index 0000000000..cbb304709e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -0,0 +1,55 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.FrustumTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public class GlobalCullTask extends CullTask { + private final Long2ReferenceMap sectionByPosition; + private final CullType cullType; + + public GlobalCullTask(OcclusionCuller occlusionCuller, Viewport viewport, float buildDistance, boolean useOcclusionCulling, int frame, Long2ReferenceMap sectionByPosition, CullType cullType) { + super(viewport, buildDistance, frame, occlusionCuller, useOcclusionCulling); + this.sectionByPosition = sectionByPosition; + this.cullType = cullType; + } + + @Override + public GlobalCullResult runTask() { + var tree = new TaskSectionTree(this.viewport, this.buildDistance, this.frame, this.cullType); + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this.getOcclusionToken()); + + var collector = new FrustumTaskCollector(this.viewport, this.buildDistance, this.sectionByPosition); + tree.traverseVisiblePendingTasks(collector, this.viewport, this.buildDistance); + + var globalTaskLists = tree.getPendingTaskLists(); + var frustumTaskLists = collector.getPendingTaskLists(); + + return new GlobalCullResult() { + @Override + public TaskSectionTree getTaskTree() { + return tree; + } + + @Override + public PendingTaskCollector.TaskListCollection getFrustumTaskLists() { + return frustumTaskLists; + } + + @Override + public PendingTaskCollector.TaskListCollection getGlobalTaskLists() { + return globalTaskLists; + } + }; + } + + @Override + public CullType getCullType() { + return this.cullType; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java index 933b493863..1d347d1f29 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java @@ -34,7 +34,7 @@ public class ChunkBuilder { public static final int HIGH_EFFORT = 10; public static final int LOW_EFFORT = 1; public static final int EFFORT_UNIT = HIGH_EFFORT + LOW_EFFORT; - public static final int EFFORT_PER_THREAD_PER_FRAME = 3 * EFFORT_UNIT; + public static final int EFFORT_PER_THREAD_PER_FRAME = EFFORT_UNIT; private static final float HIGH_EFFORT_BUDGET_FACTOR = (float)HIGH_EFFORT / EFFORT_UNIT; static final Logger LOGGER = LogManager.getLogger("ChunkBuilder"); @@ -70,7 +70,7 @@ public ChunkBuilder(ClientLevel level, ChunkVertexType vertexType) { * Returns the remaining effort for tasks which should be scheduled this frame. If an attempt is made to * spawn more tasks than the budget allows, it will block until resources become available. */ - private int getTotalRemainingBudget() { + public int getTotalRemainingBudget() { return Math.max(0, this.threads.size() * EFFORT_PER_THREAD_PER_FRAME - this.queue.getEffortSum()); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java index 69701b0a26..8c9d803f3d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java @@ -11,25 +11,16 @@ public class ChunkJobCollector { private final Semaphore semaphore = new Semaphore(0); private final Consumer> collector; private final List submitted = new ArrayList<>(); - private int submittedHighEffort = 0; - private int submittedLowEffort = 0; - private final int highEffortBudget; - private final int lowEffortBudget; - private final boolean unlimitedBudget; + private int budget; public ChunkJobCollector(Consumer> collector) { - this.unlimitedBudget = true; - this.highEffortBudget = 0; - this.lowEffortBudget = 0; + this.budget = Integer.MAX_VALUE; this.collector = collector; } - public ChunkJobCollector(int highEffortBudget, int lowEffortBudget, - Consumer> collector) { - this.unlimitedBudget = false; - this.highEffortBudget = highEffortBudget; - this.lowEffortBudget = lowEffortBudget; + public ChunkJobCollector(int budget, Consumer> collector) { + this.budget = budget; this.collector = collector; } @@ -56,28 +47,10 @@ public void awaitCompletion(ChunkBuilder builder) { public void addSubmittedJob(ChunkJob job) { this.submitted.add(job); - - if (this.unlimitedBudget) { - return; - } - var effort = job.getEffort(); - if (effort <= ChunkBuilder.LOW_EFFORT) { - this.submittedLowEffort += effort; - } else { - this.submittedHighEffort += effort; - } + this.budget -= job.getEffort(); } - public boolean hasBudgetFor(int effort, boolean ignoreEffortCategory) { - if (this.unlimitedBudget) { - return true; - } - if (ignoreEffortCategory) { - return this.submittedLowEffort + this.submittedHighEffort + effort - <= this.highEffortBudget + this.lowEffortBudget; - } - return effort <= ChunkBuilder.LOW_EFFORT - ? this.submittedLowEffort + effort <= this.lowEffortBudget - : this.submittedHighEffort + effort <= this.highEffortBudget; + public boolean hasBudgetRemaining() { + return this.budget > 0; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FrustumTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FrustumTaskCollector.java new file mode 100644 index 0000000000..ed19d03cb9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FrustumTaskCollector.java @@ -0,0 +1,28 @@ +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.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.core.SectionPos; + +public class FrustumTaskCollector extends PendingTaskCollector implements SectionTree.VisibleSectionVisitor { + private final Long2ReferenceMap sectionByPosition; + + public FrustumTaskCollector(Viewport viewport, float buildDistance, Long2ReferenceMap sectionByPosition) { + super(viewport, buildDistance, true); + + this.sectionByPosition = sectionByPosition; + } + + @Override + public void visit(int x, int y, int z) { + var section = this.sectionByPosition.get(SectionPos.asLong(x, y, z)); + + if (section == null) { + return; + } + + this.checkForTask(section); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index bacb826234..e9b9cf550a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -1,38 +1,181 @@ package net.caffeinemc.mods.sodium.client.render.chunk.lists; -import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue; +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateType; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; 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.viewport.Viewport; +import net.caffeinemc.mods.sodium.client.util.MathUtil; +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; import java.util.EnumMap; import java.util.Map; +/* +TODO: +- check if there's also bumps in the fps when crossing chunk borders on dev +- tune priority values, test frustum effect by giving it a large value +- experiment with non-linear distance scaling (if < some radius, bonus priority for being close) + */ public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { - private final EnumMap> sortedRebuildLists; + // offset is shifted by 1 to encompass all sections towards the negative + // TODO: is this the correct way of calculating the minimum possible section index? + private static final int DISTANCE_OFFSET = 1; + private static final int SECTION_Y_MIN = -128; // used instead of baseOffsetY to accommodate all permissible y values (-2048 to 2048 blocks) - public PendingTaskCollector() { - this.sortedRebuildLists = new EnumMap<>(ChunkUpdateType.class); + // tunable parameters for the priority calculation. + // each "gained" point means a reduction in the final priority score (lowest score processed first) + private static final float PENDING_TIME_FACTOR = -1.0f / 500_000_000.0f; // 1 point gained per 500ms + private static final float WITHIN_FRUSTUM_BIAS = -3.0f; // points for being within the frustum + private static final float PROXIMITY_FACTOR = 3.0f; // penalty for being far away + private static final float CLOSE_DISTANCE = 50.0f; // distance at which another proximity bonus is applied + private static final float CLOSE_PROXIMITY_FACTOR = 0.6f; // bonus for being very close + private static final float INV_MAX_DISTANCE_CLOSE = CLOSE_PROXIMITY_FACTOR / CLOSE_DISTANCE; - for (var type : ChunkUpdateType.values()) { - this.sortedRebuildLists.put(type, new ObjectArrayFIFOQueue<>()); + private final LongArrayList[] pendingTasks = new LongArrayList[DeferMode.values().length]; + + protected final boolean isFrustumTested; + protected final int baseOffsetX, baseOffsetY, baseOffsetZ; + + protected final int cameraX, cameraY, cameraZ; + private final float invMaxDistance; + private final long creationTime; + + public PendingTaskCollector(Viewport viewport, float buildDistance, boolean frustumTested) { + this.creationTime = System.nanoTime(); + this.isFrustumTested = frustumTested; + var offsetDistance = Mth.floor(buildDistance / 16.0f) + DISTANCE_OFFSET; + + var transform = viewport.getTransform(); + + // the offset applied to section coordinates to encode their position in the octree + var cameraSectionX = transform.intX >> 4; + var cameraSectionY = transform.intY >> 4; + var cameraSectionZ = transform.intZ >> 4; + this.baseOffsetX = cameraSectionX - offsetDistance; + this.baseOffsetY = cameraSectionY - offsetDistance; + this.baseOffsetZ = cameraSectionZ - offsetDistance; + + this.invMaxDistance = PROXIMITY_FACTOR / buildDistance; + + if (frustumTested) { + this.cameraX = transform.intX; + this.cameraY = transform.intY; + this.cameraZ = transform.intZ; + } else { + this.cameraX = (cameraSectionX << 4); + this.cameraY = (cameraSectionY << 4); + this.cameraZ = (cameraSectionZ << 4); } } @Override public void visit(RenderSection section) { + if (!visible) { + return; + } + + this.checkForTask(section); + } + + protected void checkForTask(RenderSection section) { ChunkUpdateType type = section.getPendingUpdate(); if (type != null && section.getTaskCancellationToken() == null) { - ObjectArrayFIFOQueue queue = this.sortedRebuildLists.get(type); + this.addPendingSection(section, type); + } + } + + protected void addPendingSection(RenderSection section, ChunkUpdateType type) { + // start with a base priority value, lowest priority of task gets processed first + float priority = getSectionPriority(section, type); + + // encode the absolute position of the section + var localX = section.getChunkX() - this.baseOffsetX; + var localY = section.getChunkY() - SECTION_Y_MIN; + var localZ = section.getChunkZ() - this.baseOffsetZ; + long taskCoordinate = (long) (localX & 0xFF) << 16 | (long) (localY & 0xFF) << 8 | (long) (localZ & 0xFF); + + var queue = this.pendingTasks[type.getDeferMode().ordinal()]; + if (queue == null) { + queue = new LongArrayList(); + this.pendingTasks[type.getDeferMode().ordinal()] = queue; + } + + // encode the priority and the section position into a single long such that all parts can be later decoded + queue.add((long) MathUtil.floatToComparableInt(priority) << 32 | taskCoordinate); + } - if (queue.size() < type.getMaximumQueueSize()) { - queue.enqueue(section); + private float getSectionPriority(RenderSection section, ChunkUpdateType type) { + float priority = type.getPriorityValue(); + + // calculate the relative distance to the camera + // alternatively: var distance = deltaX + deltaY + deltaZ; + var deltaX = Math.abs(section.getCenterX() - this.cameraX); + var deltaY = Math.abs(section.getCenterY() - this.cameraY); + var deltaZ = Math.abs(section.getCenterZ() - this.cameraZ); + var distance = (float) Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ); + priority += distance * this.invMaxDistance; // distance / maxDistance * PROXIMITY_FACTOR + priority += Math.max(distance, CLOSE_DISTANCE) * INV_MAX_DISTANCE_CLOSE; + + // tasks that have been waiting for longer are more urgent + var taskPendingTimeNanos = this.creationTime - section.getPendingUpdateSince(); + priority += taskPendingTimeNanos * PENDING_TIME_FACTOR; // upgraded by one point every second + + // explain how priority was calculated +// System.out.println("Priority " + priority + " from: distance " + distance + " = " + (distance * this.invMaxDistance) + +// ", time " + taskPendingTimeNanos + " = " + (taskPendingTimeNanos * PENDING_TIME_FACTOR) + +// ", type " + type + " = " + type.getPriorityValue() + +// ", frustum " + this.isFrustumTested + " = " + (this.isFrustumTested ? WITHIN_FRUSTUM_BIAS : 0)); + + return priority; + } + + public static float decodePriority(long encoded) { + return MathUtil.comparableIntToFloat((int) (encoded >>> 32)); + } + + public TaskListCollection getPendingTaskLists() { + var result = new EnumMap(DeferMode.class); + + for (var mode : DeferMode.values()) { + var list = this.pendingTasks[mode.ordinal()]; + if (list != null) { + var queue = new LongHeapPriorityQueue(list.elements(), list.size()); + result.put(mode, queue); } } + + return new TaskListCollection(result); } - public Map> getRebuildLists() { - return this.sortedRebuildLists; + public class TaskListCollection { + public final Map pendingTasks; + + public TaskListCollection(Map pendingTasks) { + this.pendingTasks = pendingTasks; + } + + public float getCollectorPriorityBias(long now) { + // compensate for creation time of the list and whether the sections are in the frustum + return (now - PendingTaskCollector.this.creationTime) * PENDING_TIME_FACTOR + + (PendingTaskCollector.this.isFrustumTested ? WITHIN_FRUSTUM_BIAS : 0); + } + + public RenderSection decodeAndFetchSection(Long2ReferenceMap sectionByPosition, long encoded) { + var localX = (int) (encoded >>> 16) & 0xFF; + var localY = (int) (encoded >>> 8) & 0xFF; + var localZ = (int) (encoded & 0xFF); + + var globalX = localX + PendingTaskCollector.this.baseOffsetX; + var globalY = localY + SECTION_Y_MIN; + var globalZ = localZ + PendingTaskCollector.this.baseOffsetZ; + + return sectionByPosition.get(SectionPos.asLong(globalX, globalY, globalZ)); + } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java new file mode 100644 index 0000000000..0bdc57bea9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -0,0 +1,43 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +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.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public class TaskSectionTree extends SectionTree { + final Tree mainTaskTree; + Tree secondaryTaskTree; + + public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType) { + super(viewport, buildDistance, frame, cullType); + + this.mainTaskTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + } + + @Override + protected void addPendingSection(RenderSection section, ChunkUpdateType type) { + super.addPendingSection(section, type); + + this.markTaskPresent(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + + protected void markTaskPresent(int x, int y, int z) { + if (this.mainTaskTree.add(x, y, z)) { + if (this.secondaryTaskTree == null) { + this.secondaryTaskTree = this.makeSecondaryTree(); + } + if (this.secondaryTaskTree.add(x, y, z)) { + throw new IllegalStateException("Failed to add section to trees"); + } + } + } + + public void traverseVisiblePendingTasks(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + this.mainTaskTree.traverse(visitor, viewport, distanceLimit); + if (this.secondaryTaskTree != null) { + this.secondaryTaskTree.traverse(visitor, viewport, distanceLimit); + } + } +} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java index 0f6a25fd19..a35876c9c3 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java @@ -3,10 +3,7 @@ import it.unimi.dsi.fastutil.ints.IntArrays; import it.unimi.dsi.fastutil.objects.ObjectArrayList; 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.occlusion.LinearSectionOctree; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; @@ -20,7 +17,7 @@ * The visible chunk collector is passed to the occlusion graph search culler to * collect the visible chunks. */ -public class VisibleChunkCollectorAsync implements LinearSectionOctree.VisibleSectionVisitor { +public class VisibleChunkCollectorAsync implements SectionTree.VisibleSectionVisitor { private final ObjectArrayList sortedRenderLists; private final RenderRegionManager regions; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java index 1bb9fbbc8a..d83da2b4d1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java @@ -1,30 +1,24 @@ package net.caffeinemc.mods.sodium.client.render.chunk.lists; import it.unimi.dsi.fastutil.objects.ObjectArrayList; -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.occlusion.LinearSectionOctree; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; -import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; -public class VisibleChunkCollectorSync implements OcclusionCuller.GraphOcclusionVisitor { +public class VisibleChunkCollectorSync extends SectionTree { private final ObjectArrayList sortedRenderLists; - private final LinearSectionOctree tree; - private final int frame; - - public VisibleChunkCollectorSync(LinearSectionOctree tree, int frame) { - this.tree = tree; - this.frame = frame; - + public VisibleChunkCollectorSync(Viewport viewport, float buildDistance, int frame, CullType cullType) { + super(viewport, buildDistance, frame, cullType); this.sortedRenderLists = new ObjectArrayList<>(); } @Override public void visit(RenderSection section, boolean visible) { - this.tree.visit(section, visible); + super.visit(section, visible); RenderRegion region = section.getRegion(); ChunkRenderList renderList = region.getRenderList(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 4b5282dfc6..c324d04d06 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -57,15 +57,15 @@ public void findVisible(GraphOcclusionVisitor visitor, Viewport viewport, float searchDistance, boolean useOcclusionCulling, - int frame) + int token) { final var queues = this.queue; queues.reset(); - this.init(visitor, queues.write(), viewport, searchDistance, useOcclusionCulling, frame); + this.init(visitor, queues.write(), viewport, searchDistance, useOcclusionCulling, token); while (queues.flip()) { - processQueue(visitor, viewport, searchDistance, useOcclusionCulling, frame, queues.read(), queues.write()); + processQueue(visitor, viewport, searchDistance, useOcclusionCulling, token, queues.read(), queues.write()); } this.addNearbySections(visitor, viewport, searchDistance, frame); @@ -77,7 +77,7 @@ private static void processQueue(GraphOcclusionVisitor visitor, Viewport viewport, float searchDistance, boolean useOcclusionCulling, - int frame, + int token, ReadQueue readQueue, WriteQueue writeQueue) { @@ -117,7 +117,7 @@ private static void processQueue(GraphOcclusionVisitor visitor, connections &= visitor.getOutwardDirections(viewport.getChunkCoord(), section); } - visitNeighbors(writeQueue, section, connections, frame); + visitNeighbors(writeQueue, section, connections, token); } } @@ -162,7 +162,7 @@ private static boolean isWithinRenderDistance(CameraTransform camera, RenderSect return (((dx * dx) + (dz * dz)) < (maxDistance * maxDistance)) && (Math.abs(dy) < maxDistance); } - private static void visitNeighbors(final WriteQueue queue, RenderSection section, int outgoing, int frame) { + private static void visitNeighbors(final WriteQueue queue, RenderSection section, int outgoing, int token) { // Only traverse into neighbors which are actually present. // This avoids a null-check on each invocation to enqueue, and since the compiler will see that a null // is never encountered (after profiling), it will optimize it away. @@ -177,40 +177,40 @@ private static void visitNeighbors(final WriteQueue queue, Render queue.ensureCapacity(6); if (GraphDirectionSet.contains(outgoing, GraphDirection.DOWN)) { - visitNode(queue, section.adjacentDown, GraphDirectionSet.of(GraphDirection.UP), frame); + visitNode(queue, section.adjacentDown, GraphDirectionSet.of(GraphDirection.UP), token); } if (GraphDirectionSet.contains(outgoing, GraphDirection.UP)) { - visitNode(queue, section.adjacentUp, GraphDirectionSet.of(GraphDirection.DOWN), frame); + visitNode(queue, section.adjacentUp, GraphDirectionSet.of(GraphDirection.DOWN), token); } if (GraphDirectionSet.contains(outgoing, GraphDirection.NORTH)) { - visitNode(queue, section.adjacentNorth, GraphDirectionSet.of(GraphDirection.SOUTH), frame); + visitNode(queue, section.adjacentNorth, GraphDirectionSet.of(GraphDirection.SOUTH), token); } if (GraphDirectionSet.contains(outgoing, GraphDirection.SOUTH)) { - visitNode(queue, section.adjacentSouth, GraphDirectionSet.of(GraphDirection.NORTH), frame); + visitNode(queue, section.adjacentSouth, GraphDirectionSet.of(GraphDirection.NORTH), token); } if (GraphDirectionSet.contains(outgoing, GraphDirection.WEST)) { - visitNode(queue, section.adjacentWest, GraphDirectionSet.of(GraphDirection.EAST), frame); + visitNode(queue, section.adjacentWest, GraphDirectionSet.of(GraphDirection.EAST), token); } if (GraphDirectionSet.contains(outgoing, GraphDirection.EAST)) { - visitNode(queue, section.adjacentEast, GraphDirectionSet.of(GraphDirection.WEST), frame); + visitNode(queue, section.adjacentEast, GraphDirectionSet.of(GraphDirection.WEST), token); } } - private static void visitNode(final WriteQueue queue, RenderSection render, int incoming, int frame) { + private static void visitNode(final WriteQueue queue, RenderSection render, int incoming, int token) { // isn't usually null, but can be null if the bfs is happening during loading or unloading of chunks if (render == null) { return; } - if (render.getLastVisibleFrame() != frame) { - // This is the first time we are visiting this section during the given frame, so we must + if (render.getLastVisibleSearchToken() != token) { + // This is the first time we are visiting this section during the given token, so we must // reset the state. - render.setLastVisibleFrame(frame); + render.setLastVisibleSearchToken(token); render.setIncomingDirections(GraphDirectionSet.NONE); queue.enqueue(render); @@ -278,24 +278,24 @@ private void init(GraphOcclusionVisitor visitor, Viewport viewport, float searchDistance, boolean useOcclusionCulling, - int frame) + int token) { var origin = viewport.getChunkCoord(); if (origin.getY() < this.level.getMinSectionY()) { // below the level - this.initOutsideWorldHeight(visitor, queue, viewport, searchDistance, frame, + this.initOutsideWorldHeight(visitor, queue, viewport, searchDistance, token, this.level.getMinSectionY(), GraphDirection.DOWN); } else if (origin.getY() > this.level.getMaxSectionY()) { // above the level - this.initOutsideWorldHeight(visitor, queue, viewport, searchDistance, frame, + this.initOutsideWorldHeight(visitor, queue, viewport, searchDistance, token, this.level.getMaxSectionY(), GraphDirection.UP); } else { - this.initWithinWorld(visitor, queue, viewport, useOcclusionCulling, frame); + this.initWithinWorld(visitor, queue, viewport, useOcclusionCulling, token); } } - private void initWithinWorld(GraphOcclusionVisitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, int frame) { + private void initWithinWorld(GraphOcclusionVisitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, int token) { var origin = viewport.getChunkCoord(); var section = this.getRenderSection(origin.getX(), origin.getY(), origin.getZ()); @@ -303,7 +303,7 @@ private void initWithinWorld(GraphOcclusionVisitor visitor, WriteQueue queue, Viewport viewport, float searchDistance, - int frame, + int token, int height, int direction) { @@ -337,18 +337,18 @@ private void initOutsideWorldHeight(GraphOcclusionVisitor visitor, var radius = Mth.floor(searchDistance / 16.0f); // Layer 0 - this.tryInitNode(visitor, queue, origin.getX(), height, origin.getZ(), direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX(), height, origin.getZ(), direction, token, viewport); // Complete layers, excluding layer 0 for (int layer = 1; layer <= radius; layer++) { for (int z = -layer; z < layer; z++) { int x = Math.abs(z) - layer; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); } for (int z = layer; z > -layer; z--) { int x = layer - Math.abs(z); - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); } } @@ -358,34 +358,34 @@ private void initOutsideWorldHeight(GraphOcclusionVisitor visitor, for (int z = -radius; z <= -l; z++) { int x = -z - layer; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); } for (int z = l; z <= radius; z++) { int x = z - layer; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); } for (int z = radius; z >= l; z--) { int x = layer - z; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); } for (int z = -l; z >= -radius; z--) { int x = layer + z; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, frame, viewport); + this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); } } } - private void tryInitNode(GraphOcclusionVisitor visitor, WriteQueue queue, int x, int y, int z, int direction, int frame, Viewport viewport) { + private void tryInitNode(GraphOcclusionVisitor visitor, WriteQueue queue, int x, int y, int z, int direction, int token, Viewport viewport) { RenderSection section = this.getRenderSection(x, y, z); if (section == null || !visitor.isWithinFrustum(viewport, section)) { return; } - visitNode(queue, section, GraphDirectionSet.of(direction), frame); + visitNode(queue, section, GraphDirectionSet.of(direction), token); } private RenderSection getRenderSection(int x, int y, int z) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java similarity index 78% rename from common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java rename to common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 2856e1f97b..a5993a76b1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/LinearSectionOctree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -5,67 +5,50 @@ import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import net.minecraft.core.SectionPos; -import net.minecraft.util.Mth; import org.joml.FrustumIntersection; /** - * TODO: do distance test here? what happens when the camera moves but the bfs doesn't know that? expand the distance limit? - * ideas to prevent one frame of wrong display when BFS is recalculated but not ready yet: - * - preemptively do the bfs from the next section the camera is going to be in, and maybe pad the render distance by how far the player can move before we need to recalculate. (if there's padding, then I guess the distance check would need to also be put in the traversal's test) - * - a more experimental idea would be to allow the BFS to go both left and right (as it currently does in sections that are aligned with the origin section) in the sections aligned with the origin section's neighbors. This would mean we can safely use the bfs result in all neighbors, but could slightly increase the number of false positives (which is a problem already...) + * TODO: this can't deal with very high world heights (more than 1024 blocks tall), we'd need multiple tree-cubes for that * - 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 (?) */ -public class LinearSectionOctree extends PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { - // offset is shifted by 1 to encompass all sections towards the negative - // TODO: is this the correct way of calculating the minimum possible section index? - private static final int TREE_OFFSET = 1; - - final Tree mainTree; +public class SectionTree extends PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { + public final Tree mainTree; Tree secondaryTree; - final int baseOffsetX, baseOffsetY, baseOffsetZ; - final int buildSectionX, buildSectionY, buildSectionZ; private final int bfsWidth; - private final boolean isFrustumTested; - private final float buildDistance; - private final int frame; - private final CullType cullType; - private VisibleSectionVisitor visitor; - private Viewport viewport; - private float distanceLimit; + private final float buildDistance; + protected final int frame; public interface VisibleSectionVisitor { void visit(int x, int y, int z); } - public LinearSectionOctree(Viewport viewport, float buildDistance, int frame, CullType cullType) { + public SectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType) { + super(viewport, buildDistance, cullType.isFrustumTested); + this.bfsWidth = cullType.bfsWidth; - this.isFrustumTested = cullType.isFrustumTested; this.buildDistance = buildDistance; this.frame = frame; - this.cullType = cullType; - - var transform = viewport.getTransform(); - int offsetDistance = Mth.floor(buildDistance / 16.0f) + TREE_OFFSET; - this.buildSectionX = transform.intX >> 4; - this.buildSectionY = transform.intY >> 4; - this.buildSectionZ = transform.intZ >> 4; - this.baseOffsetX = this.buildSectionX - offsetDistance; - this.baseOffsetY = this.buildSectionY - offsetDistance; - this.baseOffsetZ = this.buildSectionZ - offsetDistance; this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); } - public CullType getCullType() { - return this.cullType; + protected Tree makeSecondaryTree() { + // offset diagonally to fully encompass the required area + return new Tree(this.baseOffsetX + 4, this.baseOffsetY, this.baseOffsetZ + 4); } public int getFrame() { return this.frame; } + public boolean isValidFor(int newCameraSectionX, int newCameraSectionY, int newCameraSectionZ) { + return this.cameraX >> 4 == newCameraSectionX && + this.cameraY >> 4 == newCameraSectionY && + this.cameraZ >> 4 == newCameraSectionZ; + } + @Override public boolean isWithinFrustum(Viewport viewport, RenderSection section) { return !this.isFrustumTested || super.isWithinFrustum(viewport, section); @@ -77,8 +60,10 @@ public int getOutwardDirections(SectionPos origin, RenderSection section) { planes |= section.getChunkX() <= origin.getX() + this.bfsWidth ? 1 << GraphDirection.WEST : 0; planes |= section.getChunkX() >= origin.getX() - this.bfsWidth ? 1 << GraphDirection.EAST : 0; + planes |= section.getChunkY() <= origin.getY() + this.bfsWidth ? 1 << GraphDirection.DOWN : 0; planes |= section.getChunkY() >= origin.getY() - this.bfsWidth ? 1 << GraphDirection.UP : 0; + planes |= section.getChunkZ() <= origin.getZ() + this.bfsWidth ? 1 << GraphDirection.NORTH : 0; planes |= section.getChunkZ() >= origin.getZ() - this.bfsWidth ? 1 << GraphDirection.SOUTH : 0; @@ -90,18 +75,21 @@ public void visit(RenderSection section) { super.visit(section); // discard invisible or sections that don't need to be rendered - if (!visible || (section.getRegion().getSectionFlags(section.getSectionIndex()) & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) { + if ((section.getRegion().getSectionFlags(section.getSectionIndex()) & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) { return; } - int x = section.getChunkX(); - int y = section.getChunkY(); - int z = section.getChunkZ(); + this.addToTree(section); + } + + protected void addToTree(RenderSection section) { + this.markPresent(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + protected void markPresent(int x, int y, int z) { if (this.mainTree.add(x, y, z)) { if (this.secondaryTree == null) { - // offset diagonally to fully encompass the required area - this.secondaryTree = new Tree(this.baseOffsetX + 4, this.baseOffsetY, this.baseOffsetZ + 4); + this.secondaryTree = this.makeSecondaryTree(); } if (this.secondaryTree.add(x, y, z)) { throw new IllegalStateException("Failed to add section to trees"); @@ -137,43 +125,36 @@ private boolean isSectionPresent(int x, int y, int z) { (this.secondaryTree != null && this.secondaryTree.isSectionPresent(x, y, z)); } - private boolean isDistanceLimitActive() { - return LinearSectionOctree.this.distanceLimit < LinearSectionOctree.this.buildDistance; - } - public void traverseVisible(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { - this.visitor = visitor; - this.viewport = viewport; - this.distanceLimit = distanceLimit; - - this.mainTree.traverse(viewport); + this.mainTree.traverse(visitor, viewport, distanceLimit); if (this.secondaryTree != null) { - this.secondaryTree.traverse(viewport); + this.secondaryTree.traverse(visitor, viewport, distanceLimit); } - - this.visitor = null; - this.viewport = null; } - private class Tree { + public class Tree { + private static final int INSIDE_FRUSTUM = 0b01; + private static final int INSIDE_DISTANCE = 0b10; + private static final int FULLY_INSIDE = 0b11; + private final long[] tree = new long[64 * 64]; private final long[] treeReduced = new long[64]; - private long treeDoubleReduced = 0L; + public long treeDoubleReduced = 0L; private final int offsetX, offsetY, offsetZ; + // set temporarily during traversal private int cameraOffsetX, cameraOffsetY, cameraOffsetZ; + private VisibleSectionVisitor visitor; + protected Viewport viewport; + private float distanceLimit; - private static final int INSIDE_FRUSTUM = 0b01; - private static final int INSIDE_DISTANCE = 0b10; - private static final int FULLY_INSIDE = 0b11; - - Tree(int offsetX, int offsetY, int offsetZ) { + public Tree(int offsetX, int offsetY, int offsetZ) { this.offsetX = offsetX; this.offsetY = offsetY; this.offsetZ = offsetZ; } - boolean add(int x, int y, int z) { + public boolean add(int x, int y, int z) { x -= this.offsetX; y -= this.offsetY; z -= this.offsetZ; @@ -228,7 +209,11 @@ boolean isSectionPresent(int x, int y, int z) { return (this.tree[reducedBitIndex] & (1L << (bitIndex & 0b111111))) != 0; } - void traverse(Viewport viewport) { + public void traverse(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + this.visitor = visitor; + this.viewport = viewport; + this.distanceLimit = distanceLimit; + var transform = viewport.getTransform(); // + 1 to section position to compensate for shifted global offset @@ -236,8 +221,12 @@ void traverse(Viewport viewport) { this.cameraOffsetY = (transform.intY >> 4) - this.offsetY + 1; this.cameraOffsetZ = (transform.intZ >> 4) - this.offsetZ + 1; - var initialInside = LinearSectionOctree.this.isDistanceLimitActive() ? 0 : INSIDE_DISTANCE; + // everything is already inside the distance limit if the build distance is smaller + var initialInside = this.distanceLimit >= SectionTree.this.buildDistance ? INSIDE_DISTANCE : 0; this.traverse(0, 0, 0, 0, 5, initialInside); + + this.visitor = null; + this.viewport = null; } void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, int inside) { @@ -265,7 +254,7 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, int in int z = deinterleave6(sectionOrigin >> 2) + this.offsetZ; if (inside == FULLY_INSIDE || testLeafNode(x, y, z, inside)) { - LinearSectionOctree.this.visitor.visit(x, y, z); + this.visitor.visit(x, y, z); } } } @@ -334,7 +323,7 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { } // convert to world-space section origin in blocks, then to camera space - var transform = LinearSectionOctree.this.viewport.getTransform(); + var transform = this.viewport.getTransform(); x = ((x + this.offsetX) << 4) - transform.intX; y = ((y + this.offsetY) << 4) - transform.intY; z = ((z + this.offsetZ) << 4) - transform.intZ; @@ -342,7 +331,7 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { boolean visible = true; if ((inside & INSIDE_FRUSTUM) == 0) { - var intersectionResult = LinearSectionOctree.this.viewport.getBoxIntersectionDirect( + var intersectionResult = this.viewport.getBoxIntersectionDirect( (x + childHalfDim) - transform.fracX, (y + childHalfDim) - transform.fracY, (z + childHalfDim) - transform.fracZ, @@ -362,14 +351,14 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { float dz = nearestToZero(z, z + childFullDim) - transform.fracZ; // check if closest point inside the cylinder - visible = cylindricalDistanceTest(dx, dy, dz, LinearSectionOctree.this.distanceLimit); + visible = cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); if (visible) { // if the farthest point is also visible, the node is fully inside dx = farthestFromZero(x, x + childFullDim) - transform.fracX; dy = farthestFromZero(y, y + childFullDim) - transform.fracY; dz = farthestFromZero(z, z + childFullDim) - transform.fracZ; - if (cylindricalDistanceTest(dx, dy, dz, LinearSectionOctree.this.distanceLimit)) { + if (cylindricalDistanceTest(dx, dy, dz, this.distanceLimit)) { inside |= INSIDE_DISTANCE; } } @@ -383,7 +372,7 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { boolean testLeafNode(int x, int y, int z, int inside) { // input coordinates are section coordinates in world-space - var transform = LinearSectionOctree.this.viewport.getTransform(); + var transform = this.viewport.getTransform(); // convert to blocks and move into integer camera space x = (x << 4) - transform.intX; @@ -391,7 +380,7 @@ boolean testLeafNode(int x, int y, int z, int inside) { z = (z << 4) - transform.intZ; // test frustum if not already inside frustum - if ((inside & INSIDE_FRUSTUM) == 0 && !LinearSectionOctree.this.viewport.isBoxVisibleDirect( + if ((inside & INSIDE_FRUSTUM) == 0 && !this.viewport.isBoxVisibleDirect( (x + 8) - transform.fracX, (y + 8) - transform.fracY, (z + 8) - transform.fracZ, @@ -407,7 +396,7 @@ boolean testLeafNode(int x, int y, int z, int inside) { float dy = nearestToZero(y, y + 16) - transform.fracY; float dz = nearestToZero(z, z + 16) - transform.fracZ; - return cylindricalDistanceTest(dx, dy, dz, LinearSectionOctree.this.distanceLimit); + return cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); } return true; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java index 055270f7bf..c6b2cd5466 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/SortBehavior.java @@ -1,5 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; + public enum SortBehavior { OFF("OFF", SortMode.NONE), STATIC("S", SortMode.STATIC), @@ -12,10 +14,10 @@ public enum SortBehavior { private final String shortName; private final SortBehavior.SortMode sortMode; private final SortBehavior.PriorityMode priorityMode; - private final SortBehavior.DeferMode deferMode; + private final DeferMode deferMode; SortBehavior(String shortName, SortBehavior.SortMode sortMode, SortBehavior.PriorityMode priorityMode, - SortBehavior.DeferMode deferMode) { + DeferMode deferMode) { this.shortName = shortName; this.sortMode = sortMode; this.priorityMode = priorityMode; @@ -26,7 +28,7 @@ public enum SortBehavior { this(shortName, sortMode, null, null); } - SortBehavior(String shortName, SortBehavior.PriorityMode priorityMode, SortBehavior.DeferMode deferMode) { + SortBehavior(String shortName, SortBehavior.PriorityMode priorityMode, DeferMode deferMode) { this(shortName, SortMode.DYNAMIC, priorityMode, deferMode); } @@ -42,7 +44,7 @@ public SortBehavior.PriorityMode getPriorityMode() { return this.priorityMode; } - public SortBehavior.DeferMode getDeferMode() { + public DeferMode getDeferMode() { return this.deferMode; } @@ -53,8 +55,4 @@ public enum SortMode { public enum PriorityMode { NONE, NEARBY, ALL } - - public enum DeferMode { - ALWAYS, ONE_FRAME, ZERO_FRAMES - } } From 59489841c3d7c1a9b54890580288dc72fa350481 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 13 Sep 2024 22:18:27 +0200 Subject: [PATCH 23/81] remove debug changes --- .../sodium/client/render/chunk/lists/TaskSectionTree.java | 4 ++-- .../sodium/client/render/chunk/occlusion/SectionTree.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java index 0bdc57bea9..c29213e97e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -7,8 +7,8 @@ import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; public class TaskSectionTree extends SectionTree { - final Tree mainTaskTree; - Tree secondaryTaskTree; + private final Tree mainTaskTree; + private Tree secondaryTaskTree; public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType) { super(viewport, buildDistance, frame, cullType); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index a5993a76b1..b99a3252ca 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -12,8 +12,8 @@ * - 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 (?) */ public class SectionTree extends PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { - public final Tree mainTree; - Tree secondaryTree; + private final Tree mainTree; + private Tree secondaryTree; private final int bfsWidth; From 91b8c43708672a3538ed1eba6dd347c6325a8730 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 13 Sep 2024 22:19:04 +0200 Subject: [PATCH 24/81] fix issues with occlusion culling by making the graph search generate its own "run" token --- .../client/render/chunk/async/FrustumCullTask.java | 2 +- .../sodium/client/render/chunk/async/GlobalCullTask.java | 2 +- .../client/render/chunk/occlusion/OcclusionCuller.java | 9 +++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java index 516c8b978e..5e9aa319ab 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -14,7 +14,7 @@ public FrustumCullTask(OcclusionCuller occlusionCuller, Viewport viewport, float @Override public FrustumCullResult runTask() { var tree = new SectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM); - this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this.frame); + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); var frustumTaskLists = tree.getPendingTaskLists(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index cbb304709e..560b10ea8a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -22,7 +22,7 @@ public GlobalCullTask(OcclusionCuller occlusionCuller, Viewport viewport, float @Override public GlobalCullResult runTask() { var tree = new TaskSectionTree(this.viewport, this.buildDistance, this.frame, this.cullType); - this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this.getOcclusionToken()); + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); var collector = new FrustumTaskCollector(this.viewport, this.buildDistance, this.sectionByPosition); tree.traverseVisiblePendingTasks(collector, this.viewport, this.buildDistance); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index c324d04d06..6c1c616ace 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -14,6 +14,7 @@ public class OcclusionCuller { private final Long2ReferenceMap sections; private final Level level; + private volatile int token = 0; private final DoubleBufferedQueue queue = new DoubleBufferedQueue<>(); @@ -56,12 +57,16 @@ public OcclusionCuller(Long2ReferenceMap sections, Level level) { public void findVisible(GraphOcclusionVisitor visitor, Viewport viewport, float searchDistance, - boolean useOcclusionCulling, - int token) + boolean useOcclusionCulling) { final var queues = this.queue; queues.reset(); + // get a token for this bfs run by incrementing the counter. + // It doesn't need to be atomic since there's no concurrent access, but it needs to be synced to other threads. + var token = this.token; + this.token = token + 1; + this.init(visitor, queues.write(), viewport, searchDistance, useOcclusionCulling, token); while (queues.flip()) { From 0d5f7fe6d8b2159c74c38c4f463e6a1638c944f6 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 13 Sep 2024 22:25:56 +0200 Subject: [PATCH 25/81] introduce adaptive scheduling of cull tasks to reduce oscillation of the frame rate and section count when the world updates frequently --- .../client/render/SodiumWorldRenderer.java | 2 +- .../render/chunk/RenderSectionManager.java | 64 +++++++++++++------ .../render/chunk/occlusion/CullType.java | 3 - 3 files changed, 45 insertions(+), 24 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index bd27b04df1..4e36322293 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -207,7 +207,7 @@ public void setupTerrain(Camera camera, this.lastCameraYaw = yaw; if (cameraLocationChanged || cameraAngleChanged || cameraProjectionChanged) { - this.renderSectionManager.markRenderListDirty(); + this.renderSectionManager.notifyChangedCamera(); } this.lastFogDistance = fogDistance; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 109ba0e4a2..0dcf1f4c88 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -96,6 +96,7 @@ public class RenderSectionManager { private boolean needsGraphUpdate = true; private boolean needsRenderListUpdate = true; + private boolean cameraChanged = false; private @Nullable BlockPos cameraBlockPos; private @Nullable Vector3dc cameraPosition; @@ -138,11 +139,10 @@ public void updateCameraState(Vector3dc cameraPosition, Camera camera) { // TODO idea: increase and decrease chunk builder thread budget based on if the upload buffer was filled if the entire budget was used up. if the fallback way of uploading buffers is used, just doing 3 * the budget actually slows down frames while things are getting uploaded. For this it should limit how much (or how often?) things are uploaded. In the case of the mapped upload, just making sure we don't exceed its size is probably enough. - // TODO: use narrow-to-wide order for scheduling tasks if we can expect the player not to move much - public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator, boolean updateImmediately) { this.frame += 1; this.lastFrameAtTime = System.nanoTime(); + this.needsRenderListUpdate |= this.cameraChanged; // do sync bfs based on update immediately (flawless frames) or if the camera moved too much var shouldRenderSync = this.cameraTimingControl.getShouldRenderSync(camera); @@ -153,11 +153,10 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato if (this.needsGraphUpdate) { this.lastGraphDirtyFrame = this.frame; - this.needsGraphUpdate = false; } // discard unusable present and pending frustum-tested trees - if (this.needsRenderListUpdate) { + if (this.cameraChanged) { this.trees.remove(CullType.FRUSTUM); this.pendingTasks.removeIf(task -> { @@ -186,8 +185,9 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato processRenderListUpdate(viewport); } - var frameEnd = System.nanoTime(); -// System.out.println("Frame " + this.frame + " took " + (frameEnd - this.lastFrameAtTime) / 1000 + "µs"); + this.needsRenderListUpdate = false; + this.needsGraphUpdate = false; + this.cameraChanged = false; } private void renderSync(Camera camera, Viewport viewport, boolean spectator) { @@ -201,20 +201,22 @@ private void renderSync(Camera camera, Viewport viewport, boolean spectator) { this.pendingTasks.clear(); var tree = new VisibleChunkCollectorSync(viewport, searchDistance, this.frame, CullType.FRUSTUM); - this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, this.frame); + this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling); this.frustumTaskLists = tree.getPendingTaskLists(); this.globalTaskLists = null; this.trees.put(CullType.FRUSTUM, tree); this.renderTree = tree; + this.renderLists = tree.createRenderLists(); + // remove the other trees, they're very wrong by now this.trees.remove(CullType.WIDE); this.trees.remove(CullType.REGULAR); - this.renderLists = tree.createRenderLists(); this.needsRenderListUpdate = false; this.needsGraphUpdate = false; + this.cameraChanged = false; } private SectionTree unpackTaskResults(boolean wait) { @@ -235,7 +237,7 @@ private SectionTree unpackTaskResults(boolean wait) { this.frustumTaskLists = result.getFrustumTaskLists(); // ensure no useless frustum tree is accepted - if (!this.needsRenderListUpdate) { + if (!this.cameraChanged) { var tree = result.getTree(); this.trees.put(CullType.FRUSTUM, tree); latestTree = tree; @@ -271,16 +273,19 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat currentRunningTask = this.pendingTasks.getFirst(); } + // pick a scheduling order based on if there's been a graph update and if the render list is dirty + var scheduleOrder = getScheduleOrder(); + var transform = viewport.getTransform(); var cameraSectionX = transform.intX >> 4; var cameraSectionY = transform.intY >> 4; var cameraSectionZ = transform.intZ >> 4; - for (var type : CullType.WIDE_TO_NARROW) { + for (var type : scheduleOrder) { var tree = this.trees.get(type); - // don't schedule frustum-culled bfs until there hasn't been a dirty render list. - // otherwise a bfs is done each frame that always gets thrown away. - if (type == CullType.FRUSTUM && this.needsRenderListUpdate) { + // don't schedule frustum tasks if the camera just changed to prevent throwing them away constantly + // since they're going to be invalid in the next frame + if (type == CullType.FRUSTUM && this.cameraChanged) { continue; } @@ -292,11 +297,13 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat var searchDistance = this.getSearchDistance(); var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); + // use the last dirty frame as the frame timestamp to avoid wrongly marking task results as more recent if they're simply scheduled later but did work on the same state of the graph if there's been no graph invalidation since var task = switch (type) { case WIDE, REGULAR -> - new GlobalCullTask(this.occlusionCuller, viewport, searchDistance, useOcclusionCulling, this.frame, this.sectionByPosition, type); + new GlobalCullTask(this.occlusionCuller, viewport, searchDistance, useOcclusionCulling, this.lastGraphDirtyFrame, this.sectionByPosition, type); case FRUSTUM -> - new FrustumCullTask(this.occlusionCuller, viewport, searchDistance, useOcclusionCulling, this.frame); + // note that there is some danger with only giving the frustum tasks the last graph dirty frame and not the real current frame, but these are mitigated by deleting the frustum result when the camera changes. + new FrustumCullTask(this.occlusionCuller, viewport, searchDistance, useOcclusionCulling, this.lastGraphDirtyFrame); }; task.submitTo(this.asyncCullExecutor); this.pendingTasks.add(task); @@ -304,6 +311,25 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat } } + private static final CullType[] WIDE_TO_NARROW = { CullType.WIDE, CullType.REGULAR, CullType.FRUSTUM }; + private static final CullType[] NARROW_TO_WIDE = { CullType.FRUSTUM, CullType.REGULAR, CullType.WIDE }; + private static final CullType[] COMPROMISE = { CullType.REGULAR, CullType.FRUSTUM, CullType.WIDE }; + + private CullType[] getScheduleOrder() { + // if the camera is stationary, do the FRUSTUM update to first to prevent the render count from oscillating + if (!this.cameraChanged) { + return NARROW_TO_WIDE; + } + + // if only the render list is dirty but there's no graph update, do REGULAR first and potentially do FRUSTUM opportunistically + if (!this.needsGraphUpdate) { + return COMPROMISE; + } + + // if both are dirty, the camera is moving and loading new sections, do WIDE first to ensure there's any correct result + return WIDE_TO_NARROW; + } + private void processRenderListUpdate(Viewport viewport) { // schedule generating a frustum task list if there's no frustum tree task running if (this.globalTaskTree != null) { @@ -325,15 +351,13 @@ 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 SectionTree bestTree = null; - for (var type : CullType.NARROW_TO_WIDE) { + for (var type : NARROW_TO_WIDE) { var tree = this.trees.get(type); if (tree != null && (bestTree == null || tree.getFrame() > bestTree.getFrame())) { bestTree = tree; } } - this.needsRenderListUpdate = false; - // wait if there's no current tree (first frames after initial load/reload) if (bestTree == null) { bestTree = this.unpackTaskResults(true); @@ -354,8 +378,8 @@ public void markGraphDirty() { this.needsGraphUpdate = true; } - public void markRenderListDirty() { - this.needsRenderListUpdate = true; + public void notifyChangedCamera() { + this.cameraChanged = true; } public boolean needsUpdate() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java index 5f266616c9..de596485a1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java @@ -8,9 +8,6 @@ public enum CullType { public final int bfsWidth; public final boolean isFrustumTested; - public static final CullType[] WIDE_TO_NARROW = values(); - public static final CullType[] NARROW_TO_WIDE = {FRUSTUM, REGULAR, WIDE}; - CullType(int bfsWidth, boolean isFrustumTested) { this.bfsWidth = bfsWidth; this.isFrustumTested = isFrustumTested; From 69a081a4811f588bda473e519883d2bf2ee31c32 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 13 Sep 2024 22:30:28 +0200 Subject: [PATCH 26/81] formatting --- .../mods/sodium/client/render/chunk/RenderSectionFlags.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java index 489289e6b2..1f23664750 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java @@ -10,7 +10,7 @@ public class RenderSectionFlags { public static final int MASK_HAS_BLOCK_ENTITIES = 1 << HAS_BLOCK_ENTITIES; public static final int MASK_HAS_ANIMATED_SPRITES = 1 << HAS_ANIMATED_SPRITES; public static final int MASK_IS_BUILT = 1 << IS_BUILT; - public static final int MASK_NEEDS_RENDER = MASK_HAS_BLOCK_GEOMETRY | MASK_HAS_BLOCK_ENTITIES | MASK_HAS_ANIMATED_SPRITES; + public static final int MASK_NEEDS_RENDER = MASK_HAS_BLOCK_GEOMETRY | MASK_HAS_BLOCK_ENTITIES | MASK_HAS_ANIMATED_SPRITES; public static final int NONE = 0; } From 3521f3c780cdfedee6f6fe414a1576275e089f1e Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 14 Sep 2024 20:38:31 +0200 Subject: [PATCH 27/81] improve documentation --- .../sodium/client/render/chunk/RenderSectionManager.java | 5 +++-- .../client/render/chunk/lists/PendingTaskCollector.java | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 0dcf1f4c88..a79a2973d8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -718,8 +718,9 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode continue; } - // stop if the section doesn't need an update anymore - // TODO: check for section having a running task? + // don't schedule tasks for sections that don't need it anymore, + // since the pending update it cleared when a task is started, this includes + // sections for which there's a currently running task. var type = section.getPendingUpdate(); if (type == null) { continue; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index e9b9cf550a..352ef08049 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -33,7 +33,7 @@ public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisit private static final float WITHIN_FRUSTUM_BIAS = -3.0f; // points for being within the frustum private static final float PROXIMITY_FACTOR = 3.0f; // penalty for being far away private static final float CLOSE_DISTANCE = 50.0f; // distance at which another proximity bonus is applied - private static final float CLOSE_PROXIMITY_FACTOR = 0.6f; // bonus for being very close + private static final float CLOSE_PROXIMITY_FACTOR = 0.6f; // penalty for being CLOSE_DISTANCE or farther away private static final float INV_MAX_DISTANCE_CLOSE = CLOSE_PROXIMITY_FACTOR / CLOSE_DISTANCE; private final LongArrayList[] pendingTasks = new LongArrayList[DeferMode.values().length]; From 6459db251401c1571d2f601df4c0d376457c48af Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 14 Sep 2024 21:11:35 +0200 Subject: [PATCH 28/81] ideas --- .../mods/sodium/client/render/chunk/occlusion/SectionTree.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index b99a3252ca..317d84732d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -10,6 +10,9 @@ /** * TODO: this can't deal with very high world heights (more than 1024 blocks tall), we'd need multiple tree-cubes for that * - 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?) + * - possibly refactor the section tree and task section tree structures to be more composable instead of extending each other. + * - 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 Tree mainTree; From 78ca4bf9f3c0f7572a90083859798c27a1a967df Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 15 Sep 2024 17:42:54 +0200 Subject: [PATCH 29/81] move occlusion culler's parameter gymnastics into fields for code simplicity. also changed the frustum test to be before adding a section to the queue, and not after. from my measurements by looking at how long the OcclusionCuller call inside FrustumCullTask takes, this has no impact on performance. --- .../chunk/occlusion/OcclusionCuller.java | 155 ++++++++---------- 1 file changed, 71 insertions(+), 84 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 6c1c616ace..595e82adfc 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -14,10 +14,16 @@ public class OcclusionCuller { private final Long2ReferenceMap sections; private final Level level; - private volatile int token = 0; - private final DoubleBufferedQueue queue = new DoubleBufferedQueue<>(); + private volatile int tokenSource = 0; + + private int token; + private GraphOcclusionVisitor visitor; + private Viewport viewport; + private float searchDistance; + private boolean useOcclusionCulling; + // The bounding box of a chunk section must be large enough to contain all possible geometry within it. Block models // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon // to deal with floating point imprecision during a frustum check (see GH#2132). @@ -57,57 +63,53 @@ public OcclusionCuller(Long2ReferenceMap sections, Level level) { public void findVisible(GraphOcclusionVisitor visitor, Viewport viewport, float searchDistance, - boolean useOcclusionCulling) - { + boolean useOcclusionCulling) { + this.visitor = visitor; + this.viewport = viewport; + this.searchDistance = searchDistance; + this.useOcclusionCulling = useOcclusionCulling; + final var queues = this.queue; queues.reset(); // get a token for this bfs run by incrementing the counter. // It doesn't need to be atomic since there's no concurrent access, but it needs to be synced to other threads. - var token = this.token; - this.token = token + 1; + this.token = this.tokenSource; + this.tokenSource = this.token + 1; - this.init(visitor, queues.write(), viewport, searchDistance, useOcclusionCulling, token); + this.init(queues.write()); - while (queues.flip()) { - processQueue(visitor, viewport, searchDistance, useOcclusionCulling, token, queues.read(), queues.write()); + while (this.queue.flip()) { + processQueue(this.queue.read(), this.queue.write()); } this.addNearbySections(visitor, viewport, searchDistance, frame); - collector.traverseVisible(visitor, viewport); + this.visitor = null; + this.viewport = null; } - private static void processQueue(GraphOcclusionVisitor visitor, - Viewport viewport, - float searchDistance, - boolean useOcclusionCulling, - int token, - ReadQueue readQueue, - WriteQueue writeQueue) - { + private void processQueue(ReadQueue readQueue, + WriteQueue writeQueue) { RenderSection section; // TODO: move visibility test into visit neighbor method? while ((section = readQueue.dequeue()) != null) { - boolean sectionVisible = isWithinRenderDistance(viewport.getTransform(), section, searchDistance) && - visitor.isWithinFrustum(viewport, section); - if (!sectionVisible) { - continue; - } + this.visitor.visit(section); - visitor.visit(section); +// if (!sectionVisible) { +// continue; +// } int connections; { - if (useOcclusionCulling) { + if (this.useOcclusionCulling) { var sectionVisibilityData = section.getVisibilityData(); // occlude paths through the section if it's being viewed at an angle where // the other side can't possibly be seen sectionVisibilityData &= getAngleVisibilityMask(viewport, section); - // When using occlusion culling, we can only traverse into neighbors for which there is a path of // visibility through this chunk. This is determined by taking all the incoming paths to this chunk and // creating a union of the outgoing paths from those. @@ -119,10 +121,10 @@ private static void processQueue(GraphOcclusionVisitor visitor, // We can only traverse *outwards* from the center of the graph search, so mask off any invalid // directions. - connections &= visitor.getOutwardDirections(viewport.getChunkCoord(), section); + connections &= this.visitor.getOutwardDirections(this.viewport.getChunkCoord(), section); } - visitNeighbors(writeQueue, section, connections, token); + visitNeighbors(writeQueue, section, connections); } } @@ -167,7 +169,7 @@ private static boolean isWithinRenderDistance(CameraTransform camera, RenderSect return (((dx * dx) + (dz * dz)) < (maxDistance * maxDistance)) && (Math.abs(dy) < maxDistance); } - private static void visitNeighbors(final WriteQueue queue, RenderSection section, int outgoing, int token) { + private void visitNeighbors(WriteQueue queue, RenderSection section, int outgoing) { // Only traverse into neighbors which are actually present. // This avoids a null-check on each invocation to enqueue, and since the compiler will see that a null // is never encountered (after profiling), it will optimize it away. @@ -182,46 +184,49 @@ private static void visitNeighbors(final WriteQueue queue, Render queue.ensureCapacity(6); if (GraphDirectionSet.contains(outgoing, GraphDirection.DOWN)) { - visitNode(queue, section.adjacentDown, GraphDirectionSet.of(GraphDirection.UP), token); + visitNode(queue, section.adjacentDown, GraphDirectionSet.of(GraphDirection.UP)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.UP)) { - visitNode(queue, section.adjacentUp, GraphDirectionSet.of(GraphDirection.DOWN), token); + visitNode(queue, section.adjacentUp, GraphDirectionSet.of(GraphDirection.DOWN)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.NORTH)) { - visitNode(queue, section.adjacentNorth, GraphDirectionSet.of(GraphDirection.SOUTH), token); + visitNode(queue, section.adjacentNorth, GraphDirectionSet.of(GraphDirection.SOUTH)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.SOUTH)) { - visitNode(queue, section.adjacentSouth, GraphDirectionSet.of(GraphDirection.NORTH), token); + visitNode(queue, section.adjacentSouth, GraphDirectionSet.of(GraphDirection.NORTH)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.WEST)) { - visitNode(queue, section.adjacentWest, GraphDirectionSet.of(GraphDirection.EAST), token); + visitNode(queue, section.adjacentWest, GraphDirectionSet.of(GraphDirection.EAST)); } if (GraphDirectionSet.contains(outgoing, GraphDirection.EAST)) { - visitNode(queue, section.adjacentEast, GraphDirectionSet.of(GraphDirection.WEST), token); + visitNode(queue, section.adjacentEast, GraphDirectionSet.of(GraphDirection.WEST)); } } - private static void visitNode(final WriteQueue queue, RenderSection render, int incoming, int token) { + private void visitNode(WriteQueue queue, RenderSection section, int incoming) { // isn't usually null, but can be null if the bfs is happening during loading or unloading of chunks - if (render == null) { + if (section == null) { return; } - if (render.getLastVisibleSearchToken() != token) { + if (section.getLastVisibleSearchToken() != this.token) { // This is the first time we are visiting this section during the given token, so we must // reset the state. - render.setLastVisibleSearchToken(token); - render.setIncomingDirections(GraphDirectionSet.NONE); + section.setLastVisibleSearchToken(this.token); + section.setIncomingDirections(GraphDirectionSet.NONE); - queue.enqueue(render); + if (isWithinRenderDistance(this.viewport.getTransform(), section, this.searchDistance) + && this.visitor.isWithinFrustum(this.viewport, section)) { + queue.enqueue(section); + } } - render.addIncomingDirections(incoming); + section.addIncomingDirections(incoming); } @SuppressWarnings("ManualMinMaxCalculation") // we know what we are doing. @@ -278,44 +283,37 @@ private void addNearbySections(Visitor visitor, Viewport viewport, float searchD } } - private void init(GraphOcclusionVisitor visitor, - WriteQueue queue, - Viewport viewport, - float searchDistance, - boolean useOcclusionCulling, - int token) + private void init(WriteQueue queue) { - var origin = viewport.getChunkCoord(); + var origin = this.viewport.getChunkCoord(); if (origin.getY() < this.level.getMinSectionY()) { // below the level - this.initOutsideWorldHeight(visitor, queue, viewport, searchDistance, token, - this.level.getMinSectionY(), GraphDirection.DOWN); + this.initOutsideWorldHeight(queue, this.level.getMinSectionY(), GraphDirection.DOWN); } else if (origin.getY() > this.level.getMaxSectionY()) { // above the level - this.initOutsideWorldHeight(visitor, queue, viewport, searchDistance, token, - this.level.getMaxSectionY(), GraphDirection.UP); + this.initOutsideWorldHeight(queue, this.level.getMaxSectionY(), GraphDirection.UP); } else { - this.initWithinWorld(visitor, queue, viewport, useOcclusionCulling, token); + this.initWithinWorld(queue); } } - private void initWithinWorld(GraphOcclusionVisitor visitor, WriteQueue queue, Viewport viewport, boolean useOcclusionCulling, int token) { - var origin = viewport.getChunkCoord(); + private void initWithinWorld(WriteQueue queue) { + var origin = this.viewport.getChunkCoord(); var section = this.getRenderSection(origin.getX(), origin.getY(), origin.getZ()); if (section == null) { return; } - section.setLastVisibleSearchToken(token); + section.setLastVisibleSearchToken(this.token); section.setIncomingDirections(GraphDirectionSet.NONE); - visitor.visit(section); + this.visitor.visit(section); int outgoing; - if (useOcclusionCulling) { + if (this.useOcclusionCulling) { // Since the camera is located inside this chunk, there are no "incoming" directions. So we need to instead // find any possible paths out of this chunk and enqueue those neighbors. outgoing = VisibilityEncoding.getConnections(section.getVisibilityData()); @@ -324,36 +322,29 @@ private void initWithinWorld(GraphOcclusionVisitor visitor, WriteQueueW->S->E). - private void initOutsideWorldHeight(GraphOcclusionVisitor visitor, - WriteQueue queue, - Viewport viewport, - float searchDistance, - int token, - int height, - int direction) - { - var origin = viewport.getChunkCoord(); - var radius = Mth.floor(searchDistance / 16.0f); + private void initOutsideWorldHeight(WriteQueue queue, int height, int direction) { + var origin = this.viewport.getChunkCoord(); + var radius = Mth.floor(this.searchDistance / 16.0f); // Layer 0 - this.tryInitNode(visitor, queue, origin.getX(), height, origin.getZ(), direction, token, viewport); + this.tryInitNode(queue,origin.getX(), height, origin.getZ(), direction); // Complete layers, excluding layer 0 for (int layer = 1; layer <= radius; layer++) { for (int z = -layer; z < layer; z++) { int x = Math.abs(z) - layer; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); + this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = layer; z > -layer; z--) { int x = layer - Math.abs(z); - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); + this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); } } @@ -363,34 +354,30 @@ private void initOutsideWorldHeight(GraphOcclusionVisitor visitor, for (int z = -radius; z <= -l; z++) { int x = -z - layer; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); + this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = l; z <= radius; z++) { int x = z - layer; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); + this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = radius; z >= l; z--) { int x = layer - z; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); + this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = -l; z >= -radius; z--) { int x = layer + z; - this.tryInitNode(visitor, queue, origin.getX() + x, height, origin.getZ() + z, direction, token, viewport); + this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); } } } - private void tryInitNode(GraphOcclusionVisitor visitor, WriteQueue queue, int x, int y, int z, int direction, int token, Viewport viewport) { - RenderSection section = this.getRenderSection(x, y, z); - - if (section == null || !visitor.isWithinFrustum(viewport, section)) { - return; - } + private void tryInitNode(WriteQueue queue, int x, int y, int z, int direction) { + var section = this.getRenderSection(x, y, z); - visitNode(queue, section, GraphDirectionSet.of(direction), token); + visitNode(queue, section, GraphDirectionSet.of(direction)); } private RenderSection getRenderSection(int x, int y, int z) { From d461547bc77530098bac78a19d485e245d1e551e Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 17 Sep 2024 02:22:32 +0200 Subject: [PATCH 30/81] added efficient approximative ray cast backed by a bitmap to improve graph search culling --- .../render/chunk/async/FrustumCullTask.java | 16 +- .../render/chunk/async/GlobalCullTask.java | 14 +- .../chunk/lists/PendingTaskCollector.java | 4 - .../render/chunk/lists/TaskSectionTree.java | 6 +- .../lists/VisibleChunkCollectorAsync.java | 3 +- .../lists/VisibleChunkCollectorSync.java | 9 +- .../chunk/occlusion/OcclusionCuller.java | 34 ++-- .../occlusion/RayOcclusionSectionTree.java | 162 ++++++++++++++++++ .../render/chunk/occlusion/SectionTree.java | 33 ++-- 9 files changed, 237 insertions(+), 44 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java index 5e9aa319ab..78c2e00aa7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -1,8 +1,10 @@ package net.caffeinemc.mods.sodium.client.render.chunk.async; +import it.unimi.dsi.fastutil.longs.LongArrayList; import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.RayOcclusionSectionTree; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; @@ -11,10 +13,22 @@ public FrustumCullTask(OcclusionCuller occlusionCuller, Viewport viewport, float super(viewport, buildDistance, frame, occlusionCuller, useOcclusionCulling); } + private static final LongArrayList timings = new LongArrayList(); + @Override public FrustumCullResult runTask() { - var tree = new SectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM); + var tree = new RayOcclusionSectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM); + var start = System.nanoTime(); this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); + var end = System.nanoTime(); + var time = end - start; + timings.add(time); + final var count = 500; + if (timings.size() > count) { + var average = timings.longStream().average().orElse(0); + System.out.println("Frustum culling took " + (average) / 1000 + "µs over " + count + " samples"); + timings.clear(); + } var frustumTaskLists = tree.getPendingTaskLists(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index 560b10ea8a..fa9f1e5985 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -1,6 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.async; import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.lists.FrustumTaskCollector; import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; @@ -19,11 +20,22 @@ public GlobalCullTask(OcclusionCuller occlusionCuller, Viewport viewport, float this.cullType = cullType; } + private static final LongArrayList timings = new LongArrayList(); + @Override public GlobalCullResult runTask() { var tree = new TaskSectionTree(this.viewport, this.buildDistance, this.frame, this.cullType); + var start = System.nanoTime(); this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); - + var end = System.nanoTime(); + var time = end - start; + timings.add(time); + final var count = 500; + if (timings.size() > count) { + var average = timings.longStream().average().orElse(0); + System.out.println("Global culling took " + (average) / 1000 + "µs over " + count + " samples"); + timings.clear(); + } var collector = new FrustumTaskCollector(this.viewport, this.buildDistance, this.sectionByPosition); tree.traverseVisiblePendingTasks(collector, this.viewport, this.buildDistance); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index 352ef08049..dd781d9f86 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -75,10 +75,6 @@ public PendingTaskCollector(Viewport viewport, float buildDistance, boolean frus @Override public void visit(RenderSection section) { - if (!visible) { - return; - } - this.checkForTask(section); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java index c29213e97e..bcd15b6a76 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -3,10 +3,10 @@ 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.CullType; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.RayOcclusionSectionTree; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; -public class TaskSectionTree extends SectionTree { +public class TaskSectionTree extends RayOcclusionSectionTree { private final Tree mainTaskTree; private Tree secondaryTaskTree; @@ -29,7 +29,7 @@ protected void markTaskPresent(int x, int y, int z) { this.secondaryTaskTree = this.makeSecondaryTree(); } if (this.secondaryTaskTree.add(x, y, z)) { - throw new IllegalStateException("Failed to add section to trees"); + throw new IllegalStateException("Failed to add section to task trees"); } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java index a35876c9c3..b98d2d02e7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java @@ -14,8 +14,7 @@ import java.util.Queue; /** - * The visible chunk collector is passed to the occlusion graph search culler to - * collect the visible chunks. + * The async visible chunk collector is passed into a section tree to collect visible chunks. */ public class VisibleChunkCollectorAsync implements SectionTree.VisibleSectionVisitor { private final ObjectArrayList sortedRenderLists; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java index d83da2b4d1..7018df6881 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java @@ -8,6 +8,9 @@ import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +/** + * The sync visible chunk collector is passed into the graph search occlusion culler to collect visible chunks. + */ public class VisibleChunkCollectorSync extends SectionTree { private final ObjectArrayList sortedRenderLists; @@ -17,8 +20,8 @@ public VisibleChunkCollectorSync(Viewport viewport, float buildDistance, int fra } @Override - public void visit(RenderSection section, boolean visible) { - super.visit(section, visible); + public void visit(RenderSection section) { + super.visit(section); RenderRegion region = section.getRegion(); ChunkRenderList renderList = region.getRenderList(); @@ -32,7 +35,7 @@ public void visit(RenderSection section, boolean visible) { } var index = section.getSectionIndex(); - if (visible && (region.getSectionFlags(index) & RenderSectionFlags.MASK_NEEDS_RENDER) != 0) { + if ((region.getSectionFlags(index) & RenderSectionFlags.MASK_NEEDS_RENDER) != 0) { renderList.add(index); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 595e82adfc..7ad43ea427 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -27,11 +27,15 @@ public class OcclusionCuller { // The bounding box of a chunk section must be large enough to contain all possible geometry within it. Block models // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon // to deal with floating point imprecision during a frustum check (see GH#2132). - static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; + public static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; static final float CHUNK_SECTION_MARGIN = 1.0f /* maximum model extent */ + 0.125f /* epsilon */; static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + CHUNK_SECTION_MARGIN; public interface GraphOcclusionVisitor { + default boolean visitTestVisible(RenderSection section) { + return true; + } + void visit(RenderSection section); default boolean isWithinFrustum(Viewport viewport, RenderSection section) { @@ -93,14 +97,8 @@ private void processQueue(ReadQueue readQueue, WriteQueue writeQueue) { RenderSection section; - // TODO: move visibility test into visit neighbor method? + // only visible sections are entered into the queue while ((section = readQueue.dequeue()) != null) { - this.visitor.visit(section); - -// if (!sectionVisible) { -// continue; -// } - int connections; { @@ -220,8 +218,10 @@ private void visitNode(WriteQueue queue, RenderSection section, i section.setLastVisibleSearchToken(this.token); section.setIncomingDirections(GraphDirectionSet.NONE); - if (isWithinRenderDistance(this.viewport.getTransform(), section, this.searchDistance) - && this.visitor.isWithinFrustum(this.viewport, section)) { + if (isWithinRenderDistance(this.viewport.getTransform(), section, this.searchDistance) && + this.visitor.isWithinFrustum(this.viewport, section) && + this.visitor.visitTestVisible(section)) { + this.visitor.visit(section); queue.enqueue(section); } } @@ -333,18 +333,18 @@ private void initOutsideWorldHeight(WriteQueue queue, int height, var radius = Mth.floor(this.searchDistance / 16.0f); // Layer 0 - this.tryInitNode(queue,origin.getX(), height, origin.getZ(), direction); + this.tryInitNode(queue, origin.getX(), height, origin.getZ(), direction); // Complete layers, excluding layer 0 for (int layer = 1; layer <= radius; layer++) { for (int z = -layer; z < layer; z++) { int x = Math.abs(z) - layer; - this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = layer; z > -layer; z--) { int x = layer - Math.abs(z); - this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } } @@ -354,22 +354,22 @@ private void initOutsideWorldHeight(WriteQueue queue, int height, for (int z = -radius; z <= -l; z++) { int x = -z - layer; - this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = l; z <= radius; z++) { int x = z - layer; - this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = radius; z >= l; z--) { int x = layer - z; - this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } for (int z = -l; z >= -radius; z--) { int x = layer + z; - this.tryInitNode(queue,origin.getX() + x, height, origin.getZ() + z, direction); + this.tryInitNode(queue, origin.getX() + x, height, origin.getZ() + z, direction); } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java new file mode 100644 index 0000000000..47f55d69af --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -0,0 +1,162 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; + +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.viewport.CameraTransform; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public class RayOcclusionSectionTree extends SectionTree { + private static final float SECTION_HALF_DIAGONAL = (float) Math.sqrt(8 * 8 * 3); + private static final float RAY_MIN_STEP_SIZE_INV = 1.0f / (SECTION_HALF_DIAGONAL * 2); + private static final int RAY_TEST_MAX_STEPS = 12; + private static final int MIN_RAY_TEST_DISTANCE_SQ = (int) Math.pow(16 * 3, 2); + + private final CameraTransform transform; + + private final PortalMap mainPortalTree; + private PortalMap secondaryPortalTree; + + public RayOcclusionSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType) { + super(viewport, buildDistance, frame, cullType); + + this.transform = viewport.getTransform(); + this.mainPortalTree = new PortalMap(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + } + + @Override + public boolean visitTestVisible(RenderSection section) { + if ((section.getRegion().getSectionFlags(section.getSectionIndex()) & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) { + this.lastSectionKnownEmpty = true; + } else { + this.lastSectionKnownEmpty = false; + if (this.isRayBlockedStepped(section)) { + return false; + } + } + + return super.visitTestVisible(section); + } + + @Override + public void visit(RenderSection section) { + super.visit(section); + this.lastSectionKnownEmpty = false; + + // mark all traversed sections as portals, even if they don't have terrain that needs rendering + this.markPortal(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + + private boolean isRayBlockedStepped(RenderSection section) { + // check if this section is visible through all so far traversed sections + var x = (float) section.getCenterX(); + var y = (float) section.getCenterY(); + var z = (float) section.getCenterZ(); + var dX = (float) (this.transform.x - x); + var dY = (float) (this.transform.y - y); + var dZ = (float) (this.transform.z - z); + + var distanceSquared = dX * dX + dY * dY + dZ * dZ; + if (distanceSquared < MIN_RAY_TEST_DISTANCE_SQ) { + return false; + } + + var length = (float) Math.sqrt(distanceSquared); + var steps = Math.min((int) (length * RAY_MIN_STEP_SIZE_INV), RAY_TEST_MAX_STEPS); + + // avoid the last step being in the camera + var stepsInv = 1.0f / steps; + dX *= stepsInv; + dY *= stepsInv; + dZ *= stepsInv; + + for (int i = 1; i < steps; i++) { + x += dX; + y += dY; + z += dZ; + + if (this.blockHasObstruction((int) x, (int) y, (int) z)) { + // also test radius around to avoid false negatives + var radius = SECTION_HALF_DIAGONAL * (steps - i) * stepsInv; + + // this pattern simulates a shape similar to the sweep of the section towards the camera + if (!this.blockHasObstruction((int) (x - radius), (int) (y - radius), (int) (z - radius)) || + !this.blockHasObstruction((int) (x + radius), (int) (y + radius), (int) (z + radius))) { + continue; + } + return true; + } + } + + return false; + } + + protected void markPortal(int x, int y, int z) { + if (this.mainPortalTree.add(x, y, z)) { + if (this.secondaryPortalTree == null) { + this.secondaryPortalTree = new PortalMap( + this.baseOffsetX + SECONDARY_TREE_OFFSET_XZ, + this.baseOffsetY, + this.baseOffsetZ + SECONDARY_TREE_OFFSET_XZ); + } + if (this.secondaryPortalTree.add(x, y, z)) { + throw new IllegalStateException("Failed to add section to portal trees"); + } + } + } + + private static final int IS_OBSTRUCTED = 0; + private static final int NOT_OBSTRUCTED = 1; + private static final int OUT_OF_BOUNDS = 2; + + private boolean blockHasObstruction(int x, int y, int z) { + return this.hasObstruction(x >> 4, y >> 4, z >> 4); + } + + private boolean hasObstruction(int x, int y, int z) { + var result = this.mainPortalTree.getObstruction(x, y, z); + if (result == OUT_OF_BOUNDS) { + return this.secondaryPortalTree != null && + this.secondaryPortalTree.getObstruction(x, y, z) == IS_OBSTRUCTED; + } + return result == IS_OBSTRUCTED; + } + + protected class PortalMap { + protected final long[] bitmap = new long[64 * 64]; + protected final int offsetX, offsetY, offsetZ; + + public PortalMap(int offsetX, int offsetY, int offsetZ) { + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + } + + public boolean add(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (Tree.isOutOfBounds(x, y, z)) { + return true; + } + + var bitIndex = Tree.interleave6x3(x, y, z); + this.bitmap[bitIndex >> 6] |= 1L << (bitIndex & 0b111111); + + return false; + } + + + public int getObstruction(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (Tree.isOutOfBounds(x, y, z)) { + return OUT_OF_BOUNDS; + } + + var bitIndex = Tree.interleave6x3(x, y, z); + var mask = 1L << (bitIndex & 0b111111); + return (this.bitmap[bitIndex >> 6] & mask) == 0 ? IS_OBSTRUCTED : NOT_OBSTRUCTED; + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 317d84732d..ac6c05416a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -15,6 +15,8 @@ * - 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 { + protected static final int SECONDARY_TREE_OFFSET_XZ = 4; + private final Tree mainTree; private Tree secondaryTree; @@ -22,6 +24,7 @@ public class SectionTree extends PendingTaskCollector implements OcclusionCuller private final float buildDistance; protected final int frame; + protected boolean lastSectionKnownEmpty = false; public interface VisibleSectionVisitor { void visit(int x, int y, int z); @@ -39,7 +42,10 @@ public SectionTree(Viewport viewport, float buildDistance, int frame, CullType c protected Tree makeSecondaryTree() { // offset diagonally to fully encompass the required area - return new Tree(this.baseOffsetX + 4, this.baseOffsetY, this.baseOffsetZ + 4); + return new Tree( + this.baseOffsetX + SECONDARY_TREE_OFFSET_XZ, + this.baseOffsetY, + this.baseOffsetZ + SECONDARY_TREE_OFFSET_XZ); } public int getFrame() { @@ -77,15 +83,12 @@ public int getOutwardDirections(SectionPos origin, RenderSection section) { public void visit(RenderSection section) { super.visit(section); - // discard invisible or sections that don't need to be rendered - if ((section.getRegion().getSectionFlags(section.getSectionIndex()) & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) { + // discard invisible or sections that don't need to be rendered, + // only perform this test if it hasn't already been done before + if (this.lastSectionKnownEmpty || (section.getRegion().getSectionFlags(section.getSectionIndex()) & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) { return; } - this.addToTree(section); - } - - protected void addToTree(RenderSection section) { this.markPresent(section.getChunkX(), section.getChunkY(), section.getChunkZ()); } @@ -140,10 +143,10 @@ public class Tree { private static final int INSIDE_DISTANCE = 0b10; private static final int FULLY_INSIDE = 0b11; - private final long[] tree = new long[64 * 64]; - private final long[] treeReduced = new long[64]; + protected final long[] tree = new long[64 * 64]; + protected final long[] treeReduced = new long[64]; public long treeDoubleReduced = 0L; - private final int offsetX, offsetY, offsetZ; + protected final int offsetX, offsetY, offsetZ; // set temporarily during traversal private int cameraOffsetX, cameraOffsetY, cameraOffsetZ; @@ -161,7 +164,7 @@ public boolean add(int x, int y, int z) { x -= this.offsetX; y -= this.offsetY; z -= this.offsetZ; - if (x > 63 || y > 63 || z > 63 || x < 0 || y < 0 || z < 0) { + if (isOutOfBounds(x, y, z)) { return true; } @@ -175,7 +178,11 @@ public boolean add(int x, int y, int z) { return false; } - private static int interleave6x3(int x, int y, int z) { + public static boolean isOutOfBounds(int x, int y, int z) { + return x > 63 || y > 63 || z > 63 || x < 0 || y < 0 || z < 0; + } + + protected static int interleave6x3(int x, int y, int z) { return interleave6(x) | interleave6(y) << 1 | interleave6(z) << 2; } @@ -198,7 +205,7 @@ boolean isSectionPresent(int x, int y, int z) { x -= this.offsetX; y -= this.offsetY; z -= this.offsetZ; - if (x > 63 || y > 63 || z > 63 || x < 0 || y < 0 || z < 0) { + if (isOutOfBounds(x, y, z)) { return false; } From 4f3eb0ec4edd3a1bc64f1aac3a634ed3f14b2c0a Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 17 Sep 2024 03:25:07 +0200 Subject: [PATCH 31/81] further improve speed of tree building by writing the reduced tree data all at once instead of for each section separately --- .../render/chunk/RenderSectionManager.java | 1 + .../render/chunk/async/FrustumCullTask.java | 1 + .../render/chunk/async/GlobalCullTask.java | 1 + .../render/chunk/lists/TaskSectionTree.java | 9 +++++++ .../render/chunk/occlusion/SectionTree.java | 27 +++++++++++++++---- 5 files changed, 34 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index a79a2973d8..957e426fc1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -202,6 +202,7 @@ private void renderSync(Camera camera, Viewport viewport, boolean spectator) { var tree = new VisibleChunkCollectorSync(viewport, searchDistance, this.frame, CullType.FRUSTUM); this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling); + tree.finalizeTrees(); this.frustumTaskLists = tree.getPendingTaskLists(); this.globalTaskLists = null; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java index 78c2e00aa7..52b4a9498a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -20,6 +20,7 @@ public FrustumCullResult runTask() { var tree = new RayOcclusionSectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM); var start = System.nanoTime(); this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); + tree.finalizeTrees(); var end = System.nanoTime(); var time = end - start; timings.add(time); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index fa9f1e5985..8f09d86011 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -27,6 +27,7 @@ public GlobalCullResult runTask() { var tree = new TaskSectionTree(this.viewport, this.buildDistance, this.frame, this.cullType); var start = System.nanoTime(); this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); + tree.finalizeTrees(); var end = System.nanoTime(); var time = end - start; timings.add(time); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java index bcd15b6a76..539ee3db1b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -34,6 +34,15 @@ protected void markTaskPresent(int x, int y, int z) { } } + @Override + public void finalizeTrees() { + super.finalizeTrees(); + this.mainTaskTree.calculateReduced(); + if (this.secondaryTaskTree != null) { + this.secondaryTaskTree.calculateReduced(); + } + } + public void traverseVisiblePendingTasks(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { this.mainTaskTree.traverse(visitor, viewport, distanceLimit); if (this.secondaryTaskTree != null) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index ac6c05416a..346fc441c9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -103,6 +103,13 @@ protected void markPresent(int x, int y, int z) { } } + public void finalizeTrees() { + this.mainTree.calculateReduced(); + if (this.secondaryTree != null) { + this.secondaryTree.calculateReduced(); + } + } + public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { // check if there's a section at any part of the box int minX = SectionPos.posToSectionCoord(x1 - 0.5D); @@ -169,11 +176,7 @@ public boolean add(int x, int y, int z) { } var bitIndex = interleave6x3(x, y, z); - int reducedBitIndex = bitIndex >> 6; - int doubleReducedBitIndex = bitIndex >> 12; - this.tree[reducedBitIndex] |= 1L << (bitIndex & 0b111111); - this.treeReduced[doubleReducedBitIndex] |= 1L << (reducedBitIndex & 0b111111); - this.treeDoubleReduced |= 1L << doubleReducedBitIndex; + this.tree[bitIndex >> 6] |= 1L << (bitIndex & 0b111111); return false; } @@ -194,6 +197,20 @@ private static int interleave6(int n) { return n; } + public void calculateReduced() { + long doubleReduced = 0; + for (int i = 0; i < 64; i++) { + long reduced = 0; + var reducedOffset = i << 6; + for (int j = 0; j < 64; j++) { + reduced |= this.tree[reducedOffset + j] == 0 ? 0L : 1L << j; + } + this.treeReduced[i] = reduced; + doubleReduced |= reduced == 0 ? 0L : 1L << i; + } + this.treeDoubleReduced = doubleReduced; + } + private static int deinterleave6(int n) { n &= 0b001001001001001001; n = (n | n >> 2) & 0b000011000011000011; From 15e1b2b41b556563236e48a9d1b945e291c59951 Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 17 Sep 2024 04:25:04 +0200 Subject: [PATCH 32/81] restore some info on the debug screen --- .../client/render/SodiumWorldRenderer.java | 4 +- .../render/chunk/RenderSectionManager.java | 40 ++++++++++++++++--- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index 4e36322293..5be4f72547 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -509,9 +509,7 @@ public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y } public String getChunksDebugString() { - // C: visible/total D: distance - // TODO: add dirty and queued counts - return String.format("C: %d/%d D: %d", this.renderSectionManager.getVisibleChunkCount(), this.renderSectionManager.getTotalSections(), this.renderDistance); + return this.renderSectionManager.getChunksDebugString(); } /** diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 957e426fc1..0cb892003a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -953,6 +953,8 @@ public Collection getDebugStrings() { count++; } + // TODO: information about pending async culling tasks, restore some information about task scheduling? + list.add(String.format("Geometry Pool: %d/%d MiB (%d buffers)", MathUtil.toMib(deviceUsed), MathUtil.toMib(deviceAllocated), count)); list.add(String.format("Transfer Queue: %s", this.regions.getStagingBuffer().toString())); @@ -960,18 +962,44 @@ public Collection getDebugStrings() { this.builder.getScheduledJobCount(), this.builder.getScheduledEffort(), this.builder.getBusyThreadCount(), this.builder.getTotalThreadCount()) ); -// list.add(String.format("Chunk Queues: U=%02d (P0=%03d | P1=%03d | P2=%03d)", -// this.buildResults.size(), -// this.frustumTaskLists.get(ChunkUpdateType.IMPORTANT_REBUILD).size() + this.frustumTaskLists.get(ChunkUpdateType.IMPORTANT_SORT).size(), -// this.frustumTaskLists.get(ChunkUpdateType.REBUILD).size() + this.frustumTaskLists.get(ChunkUpdateType.SORT).size(), -// this.frustumTaskLists.get(ChunkUpdateType.INITIAL_BUILD).size()) -// ); + list.add(String.format("Chunk Queues: U=%02d", this.buildResults.size())); this.sortTriggering.addDebugStrings(list); return list; } + public String getChunksDebugString() { + // TODO: add dirty and queued counts + + // C: visible/total D: distance + return String.format( + "C: %d/%d (%s) D: %d", + this.getVisibleChunkCount(), + this.getTotalSections(), + this.getCullTypeName(), + this.renderDistance); + } + + private String getCullTypeName() { + CullType renderTreeCullType = null; + for (var type : CullType.values()) { + if (this.trees.get(type) == this.renderTree) { + renderTreeCullType = type; + break; + } + } + var cullTypeName = "-"; + if (renderTreeCullType != null) { + cullTypeName = switch (renderTreeCullType) { + case WIDE -> "W"; + case REGULAR -> "R"; + case FRUSTUM -> "F"; + }; + } + return cullTypeName; + } + public @NotNull SortedRenderLists getRenderLists() { return this.renderLists; } From 1811b98c6800ac6b54a58d880f5f1865a9da42dc Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 2 Oct 2024 03:04:02 +0200 Subject: [PATCH 33/81] fix section ordering --- .../render/chunk/occlusion/SectionTree.java | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 346fc441c9..f6522ce8bf 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -257,7 +257,10 @@ public void traverse(VisibleSectionVisitor visitor, Viewport viewport, float dis } void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, int inside) { + // half of the dimension of a child of this node, in blocks int childHalfDim = 1 << (level + 3); // * 16 / 2 + + // / 8 to get childFullDim in sections int orderModulator = getChildOrderModulator(nodeX, nodeY, nodeZ, childHalfDim >> 3); if ((level & 1) == 1) { orderModulator <<= 3; @@ -337,6 +340,9 @@ void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, int in } } + // TODO: move the modulator calculation to the caller to avoid passing coordinates + // TODO: fix flickering when above the world height (?) + void testChild(int childOrigin, int childHalfDim, int level, int inside) { // calculate section coordinates in tree-space int x = deinterleave6(childOrigin); @@ -351,17 +357,17 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { // convert to world-space section origin in blocks, then to camera space var transform = this.viewport.getTransform(); - x = ((x + this.offsetX) << 4) - transform.intX; - y = ((y + this.offsetY) << 4) - transform.intY; - z = ((z + this.offsetZ) << 4) - transform.intZ; + int worldX = ((x + this.offsetX) << 4) - transform.intX; + int worldY = ((y + this.offsetY) << 4) - transform.intY; + int worldZ = ((z + this.offsetZ) << 4) - transform.intZ; boolean visible = true; if ((inside & INSIDE_FRUSTUM) == 0) { var intersectionResult = this.viewport.getBoxIntersectionDirect( - (x + childHalfDim) - transform.fracX, - (y + childHalfDim) - transform.fracY, - (z + childHalfDim) - transform.fracZ, + (worldX + childHalfDim) - transform.fracX, + (worldY + childHalfDim) - transform.fracY, + (worldZ + childHalfDim) - transform.fracZ, childHalfDim + OcclusionCuller.CHUNK_SECTION_MARGIN); if (intersectionResult == FrustumIntersection.INSIDE) { inside |= INSIDE_FRUSTUM; @@ -373,17 +379,17 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { if ((inside & INSIDE_DISTANCE) == 0) { // calculate the point of the node closest to the camera int childFullDim = childHalfDim << 1; - float dx = nearestToZero(x, x + childFullDim) - transform.fracX; - float dy = nearestToZero(y, y + childFullDim) - transform.fracY; - float dz = nearestToZero(z, z + childFullDim) - transform.fracZ; + float dx = nearestToZero(worldX, worldX + childFullDim) - transform.fracX; + float dy = nearestToZero(worldY, worldY + childFullDim) - transform.fracY; + float dz = nearestToZero(worldZ, worldZ + childFullDim) - transform.fracZ; // check if closest point inside the cylinder visible = cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); if (visible) { // if the farthest point is also visible, the node is fully inside - dx = farthestFromZero(x, x + childFullDim) - transform.fracX; - dy = farthestFromZero(y, y + childFullDim) - transform.fracY; - dz = farthestFromZero(z, z + childFullDim) - transform.fracZ; + dx = farthestFromZero(worldX, worldX + childFullDim) - transform.fracX; + dy = farthestFromZero(worldY, worldY + childFullDim) - transform.fracY; + dz = farthestFromZero(worldZ, worldZ + childFullDim) - transform.fracZ; if (cylindricalDistanceTest(dx, dy, dz, this.distanceLimit)) { inside |= INSIDE_DISTANCE; @@ -467,10 +473,10 @@ private static int farthestFromZero(int min, int max) { return clamped; } - int getChildOrderModulator(int x, int y, int z, int childSectionDim) { - return (x + childSectionDim - this.cameraOffsetX) >>> 31 - | ((y + childSectionDim - this.cameraOffsetY) >>> 31) << 1 - | ((z + childSectionDim - this.cameraOffsetZ) >>> 31) << 2; + int getChildOrderModulator(int x, int y, int z, int childFullSectionDim) { + return (x + childFullSectionDim - this.cameraOffsetX) >>> 31 + | ((y + childFullSectionDim - this.cameraOffsetY) >>> 31) << 1 + | ((z + childFullSectionDim - this.cameraOffsetZ) >>> 31) << 2; } } } From 1ddc2eb9caaf0bf8357bf41023e105be9f75c1a7 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 2 Oct 2024 04:07:32 +0200 Subject: [PATCH 34/81] improve how coordinate parameters are passed into section tree traversal --- .../client/render/chunk/occlusion/SectionTree.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index f6522ce8bf..f78ad8675a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -250,18 +250,17 @@ public void traverse(VisibleSectionVisitor visitor, Viewport viewport, float dis // everything is already inside the distance limit if the build distance is smaller var initialInside = this.distanceLimit >= SectionTree.this.buildDistance ? INSIDE_DISTANCE : 0; - this.traverse(0, 0, 0, 0, 5, initialInside); + this.traverse(getChildOrderModulator(0, 0, 0, 1 << 5), 0, 5, initialInside); this.visitor = null; this.viewport = null; } - void traverse(int nodeX, int nodeY, int nodeZ, int nodeOrigin, int level, int inside) { + void traverse(int orderModulator, int nodeOrigin, int level, int inside) { // half of the dimension of a child of this node, in blocks int childHalfDim = 1 << (level + 3); // * 16 / 2 - // / 8 to get childFullDim in sections - int orderModulator = getChildOrderModulator(nodeX, nodeY, nodeZ, childHalfDim >> 3); + // even levels (the higher levels of each reduction) need to modulate indexes that are multiples of 8 if ((level & 1) == 1) { orderModulator <<= 3; } @@ -351,7 +350,8 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { // immediately traverse if fully inside if (inside == FULLY_INSIDE) { - this.traverse(x, y, z, childOrigin, level - 1, inside); + level --; + this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); return; } @@ -398,7 +398,8 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { } if (visible) { - this.traverse(x, y, z, childOrigin, level - 1, inside); + level --; + this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); } } From 6645a44fb381c9314173b68683d3183ff8f6f5b3 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 2 Oct 2024 05:10:39 +0200 Subject: [PATCH 35/81] fix issues when camera is outside of world --- .../render/chunk/RenderSectionManager.java | 4 +- .../render/chunk/async/FrustumCullTask.java | 8 +++- .../render/chunk/async/GlobalCullTask.java | 7 +++- .../chunk/lists/PendingTaskCollector.java | 2 +- .../render/chunk/lists/TaskSectionTree.java | 5 ++- .../occlusion/RayOcclusionSectionTree.java | 41 ++++++++++++------- .../render/chunk/occlusion/SectionTree.java | 15 +++---- 7 files changed, 49 insertions(+), 33 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 0cb892003a..79e1dd1b1a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -301,10 +301,10 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat // use the last dirty frame as the frame timestamp to avoid wrongly marking task results as more recent if they're simply scheduled later but did work on the same state of the graph if there's been no graph invalidation since var task = switch (type) { case WIDE, REGULAR -> - new GlobalCullTask(this.occlusionCuller, viewport, searchDistance, useOcclusionCulling, this.lastGraphDirtyFrame, this.sectionByPosition, type); + new GlobalCullTask(viewport, searchDistance, this.occlusionCuller, useOcclusionCulling, this.lastGraphDirtyFrame, this.sectionByPosition, type, this.level); case FRUSTUM -> // note that there is some danger with only giving the frustum tasks the last graph dirty frame and not the real current frame, but these are mitigated by deleting the frustum result when the camera changes. - new FrustumCullTask(this.occlusionCuller, viewport, searchDistance, useOcclusionCulling, this.lastGraphDirtyFrame); + new FrustumCullTask(viewport, searchDistance, this.lastGraphDirtyFrame, this.occlusionCuller, useOcclusionCulling, this.level); }; task.submitTo(this.asyncCullExecutor); this.pendingTasks.add(task); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java index 52b4a9498a..4edf34fd6f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -7,17 +7,21 @@ import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.RayOcclusionSectionTree; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; public class FrustumCullTask extends CullTask { - public FrustumCullTask(OcclusionCuller occlusionCuller, Viewport viewport, float buildDistance, boolean useOcclusionCulling, int frame) { + private final Level level; + + public FrustumCullTask(Viewport viewport, float buildDistance, int frame, OcclusionCuller occlusionCuller, boolean useOcclusionCulling, Level level) { super(viewport, buildDistance, frame, occlusionCuller, useOcclusionCulling); + this.level = level; } private static final LongArrayList timings = new LongArrayList(); @Override public FrustumCullResult runTask() { - var tree = new RayOcclusionSectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM); + var tree = new RayOcclusionSectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM, this.level); var start = System.nanoTime(); this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); tree.finalizeTrees(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index 8f09d86011..cbc6715f29 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -9,22 +9,25 @@ import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; public class GlobalCullTask extends CullTask { private final Long2ReferenceMap sectionByPosition; private final CullType cullType; + private final Level level; - public GlobalCullTask(OcclusionCuller occlusionCuller, Viewport viewport, float buildDistance, boolean useOcclusionCulling, int frame, Long2ReferenceMap sectionByPosition, CullType cullType) { + public GlobalCullTask(Viewport viewport, float buildDistance, OcclusionCuller occlusionCuller, boolean useOcclusionCulling, int frame, Long2ReferenceMap sectionByPosition, CullType cullType, Level level) { super(viewport, buildDistance, frame, occlusionCuller, useOcclusionCulling); this.sectionByPosition = sectionByPosition; this.cullType = cullType; + this.level = level; } private static final LongArrayList timings = new LongArrayList(); @Override public GlobalCullResult runTask() { - var tree = new TaskSectionTree(this.viewport, this.buildDistance, this.frame, this.cullType); + var tree = new TaskSectionTree(this.viewport, this.buildDistance, this.frame, this.cullType, this.level); var start = System.nanoTime(); this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); tree.finalizeTrees(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index dd781d9f86..317f02f388 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -57,7 +57,7 @@ public PendingTaskCollector(Viewport viewport, float buildDistance, boolean frus var cameraSectionY = transform.intY >> 4; var cameraSectionZ = transform.intZ >> 4; this.baseOffsetX = cameraSectionX - offsetDistance; - this.baseOffsetY = cameraSectionY - offsetDistance; + this.baseOffsetY = -4; // bottom of a normal world this.baseOffsetZ = cameraSectionZ - offsetDistance; this.invMaxDistance = PROXIMITY_FACTOR / buildDistance; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java index 539ee3db1b..030c445966 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -5,13 +5,14 @@ import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.RayOcclusionSectionTree; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; public class TaskSectionTree extends RayOcclusionSectionTree { private final Tree mainTaskTree; private Tree secondaryTaskTree; - public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType) { - super(viewport, buildDistance, frame, cullType); + public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { + super(viewport, buildDistance, frame, cullType, level); this.mainTaskTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java index 47f55d69af..c865dd04fc 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -4,6 +4,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; public class RayOcclusionSectionTree extends SectionTree { private static final float SECTION_HALF_DIAGONAL = (float) Math.sqrt(8 * 8 * 3); @@ -11,16 +12,24 @@ public class RayOcclusionSectionTree extends SectionTree { private static final int RAY_TEST_MAX_STEPS = 12; private static final int MIN_RAY_TEST_DISTANCE_SQ = (int) Math.pow(16 * 3, 2); + private static final int IS_OBSTRUCTED = 0; + private static final int NOT_OBSTRUCTED = 1; + private static final int OUT_OF_BOUNDS = 2; + private final CameraTransform transform; + private final int minSection, maxSection; private final PortalMap mainPortalTree; private PortalMap secondaryPortalTree; - public RayOcclusionSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType) { + public RayOcclusionSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { super(viewport, buildDistance, frame, cullType); this.transform = viewport.getTransform(); this.mainPortalTree = new PortalMap(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + + this.minSection = level.getMinSection(); + this.maxSection = level.getMaxSection(); } @Override @@ -74,16 +83,19 @@ private boolean isRayBlockedStepped(RenderSection section) { y += dY; z += dZ; - if (this.blockHasObstruction((int) x, (int) y, (int) z)) { + var result = this.blockHasObstruction((int) x, (int) y, (int) z); + if (result == IS_OBSTRUCTED) { // also test radius around to avoid false negatives var radius = SECTION_HALF_DIAGONAL * (steps - i) * stepsInv; // this pattern simulates a shape similar to the sweep of the section towards the camera - if (!this.blockHasObstruction((int) (x - radius), (int) (y - radius), (int) (z - radius)) || - !this.blockHasObstruction((int) (x + radius), (int) (y + radius), (int) (z + radius))) { + if (this.blockHasObstruction((int) (x - radius), (int) (y - radius), (int) (z - radius)) != IS_OBSTRUCTED || + this.blockHasObstruction((int) (x + radius), (int) (y + radius), (int) (z + radius)) != IS_OBSTRUCTED) { continue; } return true; + } else if (result == OUT_OF_BOUNDS) { + break; } } @@ -104,21 +116,20 @@ protected void markPortal(int x, int y, int z) { } } - private static final int IS_OBSTRUCTED = 0; - private static final int NOT_OBSTRUCTED = 1; - private static final int OUT_OF_BOUNDS = 2; + private int blockHasObstruction(int x, int y, int z) { + x >>= 4; + y >>= 4; + z >>= 4; - private boolean blockHasObstruction(int x, int y, int z) { - return this.hasObstruction(x >> 4, y >> 4, z >> 4); - } + if (y < this.minSection || y >= this.maxSection) { + return OUT_OF_BOUNDS; + } - private boolean hasObstruction(int x, int y, int z) { var result = this.mainPortalTree.getObstruction(x, y, z); - if (result == OUT_OF_BOUNDS) { - return this.secondaryPortalTree != null && - this.secondaryPortalTree.getObstruction(x, y, z) == IS_OBSTRUCTED; + if (result == OUT_OF_BOUNDS && this.secondaryPortalTree != null) { + return this.secondaryPortalTree.getObstruction(x, y, z); } - return result == IS_OBSTRUCTED; + return result; } protected class PortalMap { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index f78ad8675a..01589569b7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -67,11 +67,11 @@ public boolean isWithinFrustum(Viewport viewport, RenderSection section) { public int getOutwardDirections(SectionPos origin, RenderSection section) { int planes = 0; - planes |= section.getChunkX() <= origin.getX() + this.bfsWidth ? 1 << GraphDirection.WEST : 0; - planes |= section.getChunkX() >= origin.getX() - this.bfsWidth ? 1 << GraphDirection.EAST : 0; + planes |= section.getChunkX() <= origin.getX() + this.bfsWidth ? 1 << GraphDirection.WEST : 0; + planes |= section.getChunkX() >= origin.getX() - this.bfsWidth ? 1 << GraphDirection.EAST : 0; - planes |= section.getChunkY() <= origin.getY() + this.bfsWidth ? 1 << GraphDirection.DOWN : 0; - planes |= section.getChunkY() >= origin.getY() - this.bfsWidth ? 1 << GraphDirection.UP : 0; + planes |= section.getChunkY() <= origin.getY() + this.bfsWidth ? 1 << GraphDirection.DOWN : 0; + planes |= section.getChunkY() >= origin.getY() - this.bfsWidth ? 1 << GraphDirection.UP : 0; planes |= section.getChunkZ() <= origin.getZ() + this.bfsWidth ? 1 << GraphDirection.NORTH : 0; planes |= section.getChunkZ() >= origin.getZ() - this.bfsWidth ? 1 << GraphDirection.SOUTH : 0; @@ -339,9 +339,6 @@ void traverse(int orderModulator, int nodeOrigin, int level, int inside) { } } - // TODO: move the modulator calculation to the caller to avoid passing coordinates - // TODO: fix flickering when above the world height (?) - void testChild(int childOrigin, int childHalfDim, int level, int inside) { // calculate section coordinates in tree-space int x = deinterleave6(childOrigin); @@ -350,7 +347,7 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { // immediately traverse if fully inside if (inside == FULLY_INSIDE) { - level --; + level--; this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); return; } @@ -398,7 +395,7 @@ void testChild(int childOrigin, int childHalfDim, int level, int inside) { } if (visible) { - level --; + level--; this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); } } From 04ce0cdf1be8940720c20324e1633c83757779c8 Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 2 Oct 2024 05:18:49 +0200 Subject: [PATCH 36/81] notes --- .../sodium/client/render/chunk/occlusion/OcclusionCuller.java | 3 +++ .../mods/sodium/client/render/chunk/occlusion/SectionTree.java | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 7ad43ea427..5e5682cb65 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -11,6 +11,9 @@ import net.minecraft.util.Mth; import net.minecraft.world.level.Level; +/* + * TODO idea: traverse octants of the world with separate threads for better performance? + */ public class OcclusionCuller { private final Long2ReferenceMap sections; private final Level level; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 01589569b7..c52610d5a8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -7,7 +7,7 @@ import net.minecraft.core.SectionPos; import org.joml.FrustumIntersection; -/** +/* * TODO: this can't deal with very high world heights (more than 1024 blocks tall), we'd need multiple tree-cubes for that * - 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?) From 7efe0baaca8d358165d4458af3f29c148a70e17a Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 9 Oct 2024 19:11:28 +0200 Subject: [PATCH 37/81] fix crash when loading under water --- .../sodium/client/render/chunk/lists/PendingTaskCollector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index 317f02f388..97e627e9b0 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -48,7 +48,7 @@ public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisit public PendingTaskCollector(Viewport viewport, float buildDistance, boolean frustumTested) { this.creationTime = System.nanoTime(); this.isFrustumTested = frustumTested; - var offsetDistance = Mth.floor(buildDistance / 16.0f) + DISTANCE_OFFSET; + var offsetDistance = Mth.ceil(buildDistance / 16.0f) + DISTANCE_OFFSET; var transform = viewport.getTransform(); From 92f0308d717470cd3262582e4407c42eaf74002a Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 10 Oct 2024 20:20:15 +0200 Subject: [PATCH 38/81] fix missing world when fog distance changes, and flickering of render distance --- .../render/chunk/RenderSectionManager.java | 33 ++++++++++++++----- .../render/chunk/async/GlobalCullTask.java | 2 +- .../render/chunk/occlusion/CullType.java | 10 +++--- .../render/chunk/occlusion/SectionTree.java | 15 ++++++--- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 79e1dd1b1a..92bd93b3ad 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -277,10 +277,6 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat // pick a scheduling order based on if there's been a graph update and if the render list is dirty var scheduleOrder = getScheduleOrder(); - var transform = viewport.getTransform(); - var cameraSectionX = transform.intX >> 4; - var cameraSectionY = transform.intY >> 4; - var cameraSectionZ = transform.intZ >> 4; for (var type : scheduleOrder) { var tree = this.trees.get(type); @@ -290,20 +286,20 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat continue; } + var searchDistance = this.getSearchDistanceForCullType(type); if ((tree == null || tree.getFrame() < this.lastGraphDirtyFrame || - !tree.isValidFor(cameraSectionX, cameraSectionY, cameraSectionZ)) && ( + !tree.isValidFor(viewport, searchDistance)) && ( currentRunningTask == null || currentRunningTask instanceof CullTask cullTask && cullTask.getCullType() != type || currentRunningTask.getFrame() < this.lastGraphDirtyFrame)) { - var searchDistance = this.getSearchDistance(); var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); // use the last dirty frame as the frame timestamp to avoid wrongly marking task results as more recent if they're simply scheduled later but did work on the same state of the graph if there's been no graph invalidation since var task = switch (type) { case WIDE, REGULAR -> - new GlobalCullTask(viewport, searchDistance, this.occlusionCuller, useOcclusionCulling, this.lastGraphDirtyFrame, this.sectionByPosition, type, this.level); + new GlobalCullTask(viewport, searchDistance, this.lastGraphDirtyFrame, this.occlusionCuller, useOcclusionCulling, this.sectionByPosition, type, this.level); case FRUSTUM -> - // note that there is some danger with only giving the frustum tasks the last graph dirty frame and not the real current frame, but these are mitigated by deleting the frustum result when the camera changes. + // note that there is some danger with only giving the frustum tasks the last graph dirty frame and not the real current frame, but these are mitigated by deleting the frustum result when the camera changes. new FrustumCullTask(viewport, searchDistance, this.lastGraphDirtyFrame, this.occlusionCuller, useOcclusionCulling, this.level); }; task.submitTo(this.asyncCullExecutor); @@ -352,10 +348,21 @@ 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 SectionTree bestTree = null; + CullType bestType = null; + boolean bestTreeValid = false; for (var type : NARROW_TO_WIDE) { var tree = this.trees.get(type); - if (tree != null && (bestTree == null || tree.getFrame() > bestTree.getFrame())) { + 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) { bestTree = tree; + bestTreeValid = treeIsValid; + bestType = type; } } @@ -387,6 +394,14 @@ public boolean needsUpdate() { return this.needsGraphUpdate; } + private float getSearchDistanceForCullType(CullType cullType) { + if (cullType.isFogCulled) { + return this.getSearchDistance(); + } else { + return this.getRenderDistance(); + } + } + private float getSearchDistance() { float distance; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index cbc6715f29..0c1c700570 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -16,7 +16,7 @@ public class GlobalCullTask extends CullTask { private final CullType cullType; private final Level level; - public GlobalCullTask(Viewport viewport, float buildDistance, OcclusionCuller occlusionCuller, boolean useOcclusionCulling, int frame, Long2ReferenceMap sectionByPosition, CullType cullType, Level level) { + public GlobalCullTask(Viewport viewport, float buildDistance, int frame, OcclusionCuller occlusionCuller, boolean useOcclusionCulling, Long2ReferenceMap sectionByPosition, CullType cullType, Level level) { super(viewport, buildDistance, frame, occlusionCuller, useOcclusionCulling); this.sectionByPosition = sectionByPosition; this.cullType = cullType; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java index de596485a1..c91596a92c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java @@ -1,15 +1,17 @@ package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; public enum CullType { - WIDE(1, false), - REGULAR(0, false), - FRUSTUM(0, true); + WIDE(1, false, false), + REGULAR(0, false, false), + FRUSTUM(0, true, true); public final int bfsWidth; public final boolean isFrustumTested; + public final boolean isFogCulled; - CullType(int bfsWidth, boolean isFrustumTested) { + CullType(int bfsWidth, boolean isFrustumTested, boolean isFogCulled) { this.bfsWidth = bfsWidth; this.isFrustumTested = isFrustumTested; + this.isFogCulled = isFogCulled; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index c52610d5a8..0d8e8e1b6f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -22,7 +22,7 @@ public class SectionTree extends PendingTaskCollector implements OcclusionCuller private final int bfsWidth; - private final float buildDistance; + public final float buildDistance; protected final int frame; protected boolean lastSectionKnownEmpty = false; @@ -52,10 +52,15 @@ public int getFrame() { return this.frame; } - public boolean isValidFor(int newCameraSectionX, int newCameraSectionY, int newCameraSectionZ) { - return this.cameraX >> 4 == newCameraSectionX && - this.cameraY >> 4 == newCameraSectionY && - this.cameraZ >> 4 == newCameraSectionZ; + public boolean isValidFor(Viewport viewport, float searchDistance) { + var transform = viewport.getTransform(); + var cameraSectionX = transform.intX >> 4; + var cameraSectionY = transform.intY >> 4; + var cameraSectionZ = transform.intZ >> 4; + return this.cameraX >> 4 == cameraSectionX && + this.cameraY >> 4 == cameraSectionY && + this.cameraZ >> 4 == cameraSectionZ && + this.buildDistance >= searchDistance; } @Override From d3e5faa97c25940bdf9e4d0108a39e80b9dada00 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 10 Oct 2024 20:31:46 +0200 Subject: [PATCH 39/81] remove unused best type variable --- .../mods/sodium/client/render/chunk/RenderSectionManager.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 92bd93b3ad..6d69719598 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -348,7 +348,6 @@ 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 SectionTree bestTree = null; - CullType bestType = null; boolean bestTreeValid = false; for (var type : NARROW_TO_WIDE) { var tree = this.trees.get(type); @@ -362,7 +361,6 @@ private void processRenderListUpdate(Viewport viewport) { if (bestTree == null || tree.getFrame() > bestTree.getFrame() || !bestTreeValid && treeIsValid) { bestTree = tree; bestTreeValid = treeIsValid; - bestType = type; } } From 2c22c93258ad0ada336d5291e972e762dcc1f4f2 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 10 Oct 2024 22:24:48 +0200 Subject: [PATCH 40/81] fix broken cancellation behavior on sync bfs --- .../render/chunk/RenderSectionManager.java | 32 +++++++++-------- .../render/chunk/async/AsyncRenderTask.java | 35 ++++++++++++++----- .../render/chunk/async/FrustumCullTask.java | 7 ++-- .../render/chunk/async/GlobalCullTask.java | 7 ++-- .../chunk/occlusion/OcclusionCuller.java | 8 ++++- .../client/util/task/CancellationToken.java | 12 +++++++ 6 files changed, 69 insertions(+), 32 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 6d69719598..5e1800fe2a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -1,10 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk; import com.mojang.blaze3d.systems.RenderSystem; -import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; -import it.unimi.dsi.fastutil.longs.Long2ReferenceMaps; -import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; -import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; +import it.unimi.dsi.fastutil.longs.*; import it.unimi.dsi.fastutil.objects.*; import net.caffeinemc.mods.sodium.client.SodiumClientMod; import net.caffeinemc.mods.sodium.client.gl.device.CommandList; @@ -37,6 +34,7 @@ import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import net.caffeinemc.mods.sodium.client.util.MathUtil; +import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; import net.caffeinemc.mods.sodium.client.world.LevelSlice; import net.caffeinemc.mods.sodium.client.world.cloned.ChunkRenderContext; import net.caffeinemc.mods.sodium.client.world.cloned.ClonedChunkSectionCache; @@ -161,7 +159,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato this.pendingTasks.removeIf(task -> { if (task instanceof CullTask cullTask && cullTask.getCullType() == CullType.FRUSTUM) { - cullTask.cancelImmediately(); + cullTask.setCancelled(); return true; } return false; @@ -169,13 +167,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato } // remove all tasks that aren't in progress yet - this.pendingTasks.removeIf(task -> { - if (!task.hasStarted()) { - task.cancelImmediately(); - return true; - } - return false; - }); + this.pendingTasks.removeIf(AsyncRenderTask::cancelIfNotStarted); this.unpackTaskResults(false); @@ -196,12 +188,13 @@ private void renderSync(Camera camera, Viewport viewport, boolean spectator) { // cancel running tasks to prevent two bfs running at the same time, which will cause race conditions for (var task : this.pendingTasks) { - task.cancelImmediately(); + task.setCancelled(); + task.getResult(); } this.pendingTasks.clear(); var tree = new VisibleChunkCollectorSync(viewport, searchDistance, this.frame, CullType.FRUSTUM); - this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling); + this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, CancellationToken.NEVER_CANCELLED); tree.finalizeTrees(); this.frustumTaskLists = tree.getPendingTaskLists(); @@ -327,6 +320,8 @@ private CullType[] getScheduleOrder() { return WIDE_TO_NARROW; } + private static final LongArrayList timings = new LongArrayList(); + private void processRenderListUpdate(Viewport viewport) { // schedule generating a frustum task list if there's no frustum tree task running if (this.globalTaskTree != null) { @@ -373,9 +368,18 @@ private void processRenderListUpdate(Viewport viewport) { } } + var start = System.nanoTime(); var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame); bestTree.traverseVisible(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; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java index 03e50bde79..b8b6b2ad93 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java @@ -1,19 +1,24 @@ package net.caffeinemc.mods.sodium.client.render.chunk.async; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; -public abstract class AsyncRenderTask implements Callable { +public abstract class AsyncRenderTask implements Callable, CancellationToken { protected final Viewport viewport; protected final float buildDistance; protected final int frame; private Future future; - private volatile boolean started; + private volatile int state; + + private static final int PENDING = 0; + private static final int RUNNING = 1; + private static final int CANCELLED = 2; protected AsyncRenderTask(Viewport viewport, float buildDistance, int frame) { this.viewport = viewport; @@ -29,16 +34,25 @@ public boolean isDone() { return this.future.isDone(); } - public boolean hasStarted() { - return this.started; - } - public int getFrame() { return this.frame; } - public void cancelImmediately() { - this.future.cancel(true); + public boolean isCancelled() { + return this.state == CANCELLED; + } + + @Override + public void setCancelled() { + this.state = CANCELLED; + } + + public boolean cancelIfNotStarted() { + if (this.state == PENDING) { + this.setCancelled(); + return true; + } + return false; } public T getResult() { @@ -51,7 +65,10 @@ public T getResult() { @Override public T call() throws Exception { - this.started = true; + if (this.state == CANCELLED) { + return null; + } + this.state = RUNNING; return this.runTask(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java index 4edf34fd6f..421f068784 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -23,15 +23,14 @@ public FrustumCullTask(Viewport viewport, float buildDistance, int frame, Occlus public FrustumCullResult runTask() { var tree = new RayOcclusionSectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM, this.level); var start = System.nanoTime(); - this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this); tree.finalizeTrees(); var end = System.nanoTime(); var time = end - start; timings.add(time); - final var count = 500; - if (timings.size() > count) { + if (timings.size() >= 500) { var average = timings.longStream().average().orElse(0); - System.out.println("Frustum culling took " + (average) / 1000 + "µs over " + count + " samples"); + System.out.println("Frustum culling took " + (average) / 1000 + "µs over " + timings.size() + " samples"); timings.clear(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index 0c1c700570..80cb562875 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -29,15 +29,14 @@ public GlobalCullTask(Viewport viewport, float buildDistance, int frame, Occlusi public GlobalCullResult runTask() { var tree = new TaskSectionTree(this.viewport, this.buildDistance, this.frame, this.cullType, this.level); var start = System.nanoTime(); - this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling); + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this); tree.finalizeTrees(); var end = System.nanoTime(); var time = end - start; timings.add(time); - final var count = 500; - if (timings.size() > count) { + if (timings.size() >= 500) { var average = timings.longStream().average().orElse(0); - System.out.println("Global culling took " + (average) / 1000 + "µs over " + count + " samples"); + System.out.println("Global culling took " + (average) / 1000 + "µs over " + timings.size() + " samples"); timings.clear(); } var collector = new FrustumTaskCollector(this.viewport, this.buildDistance, this.sectionByPosition); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 5e5682cb65..af894dc6f2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -7,6 +7,7 @@ import net.caffeinemc.mods.sodium.client.util.collections.DoubleBufferedQueue; import net.caffeinemc.mods.sodium.client.util.collections.ReadQueue; import net.caffeinemc.mods.sodium.client.util.collections.WriteQueue; +import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; import net.minecraft.core.SectionPos; import net.minecraft.util.Mth; import net.minecraft.world.level.Level; @@ -70,7 +71,8 @@ public OcclusionCuller(Long2ReferenceMap sections, Level level) { public void findVisible(GraphOcclusionVisitor visitor, Viewport viewport, float searchDistance, - boolean useOcclusionCulling) { + boolean useOcclusionCulling, + CancellationToken cancellationToken) { this.visitor = visitor; this.viewport = viewport; this.searchDistance = searchDistance; @@ -87,6 +89,10 @@ public void findVisible(GraphOcclusionVisitor visitor, this.init(queues.write()); while (this.queue.flip()) { + if (cancellationToken.isCancelled()) { + break; + } + processQueue(this.queue.read(), this.queue.write()); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/task/CancellationToken.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/task/CancellationToken.java index 7ebe731d18..9f98ec3c26 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/task/CancellationToken.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/task/CancellationToken.java @@ -4,4 +4,16 @@ public interface CancellationToken { boolean isCancelled(); void setCancelled(); + + CancellationToken NEVER_CANCELLED = new CancellationToken() { + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void setCancelled() { + throw new UnsupportedOperationException("NEVER_CANCELLED cannot be cancelled"); + } + }; } From d9e0e83627d21929e5889665ee29c35004ab3b0d Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 11 Oct 2024 06:50:24 +0200 Subject: [PATCH 41/81] implement frame rate independent task scheduling with effort-based task duration estimation --- .../client/render/chunk/RenderSection.java | 9 +++ .../render/chunk/RenderSectionManager.java | 51 +++++++++++--- .../chunk/compile/ChunkBuildOutput.java | 8 +++ .../chunk/compile/executor/ChunkBuilder.java | 25 ++----- .../chunk/compile/executor/ChunkJob.java | 2 +- .../compile/executor/ChunkJobCollector.java | 12 ++-- .../chunk/compile/executor/ChunkJobQueue.java | 16 ++--- .../compile/executor/ChunkJobResult.java | 16 ++++- .../chunk/compile/executor/ChunkJobTyped.java | 7 +- .../chunk/compile/executor/JobEffort.java | 7 ++ .../compile/executor/JobEffortEstimator.java | 67 +++++++++++++++++++ .../tasks/ChunkBuilderMeshingTask.java | 5 +- .../tasks/ChunkBuilderSortingTask.java | 11 ++- .../chunk/compile/tasks/ChunkBuilderTask.java | 13 +++- .../data/DynamicBSPData.java | 2 +- .../translucent_sorting/data/DynamicData.java | 2 + .../data/DynamicSorter.java | 6 +- .../data/DynamicTopoData.java | 2 +- 18 files changed, 195 insertions(+), 66 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java index d995af3487..f32137e562 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java @@ -67,6 +67,7 @@ public class RenderSection { // Pending Update State @Nullable private CancellationToken taskCancellationToken = null; + private long lastMeshingTaskEffort = 1; @Nullable private ChunkUpdateType pendingUpdateType; @@ -182,6 +183,14 @@ private boolean clearRenderState() { return wasBuilt; } + public void setLastMeshingTaskEffort(long effort) { + this.lastMeshingTaskEffort = effort; + } + + public long getLastMeshingTaskEffort() { + return this.lastMeshingTaskEffort; + } + /** * Returns the chunk section position which this render refers to in the level. */ diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 5e1800fe2a..e563d43805 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -13,6 +13,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobCollector; import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.JobEffortEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderSortingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; @@ -53,7 +54,9 @@ import org.joml.Vector3dc; import java.util.*; -import java.util.concurrent.*; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; public class RenderSectionManager { private static final float NEARBY_REBUILD_DISTANCE = Mth.square(16.0f); @@ -67,6 +70,8 @@ public class RenderSectionManager { private final Long2ReferenceMap sectionByPosition = new Long2ReferenceOpenHashMap<>(); private final ConcurrentLinkedDeque> buildResults = new ConcurrentLinkedDeque<>(); + private ChunkJobCollector lastBlockingCollector; + private final JobEffortEstimator jobEffortEstimator = new JobEffortEstimator(); private final ChunkRenderer chunkRenderer; @@ -80,8 +85,6 @@ public class RenderSectionManager { private final SortTriggering sortTriggering; - private ChunkJobCollector lastBlockingCollector; - @NotNull private SortedRenderLists renderLists; @@ -90,7 +93,10 @@ public class RenderSectionManager { private int frame; private int lastGraphDirtyFrame; + private long lastFrameDuration = -1; + private long averageFrameDuration = -1; private long lastFrameAtTime = System.nanoTime(); + private static final float AVERAGE_FRAME_DURATION_FACTOR = 0.05f; private boolean needsGraphUpdate = true; private boolean needsRenderListUpdate = true; @@ -138,8 +144,18 @@ public void updateCameraState(Vector3dc cameraPosition, Camera camera) { // TODO idea: increase and decrease chunk builder thread budget based on if the upload buffer was filled if the entire budget was used up. if the fallback way of uploading buffers is used, just doing 3 * the budget actually slows down frames while things are getting uploaded. For this it should limit how much (or how often?) things are uploaded. In the case of the mapped upload, just making sure we don't exceed its size is probably enough. public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator, boolean updateImmediately) { + var now = System.nanoTime(); + this.lastFrameDuration = now - this.lastFrameAtTime; + this.lastFrameAtTime = now; + 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 = Mth.clamp(this.averageFrameDuration, 1_000_100, 100_000_000); + this.frame += 1; - this.lastFrameAtTime = System.nanoTime(); this.needsRenderListUpdate |= this.cameraChanged; // do sync bfs based on update immediately (flawless frames) or if the camera moved too much @@ -555,6 +571,7 @@ private boolean processChunkBuildResults(ArrayList results) { TranslucentData oldData = result.render.getTranslucentData(); if (result instanceof ChunkBuildOutput chunkBuildOutput) { touchedSectionInfo |= this.updateSectionInfo(result.render, chunkBuildOutput.info); + result.render.setLastMeshingTaskEffort(chunkBuildOutput.getEffort()); if (chunkBuildOutput.translucentData != null) { this.sortTriggering.integrateTranslucentData(oldData, chunkBuildOutput.translucentData, this.cameraPosition, this::scheduleSort); @@ -614,12 +631,19 @@ private static List filterChunkBuildResults(ArrayList collectChunkBuildResults() { ArrayList results = new ArrayList<>(); + ChunkJobResult result; while ((result = this.buildResults.poll()) != null) { results.add(result.unwrap()); + var jobEffort = result.getJobEffort(); + if (jobEffort != null) { + this.jobEffortEstimator.addJobEffort(jobEffort); + } } + this.jobEffortEstimator.flushNewData(); + return results; } @@ -643,9 +667,8 @@ public void updateChunks(boolean updateImmediately) { thisFrameBlockingCollector.awaitCompletion(this.builder); } else { var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); - var deferredCollector = new ChunkJobCollector( - this.builder.getTotalRemainingBudget(), - this.buildResults::add); + var remainingDuration = this.builder.getTotalRemainingDuration(this.averageFrameDuration); + var deferredCollector = new ChunkJobCollector(remainingDuration, this.buildResults::add); // if zero frame delay is allowed, submit important sorts with the current frame blocking collector. // otherwise submit with the collector that the next frame is blocking on. @@ -795,11 +818,17 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode return null; } - return new ChunkBuilderMeshingTask(render, frame, this.cameraPosition, context); + var task = new ChunkBuilderMeshingTask(render, frame, this.cameraPosition, context); + task.estimateDurationWith(this.jobEffortEstimator); + return task; } public ChunkBuilderSortingTask createSortTask(RenderSection render, int frame) { - return ChunkBuilderSortingTask.createTask(render, frame, this.cameraPosition); + var task = ChunkBuilderSortingTask.createTask(render, frame, this.cameraPosition); + if (task != null) { + task.estimateDurationWith(this.jobEffortEstimator); + } + return task; } public void processGFNIMovement(CameraMovement movement) { @@ -975,8 +1004,8 @@ public Collection getDebugStrings() { list.add(String.format("Geometry Pool: %d/%d MiB (%d buffers)", MathUtil.toMib(deviceUsed), MathUtil.toMib(deviceAllocated), count)); list.add(String.format("Transfer Queue: %s", this.regions.getStagingBuffer().toString())); - list.add(String.format("Chunk Builder: Permits=%02d (E %03d) | Busy=%02d | Total=%02d", - this.builder.getScheduledJobCount(), this.builder.getScheduledEffort(), this.builder.getBusyThreadCount(), this.builder.getTotalThreadCount()) + list.add(String.format("Chunk Builder: Permits=%02d (%04d%%) | Busy=%02d | Total=%02d", + this.builder.getScheduledJobCount(), (int)(this.builder.getBusyFraction(this.lastFrameDuration) * 100), this.builder.getBusyThreadCount(), this.builder.getTotalThreadCount()) ); list.add(String.format("Chunk Queues: U=%02d", this.buildResults.size())); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java index 88102d1d16..442d3669e1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java @@ -32,6 +32,14 @@ public BuiltSectionMeshParts getMesh(TerrainRenderPass pass) { return this.meshes.get(pass); } + public long getEffort() { + long size = 0; + for (var data : this.meshes.values()) { + size += data.getVertexData().getLength(); + } + return 1 + (size >> 8); // make sure the number isn't huge + } + @Override public void destroy() { super.destroy(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java index 1d347d1f29..5a230efda5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java @@ -31,20 +31,11 @@ public class ChunkBuilder { * min((mesh task upload size) / (sort task upload size), (mesh task time) / * (sort task time)). */ - public static final int HIGH_EFFORT = 10; - public static final int LOW_EFFORT = 1; - public static final int EFFORT_UNIT = HIGH_EFFORT + LOW_EFFORT; - public static final int EFFORT_PER_THREAD_PER_FRAME = EFFORT_UNIT; - private static final float HIGH_EFFORT_BUDGET_FACTOR = (float)HIGH_EFFORT / EFFORT_UNIT; - static final Logger LOGGER = LogManager.getLogger("ChunkBuilder"); private final ChunkJobQueue queue = new ChunkJobQueue(); - private final List threads = new ArrayList<>(); - private final AtomicInteger busyThreadCount = new AtomicInteger(); - private final ChunkBuildContext localContext; public ChunkBuilder(ClientLevel level, ChunkVertexType vertexType) { @@ -70,16 +61,8 @@ public ChunkBuilder(ClientLevel level, ChunkVertexType vertexType) { * Returns the remaining effort for tasks which should be scheduled this frame. If an attempt is made to * spawn more tasks than the budget allows, it will block until resources become available. */ - public int getTotalRemainingBudget() { - return Math.max(0, this.threads.size() * EFFORT_PER_THREAD_PER_FRAME - this.queue.getEffortSum()); - } - - public int getHighEffortSchedulingBudget() { - return Math.max(HIGH_EFFORT, (int) (this.getTotalRemainingBudget() * HIGH_EFFORT_BUDGET_FACTOR)); - } - - public int getLowEffortSchedulingBudget() { - return Math.max(LOW_EFFORT, this.getTotalRemainingBudget() - this.getHighEffortSchedulingBudget()); + public long getTotalRemainingDuration(long durationPerThread) { + return Math.max(0, this.threads.size() * durationPerThread - this.queue.getJobDurationSum()); } /** @@ -173,8 +156,8 @@ public int getScheduledJobCount() { return this.queue.size(); } - public int getScheduledEffort() { - return this.queue.getEffortSum(); + public float getBusyFraction(long frameDuration) { + return (float) this.queue.getJobDurationSum() / (frameDuration * this.threads.size()); } public int getBusyThreadCount() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java index a0bcb33505..458ed3a369 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java @@ -8,5 +8,5 @@ public interface ChunkJob extends CancellationToken { boolean isStarted(); - int getEffort(); + long getEstimatedDuration(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java index 8c9d803f3d..a5e5fa366e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java @@ -12,15 +12,15 @@ public class ChunkJobCollector { private final Consumer> collector; private final List submitted = new ArrayList<>(); - private int budget; + private long duration; public ChunkJobCollector(Consumer> collector) { - this.budget = Integer.MAX_VALUE; + this.duration = Long.MAX_VALUE; this.collector = collector; } - public ChunkJobCollector(int budget, Consumer> collector) { - this.budget = budget; + public ChunkJobCollector(long duration, Consumer> collector) { + this.duration = duration; this.collector = collector; } @@ -47,10 +47,10 @@ public void awaitCompletion(ChunkBuilder builder) { public void addSubmittedJob(ChunkJob job) { this.submitted.add(job); - this.budget -= job.getEffort(); + this.duration -= job.getEstimatedDuration(); } public boolean hasBudgetRemaining() { - return this.budget > 0; + return this.duration > 0; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java index 0e6c2b9aa2..2f7564eef4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobQueue.java @@ -7,12 +7,12 @@ import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; class ChunkJobQueue { private final ConcurrentLinkedDeque jobs = new ConcurrentLinkedDeque<>(); - private final AtomicInteger jobEffortSum = new AtomicInteger(); + private final AtomicLong jobDurationSum = new AtomicLong(); private final Semaphore semaphore = new Semaphore(0); @@ -30,7 +30,7 @@ public void add(ChunkJob job, boolean important) { } else { this.jobs.addLast(job); } - this.jobEffortSum.addAndGet(job.getEffort()); + this.jobDurationSum.addAndGet(job.getEstimatedDuration()); this.semaphore.release(1); } @@ -45,7 +45,7 @@ public ChunkJob waitForNextJob() throws InterruptedException { var job = this.getNextTask(); if (job != null) { - this.jobEffortSum.addAndGet(-job.getEffort()); + this.jobDurationSum.addAndGet(-job.getEstimatedDuration()); } return job; } @@ -58,7 +58,7 @@ public boolean stealJob(ChunkJob job) { var success = this.jobs.remove(job); if (success) { - this.jobEffortSum.addAndGet(-job.getEffort()); + this.jobDurationSum.addAndGet(-job.getEstimatedDuration()); } else { // If we didn't manage to actually steal the task, then we need to release the permit which we did steal this.semaphore.release(1); @@ -89,7 +89,7 @@ public Collection shutdown() { // force the worker threads to wake up and exit this.semaphore.release(Runtime.getRuntime().availableProcessors()); - this.jobEffortSum.set(0); + this.jobDurationSum.set(0); return list; } @@ -98,8 +98,8 @@ public int size() { return this.semaphore.availablePermits(); } - public int getEffortSum() { - return this.jobEffortSum.get(); + public long getJobDurationSum() { + return this.jobDurationSum.get(); } public boolean isEmpty() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java index 5619855494..07d9659d12 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java @@ -5,18 +5,24 @@ public class ChunkJobResult { private final OUTPUT output; private final Throwable throwable; + private final JobEffort jobEffort; - private ChunkJobResult(OUTPUT output, Throwable throwable) { + private ChunkJobResult(OUTPUT output, Throwable throwable, JobEffort jobEffort) { this.output = output; this.throwable = throwable; + this.jobEffort = jobEffort; } public static ChunkJobResult exceptionally(Throwable throwable) { - return new ChunkJobResult<>(null, throwable); + return new ChunkJobResult<>(null, throwable, null); + } + + public static ChunkJobResult successfully(OUTPUT output, JobEffort jobEffort) { + return new ChunkJobResult<>(output, null, jobEffort); } public static ChunkJobResult successfully(OUTPUT output) { - return new ChunkJobResult<>(output, null); + return new ChunkJobResult<>(output, null, null); } public OUTPUT unwrap() { @@ -29,4 +35,8 @@ public OUTPUT unwrap() { return this.output; } + + public JobEffort getJobEffort() { + return this.jobEffort; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java index 78e1242803..607d5bc2c8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java @@ -42,6 +42,7 @@ public void execute(ChunkBuildContext context) { ChunkJobResult result; try { + var start = System.nanoTime(); var output = this.task.execute(context, this); // Task was cancelled while executing @@ -49,7 +50,7 @@ public void execute(ChunkBuildContext context) { return; } - result = ChunkJobResult.successfully(output); + result = ChunkJobResult.successfully(output, JobEffort.untilNowWithEffort(this.task.getClass(), start, this.task.getEffort())); } catch (Throwable throwable) { result = ChunkJobResult.exceptionally(throwable); ChunkBuilder.LOGGER.error("Chunk build failed", throwable); @@ -68,7 +69,7 @@ public boolean isStarted() { } @Override - public int getEffort() { - return this.task.getEffort(); + public long getEstimatedDuration() { + return this.task.getEstimatedDuration(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java new file mode 100644 index 0000000000..978c8c9cf9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java @@ -0,0 +1,7 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; + +public record JobEffort(Class category, long duration, long effort) { + public static JobEffort untilNowWithEffort(Class effortType, long start, long effort) { + return new JobEffort(effortType,System.nanoTime() - start, effort); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java new file mode 100644 index 0000000000..3151c49ec2 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java @@ -0,0 +1,67 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; + +import it.unimi.dsi.fastutil.objects.*; + +// TODO: deal with maximum number of uploads per frame +// TODO: implement per-thread pending upload size limit with a simple semaphore? also see discussion about more complicated allocation scheme with small and large threads: https://discord.com/channels/602796788608401408/651120262129123330/1294158402859171870 +public class JobEffortEstimator { + public static final float NEW_DATA_FACTOR = 0.01f; + + Reference2FloatMap> durationPerEffort = new Reference2FloatArrayMap<>(); + Reference2ReferenceMap, FrameDataAggregation> newData = new Reference2ReferenceArrayMap<>(); + + private static class FrameDataAggregation { + private long durationSum; + private long effortSum; + + public void addDataPoint(long duration, long effort) { + this.durationSum += duration; + this.effortSum += effort; + } + + public void reset() { + this.durationSum = 0; + this.effortSum = 0; + } + + public float getEffortFactor() { + return (float) this.durationSum / this.effortSum; + } + } + + public void addJobEffort(JobEffort jobEffort) { + var category = jobEffort.category(); + if (this.newData.containsKey(category)) { + this.newData.get(category).addDataPoint(jobEffort.duration(), jobEffort.effort()); + } else { + var frameData = new FrameDataAggregation(); + frameData.addDataPoint(jobEffort.duration(), jobEffort.effort()); + this.newData.put(category, frameData); + } + } + + public void flushNewData() { + this.newData.forEach((category, frameData) -> { + var newFactor = frameData.getEffortFactor(); + if (Float.isNaN(newFactor)) { + return; + } + if (this.durationPerEffort.containsKey(category)) { + var oldFactor = this.durationPerEffort.getFloat(category); + var newValue = oldFactor * (1 - NEW_DATA_FACTOR) + newFactor * NEW_DATA_FACTOR; + this.durationPerEffort.put(category, newValue); + } else { + this.durationPerEffort.put(category, newFactor); + } + frameData.reset(); + }); + } + + public long estimateJobDuration(Class category, long effort) { + if (this.durationPerEffort.containsKey(category)) { + return (long) (this.durationPerEffort.getFloat(category) * effort); + } else { + return 10_000_000L; // 10ms as initial guess + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java index 75b7f7064c..8f6ae90436 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java @@ -7,7 +7,6 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderCache; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderer; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; @@ -228,7 +227,7 @@ private ReportedException fillCrashInfo(CrashReport report, LevelSlice slice, Bl } @Override - public int getEffort() { - return ChunkBuilder.HIGH_EFFORT; + public long getEffort() { + return this.render.getLastMeshingTaskEffort(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java index d3178ceb8e..82380ed692 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java @@ -1,6 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; -import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.Sorter; +import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicSorter; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; import org.joml.Vector3dc; @@ -8,14 +8,13 @@ import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkSortOutput; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicData; import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; public class ChunkBuilderSortingTask extends ChunkBuilderTask { - private final Sorter sorter; + private final DynamicSorter sorter; - public ChunkBuilderSortingTask(RenderSection render, int frame, Vector3dc absoluteCameraPos, Sorter sorter) { + public ChunkBuilderSortingTask(RenderSection render, int frame, Vector3dc absoluteCameraPos, DynamicSorter sorter) { super(render, frame, absoluteCameraPos); this.sorter = sorter; } @@ -43,7 +42,7 @@ public static ChunkBuilderSortingTask createTask(RenderSection render, int frame } @Override - public int getEffort() { - return ChunkBuilder.LOW_EFFORT; + public long getEffort() { + return this.sorter.getQuadCount(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java index 1735c23bb6..622ee43ef7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java @@ -1,5 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.JobEffortEstimator; import org.joml.Vector3dc; import org.joml.Vector3f; import org.joml.Vector3fc; @@ -26,6 +27,8 @@ public abstract class ChunkBuilderTask impleme protected final Vector3dc absoluteCameraPos; protected final Vector3fc cameraPos; + private long estimatedDuration; + /** * Constructs a new build task for the given chunk and converts the absolute camera position to a relative position. While the absolute position is stored as a double vector, the relative position is stored as a float vector. * @@ -54,7 +57,15 @@ public ChunkBuilderTask(RenderSection render, int time, Vector3dc absoluteCamera */ public abstract OUTPUT execute(ChunkBuildContext context, CancellationToken cancellationToken); - public abstract int getEffort(); + public abstract long getEffort(); + + public void estimateDurationWith(JobEffortEstimator estimator) { + this.estimatedDuration = estimator.estimateJobDuration(this.getClass(), this.getEffort()); + } + + public long getEstimatedDuration() { + return this.estimatedDuration; + } @Override public Vector3fc getRelativeCameraPos() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java index cfab09e721..f07668ba5b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicBSPData.java @@ -37,7 +37,7 @@ void writeSort(CombinedCameraPos cameraPos, boolean initial) { } @Override - public Sorter getSorter() { + public DynamicSorter getSorter() { return new DynamicBSPSorter(this.getQuadCount()); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java index 6c3c69080c..41d5be78f5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicData.java @@ -20,6 +20,8 @@ public SortType getSortType() { return SortType.DYNAMIC; } + public abstract DynamicSorter getSorter(); + public GeometryPlanes getGeometryPlanes() { return this.geometryPlanes; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java index 87539c346d..4ef745f8c5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicSorter.java @@ -1,6 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data; -abstract class DynamicSorter extends Sorter { +public abstract class DynamicSorter extends Sorter { private final int quadCount; DynamicSorter(int quadCount) { @@ -14,4 +14,8 @@ public void writeIndexBuffer(CombinedCameraPos cameraPos, boolean initial) { this.initBufferWithQuadLength(this.quadCount); this.writeSort(cameraPos, initial); } + + public int getQuadCount() { + return this.quadCount; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java index 2156bb3283..ca352b1a64 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/translucent_sorting/data/DynamicTopoData.java @@ -58,7 +58,7 @@ private DynamicTopoData(SectionPos sectionPos, int vertexCount, TQuad[] quads, } @Override - public Sorter getSorter() { + public DynamicSorter getSorter() { return new DynamicTopoSorter(this.getQuadCount(), this, this.pendingTriggerIsDirect, this.consecutiveTopoSortFailures, this.GFNITrigger, this.directTrigger); } From e6f6ac04396ec35d7a61af4cd63606ff39cd7d6c Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 12 Oct 2024 01:26:16 +0200 Subject: [PATCH 42/81] add angle-based section visibility path occlusion, cherry picked from douira:sharp-angle-traversal-occlusion --- .../sodium/client/render/chunk/occlusion/OcclusionCuller.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index af894dc6f2..e70335a61f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -116,7 +116,8 @@ private void processQueue(ReadQueue readQueue, // occlude paths through the section if it's being viewed at an angle where // the other side can't possibly be seen - sectionVisibilityData &= getAngleVisibilityMask(viewport, section); + sectionVisibilityData &= getAngleVisibilityMask(this.viewport, section); + // When using occlusion culling, we can only traverse into neighbors for which there is a path of // visibility through this chunk. This is determined by taking all the incoming paths to this chunk and // creating a union of the outgoing paths from those. From 0444b0f22eb3d5af1e1207d3c58462754f0e48a1 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 7 Nov 2024 04:59:49 +0100 Subject: [PATCH 43/81] make inner tree in section tree static --- .../client/render/chunk/lists/TaskSectionTree.java | 2 +- .../client/render/chunk/occlusion/SectionTree.java | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java index 030c445966..83d70a8d3c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -14,7 +14,7 @@ public class TaskSectionTree extends RayOcclusionSectionTree { public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { super(viewport, buildDistance, frame, cullType, level); - this.mainTaskTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + this.mainTaskTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, this.buildDistance); } @Override diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 0d8e8e1b6f..431f397007 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -37,7 +37,7 @@ public SectionTree(Viewport viewport, float buildDistance, int frame, CullType c this.buildDistance = buildDistance; this.frame = frame; - this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, this.buildDistance); } protected Tree makeSecondaryTree() { @@ -45,7 +45,8 @@ protected Tree makeSecondaryTree() { return new Tree( this.baseOffsetX + SECONDARY_TREE_OFFSET_XZ, this.baseOffsetY, - this.baseOffsetZ + SECONDARY_TREE_OFFSET_XZ); + this.baseOffsetZ + SECONDARY_TREE_OFFSET_XZ, + this.buildDistance); } public int getFrame() { @@ -150,7 +151,7 @@ public void traverseVisible(VisibleSectionVisitor visitor, Viewport viewport, fl } } - public class Tree { + public static class Tree { private static final int INSIDE_FRUSTUM = 0b01; private static final int INSIDE_DISTANCE = 0b10; private static final int FULLY_INSIDE = 0b11; @@ -159,6 +160,7 @@ public class Tree { protected final long[] treeReduced = new long[64]; public long treeDoubleReduced = 0L; protected final int offsetX, offsetY, offsetZ; + private final float buildDistance; // set temporarily during traversal private int cameraOffsetX, cameraOffsetY, cameraOffsetZ; @@ -166,10 +168,11 @@ public class Tree { protected Viewport viewport; private float distanceLimit; - public Tree(int offsetX, int offsetY, int offsetZ) { + public Tree(int offsetX, int offsetY, int offsetZ, float buildDistance) { this.offsetX = offsetX; this.offsetY = offsetY; this.offsetZ = offsetZ; + this.buildDistance = buildDistance; } public boolean add(int x, int y, int z) { @@ -254,7 +257,7 @@ public void traverse(VisibleSectionVisitor visitor, Viewport viewport, float dis this.cameraOffsetZ = (transform.intZ >> 4) - this.offsetZ + 1; // everything is already inside the distance limit if the build distance is smaller - var initialInside = this.distanceLimit >= SectionTree.this.buildDistance ? INSIDE_DISTANCE : 0; + var initialInside = this.distanceLimit >= this.buildDistance ? INSIDE_DISTANCE : 0; this.traverse(getChildOrderModulator(0, 0, 0, 1 << 5), 0, 5, initialInside); this.visitor = null; From cdccfa635e7e9c7157609c0775b27e675485f138 Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 10 Nov 2024 02:45:59 +0100 Subject: [PATCH 44/81] refactor tree structures to be more self-contained and easier to make variants of, add support for very tall worlds --- .../render/chunk/RenderSectionManager.java | 2 +- .../chunk/lists/PendingTaskCollector.java | 14 +- .../render/chunk/lists/TaskSectionTree.java | 29 +- .../lists/VisibleChunkCollectorSync.java | 5 +- .../chunk/occlusion/OcclusionCuller.java | 4 +- .../occlusion/RayOcclusionSectionTree.java | 105 +++-- .../render/chunk/occlusion/SectionTree.java | 380 +----------------- .../render/chunk/tree/BaseBiForest.java | 57 +++ .../client/render/chunk/tree/BaseForest.java | 20 + .../render/chunk/tree/BaseManyForest.java | 71 ++++ .../client/render/chunk/tree/Forest.java | 9 + .../chunk/tree/TraversableBiForest.java | 37 ++ .../render/chunk/tree/TraversableForest.java | 23 ++ .../chunk/tree/TraversableManyForest.java | 63 +++ .../render/chunk/tree/TraversableTree.java | 299 ++++++++++++++ .../sodium/client/render/chunk/tree/Tree.java | 54 +++ 16 files changed, 713 insertions(+), 459 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseManyForest.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableManyForest.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index e563d43805..e79050d52c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -209,7 +209,7 @@ private void renderSync(Camera camera, Viewport viewport, boolean spectator) { } this.pendingTasks.clear(); - var tree = new VisibleChunkCollectorSync(viewport, searchDistance, this.frame, CullType.FRUSTUM); + var tree = new VisibleChunkCollectorSync(viewport, searchDistance, this.frame, CullType.FRUSTUM, this.level); this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, CancellationToken.NEVER_CANCELLED); tree.finalizeTrees(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index 97e627e9b0..d22888391d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -25,7 +25,7 @@ public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisit // offset is shifted by 1 to encompass all sections towards the negative // TODO: is this the correct way of calculating the minimum possible section index? private static final int DISTANCE_OFFSET = 1; - private static final int SECTION_Y_MIN = -128; // used instead of baseOffsetY to accommodate all permissible y values (-2048 to 2048 blocks) + public static final int SECTION_Y_MIN = -128; // used instead of baseOffsetY to accommodate all permissible y values (-2048 to 2048 blocks) // tunable parameters for the priority calculation. // each "gained" point means a reduction in the final priority score (lowest score processed first) @@ -57,7 +57,7 @@ public PendingTaskCollector(Viewport viewport, float buildDistance, boolean frus var cameraSectionY = transform.intY >> 4; var cameraSectionZ = transform.intZ >> 4; this.baseOffsetX = cameraSectionX - offsetDistance; - this.baseOffsetY = -4; // bottom of a normal world + this.baseOffsetY = cameraSectionY - offsetDistance; this.baseOffsetZ = cameraSectionZ - offsetDistance; this.invMaxDistance = PROXIMITY_FACTOR / buildDistance; @@ -94,7 +94,7 @@ protected void addPendingSection(RenderSection section, ChunkUpdateType type) { var localX = section.getChunkX() - this.baseOffsetX; var localY = section.getChunkY() - SECTION_Y_MIN; var localZ = section.getChunkZ() - this.baseOffsetZ; - long taskCoordinate = (long) (localX & 0xFF) << 16 | (long) (localY & 0xFF) << 8 | (long) (localZ & 0xFF); + long taskCoordinate = (long) (localX & 0b1111111111) << 20 | (long) (localY & 0b1111111111) << 10 | (long) (localZ & 0b1111111111); var queue = this.pendingTasks[type.getDeferMode().ordinal()]; if (queue == null) { @@ -103,7 +103,7 @@ protected void addPendingSection(RenderSection section, ChunkUpdateType type) { } // encode the priority and the section position into a single long such that all parts can be later decoded - queue.add((long) MathUtil.floatToComparableInt(priority) << 32 | taskCoordinate); + queue.add((long) MathUtil.floatToComparableInt(priority) << 32 | taskCoordinate); } private float getSectionPriority(RenderSection section, ChunkUpdateType type) { @@ -163,9 +163,9 @@ public float getCollectorPriorityBias(long now) { } public RenderSection decodeAndFetchSection(Long2ReferenceMap sectionByPosition, long encoded) { - var localX = (int) (encoded >>> 16) & 0xFF; - var localY = (int) (encoded >>> 8) & 0xFF; - var localZ = (int) (encoded & 0xFF); + var localX = (int) (encoded >>> 20) & 0b1111111111; + var localY = (int) (encoded >>> 10) & 0b1111111111; + var localZ = (int) (encoded & 0b1111111111); var globalX = localX + PendingTaskCollector.this.baseOffsetX; var globalY = localY + SECTION_Y_MIN; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java index 83d70a8d3c..6ab711b692 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -4,50 +4,33 @@ import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.RayOcclusionSectionTree; +import net.caffeinemc.mods.sodium.client.render.chunk.tree.TraversableForest; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import net.minecraft.world.level.Level; public class TaskSectionTree extends RayOcclusionSectionTree { - private final Tree mainTaskTree; - private Tree secondaryTaskTree; + private final TraversableForest taskTree; public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { super(viewport, buildDistance, frame, cullType, level); - this.mainTaskTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, this.buildDistance); + this.taskTree = TraversableForest.createTraversableForest(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, buildDistance, level); } @Override protected void addPendingSection(RenderSection section, ChunkUpdateType type) { super.addPendingSection(section, type); - this.markTaskPresent(section.getChunkX(), section.getChunkY(), section.getChunkZ()); - } - - protected void markTaskPresent(int x, int y, int z) { - if (this.mainTaskTree.add(x, y, z)) { - if (this.secondaryTaskTree == null) { - this.secondaryTaskTree = this.makeSecondaryTree(); - } - if (this.secondaryTaskTree.add(x, y, z)) { - throw new IllegalStateException("Failed to add section to task trees"); - } - } + this.taskTree.add(section.getChunkX(), section.getChunkY(), section.getChunkZ()); } @Override public void finalizeTrees() { super.finalizeTrees(); - this.mainTaskTree.calculateReduced(); - if (this.secondaryTaskTree != null) { - this.secondaryTaskTree.calculateReduced(); - } + this.taskTree.calculateReduced(); } public void traverseVisiblePendingTasks(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { - this.mainTaskTree.traverse(visitor, viewport, distanceLimit); - if (this.secondaryTaskTree != null) { - this.secondaryTaskTree.traverse(visitor, viewport, distanceLimit); - } + this.taskTree.traverse(visitor, viewport, distanceLimit); } } \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java index 7018df6881..7c16017431 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java @@ -7,6 +7,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; /** * The sync visible chunk collector is passed into the graph search occlusion culler to collect visible chunks. @@ -14,8 +15,8 @@ public class VisibleChunkCollectorSync extends SectionTree { private final ObjectArrayList sortedRenderLists; - public VisibleChunkCollectorSync(Viewport viewport, float buildDistance, int frame, CullType cullType) { - super(viewport, buildDistance, frame, cullType); + public VisibleChunkCollectorSync(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { + super(viewport, buildDistance, frame, cullType, level); this.sortedRenderLists = new ObjectArrayList<>(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index e70335a61f..9f36a4edd8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -32,8 +32,8 @@ public class OcclusionCuller { // can extend outside a block volume by +/- 1.0 blocks on all axis. Additionally, we make use of a small epsilon // to deal with floating point imprecision during a frustum check (see GH#2132). public static final float CHUNK_SECTION_RADIUS = 8.0f /* chunk bounds */; - static final float CHUNK_SECTION_MARGIN = 1.0f /* maximum model extent */ + 0.125f /* epsilon */; - static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + CHUNK_SECTION_MARGIN; + public static final float CHUNK_SECTION_MARGIN = 1.0f /* maximum model extent */ + 0.125f /* epsilon */; + public static final float CHUNK_SECTION_SIZE = CHUNK_SECTION_RADIUS + CHUNK_SECTION_MARGIN; public interface GraphOcclusionVisitor { default boolean visitTestVisible(RenderSection section) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java index c865dd04fc..09477f0024 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -2,6 +2,7 @@ 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.tree.*; import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import net.minecraft.world.level.Level; @@ -12,21 +13,16 @@ public class RayOcclusionSectionTree extends SectionTree { private static final int RAY_TEST_MAX_STEPS = 12; private static final int MIN_RAY_TEST_DISTANCE_SQ = (int) Math.pow(16 * 3, 2); - private static final int IS_OBSTRUCTED = 0; - private static final int NOT_OBSTRUCTED = 1; - private static final int OUT_OF_BOUNDS = 2; - private final CameraTransform transform; private final int minSection, maxSection; - private final PortalMap mainPortalTree; - private PortalMap secondaryPortalTree; + private final Forest portalTree; public RayOcclusionSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { - super(viewport, buildDistance, frame, cullType); + super(viewport, buildDistance, frame, cullType, level); this.transform = viewport.getTransform(); - this.mainPortalTree = new PortalMap(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + this.portalTree = createPortalTree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, buildDistance, level); this.minSection = level.getMinSection(); this.maxSection = level.getMaxSection(); @@ -52,7 +48,7 @@ public void visit(RenderSection section) { this.lastSectionKnownEmpty = false; // mark all traversed sections as portals, even if they don't have terrain that needs rendering - this.markPortal(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + this.portalTree.add(section.getChunkX(), section.getChunkY(), section.getChunkZ()); } private boolean isRayBlockedStepped(RenderSection section) { @@ -83,18 +79,21 @@ private boolean isRayBlockedStepped(RenderSection section) { y += dY; z += dZ; + // if the section is not present in the tree, the path to the camera is blocked var result = this.blockHasObstruction((int) x, (int) y, (int) z); - if (result == IS_OBSTRUCTED) { + if (result == Tree.NOT_PRESENT) { // also test radius around to avoid false negatives var radius = SECTION_HALF_DIAGONAL * (steps - i) * stepsInv; // this pattern simulates a shape similar to the sweep of the section towards the camera - if (this.blockHasObstruction((int) (x - radius), (int) (y - radius), (int) (z - radius)) != IS_OBSTRUCTED || - this.blockHasObstruction((int) (x + radius), (int) (y + radius), (int) (z + radius)) != IS_OBSTRUCTED) { + if (this.blockHasObstruction((int) (x - radius), (int) (y - radius), (int) (z - radius)) != Tree.NOT_PRESENT || + this.blockHasObstruction((int) (x + radius), (int) (y + radius), (int) (z + radius)) != Tree.NOT_PRESENT) { continue; } + + // the path is blocked because there's no visited section that gives a clear line of sight return true; - } else if (result == OUT_OF_BOUNDS) { + } else if (result == Tree.OUT_OF_BOUNDS) { break; } } @@ -102,72 +101,70 @@ private boolean isRayBlockedStepped(RenderSection section) { return false; } - protected void markPortal(int x, int y, int z) { - if (this.mainPortalTree.add(x, y, z)) { - if (this.secondaryPortalTree == null) { - this.secondaryPortalTree = new PortalMap( - this.baseOffsetX + SECONDARY_TREE_OFFSET_XZ, - this.baseOffsetY, - this.baseOffsetZ + SECONDARY_TREE_OFFSET_XZ); - } - if (this.secondaryPortalTree.add(x, y, z)) { - throw new IllegalStateException("Failed to add section to portal trees"); - } - } - } - private int blockHasObstruction(int x, int y, int z) { x >>= 4; y >>= 4; z >>= 4; if (y < this.minSection || y >= this.maxSection) { - return OUT_OF_BOUNDS; + return Tree.OUT_OF_BOUNDS; } - var result = this.mainPortalTree.getObstruction(x, y, z); - if (result == OUT_OF_BOUNDS && this.secondaryPortalTree != null) { - return this.secondaryPortalTree.getObstruction(x, y, z); + return this.portalTree.getPresence(x, y, z); + } + + private static Forest createPortalTree(int baseOffsetX,int baseOffsetY, int baseOffsetZ, float buildDistance, Level level) { + if (BaseBiForest.checkApplicable(buildDistance, level)) { + return new PortalBiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); } - return result; + + return new PortalManyForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); } - protected class PortalMap { - protected final long[] bitmap = new long[64 * 64]; - protected final int offsetX, offsetY, offsetZ; + private static class PortalBiForest extends BaseBiForest { + public PortalBiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } - public PortalMap(int offsetX, int offsetY, int offsetZ) { - this.offsetX = offsetX; - this.offsetY = offsetY; - this.offsetZ = offsetZ; + @Override + protected FlatTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new FlatTree(offsetX, offsetY, offsetZ); } + } - public boolean add(int x, int y, int z) { - x -= this.offsetX; - y -= this.offsetY; - z -= this.offsetZ; - if (Tree.isOutOfBounds(x, y, z)) { - return true; - } + private static class PortalManyForest extends BaseManyForest { + public PortalManyForest(int baseOffsetX,int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } - var bitIndex = Tree.interleave6x3(x, y, z); - this.bitmap[bitIndex >> 6] |= 1L << (bitIndex & 0b111111); + @Override + protected FlatTree[] makeTrees(int length) { + return new FlatTree[length]; + } - return false; + @Override + protected FlatTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new FlatTree(offsetX, offsetY, offsetZ); } + } + protected static class FlatTree extends Tree { + public FlatTree(int offsetX, int offsetY, int offsetZ) { + super(offsetX, offsetY, offsetZ); + } - public int getObstruction(int x, int y, int z) { + @Override + public int getPresence(int x, int y, int z) { x -= this.offsetX; y -= this.offsetY; z -= this.offsetZ; - if (Tree.isOutOfBounds(x, y, z)) { - return OUT_OF_BOUNDS; + if (isOutOfBounds(x, y, z)) { + return Tree.OUT_OF_BOUNDS; } - var bitIndex = Tree.interleave6x3(x, y, z); + var bitIndex = interleave6x3(x, y, z); var mask = 1L << (bitIndex & 0b111111); - return (this.bitmap[bitIndex >> 6] & mask) == 0 ? IS_OBSTRUCTED : NOT_OBSTRUCTED; + return (this.tree[bitIndex >> 6] & mask) == 0 ? Tree.NOT_PRESENT : Tree.PRESENT; } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 431f397007..cd4fd33ac5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -3,22 +3,20 @@ 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.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.tree.TraversableForest; +import net.caffeinemc.mods.sodium.client.render.chunk.tree.Tree; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import net.minecraft.core.SectionPos; -import org.joml.FrustumIntersection; +import net.minecraft.world.level.Level; /* - * TODO: this can't deal with very high world heights (more than 1024 blocks tall), we'd need multiple tree-cubes for that * - 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?) * - possibly refactor the section tree and task section tree structures to be more composable instead of extending each other. * - 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 { - protected static final int SECONDARY_TREE_OFFSET_XZ = 4; - - private final Tree mainTree; - private Tree secondaryTree; + private final TraversableForest tree; private final int bfsWidth; @@ -30,23 +28,14 @@ public interface VisibleSectionVisitor { void visit(int x, int y, int z); } - public SectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType) { + public SectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { super(viewport, buildDistance, cullType.isFrustumTested); this.bfsWidth = cullType.bfsWidth; this.buildDistance = buildDistance; this.frame = frame; - this.mainTree = new Tree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, this.buildDistance); - } - - protected Tree makeSecondaryTree() { - // offset diagonally to fully encompass the required area - return new Tree( - this.baseOffsetX + SECONDARY_TREE_OFFSET_XZ, - this.baseOffsetY, - this.baseOffsetZ + SECONDARY_TREE_OFFSET_XZ, - this.buildDistance); + this.tree = TraversableForest.createTraversableForest(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, buildDistance, level); } public int getFrame() { @@ -99,21 +88,11 @@ public void visit(RenderSection section) { } protected void markPresent(int x, int y, int z) { - if (this.mainTree.add(x, y, z)) { - if (this.secondaryTree == null) { - this.secondaryTree = this.makeSecondaryTree(); - } - if (this.secondaryTree.add(x, y, z)) { - throw new IllegalStateException("Failed to add section to trees"); - } - } + this.tree.add(x, y, z); } public void finalizeTrees() { - this.mainTree.calculateReduced(); - if (this.secondaryTree != null) { - this.secondaryTree.calculateReduced(); - } + this.tree.calculateReduced(); } public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { @@ -140,349 +119,10 @@ public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y } private boolean isSectionPresent(int x, int y, int z) { - return this.mainTree.isSectionPresent(x, y, z) || - (this.secondaryTree != null && this.secondaryTree.isSectionPresent(x, y, z)); + return this.tree.getPresence(x, y, z) == Tree.PRESENT; } public void traverseVisible(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { - this.mainTree.traverse(visitor, viewport, distanceLimit); - if (this.secondaryTree != null) { - this.secondaryTree.traverse(visitor, viewport, distanceLimit); - } - } - - public static class Tree { - private static final int INSIDE_FRUSTUM = 0b01; - private static final int INSIDE_DISTANCE = 0b10; - private static final int FULLY_INSIDE = 0b11; - - protected final long[] tree = new long[64 * 64]; - protected final long[] treeReduced = new long[64]; - public long treeDoubleReduced = 0L; - protected final int offsetX, offsetY, offsetZ; - private final float buildDistance; - - // set temporarily during traversal - private int cameraOffsetX, cameraOffsetY, cameraOffsetZ; - private VisibleSectionVisitor visitor; - protected Viewport viewport; - private float distanceLimit; - - public Tree(int offsetX, int offsetY, int offsetZ, float buildDistance) { - this.offsetX = offsetX; - this.offsetY = offsetY; - this.offsetZ = offsetZ; - this.buildDistance = buildDistance; - } - - public boolean add(int x, int y, int z) { - x -= this.offsetX; - y -= this.offsetY; - z -= this.offsetZ; - if (isOutOfBounds(x, y, z)) { - return true; - } - - var bitIndex = interleave6x3(x, y, z); - this.tree[bitIndex >> 6] |= 1L << (bitIndex & 0b111111); - - return false; - } - - public static boolean isOutOfBounds(int x, int y, int z) { - return x > 63 || y > 63 || z > 63 || x < 0 || y < 0 || z < 0; - } - - protected static int interleave6x3(int x, int y, int z) { - return interleave6(x) | interleave6(y) << 1 | interleave6(z) << 2; - } - - private static int interleave6(int n) { - n &= 0b000000000000111111; - n = (n | n << 8) & 0b000011000000001111; - n = (n | n << 4) & 0b000011000011000011; - n = (n | n << 2) & 0b001001001001001001; - return n; - } - - public void calculateReduced() { - long doubleReduced = 0; - for (int i = 0; i < 64; i++) { - long reduced = 0; - var reducedOffset = i << 6; - for (int j = 0; j < 64; j++) { - reduced |= this.tree[reducedOffset + j] == 0 ? 0L : 1L << j; - } - this.treeReduced[i] = reduced; - doubleReduced |= reduced == 0 ? 0L : 1L << i; - } - this.treeDoubleReduced = doubleReduced; - } - - private static int deinterleave6(int n) { - n &= 0b001001001001001001; - n = (n | n >> 2) & 0b000011000011000011; - n = (n | n >> 4 | n >> 8) & 0b000000000000111111; - return n; - } - - boolean isSectionPresent(int x, int y, int z) { - x -= this.offsetX; - y -= this.offsetY; - z -= this.offsetZ; - if (isOutOfBounds(x, y, z)) { - return false; - } - - var bitIndex = interleave6x3(x, y, z); - int doubleReducedBitIndex = bitIndex >> 12; - if ((this.treeDoubleReduced & (1L << doubleReducedBitIndex)) == 0) { - return false; - } - - int reducedBitIndex = bitIndex >> 6; - return (this.tree[reducedBitIndex] & (1L << (bitIndex & 0b111111))) != 0; - } - - public void traverse(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { - this.visitor = visitor; - this.viewport = viewport; - this.distanceLimit = distanceLimit; - - var transform = viewport.getTransform(); - - // + 1 to section position to compensate for shifted global offset - this.cameraOffsetX = (transform.intX >> 4) - this.offsetX + 1; - this.cameraOffsetY = (transform.intY >> 4) - this.offsetY + 1; - this.cameraOffsetZ = (transform.intZ >> 4) - this.offsetZ + 1; - - // everything is already inside the distance limit if the build distance is smaller - var initialInside = this.distanceLimit >= this.buildDistance ? INSIDE_DISTANCE : 0; - this.traverse(getChildOrderModulator(0, 0, 0, 1 << 5), 0, 5, initialInside); - - this.visitor = null; - this.viewport = null; - } - - void traverse(int orderModulator, int nodeOrigin, int level, int inside) { - // half of the dimension of a child of this node, in blocks - int childHalfDim = 1 << (level + 3); // * 16 / 2 - - // even levels (the higher levels of each reduction) need to modulate indexes that are multiples of 8 - if ((level & 1) == 1) { - orderModulator <<= 3; - } - - if (level <= 1) { - // check using the full bitmap - int childOriginBase = nodeOrigin & 0b111111_111111_000000; - long map = this.tree[nodeOrigin >> 6]; - - if (level == 0) { - int startBit = nodeOrigin & 0b111111; - int endBit = startBit + 8; - - for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { - int childIndex = bitIndex ^ orderModulator; - if ((map & (1L << childIndex)) != 0) { - int sectionOrigin = childOriginBase | childIndex; - int x = deinterleave6(sectionOrigin) + this.offsetX; - int y = deinterleave6(sectionOrigin >> 1) + this.offsetY; - int z = deinterleave6(sectionOrigin >> 2) + this.offsetZ; - - if (inside == FULLY_INSIDE || testLeafNode(x, y, z, inside)) { - this.visitor.visit(x, y, z); - } - } - } - } else { - for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { - int childIndex = bitIndex ^ orderModulator; - if ((map & (0xFFL << childIndex)) != 0) { - this.testChild(childOriginBase | childIndex, childHalfDim, level, inside); - } - } - } - } else if (level <= 3) { - int childOriginBase = nodeOrigin & 0b111111_000000_000000; - long map = this.treeReduced[nodeOrigin >> 12]; - - if (level == 2) { - int startBit = (nodeOrigin >> 6) & 0b111111; - int endBit = startBit + 8; - - for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { - int childIndex = bitIndex ^ orderModulator; - if ((map & (1L << childIndex)) != 0) { - this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); - } - } - } else { - for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { - int childIndex = bitIndex ^ orderModulator; - if ((map & (0xFFL << childIndex)) != 0) { - this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); - } - } - } - } else { - if (level == 4) { - int startBit = nodeOrigin >> 12; - int endBit = startBit + 8; - - for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { - int childIndex = bitIndex ^ orderModulator; - if ((this.treeDoubleReduced & (1L << childIndex)) != 0) { - this.testChild(childIndex << 12, childHalfDim, level, inside); - } - } - } else { - for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { - int childIndex = bitIndex ^ orderModulator; - if ((this.treeDoubleReduced & (0xFFL << childIndex)) != 0) { - this.testChild(childIndex << 12, childHalfDim, level, inside); - } - } - } - } - } - - void testChild(int childOrigin, int childHalfDim, int level, int inside) { - // calculate section coordinates in tree-space - int x = deinterleave6(childOrigin); - int y = deinterleave6(childOrigin >> 1); - int z = deinterleave6(childOrigin >> 2); - - // immediately traverse if fully inside - if (inside == FULLY_INSIDE) { - level--; - this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); - return; - } - - // convert to world-space section origin in blocks, then to camera space - var transform = this.viewport.getTransform(); - int worldX = ((x + this.offsetX) << 4) - transform.intX; - int worldY = ((y + this.offsetY) << 4) - transform.intY; - int worldZ = ((z + this.offsetZ) << 4) - transform.intZ; - - boolean visible = true; - - if ((inside & INSIDE_FRUSTUM) == 0) { - var intersectionResult = this.viewport.getBoxIntersectionDirect( - (worldX + childHalfDim) - transform.fracX, - (worldY + childHalfDim) - transform.fracY, - (worldZ + childHalfDim) - transform.fracZ, - childHalfDim + OcclusionCuller.CHUNK_SECTION_MARGIN); - if (intersectionResult == FrustumIntersection.INSIDE) { - inside |= INSIDE_FRUSTUM; - } else { - visible = intersectionResult == FrustumIntersection.INTERSECT; - } - } - - if ((inside & INSIDE_DISTANCE) == 0) { - // calculate the point of the node closest to the camera - int childFullDim = childHalfDim << 1; - float dx = nearestToZero(worldX, worldX + childFullDim) - transform.fracX; - float dy = nearestToZero(worldY, worldY + childFullDim) - transform.fracY; - float dz = nearestToZero(worldZ, worldZ + childFullDim) - transform.fracZ; - - // check if closest point inside the cylinder - visible = cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); - if (visible) { - // if the farthest point is also visible, the node is fully inside - dx = farthestFromZero(worldX, worldX + childFullDim) - transform.fracX; - dy = farthestFromZero(worldY, worldY + childFullDim) - transform.fracY; - dz = farthestFromZero(worldZ, worldZ + childFullDim) - transform.fracZ; - - if (cylindricalDistanceTest(dx, dy, dz, this.distanceLimit)) { - inside |= INSIDE_DISTANCE; - } - } - } - - if (visible) { - level--; - this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); - } - } - - boolean testLeafNode(int x, int y, int z, int inside) { - // input coordinates are section coordinates in world-space - - var transform = this.viewport.getTransform(); - - // convert to blocks and move into integer camera space - x = (x << 4) - transform.intX; - y = (y << 4) - transform.intY; - z = (z << 4) - transform.intZ; - - // test frustum if not already inside frustum - if ((inside & INSIDE_FRUSTUM) == 0 && !this.viewport.isBoxVisibleDirect( - (x + 8) - transform.fracX, - (y + 8) - transform.fracY, - (z + 8) - transform.fracZ, - OcclusionCuller.CHUNK_SECTION_RADIUS)) { - return false; - } - - // test distance if not already inside distance - if ((inside & INSIDE_DISTANCE) == 0) { - // coordinates of the point to compare (in view space) - // this is the closest point within the bounding box to the center (0, 0, 0) - float dx = nearestToZero(x, x + 16) - transform.fracX; - float dy = nearestToZero(y, y + 16) - transform.fracY; - float dz = nearestToZero(z, z + 16) - transform.fracZ; - - return cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); - } - - return true; - } - - static boolean cylindricalDistanceTest(float dx, float dy, float dz, float distanceLimit) { - // vanilla's "cylindrical fog" algorithm - // max(length(distance.xz), abs(distance.y)) - return (((dx * dx) + (dz * dz)) < (distanceLimit * distanceLimit)) && - (Math.abs(dy) < distanceLimit); - } - - @SuppressWarnings("ManualMinMaxCalculation") // we know what we are doing. - private static int nearestToZero(int min, int max) { - // this compiles to slightly better code than Math.min(Math.max(0, min), max) - int clamped = 0; - if (min > 0) { - clamped = min; - } - if (max < 0) { - clamped = max; - } - return clamped; - } - - private static int farthestFromZero(int min, int max) { - int clamped = 0; - if (min > 0) { - clamped = max; - } - if (max < 0) { - clamped = min; - } - if (clamped == 0) { - if (Math.abs(min) > Math.abs(max)) { - clamped = min; - } else { - clamped = max; - } - } - return clamped; - } - - int getChildOrderModulator(int x, int y, int z, int childFullSectionDim) { - return (x + childFullSectionDim - this.cameraOffsetX) >>> 31 - | ((y + childFullSectionDim - this.cameraOffsetY) >>> 31) << 1 - | ((z + childFullSectionDim - this.cameraOffsetZ) >>> 31) << 2; - } + this.tree.traverse(visitor, viewport, distanceLimit); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java new file mode 100644 index 0000000000..87709af08c --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java @@ -0,0 +1,57 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.minecraft.world.level.Level; + +public abstract class BaseBiForest extends BaseForest { + private static final int SECONDARY_TREE_OFFSET_XZ = 4; + + protected final T mainTree; + protected T secondaryTree; + + public BaseBiForest(int baseOffsetX,int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + + this.mainTree = this.makeTree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ); + } + + protected T makeSecondaryTree() { + // offset diagonally to fully encompass the required 65x65 area + return this.makeTree( + this.baseOffsetX + SECONDARY_TREE_OFFSET_XZ, + this.baseOffsetY, + this.baseOffsetZ + SECONDARY_TREE_OFFSET_XZ); + } + + @Override + public void add(int x, int y, int z) { + if (!this.mainTree.add(x, y, z)) { + if (this.secondaryTree == null) { + this.secondaryTree = this.makeSecondaryTree(); + } + if (!this.secondaryTree.add(x, y, z)) { + throw new IllegalStateException("Failed to add section to trees"); + } + } + } + + @Override + public int getPresence(int x, int y, int z) { + var result = this.mainTree.getPresence(x, y, z); + if (result != Tree.OUT_OF_BOUNDS) { + return result; + } + + if (this.secondaryTree != null) { + return this.secondaryTree.getPresence(x, y, z); + } + return Tree.OUT_OF_BOUNDS; + } + + public static boolean checkApplicable(float buildDistance, Level level) { + if (buildDistance / 16.0f > 64.0f) { + return false; + } + + return level.getHeight() >> 4 <= 64; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java new file mode 100644 index 0000000000..3ffae16243 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java @@ -0,0 +1,20 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public abstract class BaseForest implements Forest { + protected final int baseOffsetX, baseOffsetY, baseOffsetZ; + final float buildDistance; + + protected BaseForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + this.baseOffsetX = baseOffsetX; + this.baseOffsetY = baseOffsetY; + this.baseOffsetZ = baseOffsetZ; + this.buildDistance = buildDistance; + } + + @Override + public float getBuildDistance() { + return this.buildDistance; + } + + protected abstract T makeTree(int offsetX, int offsetY, int offsetZ); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseManyForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseManyForest.java new file mode 100644 index 0000000000..f8d30e19c2 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseManyForest.java @@ -0,0 +1,71 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public abstract class BaseManyForest extends BaseForest { + protected final T[] trees; + protected final int forestDim; + + protected T lastTree; + + public BaseManyForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + + this.forestDim = (int) Math.ceil(buildDistance / 64.0); + this.trees = this.makeTrees(this.forestDim * this.forestDim * this.forestDim); + } + + protected int getTreeIndex(int localX, int localY, int localZ) { + var treeX = localX >> 6; + var treeY = localY >> 6; + var treeZ = localZ >> 6; + + return treeX + (treeZ * this.forestDim + treeY) * this.forestDim; + } + + protected int getTreeIndexAbsolute(int x, int y, int z) { + return this.getTreeIndex(x - this.baseOffsetX, y - this.baseOffsetY, z - this.baseOffsetZ); + } + + @Override + public void add(int x, int y, int z) { + if (this.lastTree != null && this.lastTree.add(x, y, z)) { + return; + } + + var localX = x - this.baseOffsetX; + var localY = y - this.baseOffsetY; + var localZ = z - this.baseOffsetZ; + + var treeIndex = this.getTreeIndex(localX, localY, localZ); + var tree = this.trees[treeIndex]; + + if (tree == null) { + var treeOffsetX = this.baseOffsetX + (localX & ~0b111111); + var treeOffsetY = this.baseOffsetY + (localY & ~0b111111); + var treeOffsetZ = this.baseOffsetZ + (localZ & ~0b111111); + tree = this.makeTree(treeOffsetX, treeOffsetY, treeOffsetZ); + this.trees[treeIndex] = tree; + } + + tree.add(x, y, z); + this.lastTree = tree; + } + + @Override + public int getPresence(int x, int y, int z) { + if (this.lastTree != null) { + var result = this.lastTree.getPresence(x, y, z); + if (result != TraversableTree.OUT_OF_BOUNDS) { + return result; + } + } + + var treeIndex = this.getTreeIndexAbsolute(x, y, z); + var tree = this.trees[treeIndex]; + if (tree != null) { + return tree.getPresence(x, y, z); + } + return TraversableTree.OUT_OF_BOUNDS; + } + + protected abstract T[] makeTrees(int length); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java new file mode 100644 index 0000000000..19d8d174c9 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java @@ -0,0 +1,9 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public interface Forest { + void add(int x, int y, int z); + + float getBuildDistance(); + + int getPresence(int x, int y, int z); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java new file mode 100644 index 0000000000..d10991a494 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java @@ -0,0 +1,37 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public class TraversableBiForest extends BaseBiForest implements TraversableForest { + public TraversableBiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + public void calculateReduced() { + this.mainTree.calculateReduced(); + if (this.secondaryTree != null) { + this.secondaryTree.calculateReduced(); + } + } + + @Override + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + TraversableForest.super.traverse(visitor, viewport, distanceLimit); + } + + @Override + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance) { + // no sorting is necessary because we assume the camera will never be closer to the secondary tree than the main tree + this.mainTree.traverse(visitor, viewport, distanceLimit, buildDistance); + if (this.secondaryTree != null) { + this.secondaryTree.traverse(visitor, viewport, distanceLimit, buildDistance); + } + } + + @Override + protected TraversableTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new TraversableTree(offsetX, offsetY, offsetZ); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java new file mode 100644 index 0000000000..c3c6dddd6c --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java @@ -0,0 +1,23 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.world.level.Level; + +public interface TraversableForest extends Forest { + void calculateReduced(); + + default void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + this.traverse(visitor, viewport, distanceLimit, this.getBuildDistance()); + } + + void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance); + + static TraversableForest createTraversableForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance, Level level) { + if (BaseBiForest.checkApplicable(buildDistance, level)) { + return new TraversableBiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + return new TraversableManyForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableManyForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableManyForest.java new file mode 100644 index 0000000000..e69cb3f623 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableManyForest.java @@ -0,0 +1,63 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import it.unimi.dsi.fastutil.ints.IntArrays; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public class TraversableManyForest extends BaseManyForest implements TraversableForest { + public TraversableManyForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + public void calculateReduced() { + for (var tree : this.trees) { + if (tree != null) { + tree.calculateReduced(); + } + } + } + + @Override + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance) { + var transform = viewport.getTransform(); + var cameraSectionX = transform.intX >> 4; + var cameraSectionY = transform.intY >> 4; + var cameraSectionZ = transform.intZ >> 4; + + // sort the trees by distance from the camera by sorting a packed index array. + var items = new int[this.trees.length]; + for (int i = 0; i < this.trees.length; i++) { + var tree = this.trees[i]; + if (tree != null) { + var deltaX = Math.abs(tree.offsetX + 32 - cameraSectionX); + var deltaY = Math.abs(tree.offsetY + 32 - cameraSectionY); + var deltaZ = Math.abs(tree.offsetZ + 32 - cameraSectionZ); + items[i] = (deltaX + deltaY + deltaZ + 1) << 16 | i; + } + } + + IntArrays.unstableSort(items); + + // traverse in sorted front-to-back order for correct render order + for (var item : items) { + if (item == 0) { + continue; + } + var tree = this.trees[item & 0xFFFF]; + if (tree != null) { + tree.traverse(visitor, viewport, distanceLimit, this.buildDistance); + } + } + } + + @Override + protected TraversableTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new TraversableTree(offsetX, offsetY, offsetZ); + } + + @Override + protected TraversableTree[] makeTrees(int length) { + return new TraversableTree[length]; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java new file mode 100644 index 0000000000..a2a5c43b3f --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java @@ -0,0 +1,299 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import org.joml.FrustumIntersection; + +public class TraversableTree extends Tree { + private static final int INSIDE_FRUSTUM = 0b01; + private static final int INSIDE_DISTANCE = 0b10; + private static final int FULLY_INSIDE = INSIDE_FRUSTUM | INSIDE_DISTANCE; + + protected final long[] treeReduced = new long[64]; + public long treeDoubleReduced = 0L; + + // set temporarily during traversal + private int cameraOffsetX, cameraOffsetY, cameraOffsetZ; + private SectionTree.VisibleSectionVisitor visitor; + protected Viewport viewport; + private float distanceLimit; + + public TraversableTree(int offsetX, int offsetY, int offsetZ) { + super(offsetX, offsetY, offsetZ); + } + + public void calculateReduced() { + long doubleReduced = 0; + for (int i = 0; i < 64; i++) { + long reduced = 0; + var reducedOffset = i << 6; + for (int j = 0; j < 64; j++) { + reduced |= this.tree[reducedOffset + j] == 0 ? 0L : 1L << j; + } + this.treeReduced[i] = reduced; + doubleReduced |= reduced == 0 ? 0L : 1L << i; + } + this.treeDoubleReduced = doubleReduced; + } + + @Override + public int getPresence(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (isOutOfBounds(x, y, z)) { + return OUT_OF_BOUNDS; + } + + var bitIndex = interleave6x3(x, y, z); + int doubleReducedBitIndex = bitIndex >> 12; + if ((this.treeDoubleReduced & (1L << doubleReducedBitIndex)) == 0) { + return NOT_PRESENT; + } + + int reducedBitIndex = bitIndex >> 6; + return (this.tree[reducedBitIndex] & (1L << (bitIndex & 0b111111))) != 0 ? PRESENT : NOT_PRESENT; + } + + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance) { + this.visitor = visitor; + this.viewport = viewport; + this.distanceLimit = distanceLimit; + + var transform = viewport.getTransform(); + + // + 1 to section position to compensate for shifted global offset + this.cameraOffsetX = (transform.intX >> 4) - this.offsetX + 1; + this.cameraOffsetY = (transform.intY >> 4) - this.offsetY + 1; + this.cameraOffsetZ = (transform.intZ >> 4) - this.offsetZ + 1; + + // everything is already inside the distance limit if the build distance is smaller + var initialInside = this.distanceLimit >= buildDistance ? INSIDE_DISTANCE : 0; + this.traverse(getChildOrderModulator(0, 0, 0, 1 << 5), 0, 5, initialInside); + + this.visitor = null; + this.viewport = null; + } + + void traverse(int orderModulator, int nodeOrigin, int level, int inside) { + // half of the dimension of a child of this node, in blocks + int childHalfDim = 1 << (level + 3); // * 16 / 2 + + // odd levels (the higher levels of each reduction) need to modulate indexes that are multiples of 8 + if ((level & 1) == 1) { + orderModulator <<= 3; + } + + if (level <= 1) { + // check using the full bitmap + int childOriginBase = nodeOrigin & 0b111111_111111_000000; + long map = this.tree[nodeOrigin >> 6]; + + if (level == 0) { + int startBit = nodeOrigin & 0b111111; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (1L << childIndex)) != 0) { + int sectionOrigin = childOriginBase | childIndex; + int x = deinterleave6(sectionOrigin) + this.offsetX; + int y = deinterleave6(sectionOrigin >> 1) + this.offsetY; + int z = deinterleave6(sectionOrigin >> 2) + this.offsetZ; + + if (inside == FULLY_INSIDE || testLeafNode(x, y, z, inside)) { + this.visitor.visit(x, y, z); + } + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (0xFFL << childIndex)) != 0) { + this.testChild(childOriginBase | childIndex, childHalfDim, level, inside); + } + } + } + } else if (level <= 3) { + int childOriginBase = nodeOrigin & 0b111111_000000_000000; + long map = this.treeReduced[nodeOrigin >> 12]; + + if (level == 2) { + int startBit = (nodeOrigin >> 6) & 0b111111; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (1L << childIndex)) != 0) { + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((map & (0xFFL << childIndex)) != 0) { + this.testChild(childOriginBase | (childIndex << 6), childHalfDim, level, inside); + } + } + } + } else { + if (level == 4) { + int startBit = nodeOrigin >> 12; + int endBit = startBit + 8; + + for (int bitIndex = startBit; bitIndex < endBit; bitIndex++) { + int childIndex = bitIndex ^ orderModulator; + if ((this.treeDoubleReduced & (1L << childIndex)) != 0) { + this.testChild(childIndex << 12, childHalfDim, level, inside); + } + } + } else { + for (int bitIndex = 0; bitIndex < 64; bitIndex += 8) { + int childIndex = bitIndex ^ orderModulator; + if ((this.treeDoubleReduced & (0xFFL << childIndex)) != 0) { + this.testChild(childIndex << 12, childHalfDim, level, inside); + } + } + } + } + } + + void testChild(int childOrigin, int childHalfDim, int level, int inside) { + // calculate section coordinates in tree-space + int x = deinterleave6(childOrigin); + int y = deinterleave6(childOrigin >> 1); + int z = deinterleave6(childOrigin >> 2); + + // immediately traverse if fully inside + if (inside == FULLY_INSIDE) { + level--; + this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); + return; + } + + // convert to world-space section origin in blocks, then to camera space + var transform = this.viewport.getTransform(); + int worldX = ((x + this.offsetX) << 4) - transform.intX; + int worldY = ((y + this.offsetY) << 4) - transform.intY; + int worldZ = ((z + this.offsetZ) << 4) - transform.intZ; + + boolean visible = true; + + if ((inside & INSIDE_FRUSTUM) == 0) { + var intersectionResult = this.viewport.getBoxIntersectionDirect( + (worldX + childHalfDim) - transform.fracX, + (worldY + childHalfDim) - transform.fracY, + (worldZ + childHalfDim) - transform.fracZ, + childHalfDim + OcclusionCuller.CHUNK_SECTION_MARGIN); + if (intersectionResult == FrustumIntersection.INSIDE) { + inside |= INSIDE_FRUSTUM; + } else { + visible = intersectionResult == FrustumIntersection.INTERSECT; + } + } + + if ((inside & INSIDE_DISTANCE) == 0) { + // calculate the point of the node closest to the camera + int childFullDim = childHalfDim << 1; + float dx = nearestToZero(worldX, worldX + childFullDim) - transform.fracX; + float dy = nearestToZero(worldY, worldY + childFullDim) - transform.fracY; + float dz = nearestToZero(worldZ, worldZ + childFullDim) - transform.fracZ; + + // check if closest point inside the cylinder + visible = cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); + if (visible) { + // if the farthest point is also visible, the node is fully inside + dx = farthestFromZero(worldX, worldX + childFullDim) - transform.fracX; + dy = farthestFromZero(worldY, worldY + childFullDim) - transform.fracY; + dz = farthestFromZero(worldZ, worldZ + childFullDim) - transform.fracZ; + + if (cylindricalDistanceTest(dx, dy, dz, this.distanceLimit)) { + inside |= INSIDE_DISTANCE; + } + } + } + + if (visible) { + level--; + this.traverse(getChildOrderModulator(x, y, z, 1 << level), childOrigin, level, inside); + } + } + + boolean testLeafNode(int x, int y, int z, int inside) { + // input coordinates are section coordinates in world-space + + var transform = this.viewport.getTransform(); + + // convert to blocks and move into integer camera space + x = (x << 4) - transform.intX; + y = (y << 4) - transform.intY; + z = (z << 4) - transform.intZ; + + // test frustum if not already inside frustum + if ((inside & INSIDE_FRUSTUM) == 0 && !this.viewport.isBoxVisibleDirect( + (x + 8) - transform.fracX, + (y + 8) - transform.fracY, + (z + 8) - transform.fracZ, + OcclusionCuller.CHUNK_SECTION_RADIUS)) { + return false; + } + + // test distance if not already inside distance + if ((inside & INSIDE_DISTANCE) == 0) { + // coordinates of the point to compare (in view space) + // this is the closest point within the bounding box to the center (0, 0, 0) + float dx = nearestToZero(x, x + 16) - transform.fracX; + float dy = nearestToZero(y, y + 16) - transform.fracY; + float dz = nearestToZero(z, z + 16) - transform.fracZ; + + return cylindricalDistanceTest(dx, dy, dz, this.distanceLimit); + } + + return true; + } + + static boolean cylindricalDistanceTest(float dx, float dy, float dz, float distanceLimit) { + // vanilla's "cylindrical fog" algorithm + // max(length(distance.xz), abs(distance.y)) + return (((dx * dx) + (dz * dz)) < (distanceLimit * distanceLimit)) && + (Math.abs(dy) < distanceLimit); + } + + @SuppressWarnings("ManualMinMaxCalculation") // we know what we are doing. + private static int nearestToZero(int min, int max) { + // this compiles to slightly better code than Math.min(Math.max(0, min), max) + int clamped = 0; + if (min > 0) { + clamped = min; + } + if (max < 0) { + clamped = max; + } + return clamped; + } + + private static int farthestFromZero(int min, int max) { + int clamped = 0; + if (min > 0) { + clamped = max; + } + if (max < 0) { + clamped = min; + } + if (clamped == 0) { + if (Math.abs(min) > Math.abs(max)) { + clamped = min; + } else { + clamped = max; + } + } + return clamped; + } + + int getChildOrderModulator(int x, int y, int z, int childFullSectionDim) { + return (x + childFullSectionDim - this.cameraOffsetX) >>> 31 + | ((y + childFullSectionDim - this.cameraOffsetY) >>> 31) << 1 + | ((z + childFullSectionDim - this.cameraOffsetZ) >>> 31) << 2; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java new file mode 100644 index 0000000000..b5c9304904 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java @@ -0,0 +1,54 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public abstract class Tree { + public static final int OUT_OF_BOUNDS = 0; + public static final int NOT_PRESENT = 1; + public static final int PRESENT = 2; + + protected final long[] tree = new long[64 * 64]; + protected final int offsetX, offsetY, offsetZ; + + public Tree(int offsetX, int offsetY, int offsetZ) { + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + } + + public static boolean isOutOfBounds(int x, int y, int z) { + return x > 63 || y > 63 || z > 63 || x < 0 || y < 0 || z < 0; + } + + protected static int interleave6x3(int x, int y, int z) { + return Tree.interleave6(x) | Tree.interleave6(y) << 1 | Tree.interleave6(z) << 2; + } + + private static int interleave6(int n) { + n &= 0b000000000000111111; + n = (n | n << 4 | n << 8) & 0b000011000011000011; + n = (n | n << 2) & 0b001001001001001001; + return n; + } + + protected static int deinterleave6(int n) { + n &= 0b001001001001001001; + n = (n | n >> 2) & 0b000011000011000011; + n = (n | n >> 4 | n >> 8) & 0b000000000000111111; + return n; + } + + public boolean add(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (Tree.isOutOfBounds(x, y, z)) { + return false; + } + + var bitIndex = Tree.interleave6x3(x, y, z); + this.tree[bitIndex >> 6] |= 1L << (bitIndex & 0b111111); + + return true; + } + + public abstract int getPresence(int x, int y, int z); +} From abbdea48c324de4977c47bebb8ca59240a3a5eb4 Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 10 Nov 2024 02:52:05 +0100 Subject: [PATCH 45/81] fix rebase changes --- .../render/chunk/occlusion/RayOcclusionSectionTree.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java index 09477f0024..30916f1dfc 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -24,8 +24,8 @@ public RayOcclusionSectionTree(Viewport viewport, float buildDistance, int frame this.transform = viewport.getTransform(); this.portalTree = createPortalTree(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, buildDistance, level); - this.minSection = level.getMinSection(); - this.maxSection = level.getMaxSection(); + this.minSection = level.getMinSectionY(); + this.maxSection = level.getMaxSectionY(); } @Override From 83e2a330056d9d4dc9c0c9cfd4c18c058325d431 Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 10 Nov 2024 03:31:37 +0100 Subject: [PATCH 46/81] make traversable trees abstract so that they can be subclassed with a different tree implementation --- .../occlusion/RayOcclusionSectionTree.java | 6 ++-- .../tree/AbstractTraversableBiForest.java | 32 +++++++++++++++++++ ...va => AbstractTraversableMultiForest.java} | 13 ++------ ...seManyForest.java => BaseMultiForest.java} | 4 +-- .../chunk/tree/TraversableBiForest.java | 27 +--------------- .../render/chunk/tree/TraversableForest.java | 2 +- .../chunk/tree/TraversableMultiForest.java | 17 ++++++++++ 7 files changed, 58 insertions(+), 43 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java rename common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/{TraversableManyForest.java => AbstractTraversableMultiForest.java} (78%) rename common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/{BaseManyForest.java => BaseMultiForest.java} (92%) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java index 30916f1dfc..c1fe4174e1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -118,7 +118,7 @@ private static Forest createPortalTree(int baseOffsetX,int baseOffsetY, int base return new PortalBiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); } - return new PortalManyForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + return new PortalMultiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); } private static class PortalBiForest extends BaseBiForest { @@ -132,8 +132,8 @@ protected FlatTree makeTree(int offsetX, int offsetY, int offsetZ) { } } - private static class PortalManyForest extends BaseManyForest { - public PortalManyForest(int baseOffsetX,int baseOffsetY, int baseOffsetZ, float buildDistance) { + private static class PortalMultiForest extends BaseMultiForest { + public PortalMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java new file mode 100644 index 0000000000..a023d148ff --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java @@ -0,0 +1,32 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public abstract class AbstractTraversableBiForest extends BaseBiForest implements TraversableForest { + public AbstractTraversableBiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + public void calculateReduced() { + this.mainTree.calculateReduced(); + if (this.secondaryTree != null) { + this.secondaryTree.calculateReduced(); + } + } + + @Override + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + TraversableForest.super.traverse(visitor, viewport, distanceLimit); + } + + @Override + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance) { + // no sorting is necessary because we assume the camera will never be closer to the secondary tree than the main tree + this.mainTree.traverse(visitor, viewport, distanceLimit, buildDistance); + if (this.secondaryTree != null) { + this.secondaryTree.traverse(visitor, viewport, distanceLimit, buildDistance); + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableManyForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java similarity index 78% rename from common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableManyForest.java rename to common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java index e69cb3f623..2c9f310b38 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableManyForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java @@ -4,8 +4,8 @@ import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; -public class TraversableManyForest extends BaseManyForest implements TraversableForest { - public TraversableManyForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { +public abstract class AbstractTraversableMultiForest extends BaseMultiForest implements TraversableForest { + public AbstractTraversableMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); } @@ -51,13 +51,4 @@ public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewpor } } - @Override - protected TraversableTree makeTree(int offsetX, int offsetY, int offsetZ) { - return new TraversableTree(offsetX, offsetY, offsetZ); - } - - @Override - protected TraversableTree[] makeTrees(int length) { - return new TraversableTree[length]; - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseManyForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java similarity index 92% rename from common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseManyForest.java rename to common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java index f8d30e19c2..fdf0af8483 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseManyForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java @@ -1,12 +1,12 @@ package net.caffeinemc.mods.sodium.client.render.chunk.tree; -public abstract class BaseManyForest extends BaseForest { +public abstract class BaseMultiForest extends BaseForest { protected final T[] trees; protected final int forestDim; protected T lastTree; - public BaseManyForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + public BaseMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); this.forestDim = (int) Math.ceil(buildDistance / 64.0); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java index d10991a494..ae75cc441b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableBiForest.java @@ -1,35 +1,10 @@ package net.caffeinemc.mods.sodium.client.render.chunk.tree; -import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; -import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; - -public class TraversableBiForest extends BaseBiForest implements TraversableForest { +public class TraversableBiForest extends AbstractTraversableBiForest { public TraversableBiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); } - @Override - public void calculateReduced() { - this.mainTree.calculateReduced(); - if (this.secondaryTree != null) { - this.secondaryTree.calculateReduced(); - } - } - - @Override - public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { - TraversableForest.super.traverse(visitor, viewport, distanceLimit); - } - - @Override - public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance) { - // no sorting is necessary because we assume the camera will never be closer to the secondary tree than the main tree - this.mainTree.traverse(visitor, viewport, distanceLimit, buildDistance); - if (this.secondaryTree != null) { - this.secondaryTree.traverse(visitor, viewport, distanceLimit, buildDistance); - } - } - @Override protected TraversableTree makeTree(int offsetX, int offsetY, int offsetZ) { return new TraversableTree(offsetX, offsetY, offsetZ); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java index c3c6dddd6c..548fa88038 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java @@ -18,6 +18,6 @@ static TraversableForest createTraversableForest(int baseOffsetX, int baseOffset return new TraversableBiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); } - return new TraversableManyForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + return new TraversableMultiForest(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java new file mode 100644 index 0000000000..d547655de8 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableMultiForest.java @@ -0,0 +1,17 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +public class TraversableMultiForest extends AbstractTraversableMultiForest { + public TraversableMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); + } + + @Override + protected TraversableTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new TraversableTree(offsetX, offsetY, offsetZ); + } + + @Override + protected TraversableTree[] makeTrees(int length) { + return new TraversableTree[length]; + } +} From e1cda93296edd5e00233c063ad4f4bb84b5ca542 Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 10 Nov 2024 03:35:54 +0100 Subject: [PATCH 47/81] ordering consistency --- .../render/chunk/occlusion/RayOcclusionSectionTree.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java index c1fe4174e1..a65fc851e7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -138,13 +138,13 @@ public PortalMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, floa } @Override - protected FlatTree[] makeTrees(int length) { - return new FlatTree[length]; + protected FlatTree makeTree(int offsetX, int offsetY, int offsetZ) { + return new FlatTree(offsetX, offsetY, offsetZ); } @Override - protected FlatTree makeTree(int offsetX, int offsetY, int offsetZ) { - return new FlatTree(offsetX, offsetY, offsetZ); + protected FlatTree[] makeTrees(int length) { + return new FlatTree[length]; } } From a3eaecd95237cd7cbaf3a93b9cacf8e2193518ff Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 10 Nov 2024 04:53:29 +0100 Subject: [PATCH 48/81] fix incorrect multi forest size calculation --- .../client/render/chunk/lists/PendingTaskCollector.java | 5 +---- .../sodium/client/render/chunk/tree/BaseMultiForest.java | 3 ++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index d22888391d..57bb7a2c98 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -22,9 +22,6 @@ - experiment with non-linear distance scaling (if < some radius, bonus priority for being close) */ public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { - // offset is shifted by 1 to encompass all sections towards the negative - // TODO: is this the correct way of calculating the minimum possible section index? - private static final int DISTANCE_OFFSET = 1; public static final int SECTION_Y_MIN = -128; // used instead of baseOffsetY to accommodate all permissible y values (-2048 to 2048 blocks) // tunable parameters for the priority calculation. @@ -48,7 +45,7 @@ public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisit public PendingTaskCollector(Viewport viewport, float buildDistance, boolean frustumTested) { this.creationTime = System.nanoTime(); this.isFrustumTested = frustumTested; - var offsetDistance = Mth.ceil(buildDistance / 16.0f) + DISTANCE_OFFSET; + var offsetDistance = Mth.ceil(buildDistance / 16.0f) + 1; var transform = viewport.getTransform(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java index fdf0af8483..52f9de4e2f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java @@ -9,7 +9,7 @@ public abstract class BaseMultiForest extends BaseForest { public BaseMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); - this.forestDim = (int) Math.ceil(buildDistance / 64.0); + this.forestDim = (int) Math.ceil((buildDistance / 8.0 + 1) / 64.0); this.trees = this.makeTrees(this.forestDim * this.forestDim * this.forestDim); } @@ -62,6 +62,7 @@ public int getPresence(int x, int y, int z) { var treeIndex = this.getTreeIndexAbsolute(x, y, z); var tree = this.trees[treeIndex]; if (tree != null) { + this.lastTree = tree; return tree.getPresence(x, y, z); } return TraversableTree.OUT_OF_BOUNDS; From 2215691b81ef59445d8c34ab0d2d949794d88252 Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 14 Nov 2024 22:02:47 +0100 Subject: [PATCH 49/81] cleanup forest build distance handling --- .../render/chunk/tree/AbstractTraversableBiForest.java | 9 ++------- .../chunk/tree/AbstractTraversableMultiForest.java | 3 +-- .../mods/sodium/client/render/chunk/tree/BaseForest.java | 5 ----- .../sodium/client/render/chunk/tree/BaseMultiForest.java | 9 +++++++-- .../mods/sodium/client/render/chunk/tree/Forest.java | 2 -- .../client/render/chunk/tree/TraversableForest.java | 6 +----- 6 files changed, 11 insertions(+), 23 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java index a023d148ff..42b2bec411 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java @@ -18,15 +18,10 @@ public void calculateReduced() { @Override public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { - TraversableForest.super.traverse(visitor, viewport, distanceLimit); - } - - @Override - public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance) { // no sorting is necessary because we assume the camera will never be closer to the secondary tree than the main tree - this.mainTree.traverse(visitor, viewport, distanceLimit, buildDistance); + this.mainTree.traverse(visitor, viewport, distanceLimit, this.buildDistance); if (this.secondaryTree != null) { - this.secondaryTree.traverse(visitor, viewport, distanceLimit, buildDistance); + this.secondaryTree.traverse(visitor, viewport, distanceLimit, this.buildDistance); } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java index 2c9f310b38..eb758d8e36 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java @@ -19,7 +19,7 @@ public void calculateReduced() { } @Override - public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance) { + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { var transform = viewport.getTransform(); var cameraSectionX = transform.intX >> 4; var cameraSectionY = transform.intY >> 4; @@ -50,5 +50,4 @@ public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewpor } } } - } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java index 3ffae16243..2501fbd68e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseForest.java @@ -11,10 +11,5 @@ protected BaseForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float bu this.buildDistance = buildDistance; } - @Override - public float getBuildDistance() { - return this.buildDistance; - } - protected abstract T makeTree(int offsetX, int offsetY, int offsetZ); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java index 52f9de4e2f..43485b8bbc 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java @@ -6,13 +6,18 @@ public abstract class BaseMultiForest extends BaseForest { protected T lastTree; - public BaseMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { + public BaseMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ,float buildDistance) { super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); - this.forestDim = (int) Math.ceil((buildDistance / 8.0 + 1) / 64.0); + this.forestDim = forestDimFromBuildDistance(buildDistance); this.trees = this.makeTrees(this.forestDim * this.forestDim * this.forestDim); } + public static int forestDimFromBuildDistance(float buildDistance) { + // / 16 (block to chunk) * 2 (radius to diameter) + 1 (center chunk) / 64 (chunks per tree) + return (int) Math.ceil((buildDistance / 8.0 + 1) / 64.0); + } + protected int getTreeIndex(int localX, int localY, int localZ) { var treeX = localX >> 6; var treeY = localY >> 6; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java index 19d8d174c9..ed425201d5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java @@ -3,7 +3,5 @@ public interface Forest { void add(int x, int y, int z); - float getBuildDistance(); - int getPresence(int x, int y, int z); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java index 548fa88038..d60c8f1529 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java @@ -7,11 +7,7 @@ public interface TraversableForest extends Forest { void calculateReduced(); - default void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { - this.traverse(visitor, viewport, distanceLimit, this.getBuildDistance()); - } - - void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit, float buildDistance); + void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit); static TraversableForest createTraversableForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance, Level level) { if (BaseBiForest.checkApplicable(buildDistance, level)) { From 2f82fc6acfa36968376d076698b5c8d7162b3013 Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 16 Nov 2024 18:15:10 +0100 Subject: [PATCH 50/81] fix section sorting by correcting the camera offset --- .../sodium/client/render/chunk/tree/TraversableTree.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java index a2a5c43b3f..a1dda59668 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java @@ -63,10 +63,11 @@ public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewpor var transform = viewport.getTransform(); - // + 1 to section position to compensate for shifted global offset - this.cameraOffsetX = (transform.intX >> 4) - this.offsetX + 1; - this.cameraOffsetY = (transform.intY >> 4) - this.offsetY + 1; - this.cameraOffsetZ = (transform.intZ >> 4) - this.offsetZ + 1; + // + 1 to offset section position to compensate for shifted global offset + // adjust camera block position to account for fractional part of camera position + this.cameraOffsetX = ((transform.intX + (int) Math.signum(transform.fracX)) >> 4) - this.offsetX + 1; + this.cameraOffsetY = ((transform.intY + (int) Math.signum(transform.fracY)) >> 4) - this.offsetY + 1; + this.cameraOffsetZ = ((transform.intZ + (int) Math.signum(transform.fracZ)) >> 4) - this.offsetZ + 1; // everything is already inside the distance limit if the build distance is smaller var initialInside = this.distanceLimit >= buildDistance ? INSIDE_DISTANCE : 0; From 3c7ebb25160fea236c85a62f51b7e45a37c6c56b Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 21 Nov 2024 21:44:29 +0100 Subject: [PATCH 51/81] update todos --- .../client/render/chunk/RenderSection.java | 21 ------------------- .../chunk/lists/PendingTaskCollector.java | 6 ------ .../render/chunk/occlusion/SectionTree.java | 1 - 3 files changed, 28 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java index f32137e562..7680df2408 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java @@ -12,27 +12,6 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -// TODO: idea for topic-related section data storage: have an array of encoded data as longs (or ints) and enter or remove sections from it as they get loaded and unloaded. Keep track of empty spots in this array by building a single-linked free-list, using a bit to see if there's actually still data there or if it's free now. Newly loaded sections get the first available free entry and the location of this entry is stored in the section object for reference. The index of the next free entry is stored in a "head" pointer, and when a section gets removed it needs to "mend" the hole created by the removal by pointing its entry to the head and the head to it. We could also store more data in multiple longs by just treating multiple as one "entry". It could regularly re-organize the data to compact it and move nearby sections to nearby positions in the array (z order curve). This structure should be generic so various types of section data can be stored in it. -// problem: how does it initialize the occlusion culling? does it need to use the position-to-section hashmap to get the section objects and then extract the indexes? might be ok since it's just the init - -/* "struct" layout for occlusion culling: -- 1 bit for entry management (is present or not), if zero, the rest of the first 8 bytes is the next free entry index -- 24 bits = 3 bytes for the section position (x, y, z with each 0-255) -- 36 bits for the visibility data (6 bits per direction, 6 directions) -- 6 bits for adjacent mask -- 6 bits for incoming directions -- 144 bits = 18 bytes = 6 * 3 bytes for the adjacent sections (up to 6 directions, 3 bytes for section index) -- 32 bits for the last visible frame - */ - -/* "struct" layout for task management: -- 1 bit for entry management (entry deleted if disposed) -- 1 bit for if there's a running task -- 5 bits for the pending update type (ChunkUpdateType) -- 24 bits for the pending time since start in milliseconds -- 24 bits = 3 bytes for the section position (x, y, z with each 0-255), - */ - /** * The render state object for a chunk section. This contains all the graphics state for each render pass along with * data about the render in the chunk visibility graph. diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index 57bb7a2c98..7aba4099a8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -15,12 +15,6 @@ import java.util.EnumMap; import java.util.Map; -/* -TODO: -- check if there's also bumps in the fps when crossing chunk borders on dev -- tune priority values, test frustum effect by giving it a large value -- experiment with non-linear distance scaling (if < some radius, bonus priority for being close) - */ public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { public static final int SECTION_Y_MIN = -128; // used instead of baseOffsetY to accommodate all permissible y values (-2048 to 2048 blocks) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index cd4fd33ac5..4132111d85 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -12,7 +12,6 @@ /* * - 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?) - * - possibly refactor the section tree and task section tree structures to be more composable instead of extending each other. * - 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 { From ccb932141b221c05794b732e2a029bf5061b0707 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 22 Nov 2024 03:42:51 +0100 Subject: [PATCH 52/81] update debug info to be relevant and useful again --- .../render/chunk/RenderSectionManager.java | 41 ++++++++++++++----- .../render/chunk/async/AsyncRenderTask.java | 2 + .../render/chunk/async/AsyncTaskType.java | 18 ++++++++ .../client/render/chunk/async/CullTask.java | 4 -- .../render/chunk/async/FrustumCullTask.java | 5 +++ .../async/FrustumTaskCollectionTask.java | 6 ++- .../render/chunk/async/GlobalCullTask.java | 9 ++++ .../compile/executor/ChunkJobCollector.java | 4 ++ .../render/chunk/occlusion/CullType.java | 10 +++-- 9 files changed, 79 insertions(+), 20 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncTaskType.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index e79050d52c..babc0db806 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -57,6 +57,7 @@ import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.stream.Collectors; public class RenderSectionManager { private static final float NEARBY_REBUILD_DISTANCE = Mth.square(16.0f); @@ -70,8 +71,11 @@ public class RenderSectionManager { private final Long2ReferenceMap sectionByPosition = new Long2ReferenceOpenHashMap<>(); private final ConcurrentLinkedDeque> buildResults = new ConcurrentLinkedDeque<>(); - private ChunkJobCollector lastBlockingCollector; private final JobEffortEstimator jobEffortEstimator = new JobEffortEstimator(); + private ChunkJobCollector lastBlockingCollector; + private long thisFrameBlockingTasks; + private long nextFrameBlockingTasks; + private long deferredTasks; private final ChunkRenderer chunkRenderer; @@ -653,6 +657,10 @@ public void cleanupAndFlip() { } public void updateChunks(boolean updateImmediately) { + this.thisFrameBlockingTasks = 0; + this.nextFrameBlockingTasks = 0; + this.deferredTasks = 0; + var thisFrameBlockingCollector = this.lastBlockingCollector; this.lastBlockingCollector = null; if (thisFrameBlockingCollector == null) { @@ -664,6 +672,7 @@ public void updateChunks(boolean updateImmediately) { // and add all tasks to it so that they're waited on this.submitSectionTasks(thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector); + this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); thisFrameBlockingCollector.awaitCompletion(this.builder); } else { var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); @@ -678,6 +687,10 @@ public void updateChunks(boolean updateImmediately) { this.submitSectionTasks(nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); } + this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); + this.nextFrameBlockingTasks = nextFrameBlockingCollector.getSubmittedTaskCount(); + this.deferredTasks = deferredCollector.getSubmittedTaskCount(); + // wait on this frame's blocking collector which contains the important tasks from this frame // and semi-important tasks from the last frame thisFrameBlockingCollector.awaitCompletion(this.builder); @@ -999,19 +1012,29 @@ public Collection getDebugStrings() { count++; } - // TODO: information about pending async culling tasks, restore some information about task scheduling? - list.add(String.format("Geometry Pool: %d/%d MiB (%d buffers)", MathUtil.toMib(deviceUsed), MathUtil.toMib(deviceAllocated), count)); list.add(String.format("Transfer Queue: %s", this.regions.getStagingBuffer().toString())); - list.add(String.format("Chunk Builder: Permits=%02d (%04d%%) | Busy=%02d | Total=%02d", - this.builder.getScheduledJobCount(), (int)(this.builder.getBusyFraction(this.lastFrameDuration) * 100), this.builder.getBusyThreadCount(), this.builder.getTotalThreadCount()) + 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()) ); - list.add(String.format("Chunk Queues: U=%02d", this.buildResults.size())); + list.add(String.format("Tasks: N0=%03d | N1=%03d | Def=%03d, Recv=%03d", + this.thisFrameBlockingTasks, this.nextFrameBlockingTasks, this.deferredTasks, this.buildResults.size()) + ); this.sortTriggering.addDebugStrings(list); + var taskSlots = new String[AsyncTaskType.VALUES.length]; + for (var task : this.pendingTasks) { + var type = task.getTaskType(); + taskSlots[type.ordinal()] = type.abbreviation; + } + list.add("Tree Builds: " + Arrays + .stream(taskSlots) + .map(slot -> slot == null ? "_" : slot) + .collect(Collectors.joining(" "))); + return list; } @@ -1037,11 +1060,7 @@ private String getCullTypeName() { } var cullTypeName = "-"; if (renderTreeCullType != null) { - cullTypeName = switch (renderTreeCullType) { - case WIDE -> "W"; - case REGULAR -> "R"; - case FRUSTUM -> "F"; - }; + cullTypeName = renderTreeCullType.abbreviation; } return cullTypeName; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java index b8b6b2ad93..2057621bc1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncRenderTask.java @@ -73,4 +73,6 @@ public T call() throws Exception { } protected abstract T runTask(); + + public abstract AsyncTaskType getTaskType(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncTaskType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncTaskType.java new file mode 100644 index 0000000000..6a0ddb433e --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/AsyncTaskType.java @@ -0,0 +1,18 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.async; + +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; + +public enum AsyncTaskType { + FRUSTUM_CULL(CullType.FRUSTUM.abbreviation), + REGULAR_CULL(CullType.REGULAR.abbreviation), + WIDE_CULL(CullType.WIDE.abbreviation), + FRUSTUM_TASK_COLLECTION("T"); + + public static final AsyncTaskType[] VALUES = values(); + + public final String abbreviation; + + AsyncTaskType(String abbreviation) { + this.abbreviation = abbreviation; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java index fc3b7807e7..cca577f814 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/CullTask.java @@ -15,8 +15,4 @@ protected CullTask(Viewport viewport, float buildDistance, int frame, OcclusionC } public abstract CullType getCullType(); - - protected int getOcclusionToken() { - return (this.getCullType().ordinal() << 28) ^ this.frame; - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java index 421f068784..39f9c1659a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -49,6 +49,11 @@ public PendingTaskCollector.TaskListCollection getFrustumTaskLists() { }; } + @Override + public AsyncTaskType getTaskType() { + return AsyncTaskType.FRUSTUM_CULL; + } + @Override public CullType getCullType() { return CullType.FRUSTUM; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java index cea89a17c4..772a712e3c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskCollectionTask.java @@ -3,7 +3,6 @@ import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.lists.FrustumTaskCollector; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; @@ -25,4 +24,9 @@ public FrustumTaskListsResult runTask() { var frustumTaskLists = collector.getPendingTaskLists(); return () -> frustumTaskLists; } + + @Override + public AsyncTaskType getTaskType() { + return AsyncTaskType.FRUSTUM_TASK_COLLECTION; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index 80cb562875..b28add6598 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -63,6 +63,15 @@ public PendingTaskCollector.TaskListCollection getGlobalTaskLists() { }; } + @Override + public AsyncTaskType getTaskType() { + return switch (this.cullType) { + case WIDE -> AsyncTaskType.WIDE_CULL; + case REGULAR -> AsyncTaskType.REGULAR_CULL; + default -> throw new IllegalStateException("Unexpected value: " + this.cullType); + }; + } + @Override public CullType getCullType() { return this.cullType; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java index a5e5fa366e..9d1fae00f8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java @@ -53,4 +53,8 @@ public void addSubmittedJob(ChunkJob job) { public boolean hasBudgetRemaining() { return this.duration > 0; } + + public long getSubmittedTaskCount() { + return this.submitted.size(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java index c91596a92c..a925277218 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/CullType.java @@ -1,15 +1,17 @@ package net.caffeinemc.mods.sodium.client.render.chunk.occlusion; public enum CullType { - WIDE(1, false, false), - REGULAR(0, false, false), - FRUSTUM(0, true, true); + WIDE("W", 1, false, false), + REGULAR("R", 0, false, false), + FRUSTUM("F", 0, true, true); + public final String abbreviation; public final int bfsWidth; public final boolean isFrustumTested; public final boolean isFogCulled; - CullType(int bfsWidth, boolean isFrustumTested, boolean isFogCulled) { + CullType(String abbreviation, int bfsWidth, boolean isFrustumTested, boolean isFogCulled) { + this.abbreviation = abbreviation; this.bfsWidth = bfsWidth; this.isFrustumTested = isFrustumTested; this.isFogCulled = isFogCulled; From 18b3c2132956547210764022e8b5f1a65f48e02a Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 22 Nov 2024 20:42:30 +0100 Subject: [PATCH 53/81] refactor job effort estimation, add category-based meshing task size estimation, limit upload size based on previous mesh task result size or an estimate of it, the limit behavior changes depending on which type of upload buffer is used --- .../arena/staging/FallbackStagingBuffer.java | 7 ++ .../gl/arena/staging/MappedStagingBuffer.java | 7 ++ .../gl/arena/staging/StagingBuffer.java | 2 + .../client/render/chunk/RenderSection.java | 11 +-- .../render/chunk/RenderSectionManager.java | 57 ++++++++----- .../chunk/compile/BuilderTaskOutput.java | 11 +++ .../chunk/compile/ChunkBuildOutput.java | 21 +++-- .../render/chunk/compile/ChunkSortOutput.java | 5 ++ .../estimation/CategoryFactorEstimator.java | 81 +++++++++++++++++++ .../estimation/JobDurationEstimator.java | 14 ++++ .../chunk/compile/estimation/JobEffort.java | 22 +++++ .../compile/estimation/MeshResultSize.java | 49 +++++++++++ .../estimation/MeshTaskSizeEstimator.java | 25 ++++++ .../chunk/compile/executor/ChunkBuilder.java | 11 --- .../chunk/compile/executor/ChunkJob.java | 2 + .../compile/executor/ChunkJobResult.java | 1 + .../chunk/compile/executor/ChunkJobTyped.java | 8 +- .../chunk/compile/executor/JobEffort.java | 7 -- .../compile/executor/JobEffortEstimator.java | 67 --------------- .../tasks/ChunkBuilderMeshingTask.java | 5 +- .../tasks/ChunkBuilderSortingTask.java | 3 +- .../chunk/compile/tasks/ChunkBuilderTask.java | 15 +++- .../render/chunk/region/RenderRegion.java | 11 +-- 23 files changed, 310 insertions(+), 132 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java delete mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java delete mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java index cfe9aae5a8..0230e6f82b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/FallbackStagingBuffer.java @@ -8,6 +8,8 @@ import java.nio.ByteBuffer; public class FallbackStagingBuffer implements StagingBuffer { + private static final float BYTES_PER_NANO_LIMIT = 8_000_000.0f / (1_000_000_000.0f / 60.0f); // MB per frame at 60fps + private final GlMutableBuffer fallbackBufferObject; public FallbackStagingBuffer(CommandList commandList) { @@ -39,4 +41,9 @@ public void flip() { public String toString() { return "Fallback"; } + + @Override + public long getUploadSizeLimit(long frameDuration) { + return (long) (frameDuration * BYTES_PER_NANO_LIMIT); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java index 5252861cad..a7b6223204 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/MappedStagingBuffer.java @@ -15,6 +15,8 @@ import java.util.List; public class MappedStagingBuffer implements StagingBuffer { + private static final float UPLOAD_LIMIT_MARGIN = 0.8f; + private static final EnumBitField STORAGE_FLAGS = EnumBitField.of(GlBufferStorageFlags.PERSISTENT, GlBufferStorageFlags.CLIENT_STORAGE, GlBufferStorageFlags.MAP_WRITE); @@ -156,6 +158,11 @@ public void flip() { } } + @Override + public long getUploadSizeLimit(long frameDuration) { + return (long) (this.capacity * UPLOAD_LIMIT_MARGIN); + } + private static final class CopyCommand { private final GlBuffer buffer; private final long readOffset; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java index d3087e8df5..0fe71170d9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gl/arena/staging/StagingBuffer.java @@ -13,4 +13,6 @@ public interface StagingBuffer { void delete(CommandList commandList); void flip(); + + long getUploadSizeLimit(long frameDuration); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java index 7680df2408..0091976649 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java @@ -1,5 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirection; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.GraphDirectionSet; @@ -46,7 +47,7 @@ public class RenderSection { // Pending Update State @Nullable private CancellationToken taskCancellationToken = null; - private long lastMeshingTaskEffort = 1; + private long lastMeshResultSize = MeshResultSize.NO_DATA; @Nullable private ChunkUpdateType pendingUpdateType; @@ -162,12 +163,12 @@ private boolean clearRenderState() { return wasBuilt; } - public void setLastMeshingTaskEffort(long effort) { - this.lastMeshingTaskEffort = effort; + public void setLastMeshResultSize(long size) { + this.lastMeshResultSize = size; } - public long getLastMeshingTaskEffort() { - return this.lastMeshingTaskEffort; + public long getLastMeshResultSize() { + return this.lastMeshResultSize; } /** diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index babc0db806..ff34706b51 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -10,10 +10,10 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkSortOutput; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobCollector; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.JobEffortEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobDurationEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.*; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderSortingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; @@ -71,7 +71,8 @@ public class RenderSectionManager { private final Long2ReferenceMap sectionByPosition = new Long2ReferenceOpenHashMap<>(); private final ConcurrentLinkedDeque> buildResults = new ConcurrentLinkedDeque<>(); - private final JobEffortEstimator jobEffortEstimator = new JobEffortEstimator(); + private final JobDurationEstimator jobDurationEstimator = new JobDurationEstimator(); + private final MeshTaskSizeEstimator meshTaskSizeEstimator = new MeshTaskSizeEstimator(); private ChunkJobCollector lastBlockingCollector; private long thisFrameBlockingTasks; private long nextFrameBlockingTasks; @@ -575,7 +576,10 @@ private boolean processChunkBuildResults(ArrayList results) { TranslucentData oldData = result.render.getTranslucentData(); if (result instanceof ChunkBuildOutput chunkBuildOutput) { touchedSectionInfo |= this.updateSectionInfo(result.render, chunkBuildOutput.info); - result.render.setLastMeshingTaskEffort(chunkBuildOutput.getEffort()); + + var resultSize = chunkBuildOutput.getResultSize(); + result.render.setLastMeshResultSize(resultSize); + this.meshTaskSizeEstimator.addBatchEntry(MeshResultSize.forSection(result.render, resultSize)); if (chunkBuildOutput.translucentData != null) { this.sortTriggering.integrateTranslucentData(oldData, chunkBuildOutput.translucentData, this.cameraPosition, this::scheduleSort); @@ -600,6 +604,8 @@ private boolean processChunkBuildResults(ArrayList results) { result.render.setLastUploadFrame(result.submitTime); } + this.meshTaskSizeEstimator.flushNewData(); + return touchedSectionInfo; } @@ -642,11 +648,11 @@ private ArrayList collectChunkBuildResults() { results.add(result.unwrap()); var jobEffort = result.getJobEffort(); if (jobEffort != null) { - this.jobEffortEstimator.addJobEffort(jobEffort); + this.jobDurationEstimator.addBatchEntry(jobEffort); } } - this.jobEffortEstimator.flushNewData(); + this.jobDurationEstimator.flushNewData(); return results; } @@ -670,21 +676,22 @@ public void updateChunks(boolean updateImmediately) { if (updateImmediately) { // for a perfect frame where everything is finished use the last frame's blocking collector // and add all tasks to it so that they're waited on - this.submitSectionTasks(thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector); + this.submitSectionTasks(Long.MAX_VALUE, thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector); this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); thisFrameBlockingCollector.awaitCompletion(this.builder); } else { var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); var remainingDuration = this.builder.getTotalRemainingDuration(this.averageFrameDuration); + var remainingUploadSize = this.regions.getStagingBuffer().getUploadSizeLimit(this.averageFrameDuration); var deferredCollector = new ChunkJobCollector(remainingDuration, this.buildResults::add); // if zero frame delay is allowed, submit important sorts with the current frame blocking collector. // otherwise submit with the collector that the next frame is blocking on. if (SodiumClientMod.options().performance.getSortBehavior().getDeferMode() == DeferMode.ZERO_FRAMES) { - this.submitSectionTasks(thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); + this.submitSectionTasks(remainingUploadSize, thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); } else { - this.submitSectionTasks(nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); + this.submitSectionTasks(remainingUploadSize, nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); } this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); @@ -701,6 +708,7 @@ public void updateChunks(boolean updateImmediately) { } private void submitSectionTasks( + long remainingUploadSize, ChunkJobCollector importantCollector, ChunkJobCollector semiImportantCollector, ChunkJobCollector deferredCollector) { @@ -711,11 +719,15 @@ private void submitSectionTasks( case ALWAYS -> deferredCollector; }; - submitSectionTasks(collector, deferMode); + // don't limit on size for zero frame defer (needs to be done, no matter the limit) + remainingUploadSize = submitSectionTasks(remainingUploadSize, deferMode != DeferMode.ZERO_FRAMES, collector, deferMode); + if (remainingUploadSize <= 0) { + break; + } } } - private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode) { + private long submitSectionTasks(long remainingUploadSize, boolean limitOnSize, ChunkJobCollector collector, DeferMode deferMode) { LongHeapPriorityQueue frustumQueue = null; LongHeapPriorityQueue globalQueue = null; float frustumPriorityBias = 0; @@ -743,7 +755,8 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode long frustumItem = 0; long globalItem = 0; - while ((!frustumQueue.isEmpty() || !globalQueue.isEmpty()) && collector.hasBudgetRemaining()) { + while ((!frustumQueue.isEmpty() || !globalQueue.isEmpty()) && + collector.hasBudgetRemaining() && (!limitOnSize || remainingUploadSize > 0)) { // get the first item from the non-empty queues and see which one has higher priority. // if the priority is not infinity, then the item priority was fetched the last iteration and doesn't need updating. if (!frustumQueue.isEmpty() && Float.isInfinite(frustumPriority)) { @@ -780,10 +793,9 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode continue; } - int frame = this.frame; ChunkBuilderTask task; if (type == ChunkUpdateType.SORT || type == ChunkUpdateType.IMPORTANT_SORT) { - task = this.createSortTask(section, frame); + task = this.createSortTask(section, this.frame); if (task == null) { // when a sort task is null it means the render section has no dynamic data and @@ -791,7 +803,7 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode continue; } } else { - task = this.createRebuildTask(section, frame); + task = this.createRebuildTask(section, this.frame); if (task == null) { // if the section is empty or doesn't exist submit this null-task to set the @@ -804,7 +816,7 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode // rebuild that must have happened in the meantime includes new non-dynamic // index data. var result = ChunkJobResult.successfully(new ChunkBuildOutput( - section, frame, NoData.forEmptySection(section.getPosition()), + section, this.frame, NoData.forEmptySection(section.getPosition()), BuiltSectionInfo.EMPTY, Collections.emptyMap())); this.buildResults.add(result); @@ -815,13 +827,16 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode if (task != null) { var job = this.builder.scheduleTask(task, type.isImportant(), collector::onJobFinished); collector.addSubmittedJob(job); + remainingUploadSize -= job.getEstimatedSize(); section.setTaskCancellationToken(job); } - section.setLastSubmittedFrame(frame); + section.setLastSubmittedFrame(this.frame); section.clearPendingUpdate(); } + + return remainingUploadSize; } public @Nullable ChunkBuilderMeshingTask createRebuildTask(RenderSection render, int frame) { @@ -832,14 +847,14 @@ private void submitSectionTasks(ChunkJobCollector collector, DeferMode deferMode } var task = new ChunkBuilderMeshingTask(render, frame, this.cameraPosition, context); - task.estimateDurationWith(this.jobEffortEstimator); + task.calculateEstimations(this.jobDurationEstimator, this.meshTaskSizeEstimator); return task; } public ChunkBuilderSortingTask createSortTask(RenderSection render, int frame) { var task = ChunkBuilderSortingTask.createTask(render, frame, this.cameraPosition); if (task != null) { - task.estimateDurationWith(this.jobEffortEstimator); + task.calculateEstimations(this.jobDurationEstimator, this.meshTaskSizeEstimator); } return task; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java index 102f93727a..89fcc25d5f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/BuilderTaskOutput.java @@ -1,10 +1,12 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; public abstract class BuilderTaskOutput { public final RenderSection render; public final int submitTime; + private long resultSize = MeshResultSize.NO_DATA; public BuilderTaskOutput(RenderSection render, int buildTime) { this.render = render; @@ -13,4 +15,13 @@ public BuilderTaskOutput(RenderSection render, int buildTime) { public void destroy() { } + + protected abstract long calculateResultSize(); + + public long getResultSize() { + if (this.resultSize == MeshResultSize.NO_DATA) { + this.resultSize = this.calculateResultSize(); + } + return this.resultSize; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java index 442d3669e1..20f075681d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkBuildOutput.java @@ -32,14 +32,6 @@ public BuiltSectionMeshParts getMesh(TerrainRenderPass pass) { return this.meshes.get(pass); } - public long getEffort() { - long size = 0; - for (var data : this.meshes.values()) { - size += data.getVertexData().getLength(); - } - return 1 + (size >> 8); // make sure the number isn't huge - } - @Override public void destroy() { super.destroy(); @@ -48,4 +40,17 @@ public void destroy() { data.getVertexData().free(); } } + + private long getMeshSize() { + long size = 0; + for (var data : this.meshes.values()) { + size += data.getVertexData().getLength(); + } + return size; + } + + @Override + public long calculateResultSize() { + return super.calculateResultSize() + this.getMeshSize(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java index 52236a161e..e782dafb8e 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/ChunkSortOutput.java @@ -54,4 +54,9 @@ public void destroy() { this.indexBuffer.free(); } } + + @Override + protected long calculateResultSize() { + return this.indexBuffer == null ? 0 : this.indexBuffer.getLength(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java new file mode 100644 index 0000000000..d207f95f62 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java @@ -0,0 +1,81 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import it.unimi.dsi.fastutil.objects.Reference2FloatMap; +import it.unimi.dsi.fastutil.objects.Reference2FloatOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; + +public class CategoryFactorEstimator { + private final Reference2FloatMap aPerB = new Reference2FloatOpenHashMap<>(); + private final Reference2ReferenceMap newData = new Reference2ReferenceOpenHashMap<>(); + private final float newDataFactor; + private final long initialAEstimate; + + public CategoryFactorEstimator(float newDataFactor, long initialAEstimate) { + this.newDataFactor = newDataFactor; + this.initialAEstimate = initialAEstimate; + } + + private static class BatchDataAggregation { + private long aSum; + private long bSum; + + public void addDataPoint(long a, long b) { + this.aSum += a; + this.bSum += b; + } + + public void reset() { + this.aSum = 0; + this.bSum = 0; + } + + public float getAPerBFactor() { + return (float) this.aSum / this.bSum; + } + } + + public interface BatchEntry { + C getCategory(); + + long getA(); + + long getB(); + } + + public void addBatchEntry(BatchEntry batchEntry) { + var category = batchEntry.getCategory(); + if (this.newData.containsKey(category)) { + this.newData.get(category).addDataPoint(batchEntry.getA(), batchEntry.getB()); + } else { + var batchData = new BatchDataAggregation(); + batchData.addDataPoint(batchEntry.getA(), batchEntry.getB()); + this.newData.put(category, batchData); + } + } + + public void flushNewData() { + this.newData.forEach((category, frameData) -> { + var newFactor = frameData.getAPerBFactor(); + if (Float.isNaN(newFactor)) { + return; + } + if (this.aPerB.containsKey(category)) { + var oldFactor = this.aPerB.getFloat(category); + var newValue = oldFactor * (1 - this.newDataFactor) + newFactor * this.newDataFactor; + this.aPerB.put(category, newValue); + } else { + this.aPerB.put(category, newFactor); + } + frameData.reset(); + }); + } + + public long estimateAWithB(C category, long b) { + if (this.aPerB.containsKey(category)) { + return (long) (this.aPerB.getFloat(category) * b); + } else { + return this.initialAEstimate; + } + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java new file mode 100644 index 0000000000..d2cb6c85a7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java @@ -0,0 +1,14 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public class JobDurationEstimator extends CategoryFactorEstimator> { + public static final float NEW_DATA_FACTOR = 0.01f; + private static final long INITIAL_JOB_DURATION_ESTIMATE = 5_000_000L; + + public JobDurationEstimator() { + super(NEW_DATA_FACTOR, INITIAL_JOB_DURATION_ESTIMATE); + } + + public long estimateJobDuration(Class jobType, long effort) { + return this.estimateAWithB(jobType, effort); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java new file mode 100644 index 0000000000..0802e57ccf --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java @@ -0,0 +1,22 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +public record JobEffort(Class category, long duration, long effort) implements CategoryFactorEstimator.BatchEntry> { + public static JobEffort untilNowWithEffort(Class effortType, long start, long effort) { + return new JobEffort(effortType,System.nanoTime() - start, effort); + } + + @Override + public Class getCategory() { + return this.category; + } + + @Override + public long getA() { + return this.duration; + } + + @Override + public long getB() { + return this.effort; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java new file mode 100644 index 0000000000..1552630efe --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java @@ -0,0 +1,49 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; + +public record MeshResultSize(SectionCategory category, long resultSize) implements CategoryFactorEstimator.BatchEntry { + public static long NO_DATA = -1; + + public enum SectionCategory { + LOW, + UNDERGROUND, + WATER_LEVEL, + SURFACE, + HIGH; + + public static SectionCategory forSection(RenderSection section) { + var sectionY = section.getChunkY(); + if (sectionY < 0) { + return LOW; + } else if (sectionY < 3) { + return UNDERGROUND; + } else if (sectionY == 3) { + return WATER_LEVEL; + } else if (sectionY < 7) { + return SURFACE; + } else { + return HIGH; + } + } + } + + public static MeshResultSize forSection(RenderSection section, long resultSize) { + return new MeshResultSize(SectionCategory.forSection(section), resultSize); + } + + @Override + public SectionCategory getCategory() { + return this.category; + } + + @Override + public long getA() { + return this.resultSize; + } + + @Override + public long getB() { + return 1; + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java new file mode 100644 index 0000000000..21f10787ca --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java @@ -0,0 +1,25 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; + +public class MeshTaskSizeEstimator extends CategoryFactorEstimator { + public static final float NEW_DATA_FACTOR = 0.02f; + + public MeshTaskSizeEstimator() { + super(NEW_DATA_FACTOR, RenderRegion.SECTION_BUFFER_ESTIMATE); + } + + public long estimateSize(RenderSection section) { + var lastResultSize = section.getLastMeshResultSize(); + if (lastResultSize != MeshResultSize.NO_DATA) { + return lastResultSize; + } + return this.estimateAWithB(MeshResultSize.SectionCategory.forSection(section), 1); + } + + @Override + public void flushNewData() { + super.flushNewData(); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java index 5a230efda5..4b894728c9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkBuilder.java @@ -20,17 +20,6 @@ import java.util.function.Consumer; public class ChunkBuilder { - /** - * The low and high efforts given to the sorting and meshing tasks, - * respectively. This split into two separate effort categories means more - * sorting tasks, which are faster, can be scheduled compared to mesh tasks. - * These values need to capture that there's a limit to how much data can be - * uploaded per frame. Since sort tasks generate index data, which is smaller - * per quad and (on average) per section, more of their results can be uploaded - * in one frame. This number should essentially be a conservative estimate of - * min((mesh task upload size) / (sort task upload size), (mesh task time) / - * (sort task time)). - */ static final Logger LOGGER = LogManager.getLogger("ChunkBuilder"); private final ChunkJobQueue queue = new ChunkJobQueue(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java index 458ed3a369..65dd30a5fb 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJob.java @@ -8,5 +8,7 @@ public interface ChunkJob extends CancellationToken { boolean isStarted(); + long getEstimatedSize(); + long getEstimatedDuration(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java index 07d9659d12..2a1b98ac54 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobResult.java @@ -1,5 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobEffort; import net.minecraft.ReportedException; public class ChunkJobResult { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java index 607d5bc2c8..97f877fba4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobTyped.java @@ -2,6 +2,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.BuilderTaskOutput; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobEffort; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; import java.util.function.Consumer; @@ -50,7 +51,7 @@ public void execute(ChunkBuildContext context) { return; } - result = ChunkJobResult.successfully(output, JobEffort.untilNowWithEffort(this.task.getClass(), start, this.task.getEffort())); + result = ChunkJobResult.successfully(output, JobEffort.untilNowWithEffort(this.task.getClass(), start, output.getResultSize())); } catch (Throwable throwable) { result = ChunkJobResult.exceptionally(throwable); ChunkBuilder.LOGGER.error("Chunk build failed", throwable); @@ -68,6 +69,11 @@ public boolean isStarted() { return this.started; } + @Override + public long getEstimatedSize() { + return this.task.getEstimatedSize(); + } + @Override public long getEstimatedDuration() { return this.task.getEstimatedDuration(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java deleted file mode 100644 index 978c8c9cf9..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffort.java +++ /dev/null @@ -1,7 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; - -public record JobEffort(Class category, long duration, long effort) { - public static JobEffort untilNowWithEffort(Class effortType, long start, long effort) { - return new JobEffort(effortType,System.nanoTime() - start, effort); - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java deleted file mode 100644 index 3151c49ec2..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/JobEffortEstimator.java +++ /dev/null @@ -1,67 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.compile.executor; - -import it.unimi.dsi.fastutil.objects.*; - -// TODO: deal with maximum number of uploads per frame -// TODO: implement per-thread pending upload size limit with a simple semaphore? also see discussion about more complicated allocation scheme with small and large threads: https://discord.com/channels/602796788608401408/651120262129123330/1294158402859171870 -public class JobEffortEstimator { - public static final float NEW_DATA_FACTOR = 0.01f; - - Reference2FloatMap> durationPerEffort = new Reference2FloatArrayMap<>(); - Reference2ReferenceMap, FrameDataAggregation> newData = new Reference2ReferenceArrayMap<>(); - - private static class FrameDataAggregation { - private long durationSum; - private long effortSum; - - public void addDataPoint(long duration, long effort) { - this.durationSum += duration; - this.effortSum += effort; - } - - public void reset() { - this.durationSum = 0; - this.effortSum = 0; - } - - public float getEffortFactor() { - return (float) this.durationSum / this.effortSum; - } - } - - public void addJobEffort(JobEffort jobEffort) { - var category = jobEffort.category(); - if (this.newData.containsKey(category)) { - this.newData.get(category).addDataPoint(jobEffort.duration(), jobEffort.effort()); - } else { - var frameData = new FrameDataAggregation(); - frameData.addDataPoint(jobEffort.duration(), jobEffort.effort()); - this.newData.put(category, frameData); - } - } - - public void flushNewData() { - this.newData.forEach((category, frameData) -> { - var newFactor = frameData.getEffortFactor(); - if (Float.isNaN(newFactor)) { - return; - } - if (this.durationPerEffort.containsKey(category)) { - var oldFactor = this.durationPerEffort.getFloat(category); - var newValue = oldFactor * (1 - NEW_DATA_FACTOR) + newFactor * NEW_DATA_FACTOR; - this.durationPerEffort.put(category, newValue); - } else { - this.durationPerEffort.put(category, newFactor); - } - frameData.reset(); - }); - } - - public long estimateJobDuration(Class category, long effort) { - if (this.durationPerEffort.containsKey(category)) { - return (long) (this.durationPerEffort.getFloat(category) * effort); - } else { - return 10_000_000L; // 10ms as initial guess - } - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java index 8f6ae90436..24b50360d9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderMeshingTask.java @@ -7,6 +7,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildBuffers; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildContext; import net.caffeinemc.mods.sodium.client.render.chunk.compile.ChunkBuildOutput; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderCache; import net.caffeinemc.mods.sodium.client.render.chunk.compile.pipeline.BlockRenderer; import net.caffeinemc.mods.sodium.client.render.chunk.data.BuiltSectionInfo; @@ -227,7 +228,7 @@ private ReportedException fillCrashInfo(CrashReport report, LevelSlice slice, Bl } @Override - public long getEffort() { - return this.render.getLastMeshingTaskEffort(); + public long estimateTaskSizeWith(MeshTaskSizeEstimator estimator) { + return estimator.estimateSize(this.render); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java index 82380ed692..f0fc6ca110 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderSortingTask.java @@ -1,5 +1,6 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.data.DynamicSorter; import net.minecraft.util.profiling.Profiler; import net.minecraft.util.profiling.ProfilerFiller; @@ -42,7 +43,7 @@ public static ChunkBuilderSortingTask createTask(RenderSection render, int frame } @Override - public long getEffort() { + public long estimateTaskSizeWith(MeshTaskSizeEstimator estimator) { return this.sorter.getQuadCount(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java index 622ee43ef7..d30a23a0ee 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/tasks/ChunkBuilderTask.java @@ -1,6 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.JobEffortEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobDurationEstimator; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; import org.joml.Vector3dc; import org.joml.Vector3f; import org.joml.Vector3fc; @@ -27,6 +28,7 @@ public abstract class ChunkBuilderTask impleme protected final Vector3dc absoluteCameraPos; protected final Vector3fc cameraPos; + private long estimatedSize; private long estimatedDuration; /** @@ -57,10 +59,15 @@ public ChunkBuilderTask(RenderSection render, int time, Vector3dc absoluteCamera */ public abstract OUTPUT execute(ChunkBuildContext context, CancellationToken cancellationToken); - public abstract long getEffort(); + public abstract long estimateTaskSizeWith(MeshTaskSizeEstimator estimator); - public void estimateDurationWith(JobEffortEstimator estimator) { - this.estimatedDuration = estimator.estimateJobDuration(this.getClass(), this.getEffort()); + public void calculateEstimations(JobDurationEstimator jobEstimator, MeshTaskSizeEstimator sizeEstimator) { + this.estimatedSize = this.estimateTaskSizeWith(sizeEstimator); + this.estimatedDuration = jobEstimator.estimateJobDuration(this.getClass(), this.estimatedSize); + } + + public long getEstimatedSize() { + return this.estimatedSize; } public long getEstimatedDuration() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java index 88cdddbc50..8149845e74 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java @@ -25,6 +25,10 @@ import java.util.Map; public class RenderRegion { + public static final int SECTION_VERTEX_COUNT_ESTIMATE = 756; + public static final int SECTION_INDEX_COUNT_ESTIMATE = (SECTION_VERTEX_COUNT_ESTIMATE / 4) * 6; + public static final int SECTION_BUFFER_ESTIMATE = SECTION_VERTEX_COUNT_ESTIMATE * ChunkMeshFormats.COMPACT.getVertexFormat().getStride() + SECTION_INDEX_COUNT_ESTIMATE * Integer.BYTES; + public static final int REGION_WIDTH = 8; public static final int REGION_HEIGHT = 4; public static final int REGION_LENGTH = 8; @@ -285,11 +289,8 @@ public static class DeviceResources { public DeviceResources(CommandList commandList, StagingBuffer stagingBuffer) { int stride = ChunkMeshFormats.COMPACT.getVertexFormat().getStride(); - // the magic number 756 for the initial size is arbitrary, it was made up. - var initialVertices = 756; - this.geometryArena = new GlBufferArena(commandList, REGION_SIZE * initialVertices, stride, stagingBuffer); - var initialIndices = (initialVertices / 4) * 6; - this.indexArena = new GlBufferArena(commandList, REGION_SIZE * initialIndices, Integer.BYTES, stagingBuffer); + this.geometryArena = new GlBufferArena(commandList, REGION_SIZE * SECTION_VERTEX_COUNT_ESTIMATE, stride, stagingBuffer); + this.indexArena = new GlBufferArena(commandList, REGION_SIZE * SECTION_INDEX_COUNT_ESTIMATE, Integer.BYTES, stagingBuffer); } public void updateTessellation(CommandList commandList, GlTessellation tessellation) { From 1075f4b9ca3a4ddd3f331796abcc868e025a00e6 Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 7 Dec 2024 21:30:19 +0100 Subject: [PATCH 54/81] sort render lists separately when doing sync rendering, since they're not automatically correctly ordered since they're not coming from a tree  Conflicts:  common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java --- .../render/chunk/RenderSectionManager.java | 2 +- .../lists/VisibleChunkCollectorAsync.java | 40 +------------------ .../lists/VisibleChunkCollectorSync.java | 39 +++++++++++++++++- .../chunk/occlusion/OcclusionCuller.java | 8 ++-- 4 files changed, 43 insertions(+), 46 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index ff34706b51..9724ba40ec 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -223,7 +223,7 @@ private void renderSync(Camera camera, Viewport viewport, boolean spectator) { this.trees.put(CullType.FRUSTUM, tree); this.renderTree = tree; - this.renderLists = tree.createRenderLists(); + this.renderLists = tree.createRenderLists(viewport); // remove the other trees, they're very wrong by now this.trees.remove(CullType.WIDE); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java index b98d2d02e7..d110b19317 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java @@ -16,7 +16,7 @@ /** * The async visible chunk collector is passed into a section tree to collect visible chunks. */ -public class VisibleChunkCollectorAsync implements SectionTree.VisibleSectionVisitor { +public class VisibleChunkCollectorAsync implements SectionTree.VisibleSectionVisitor { private final ObjectArrayList sortedRenderLists; private final RenderRegionManager regions; @@ -56,44 +56,6 @@ public void visit(int x, int y, int z) { renderList.add(sectionIndex); } - private static int[] sortItems = new int[RenderRegion.REGION_SIZE]; - - public SortedRenderLists createSortedRenderLists(Viewport viewport) { - // sort the regions by distance to fix rare region ordering bugs - var sectionPos = viewport.getChunkCoord(); - var cameraX = sectionPos.getX() >> RenderRegion.REGION_WIDTH_SH; - var cameraY = sectionPos.getY() >> RenderRegion.REGION_HEIGHT_SH; - var cameraZ = sectionPos.getZ() >> 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(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(sectionPos, sortItems); - } - - return new SortedRenderLists(sorted); - } - public SortedRenderLists createRenderLists() { return new SortedRenderLists(this.sortedRenderLists); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java index 7c16017431..6c1edffcc1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java @@ -1,5 +1,6 @@ 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.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; @@ -41,7 +42,41 @@ public void visit(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 sectionPos = viewport.getChunkCoord(); + var cameraX = sectionPos.getX() >> RenderRegion.REGION_WIDTH_SH; + var cameraY = sectionPos.getY() >> RenderRegion.REGION_HEIGHT_SH; + var cameraZ = sectionPos.getZ() >> 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(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(sectionPos, sortItems); + } + + return new SortedRenderLists(sorted); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 9f36a4edd8..4d5c0e3e03 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -96,7 +96,7 @@ public void findVisible(GraphOcclusionVisitor visitor, processQueue(this.queue.read(), this.queue.write()); } - this.addNearbySections(visitor, viewport, searchDistance, frame); + this.addNearbySections(visitor, viewport); this.visitor = null; this.viewport = null; @@ -266,7 +266,7 @@ public static boolean isWithinNearbySectionFrustum(Viewport viewport, RenderSect // for all neighboring, even diagonally neighboring, sections around the origin to render them // if their extended bounding box is visible, and they may render large models that extend // outside the 16x16x16 base volume of the section. - private void addNearbySections(Visitor visitor, Viewport viewport, float searchDistance, int frame) { + private void addNearbySections(GraphOcclusionVisitor visitor, Viewport viewport) { var origin = viewport.getChunkCoord(); var originX = origin.getX(); var originY = origin.getY(); @@ -282,9 +282,9 @@ private void addNearbySections(Visitor visitor, Viewport viewport, float searchD var section = this.getRenderSection(originX + dx, originY + dy, originZ + dz); // additionally render not yet visited but visible sections - if (section != null && section.getLastVisibleFrame() != frame && isWithinNearbySectionFrustum(viewport, section)) { + if (section != null && section.getLastVisibleSearchToken() != this.token && isWithinNearbySectionFrustum(viewport, section)) { // reset state on first visit, but don't enqueue - section.setLastVisibleFrame(frame); + section.setLastVisibleSearchToken(this.token); visitor.visit(section); } From d47b9d16f4018483cc97f6609077273cc690f92e Mon Sep 17 00:00:00 2001 From: douira Date: Wed, 27 Nov 2024 21:40:33 +0100 Subject: [PATCH 55/81] remove completed todos --- .../mods/sodium/client/render/chunk/RenderSectionManager.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 9724ba40ec..299890c7e1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -144,10 +144,6 @@ public void updateCameraState(Vector3dc cameraPosition, Camera camera) { this.cameraPosition = cameraPosition; } - // TODO: schedule only as many tasks as fit within the frame time (measure how long the last frame took and how long the tasks took, though both of these will change over time) - - // TODO idea: increase and decrease chunk builder thread budget based on if the upload buffer was filled if the entire budget was used up. if the fallback way of uploading buffers is used, just doing 3 * the budget actually slows down frames while things are getting uploaded. For this it should limit how much (or how often?) things are uploaded. In the case of the mapped upload, just making sure we don't exceed its size is probably enough. - public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator, boolean updateImmediately) { var now = System.nanoTime(); this.lastFrameDuration = now - this.lastFrameAtTime; From 339eb59cec9bc3d7dedaffbf26b1044f5a918239 Mon Sep 17 00:00:00 2001 From: douira Date: Mon, 2 Dec 2024 04:09:20 +0100 Subject: [PATCH 56/81] fix some issues with very delayed section sorting. There are still other issues with (at least vertical) sorting though and even important zero-frame blocking tasks are not scheduled within the same frame --- .../render/chunk/RenderSectionManager.java | 35 ++++++++++++++----- .../render/chunk/lists/TaskSectionTree.java | 19 ++++++---- .../render/chunk/tree/BaseBiForest.java | 14 ++++---- 3 files changed, 46 insertions(+), 22 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 299890c7e1..8dc56707c3 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -110,11 +110,7 @@ public class RenderSectionManager { private @Nullable BlockPos cameraBlockPos; private @Nullable Vector3dc cameraPosition; - private final ExecutorService asyncCullExecutor = Executors.newSingleThreadExecutor(runnable -> { - Thread thread = new Thread(runnable); - thread.setName("Sodium Async Cull Thread"); - return thread; - }); + private final ExecutorService asyncCullExecutor = Executors.newSingleThreadExecutor(RenderSectionManager::makeAsyncCullThread); private final ObjectArrayList> pendingTasks = new ObjectArrayList<>(); private SectionTree renderTree = null; private TaskSectionTree globalTaskTree = null; @@ -277,6 +273,12 @@ private SectionTree unpackTaskResults(boolean wait) { return latestTree; } + private static Thread makeAsyncCullThread(Runnable runnable) { + Thread thread = new Thread(runnable); + thread.setName("Sodium Async Cull Thread"); + return thread; + } + private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectator) { // submit tasks of types that are applicable and not yet running AsyncRenderTask currentRunningTask = null; @@ -902,6 +904,24 @@ public int getVisibleChunkCount() { return sections; } + // TODO: this fixes very delayed tasks, but it still regresses on same-frame tasks that don't get to run in time because the frustum task collection task takes at least one (and usually only one) frame to run + // maybe intercept tasks that are scheduled in zero- or one-frame defer mode? + // collect and prioritize regardless of visibility if it's an important defer mode? + // TODO: vertical sorting seems to be broken? + private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateType type) { + var current = section.getPendingUpdate(); + type = ChunkUpdateType.getPromotionUpdateType(current, type); + + section.setPendingUpdate(type, this.lastFrameAtTime); + + // if the section received a new task, mark in the task tree so an update can happen before a global cull task runs + if (this.globalTaskTree != null && type != null && current == null) { + this.globalTaskTree.markSectionTask(section); + } + + return type; + } + public void scheduleSort(long sectionPos, boolean isDirectTrigger) { RenderSection section = this.sectionByPosition.get(sectionPos); @@ -912,9 +932,8 @@ public void scheduleSort(long sectionPos, boolean isDirectTrigger) { || priorityMode == PriorityMode.NEARBY && this.shouldPrioritizeTask(section, NEARBY_SORT_DISTANCE)) { pendingUpdate = ChunkUpdateType.IMPORTANT_SORT; } - pendingUpdate = ChunkUpdateType.getPromotionUpdateType(section.getPendingUpdate(), pendingUpdate); - if (pendingUpdate != null) { - section.setPendingUpdate(pendingUpdate, this.lastFrameAtTime); + + if (this.upgradePendingUpdate(section, pendingUpdate) != null) { section.prepareTrigger(isDirectTrigger); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java index 6ab711b692..708872b383 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -10,6 +10,7 @@ public class TaskSectionTree extends RayOcclusionSectionTree { private final TraversableForest taskTree; + private boolean taskTreeFinalized = false; public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { super(viewport, buildDistance, frame, cullType, level); @@ -17,20 +18,24 @@ public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullTy this.taskTree = TraversableForest.createTraversableForest(this.baseOffsetX, this.baseOffsetY, this.baseOffsetZ, buildDistance, level); } - @Override - protected void addPendingSection(RenderSection section, ChunkUpdateType type) { - super.addPendingSection(section, type); - + public void markSectionTask(RenderSection section) { this.taskTree.add(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + this.taskTreeFinalized = false; } @Override - public void finalizeTrees() { - super.finalizeTrees(); - this.taskTree.calculateReduced(); + protected void addPendingSection(RenderSection section, ChunkUpdateType type) { + super.addPendingSection(section, type); + + this.markSectionTask(section); } public void traverseVisiblePendingTasks(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + if (!this.taskTreeFinalized) { + this.taskTree.calculateReduced(); + this.taskTreeFinalized = true; + } + this.taskTree.traverse(visitor, viewport, distanceLimit); } } \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java index 87709af08c..ddbfe1ae05 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseBiForest.java @@ -24,14 +24,14 @@ protected T makeSecondaryTree() { @Override public void add(int x, int y, int z) { - if (!this.mainTree.add(x, y, z)) { - if (this.secondaryTree == null) { - this.secondaryTree = this.makeSecondaryTree(); - } - if (!this.secondaryTree.add(x, y, z)) { - throw new IllegalStateException("Failed to add section to trees"); - } + if (this.mainTree.add(x, y, z)) { + return; } + + if (this.secondaryTree == null) { + this.secondaryTree = this.makeSecondaryTree(); + } + this.secondaryTree.add(x, y, z); } @Override From 36ecf8e795b00aea2e08fa3b8cbc739d207f7dcc Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 7 Dec 2024 23:30:50 +0100 Subject: [PATCH 57/81] fix sorting by using correct section pos calculation and improve frustum task list update --- .../render/chunk/RenderSectionManager.java | 20 ++++++++++--------- .../chunk/lists/PendingTaskCollector.java | 16 +++++++-------- .../render/chunk/occlusion/SectionTree.java | 11 ++++------ .../tree/AbstractTraversableMultiForest.java | 8 ++++---- .../render/chunk/tree/TraversableTree.java | 9 ++++----- 5 files changed, 31 insertions(+), 33 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 8dc56707c3..671c094829 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -106,6 +106,7 @@ public class RenderSectionManager { private boolean needsGraphUpdate = true; private boolean needsRenderListUpdate = true; private boolean cameraChanged = false; + private boolean needsFrustumTaskListUpdate = true; private @Nullable BlockPos cameraBlockPos; private @Nullable Vector3dc cameraPosition; @@ -154,6 +155,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato this.frame += 1; this.needsRenderListUpdate |= this.cameraChanged; + this.needsFrustumTaskListUpdate |= this.needsRenderListUpdate; // do sync bfs based on update immediately (flawless frames) or if the camera moved too much var shouldRenderSync = this.cameraTimingControl.getShouldRenderSync(camera); @@ -186,11 +188,15 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato this.scheduleAsyncWork(camera, viewport, spectator); + if (this.needsFrustumTaskListUpdate) { + this.updateFrustumTaskList(viewport); + } if (this.needsRenderListUpdate) { processRenderListUpdate(viewport); } this.needsRenderListUpdate = false; + this.needsFrustumTaskListUpdate = false; this.needsGraphUpdate = false; this.cameraChanged = false; } @@ -341,7 +347,7 @@ private CullType[] getScheduleOrder() { private static final LongArrayList timings = new LongArrayList(); - private void processRenderListUpdate(Viewport viewport) { + private void updateFrustumTaskList(Viewport viewport) { // schedule generating a frustum task list if there's no frustum tree task running if (this.globalTaskTree != null) { var frustumTaskListPending = false; @@ -359,7 +365,9 @@ private void processRenderListUpdate(Viewport viewport) { this.pendingTasks.add(task); } } + } + 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 SectionTree bestTree = null; boolean bestTreeValid = false; @@ -907,7 +915,6 @@ public int getVisibleChunkCount() { // TODO: this fixes very delayed tasks, but it still regresses on same-frame tasks that don't get to run in time because the frustum task collection task takes at least one (and usually only one) frame to run // maybe intercept tasks that are scheduled in zero- or one-frame defer mode? // collect and prioritize regardless of visibility if it's an important defer mode? - // TODO: vertical sorting seems to be broken? private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateType type) { var current = section.getPendingUpdate(); type = ChunkUpdateType.getPromotionUpdateType(current, type); @@ -917,6 +924,7 @@ private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateT // if the section received a new task, mark in the task tree so an update can happen before a global cull task runs if (this.globalTaskTree != null && type != null && current == null) { this.globalTaskTree.markSectionTask(section); + this.needsFrustumTaskListUpdate = true; } return type; @@ -955,13 +963,7 @@ public void scheduleRebuild(int x, int y, int z, boolean important) { pendingUpdate = ChunkUpdateType.REBUILD; } - pendingUpdate = ChunkUpdateType.getPromotionUpdateType(section.getPendingUpdate(), pendingUpdate); - if (pendingUpdate != null) { - section.setPendingUpdate(pendingUpdate, this.lastFrameAtTime); - - // force update to schedule rebuild task on this section - this.markGraphDirty(); - } + this.upgradePendingUpdate(section, pendingUpdate); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index 7aba4099a8..ba3ae036f7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -41,12 +41,11 @@ public PendingTaskCollector(Viewport viewport, float buildDistance, boolean frus this.isFrustumTested = frustumTested; var offsetDistance = Mth.ceil(buildDistance / 16.0f) + 1; - var transform = viewport.getTransform(); - // the offset applied to section coordinates to encode their position in the octree - var cameraSectionX = transform.intX >> 4; - var cameraSectionY = transform.intY >> 4; - var cameraSectionZ = transform.intZ >> 4; + var sectionPos = viewport.getChunkCoord(); + var cameraSectionX = sectionPos.getX(); + var cameraSectionY = sectionPos.getY(); + var cameraSectionZ = sectionPos.getZ(); this.baseOffsetX = cameraSectionX - offsetDistance; this.baseOffsetY = cameraSectionY - offsetDistance; this.baseOffsetZ = cameraSectionZ - offsetDistance; @@ -54,9 +53,10 @@ public PendingTaskCollector(Viewport viewport, float buildDistance, boolean frus this.invMaxDistance = PROXIMITY_FACTOR / buildDistance; if (frustumTested) { - this.cameraX = transform.intX; - this.cameraY = transform.intY; - this.cameraZ = transform.intZ; + var blockPos = viewport.getBlockCoord(); + this.cameraX = blockPos.getX(); + this.cameraY = blockPos.getY(); + this.cameraZ = blockPos.getZ(); } else { this.cameraX = (cameraSectionX << 4); this.cameraY = (cameraSectionY << 4); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 4132111d85..3721a298ed 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -42,13 +42,10 @@ public int getFrame() { } public boolean isValidFor(Viewport viewport, float searchDistance) { - var transform = viewport.getTransform(); - var cameraSectionX = transform.intX >> 4; - var cameraSectionY = transform.intY >> 4; - var cameraSectionZ = transform.intZ >> 4; - return this.cameraX >> 4 == cameraSectionX && - this.cameraY >> 4 == cameraSectionY && - this.cameraZ >> 4 == cameraSectionZ && + var cameraPos = viewport.getChunkCoord(); + return this.cameraX >> 4 == cameraPos.getX() && + this.cameraY >> 4 == cameraPos.getY() && + this.cameraZ >> 4 == cameraPos.getZ() && this.buildDistance >= searchDistance; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java index eb758d8e36..b8673fc5a7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java @@ -20,10 +20,10 @@ public void calculateReduced() { @Override public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { - var transform = viewport.getTransform(); - var cameraSectionX = transform.intX >> 4; - var cameraSectionY = transform.intY >> 4; - var cameraSectionZ = transform.intZ >> 4; + var cameraPos = viewport.getChunkCoord(); + var cameraSectionX = cameraPos.getX(); + var cameraSectionY = cameraPos.getY(); + var cameraSectionZ = cameraPos.getZ(); // sort the trees by distance from the camera by sorting a packed index array. var items = new int[this.trees.length]; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java index a1dda59668..26fb9b227d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java @@ -61,13 +61,12 @@ public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewpor this.viewport = viewport; this.distanceLimit = distanceLimit; - var transform = viewport.getTransform(); - // + 1 to offset section position to compensate for shifted global offset // adjust camera block position to account for fractional part of camera position - this.cameraOffsetX = ((transform.intX + (int) Math.signum(transform.fracX)) >> 4) - this.offsetX + 1; - this.cameraOffsetY = ((transform.intY + (int) Math.signum(transform.fracY)) >> 4) - this.offsetY + 1; - this.cameraOffsetZ = ((transform.intZ + (int) Math.signum(transform.fracZ)) >> 4) - this.offsetZ + 1; + var sectionPos = viewport.getChunkCoord(); + this.cameraOffsetX = sectionPos.getX() - this.offsetX + 1; + this.cameraOffsetY = sectionPos.getY() - this.offsetY + 1; + this.cameraOffsetZ = sectionPos.getZ() - this.offsetZ + 1; // everything is already inside the distance limit if the build distance is smaller var initialInside = this.distanceLimit >= buildDistance ? INSIDE_DISTANCE : 0; From 38b3affe6bcd75a70fba4080a2eb7ddd014ec2e5 Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 21 Dec 2024 03:19:05 +0100 Subject: [PATCH 58/81] add documentation and rename some methods --- .../render/chunk/RenderSectionManager.java | 17 +++++++++-------- .../render/chunk/async/FrustumCullTask.java | 5 ++++- .../render/chunk/async/GlobalCullTask.java | 6 +++++- .../render/chunk/lists/TaskSectionTree.java | 4 ++-- .../chunk/lists/VisibleChunkCollectorAsync.java | 5 ++--- .../occlusion/RayOcclusionSectionTree.java | 2 +- .../render/chunk/occlusion/SectionTree.java | 6 +++--- .../chunk/tree/AbstractTraversableBiForest.java | 6 +++--- .../tree/AbstractTraversableMultiForest.java | 4 ++-- .../sodium/client/render/chunk/tree/Forest.java | 6 ++++++ .../render/chunk/tree/TraversableForest.java | 2 +- .../render/chunk/tree/TraversableTree.java | 2 +- 12 files changed, 39 insertions(+), 26 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 671c094829..4ee29120d2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -214,7 +214,7 @@ private void renderSync(Camera camera, Viewport viewport, boolean spectator) { var tree = new VisibleChunkCollectorSync(viewport, searchDistance, this.frame, CullType.FRUSTUM, this.level); this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, CancellationToken.NEVER_CANCELLED); - tree.finalizeTrees(); + tree.prepareForTraversal(); this.frustumTaskLists = tree.getPendingTaskLists(); this.globalTaskLists = null; @@ -299,15 +299,16 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat var tree = this.trees.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 in the next frame + // since they're going to be invalid by the time they're completed in the next frame if (type == CullType.FRUSTUM && this.cameraChanged) { continue; } + // schedule a task of this type if there's no valid and current result for it yet var searchDistance = this.getSearchDistanceForCullType(type); - if ((tree == null || tree.getFrame() < this.lastGraphDirtyFrame || - !tree.isValidFor(viewport, searchDistance)) && ( - currentRunningTask == null || + if ((tree == null || tree.getFrame() < this.lastGraphDirtyFrame || !tree.isValidFor(viewport, searchDistance)) && + // and if there's no currently running task that will produce a valid and current result + (currentRunningTask == null || currentRunningTask instanceof CullTask cullTask && cullTask.getCullType() != type || currentRunningTask.getFrame() < this.lastGraphDirtyFrame)) { var useOcclusionCulling = this.shouldUseOcclusionCulling(camera, spectator); @@ -331,7 +332,7 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat private static final CullType[] COMPROMISE = { CullType.REGULAR, CullType.FRUSTUM, CullType.WIDE }; private CullType[] getScheduleOrder() { - // if the camera is stationary, do the FRUSTUM update to first to prevent the render count from oscillating + // if the camera is stationary, do the FRUSTUM update first to prevent the rendered section count from oscillating if (!this.cameraChanged) { return NARROW_TO_WIDE; } @@ -386,7 +387,7 @@ private void processRenderListUpdate(Viewport viewport) { } } - // wait if there's no current tree (first frames after initial load/reload) + // wait for pending tasks if there's no current tree (first frames after initial load/reload) if (bestTree == null) { bestTree = this.unpackTaskResults(true); @@ -397,7 +398,7 @@ private void processRenderListUpdate(Viewport viewport) { var start = System.nanoTime(); var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame); - bestTree.traverseVisible(visibleCollector, viewport, this.getSearchDistance()); + bestTree.traverse(visibleCollector, viewport, this.getSearchDistance()); this.renderLists = visibleCollector.createRenderLists(); var end = System.nanoTime(); var time = end - start; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java index 39f9c1659a..551973f1da 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -22,9 +22,12 @@ public FrustumCullTask(Viewport viewport, float buildDistance, int frame, Occlus @Override public FrustumCullResult runTask() { var tree = new RayOcclusionSectionTree(this.viewport, this.buildDistance, this.frame, CullType.FRUSTUM, this.level); + var start = System.nanoTime(); + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this); - tree.finalizeTrees(); + tree.prepareForTraversal(); + var end = System.nanoTime(); var time = end - start; timings.add(time); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index b28add6598..e464110c81 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -28,9 +28,12 @@ public GlobalCullTask(Viewport viewport, float buildDistance, int frame, Occlusi @Override public GlobalCullResult runTask() { var tree = new TaskSectionTree(this.viewport, this.buildDistance, this.frame, this.cullType, this.level); + var start = System.nanoTime(); + this.occlusionCuller.findVisible(tree, this.viewport, this.buildDistance, this.useOcclusionCulling, this); - tree.finalizeTrees(); + tree.prepareForTraversal(); + var end = System.nanoTime(); var time = end - start; timings.add(time); @@ -39,6 +42,7 @@ public GlobalCullResult runTask() { System.out.println("Global culling took " + (average) / 1000 + "µs over " + timings.size() + " samples"); timings.clear(); } + var collector = new FrustumTaskCollector(this.viewport, this.buildDistance, this.sectionByPosition); tree.traverseVisiblePendingTasks(collector, this.viewport, this.buildDistance); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java index 708872b383..d9232d552b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -19,7 +19,7 @@ public TaskSectionTree(Viewport viewport, float buildDistance, int frame, CullTy } public void markSectionTask(RenderSection section) { - this.taskTree.add(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + this.taskTree.add(section); this.taskTreeFinalized = false; } @@ -32,7 +32,7 @@ protected void addPendingSection(RenderSection section, ChunkUpdateType type) { public void traverseVisiblePendingTasks(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { if (!this.taskTreeFinalized) { - this.taskTree.calculateReduced(); + this.taskTree.prepareForTraversal(); this.taskTreeFinalized = true; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java index d110b19317..cc82e94d33 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java @@ -17,12 +17,11 @@ * The async visible chunk collector is passed into a section tree to collect visible chunks. */ public class VisibleChunkCollectorAsync implements SectionTree.VisibleSectionVisitor { - private final ObjectArrayList sortedRenderLists; - private final RenderRegionManager regions; - private final int frame; + private final ObjectArrayList sortedRenderLists; + public VisibleChunkCollectorAsync(RenderRegionManager regions, int frame) { this.regions = regions; this.frame = frame; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java index a65fc851e7..1f0ccfda79 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -48,7 +48,7 @@ public void visit(RenderSection section) { this.lastSectionKnownEmpty = false; // mark all traversed sections as portals, even if they don't have terrain that needs rendering - this.portalTree.add(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + this.portalTree.add(section); } private boolean isRayBlockedStepped(RenderSection section) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 3721a298ed..a699ef2801 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -87,8 +87,8 @@ protected void markPresent(int x, int y, int z) { this.tree.add(x, y, z); } - public void finalizeTrees() { - this.tree.calculateReduced(); + public void prepareForTraversal() { + this.tree.prepareForTraversal(); } public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { @@ -118,7 +118,7 @@ private boolean isSectionPresent(int x, int y, int z) { return this.tree.getPresence(x, y, z) == Tree.PRESENT; } - public void traverseVisible(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + public void traverse(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { this.tree.traverse(visitor, viewport, distanceLimit); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java index 42b2bec411..68826ddeee 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableBiForest.java @@ -9,10 +9,10 @@ public AbstractTraversableBiForest(int baseOffsetX, int baseOffsetY, int baseOff } @Override - public void calculateReduced() { - this.mainTree.calculateReduced(); + public void prepareForTraversal() { + this.mainTree.prepareForTraversal(); if (this.secondaryTree != null) { - this.secondaryTree.calculateReduced(); + this.secondaryTree.prepareForTraversal(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java index b8673fc5a7..d4be826f60 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/AbstractTraversableMultiForest.java @@ -10,10 +10,10 @@ public AbstractTraversableMultiForest(int baseOffsetX, int baseOffsetY, int base } @Override - public void calculateReduced() { + public void prepareForTraversal() { for (var tree : this.trees) { if (tree != null) { - tree.calculateReduced(); + tree.prepareForTraversal(); } } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java index ed425201d5..98623debe8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java @@ -1,7 +1,13 @@ package net.caffeinemc.mods.sodium.client.render.chunk.tree; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; + public interface Forest { void add(int x, int y, int z); + default void add(RenderSection section) { + add(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + int getPresence(int x, int y, int z); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java index d60c8f1529..f297ee77b0 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableForest.java @@ -5,7 +5,7 @@ import net.minecraft.world.level.Level; public interface TraversableForest extends Forest { - void calculateReduced(); + void prepareForTraversal(); void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java index 26fb9b227d..f4f3c881b1 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java @@ -23,7 +23,7 @@ public TraversableTree(int offsetX, int offsetY, int offsetZ) { super(offsetX, offsetY, offsetZ); } - public void calculateReduced() { + public void prepareForTraversal() { long doubleReduced = 0; for (int i = 0; i < 64; i++) { long reduced = 0; From 4c4c0b23c81a5b9f9427dd296106185bedd5b09c Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 27 Dec 2024 04:35:01 +0100 Subject: [PATCH 59/81] add removable tree and forest system for supporting the out-of-graph 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) --- .../client/render/SodiumWorldRenderer.java | 1 + .../render/chunk/RenderSectionManager.java | 125 +++++++++++----- .../lists/FallbackVisibleChunkCollector.java | 25 ++++ .../chunk/occlusion/OcclusionCuller.java | 8 + .../render/chunk/occlusion/SectionTree.java | 11 +- .../render/chunk/tree/RemovableForest.java | 5 + .../chunk/tree/RemovableMultiForest.java | 137 ++++++++++++++++++ .../render/chunk/tree/RemovableTree.java | 69 +++++++++ 8 files changed, 333 insertions(+), 48 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index 5be4f72547..ef59db4344 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -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); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 4ee29120d2..a455ecc737 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -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; @@ -115,7 +116,8 @@ public class RenderSectionManager { private final ObjectArrayList> pendingTasks = new ObjectArrayList<>(); private SectionTree renderTree = null; private TaskSectionTree globalTaskTree = null; - private final Map trees = new EnumMap<>(CullType.class); + private final Map cullResults = new EnumMap<>(CullType.class); + private final RemovableMultiForest renderableSectionTree; private final AsyncCameraTimingControl cameraTimingControl = new AsyncCameraTimingControl(); @@ -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) { @@ -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); @@ -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) { @@ -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); @@ -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(); @@ -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; } @@ -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; } @@ -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) { @@ -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()) { @@ -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 @@ -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() { @@ -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); @@ -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); } @@ -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); } @@ -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); } @@ -617,6 +656,12 @@ private boolean processChunkBuildResults(ArrayList 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)) { @@ -1049,7 +1094,7 @@ public Collection 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", @@ -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; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java new file mode 100644 index 0000000000..9eec7b4eaa --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java @@ -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 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); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java index 4d5c0e3e03..df88a4e274 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/OcclusionCuller.java @@ -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 queue) { var origin = this.viewport.getChunkCoord(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index a699ef2801..7525330c0b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -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; @@ -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; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java new file mode 100644 index 0000000000..4729c43b26 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableForest.java @@ -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); +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java new file mode 100644 index 0000000000..5065f33e57 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java @@ -0,0 +1,137 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceArrayList; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.minecraft.core.SectionPos; + +import java.util.Comparator; + +public class RemovableMultiForest implements RemovableForest { + private final Long2ReferenceLinkedOpenHashMap trees; + private final ReferenceArrayList treeSortList = new ReferenceArrayList<>(); + private RemovableTree lastTree; + + // the removable tree separately tracks if it needs to prepared for traversal because it's not just built once, prepared, and then traversed. Since it can receive updates, it needs to be prepared for traversal again and to avoid unnecessary preparation, it tracks whether it's ready. + private boolean treesAreReady = true; + + public RemovableMultiForest(float buildDistance) { + this.trees = new Long2ReferenceLinkedOpenHashMap<>(getCapacity(buildDistance)); + } + + private static int getCapacity(float buildDistance) { + var forestDim = BaseMultiForest.forestDimFromBuildDistance(buildDistance) + 1; + return forestDim * forestDim * forestDim; + } + + public void ensureCapacity(float buildDistance) { + this.trees.ensureCapacity(getCapacity(buildDistance)); + } + + @Override + public void prepareForTraversal() { + if (this.treesAreReady) { + return; + } + + for (var tree : this.trees.values()) { + tree.prepareForTraversal(); + if (tree.isEmpty()) { + this.trees.remove(tree.getTreeKey()); + if (this.lastTree == tree) { + this.lastTree = null; + } + } + } + + this.treesAreReady = true; + } + + @Override + public void traverse(SectionTree.VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { + var transform = viewport.getTransform(); + var cameraSectionX = transform.intX >> 4; + var cameraSectionY = transform.intY >> 4; + var cameraSectionZ = transform.intZ >> 4; + + // sort the trees by distance from the camera by sorting a packed index array. + this.treeSortList.clear(); + this.treeSortList.ensureCapacity(this.trees.size()); + this.treeSortList.addAll(this.trees.values()); + for (var tree : this.treeSortList) { + tree.updateSortKeyFor(cameraSectionX, cameraSectionY, cameraSectionZ); + } + + this.treeSortList.unstableSort(Comparator.comparingInt(RemovableTree::getSortKey)); + + // traverse in sorted front-to-back order for correct render order + for (var tree : this.treeSortList) { + // disable distance test in traversal because we don't use it here + tree.traverse(visitor, viewport, 0, 0); + } + } + + @Override + public void add(int x, int y, int z) { + this.treesAreReady = false; + + if (this.lastTree != null && this.lastTree.add(x, y, z)) { + return; + } + + // get the tree coordinate by dividing by 64 + var treeX = x >> 6; + var treeY = y >> 6; + var treeZ = z >> 6; + + var treeKey = SectionPos.asLong(treeX, treeY, treeZ); + var tree = this.trees.get(treeKey); + + if (tree == null) { + var treeOffsetX = treeX << 6; + var treeOffsetY = treeY << 6; + var treeOffsetZ = treeZ << 6; + tree = new RemovableTree(treeOffsetX, treeOffsetY, treeOffsetZ); + this.trees.put(treeKey, tree); + } + + tree.add(x, y, z); + this.lastTree = tree; + } + + public void remove(int x, int y, int z) { + this.treesAreReady = false; + + if (this.lastTree != null && this.lastTree.remove(x, y, z)) { + return; + } + + // get the tree coordinate by dividing by 64 + var treeX = x >> 6; + var treeY = y >> 6; + var treeZ = z >> 6; + + var treeKey = SectionPos.asLong(treeX, treeY, treeZ); + var tree = this.trees.get(treeKey); + + if (tree == null) { + return; + } + + tree.remove(x, y, z); + + this.lastTree = tree; + } + + public void remove(RenderSection section) { + this.remove(section.getChunkX(), section.getChunkY(), section.getChunkZ()); + } + + @Override + public int getPresence(int x, int y, int z) { + // unused operation on removable trees + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java new file mode 100644 index 0000000000..4b7cea63e4 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableTree.java @@ -0,0 +1,69 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.tree; + +import net.minecraft.core.SectionPos; + +public class RemovableTree extends TraversableTree { + private boolean reducedIsValid = true; + private int sortKey; + + public RemovableTree(int offsetX, int offsetY, int offsetZ) { + super(offsetX, offsetY, offsetZ); + } + + public boolean remove(int x, int y, int z) { + x -= this.offsetX; + y -= this.offsetY; + z -= this.offsetZ; + if (Tree.isOutOfBounds(x, y, z)) { + return false; + } + + var bitIndex = Tree.interleave6x3(x, y, z); + this.tree[bitIndex >> 6] &= ~(1L << (bitIndex & 0b111111)); + + this.reducedIsValid = false; + + return true; + } + + @Override + public void prepareForTraversal() { + if (!this.reducedIsValid) { + super.prepareForTraversal(); + this.reducedIsValid = true; + } + } + + @Override + public boolean add(int x, int y, int z) { + var result = super.add(x, y, z); + if (result) { + this.reducedIsValid = false; + } + return result; + } + + public boolean isEmpty() { + return this.treeDoubleReduced == 0; + } + + public long getTreeKey() { + return SectionPos.asLong(this.offsetX, this.offsetY, this.offsetZ); + } + + public void updateSortKeyFor(int cameraSectionX, int cameraSectionY, int cameraSectionZ) { + var deltaX = Math.abs(this.offsetX + 32 - cameraSectionX); + var deltaY = Math.abs(this.offsetY + 32 - cameraSectionY); + var deltaZ = Math.abs(this.offsetZ + 32 - cameraSectionZ); + this.sortKey = deltaX + deltaY + deltaZ + 1; + } + + public int getSortKey() { + return this.sortKey; + } + + @Override + public int getPresence(int i, int i1, int i2) { + throw new UnsupportedOperationException("Not implemented"); + } +} \ No newline at end of file From e9992c4e5e959b5cdf62bfebffd0a5b6b747e266 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 27 Dec 2024 22:49:17 +0100 Subject: [PATCH 60/81] cleanup some todos --- .../mods/sodium/client/render/chunk/RenderSectionManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index a455ecc737..7a78e05528 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -587,8 +587,8 @@ public void tickVisibleRenders() { } } + // renderTree is not necessarily frustum-filtered but that is ok since the caller makes sure to eventually also perform a frustum test on the box being tested (see EntityRendererMixin) 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); } @@ -1117,8 +1117,6 @@ public Collection getDebugStrings() { } public String getChunksDebugString() { - // TODO: add dirty and queued counts - // C: visible/total D: distance return String.format( "C: %d/%d (%s) D: %d", From 5c5ec7d13b5e5b06703dbda70d88b0fa6b56c59c Mon Sep 17 00:00:00 2001 From: douira Date: Mon, 30 Dec 2024 02:28:53 +0100 Subject: [PATCH 61/81] count tasks using ints instead of longs --- .../mods/sodium/client/render/chunk/ChunkUpdateType.java | 8 ++++---- .../sodium/client/render/chunk/RenderSectionManager.java | 6 +++--- .../render/chunk/compile/executor/ChunkJobCollector.java | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java index f3a9197920..57fa6d3bed 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java @@ -27,14 +27,14 @@ public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, Chunk return null; } - public DeferMode getDeferMode() { - return this.deferMode; - } - public boolean isImportant() { return this == IMPORTANT_REBUILD || this == IMPORTANT_SORT; } + public DeferMode getDeferMode() { + return this.deferMode; + } + public float getPriorityValue() { return this.priorityValue; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 7a78e05528..3302fbd626 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -75,9 +75,9 @@ public class RenderSectionManager { private final JobDurationEstimator jobDurationEstimator = new JobDurationEstimator(); private final MeshTaskSizeEstimator meshTaskSizeEstimator = new MeshTaskSizeEstimator(); private ChunkJobCollector lastBlockingCollector; - private long thisFrameBlockingTasks; - private long nextFrameBlockingTasks; - private long deferredTasks; + private int thisFrameBlockingTasks; + private int nextFrameBlockingTasks; + private int deferredTasks; private final ChunkRenderer chunkRenderer; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java index 9d1fae00f8..5566a97e06 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/executor/ChunkJobCollector.java @@ -54,7 +54,7 @@ public boolean hasBudgetRemaining() { return this.duration > 0; } - public long getSubmittedTaskCount() { + public int getSubmittedTaskCount() { return this.submitted.size(); } } From f205f0ba41e6c82d4bb48082cd1d1bbe463cc671 Mon Sep 17 00:00:00 2001 From: douira Date: Mon, 30 Dec 2024 03:23:03 +0100 Subject: [PATCH 62/81] put task list collection into its own file --- .../render/chunk/RenderSectionManager.java | 8 ++-- .../render/chunk/async/FrustumCullTask.java | 4 +- .../chunk/async/FrustumTaskListsResult.java | 4 +- .../render/chunk/async/GlobalCullResult.java | 4 +- .../render/chunk/async/GlobalCullTask.java | 6 +-- .../chunk/lists/PendingTaskCollector.java | 46 ++++--------------- .../chunk/lists/TaskListCollection.java | 42 +++++++++++++++++ 7 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskListCollection.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 3302fbd626..8fac9f716d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -94,8 +94,8 @@ public class RenderSectionManager { @NotNull private SortedRenderLists renderLists; - private PendingTaskCollector.TaskListCollection frustumTaskLists; - private PendingTaskCollector.TaskListCollection globalTaskLists; + private TaskListCollection frustumTaskLists; + private TaskListCollection globalTaskLists; private int frame; private int lastGraphDirtyFrame; @@ -785,7 +785,7 @@ private long submitSectionTasks(long remainingUploadSize, boolean limitOnSize, C float frustumPriorityBias = 0; float globalPriorityBias = 0; if (this.frustumTaskLists != null) { - frustumQueue = this.frustumTaskLists.pendingTasks.get(deferMode); + frustumQueue = this.frustumTaskLists.get(deferMode); } if (frustumQueue != null) { frustumPriorityBias = this.frustumTaskLists.getCollectorPriorityBias(this.lastFrameAtTime); @@ -794,7 +794,7 @@ private long submitSectionTasks(long remainingUploadSize, boolean limitOnSize, C } if (this.globalTaskLists != null) { - globalQueue = this.globalTaskLists.pendingTasks.get(deferMode); + globalQueue = this.globalTaskLists.get(deferMode); } if (globalQueue != null) { globalPriorityBias = this.globalTaskLists.getCollectorPriorityBias(this.lastFrameAtTime); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java index 551973f1da..0d1639d9d9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -1,7 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.async; import it.unimi.dsi.fastutil.longs.LongArrayList; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskListCollection; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.RayOcclusionSectionTree; @@ -46,7 +46,7 @@ public SectionTree getTree() { } @Override - public PendingTaskCollector.TaskListCollection getFrustumTaskLists() { + public TaskListCollection getFrustumTaskLists() { return frustumTaskLists; } }; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java index 874d48ee4e..460092c0e5 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java @@ -1,7 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.async; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskListCollection; public interface FrustumTaskListsResult { - PendingTaskCollector.TaskListCollection getFrustumTaskLists(); + TaskListCollection getFrustumTaskLists(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java index 74d1f3496b..61e8f70a93 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java @@ -1,10 +1,10 @@ package net.caffeinemc.mods.sodium.client.render.chunk.async; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskListCollection; import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; public interface GlobalCullResult extends FrustumTaskListsResult { TaskSectionTree getTaskTree(); - PendingTaskCollector.TaskListCollection getGlobalTaskLists(); + TaskListCollection getGlobalTaskLists(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index e464110c81..0ab14c9f9d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -4,7 +4,7 @@ import it.unimi.dsi.fastutil.longs.LongArrayList; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.lists.FrustumTaskCollector; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.PendingTaskCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskListCollection; import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; @@ -56,12 +56,12 @@ public TaskSectionTree getTaskTree() { } @Override - public PendingTaskCollector.TaskListCollection getFrustumTaskLists() { + public TaskListCollection getFrustumTaskLists() { return frustumTaskLists; } @Override - public PendingTaskCollector.TaskListCollection getGlobalTaskLists() { + public TaskListCollection getGlobalTaskLists() { return globalTaskLists; } }; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index ba3ae036f7..9f6c4a7551 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -1,6 +1,5 @@ package net.caffeinemc.mods.sodium.client.render.chunk.lists; -import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; import net.caffeinemc.mods.sodium.client.render.chunk.ChunkUpdateType; @@ -9,23 +8,19 @@ import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import net.caffeinemc.mods.sodium.client.util.MathUtil; -import net.minecraft.core.SectionPos; import net.minecraft.util.Mth; -import java.util.EnumMap; -import java.util.Map; - public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisitor { public static final int SECTION_Y_MIN = -128; // used instead of baseOffsetY to accommodate all permissible y values (-2048 to 2048 blocks) // tunable parameters for the priority calculation. // each "gained" point means a reduction in the final priority score (lowest score processed first) - private static final float PENDING_TIME_FACTOR = -1.0f / 500_000_000.0f; // 1 point gained per 500ms - private static final float WITHIN_FRUSTUM_BIAS = -3.0f; // points for being within the frustum - private static final float PROXIMITY_FACTOR = 3.0f; // penalty for being far away - private static final float CLOSE_DISTANCE = 50.0f; // distance at which another proximity bonus is applied - private static final float CLOSE_PROXIMITY_FACTOR = 0.6f; // penalty for being CLOSE_DISTANCE or farther away - private static final float INV_MAX_DISTANCE_CLOSE = CLOSE_PROXIMITY_FACTOR / CLOSE_DISTANCE; + static final float PENDING_TIME_FACTOR = -1.0f / 500_000_000.0f; // 1 point gained per 500ms + static final float WITHIN_FRUSTUM_BIAS = -3.0f; // points for being within the frustum + static final float PROXIMITY_FACTOR = 3.0f; // penalty for being far away + static final float CLOSE_DISTANCE = 50.0f; // distance at which another proximity bonus is applied + static final float CLOSE_PROXIMITY_FACTOR = 0.6f; // penalty for being CLOSE_DISTANCE or farther away + static final float INV_MAX_DISTANCE_CLOSE = CLOSE_PROXIMITY_FACTOR / CLOSE_DISTANCE; private final LongArrayList[] pendingTasks = new LongArrayList[DeferMode.values().length]; @@ -127,7 +122,7 @@ public static float decodePriority(long encoded) { } public TaskListCollection getPendingTaskLists() { - var result = new EnumMap(DeferMode.class); + var result = new TaskListCollection(DeferMode.class, this.creationTime, this.isFrustumTested, this.baseOffsetX, this.baseOffsetZ); for (var mode : DeferMode.values()) { var list = this.pendingTasks[mode.ordinal()]; @@ -137,32 +132,7 @@ public TaskListCollection getPendingTaskLists() { } } - return new TaskListCollection(result); + return result; } - public class TaskListCollection { - public final Map pendingTasks; - - public TaskListCollection(Map pendingTasks) { - this.pendingTasks = pendingTasks; - } - - public float getCollectorPriorityBias(long now) { - // compensate for creation time of the list and whether the sections are in the frustum - return (now - PendingTaskCollector.this.creationTime) * PENDING_TIME_FACTOR + - (PendingTaskCollector.this.isFrustumTested ? WITHIN_FRUSTUM_BIAS : 0); - } - - public RenderSection decodeAndFetchSection(Long2ReferenceMap sectionByPosition, long encoded) { - var localX = (int) (encoded >>> 20) & 0b1111111111; - var localY = (int) (encoded >>> 10) & 0b1111111111; - var localZ = (int) (encoded & 0b1111111111); - - var globalX = localX + PendingTaskCollector.this.baseOffsetX; - var globalY = localY + SECTION_Y_MIN; - var globalZ = localZ + PendingTaskCollector.this.baseOffsetZ; - - return sectionByPosition.get(SectionPos.asLong(globalX, globalY, globalZ)); - } - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskListCollection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskListCollection.java new file mode 100644 index 0000000000..022b3dd7b7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskListCollection.java @@ -0,0 +1,42 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.lists; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; +import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; +import net.minecraft.core.SectionPos; + +import java.util.EnumMap; + +public class TaskListCollection extends EnumMap { + private final long creationTime; + private final boolean isFrustumTested; + private final int baseOffsetX; + private final int baseOffsetZ; + + public TaskListCollection(Class keyType, long creationTime, boolean isFrustumTested, int baseOffsetX, int baseOffsetZ) { + super(keyType); + this.creationTime = creationTime; + this.isFrustumTested = isFrustumTested; + this.baseOffsetX = baseOffsetX; + this.baseOffsetZ = baseOffsetZ; + } + + public float getCollectorPriorityBias(long now) { + // compensate for creation time of the list and whether the sections are in the frustum + return (now - this.creationTime) * PendingTaskCollector.PENDING_TIME_FACTOR + + (this.isFrustumTested ? PendingTaskCollector.WITHIN_FRUSTUM_BIAS : 0); + } + + public RenderSection decodeAndFetchSection(Long2ReferenceMap sectionByPosition, long encoded) { + var localX = (int) (encoded >>> 20) & 0b1111111111; + var localY = (int) (encoded >>> 10) & 0b1111111111; + var localZ = (int) (encoded & 0b1111111111); + + var globalX = localX + this.baseOffsetX; + var globalY = localY + PendingTaskCollector.SECTION_Y_MIN; + var globalZ = localZ + this.baseOffsetZ; + + return sectionByPosition.get(SectionPos.asLong(globalX, globalY, globalZ)); + } +} From 8874df11160cb3f9b242ea0174d2d1612d5b1133 Mon Sep 17 00:00:00 2001 From: douira Date: Mon, 30 Dec 2024 04:10:43 +0100 Subject: [PATCH 63/81] move exponential moving average to the math util file --- .../sodium/client/render/chunk/RenderSectionManager.java | 3 +-- .../chunk/compile/estimation/CategoryFactorEstimator.java | 3 ++- .../net/caffeinemc/mods/sodium/client/util/MathUtil.java | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 8fac9f716d..bc4fd6432a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -152,8 +152,7 @@ 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 = MathUtil.exponentialMovingAverage(this.averageFrameDuration, this.lastFrameDuration, AVERAGE_FRAME_DURATION_FACTOR); } this.averageFrameDuration = Mth.clamp(this.averageFrameDuration, 1_000_100, 100_000_000); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java index d207f95f62..a6e1a0b2c4 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java @@ -4,6 +4,7 @@ import it.unimi.dsi.fastutil.objects.Reference2FloatOpenHashMap; import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import net.caffeinemc.mods.sodium.client.util.MathUtil; public class CategoryFactorEstimator { private final Reference2FloatMap aPerB = new Reference2FloatOpenHashMap<>(); @@ -62,7 +63,7 @@ public void flushNewData() { } if (this.aPerB.containsKey(category)) { var oldFactor = this.aPerB.getFloat(category); - var newValue = oldFactor * (1 - this.newDataFactor) + newFactor * this.newDataFactor; + var newValue = MathUtil.exponentialMovingAverage(oldFactor, newFactor, this.newDataFactor); this.aPerB.put(category, newValue); } else { this.aPerB.put(category, newFactor); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java index 01d267e128..53d729e089 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/util/MathUtil.java @@ -43,4 +43,12 @@ public static int floatToComparableInt(float f) { public static float comparableIntToFloat(int i) { return Float.intBitsToFloat(i ^ ((i >> 31) & 0x7FFFFFFF)); } + + public static float exponentialMovingAverage(float oldValue, float newValue, float newValueContribution) { + return newValueContribution * newValue + (1 - newValueContribution) * oldValue; + } + + public static long exponentialMovingAverage(long oldValue, long newValue, float newValueContribution) { + return (long) (newValueContribution * newValue) + (long) ((1 - newValueContribution) * oldValue); + } } From fa3bb7da02789a92ca6e3f691c0805b8aba50784 Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 31 Dec 2024 02:56:00 +0100 Subject: [PATCH 64/81] fix synchronous sort tasks, change important task scheduling to be separate from regular deferred task scheduling --- .../client/render/SodiumWorldRenderer.java | 6 +- .../client/render/chunk/ChunkUpdateType.java | 5 +- .../render/chunk/RenderSectionManager.java | 243 +++++++++++------- .../render/chunk/async/FrustumCullTask.java | 4 +- .../chunk/async/FrustumTaskListsResult.java | 4 +- .../render/chunk/async/GlobalCullResult.java | 4 +- .../render/chunk/async/GlobalCullTask.java | 6 +- ...tCollection.java => DeferredTaskList.java} | 15 +- .../chunk/lists/PendingTaskCollector.java | 27 +- .../render/chunk/occlusion/SectionTree.java | 11 +- 10 files changed, 191 insertions(+), 134 deletions(-) rename common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/{TaskListCollection.java => DeferredTaskList.java} (70%) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index ef59db4344..6381f1ee76 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -192,7 +192,7 @@ public void setupTerrain(Camera camera, float fogDistance = RenderSystem.getShaderFog().end(); if (this.lastCameraPos == null) { - this.lastCameraPos = new Vector3d(pos); + this.lastCameraPos = pos; } if (this.lastProjectionMatrix == null) { this.lastProjectionMatrix = new Matrix4f(projectionMatrix); @@ -218,7 +218,7 @@ public void setupTerrain(Camera camera, profiler.popPush("translucent_triggering"); this.renderSectionManager.processGFNIMovement(new CameraMovement(this.lastCameraPos, pos)); - this.lastCameraPos = new Vector3d(pos); + this.lastCameraPos = pos; } int maxChunkUpdates = updateChunksImmediately ? this.renderDistance : 1; @@ -230,7 +230,7 @@ public void setupTerrain(Camera camera, profiler.popPush("chunk_update"); this.renderSectionManager.cleanupAndFlip(); - this.renderSectionManager.updateChunks(updateChunksImmediately); + this.renderSectionManager.updateChunks(viewport, updateChunksImmediately); profiler.popPush("chunk_upload"); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java index 57fa6d3bed..f225da54b3 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java @@ -1,5 +1,8 @@ package net.caffeinemc.mods.sodium.client.render.chunk; +/** + * NOTE: Update types with not-ALWAYS defer mode should be prefixed "important". + */ public enum ChunkUpdateType { SORT(DeferMode.ALWAYS, 2), INITIAL_BUILD(DeferMode.ALWAYS, 0), @@ -28,7 +31,7 @@ public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, Chunk } public boolean isImportant() { - return this == IMPORTANT_REBUILD || this == IMPORTANT_SORT; + return this.deferMode != DeferMode.ALWAYS; } public DeferMode getDeferMode() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index bc4fd6432a..29f6861eaa 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -94,8 +94,9 @@ public class RenderSectionManager { @NotNull private SortedRenderLists renderLists; - private TaskListCollection frustumTaskLists; - private TaskListCollection globalTaskLists; + private DeferredTaskList frustumTaskLists; + private DeferredTaskList globalTaskLists; + private final EnumMap> importantTasks; private int frame; private int lastGraphDirtyFrame; @@ -138,14 +139,13 @@ public RenderSectionManager(ClientLevel level, int renderDistance, CommandList c this.occlusionCuller = new OcclusionCuller(Long2ReferenceMaps.unmodifiable(this.sectionByPosition), this.level); this.renderableSectionTree = new RemovableMultiForest(renderDistance); - } - public void updateCameraState(Vector3dc cameraPosition, Camera camera) { - this.cameraBlockPos = camera.getBlockPosition(); - this.cameraPosition = cameraPosition; + this.importantTasks = new EnumMap<>(DeferMode.class); + this.importantTasks.put(DeferMode.ZERO_FRAMES, new ReferenceLinkedOpenHashSet<>()); + this.importantTasks.put(DeferMode.ONE_FRAME, new ReferenceLinkedOpenHashSet<>()); } - public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator, boolean updateImmediately) { + public void updateCameraState(Vector3dc cameraPosition, Camera camera) { var now = System.nanoTime(); this.lastFrameDuration = now - this.lastFrameAtTime; this.lastFrameAtTime = now; @@ -160,6 +160,11 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato this.needsRenderListUpdate |= this.cameraChanged; this.needsFrustumTaskListUpdate |= this.needsRenderListUpdate; + this.cameraBlockPos = camera.getBlockPosition(); + this.cameraPosition = cameraPosition; + } + + public void updateRenderLists(Camera camera, Viewport viewport, boolean spectator, boolean updateImmediately) { // do sync bfs based on update immediately (flawless frames) or if the camera moved too much var shouldRenderSync = this.cameraTimingControl.getShouldRenderSync(camera); if ((updateImmediately || shouldRenderSync) && (this.needsGraphUpdate || this.needsRenderListUpdate)) { @@ -670,7 +675,7 @@ private boolean updateSectionInfo(RenderSection render, BuiltSectionInfo info) { } } - private static List filterChunkBuildResults(ArrayList outputs) { + private List filterChunkBuildResults(ArrayList outputs) { var map = new Reference2ReferenceLinkedOpenHashMap(); for (var output : outputs) { @@ -713,7 +718,7 @@ public void cleanupAndFlip() { this.regions.update(); } - public void updateChunks(boolean updateImmediately) { + public void updateChunks(Viewport viewport, boolean updateImmediately) { this.thisFrameBlockingTasks = 0; this.nextFrameBlockingTasks = 0; this.deferredTasks = 0; @@ -727,22 +732,23 @@ public void updateChunks(boolean updateImmediately) { if (updateImmediately) { // for a perfect frame where everything is finished use the last frame's blocking collector // and add all tasks to it so that they're waited on - this.submitSectionTasks(Long.MAX_VALUE, thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector); + this.submitSectionTasks(thisFrameBlockingCollector, thisFrameBlockingCollector, thisFrameBlockingCollector, Long.MAX_VALUE, viewport); this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); thisFrameBlockingCollector.awaitCompletion(this.builder); } else { - var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); var remainingDuration = this.builder.getTotalRemainingDuration(this.averageFrameDuration); var remainingUploadSize = this.regions.getStagingBuffer().getUploadSizeLimit(this.averageFrameDuration); + + var nextFrameBlockingCollector = new ChunkJobCollector(this.buildResults::add); var deferredCollector = new ChunkJobCollector(remainingDuration, this.buildResults::add); // if zero frame delay is allowed, submit important sorts with the current frame blocking collector. // otherwise submit with the collector that the next frame is blocking on. if (SodiumClientMod.options().performance.getSortBehavior().getDeferMode() == DeferMode.ZERO_FRAMES) { - this.submitSectionTasks(remainingUploadSize, thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); + this.submitSectionTasks(thisFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector, remainingUploadSize, viewport); } else { - this.submitSectionTasks(remainingUploadSize, nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector); + this.submitSectionTasks(nextFrameBlockingCollector, nextFrameBlockingCollector, deferredCollector, remainingUploadSize, viewport); } this.thisFrameBlockingTasks = thisFrameBlockingCollector.getSubmittedTaskCount(); @@ -759,46 +765,65 @@ public void updateChunks(boolean updateImmediately) { } private void submitSectionTasks( - long remainingUploadSize, - ChunkJobCollector importantCollector, - ChunkJobCollector semiImportantCollector, - ChunkJobCollector deferredCollector) { - for (var deferMode : DeferMode.values()) { - var collector = switch (deferMode) { - case ZERO_FRAMES -> importantCollector; - case ONE_FRAME -> semiImportantCollector; - case ALWAYS -> deferredCollector; - }; - - // don't limit on size for zero frame defer (needs to be done, no matter the limit) - remainingUploadSize = submitSectionTasks(remainingUploadSize, deferMode != DeferMode.ZERO_FRAMES, collector, deferMode); - if (remainingUploadSize <= 0) { - break; - } - } + ChunkJobCollector importantCollector, ChunkJobCollector semiImportantCollector, ChunkJobCollector deferredCollector, long remainingUploadSize, Viewport viewport) { + remainingUploadSize = submitImportantSectionTasks(importantCollector, remainingUploadSize, DeferMode.ZERO_FRAMES, viewport); + remainingUploadSize = submitImportantSectionTasks(semiImportantCollector, remainingUploadSize, DeferMode.ONE_FRAME, viewport); + submitDeferredSectionTasks(deferredCollector, remainingUploadSize); } - private long submitSectionTasks(long remainingUploadSize, boolean limitOnSize, ChunkJobCollector collector, DeferMode deferMode) { - LongHeapPriorityQueue frustumQueue = null; - LongHeapPriorityQueue globalQueue = null; + private static final LongPriorityQueue EMPTY_TASK_QUEUE = new LongPriorityQueue() { + @Override + public void enqueue(long x) { + throw new UnsupportedOperationException(); + } + + @Override + public long dequeueLong() { + throw new UnsupportedOperationException(); + } + + @Override + public long firstLong() { + throw new UnsupportedOperationException(); + } + + @Override + public LongComparator comparator() { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + }; + + private void submitDeferredSectionTasks(ChunkJobCollector collector, long remainingUploadSize) { + LongPriorityQueue frustumQueue = this.frustumTaskLists; + LongPriorityQueue globalQueue = this.globalTaskLists; float frustumPriorityBias = 0; float globalPriorityBias = 0; - if (this.frustumTaskLists != null) { - frustumQueue = this.frustumTaskLists.get(deferMode); - } + if (frustumQueue != null) { frustumPriorityBias = this.frustumTaskLists.getCollectorPriorityBias(this.lastFrameAtTime); } else { - frustumQueue = new LongHeapPriorityQueue(); + frustumQueue = EMPTY_TASK_QUEUE; } - if (this.globalTaskLists != null) { - globalQueue = this.globalTaskLists.get(deferMode); - } if (globalQueue != null) { globalPriorityBias = this.globalTaskLists.getCollectorPriorityBias(this.lastFrameAtTime); } else { - globalQueue = new LongHeapPriorityQueue(); + globalQueue = EMPTY_TASK_QUEUE; } float frustumPriority = Float.POSITIVE_INFINITY; @@ -806,8 +831,7 @@ private long submitSectionTasks(long remainingUploadSize, boolean limitOnSize, C long frustumItem = 0; long globalItem = 0; - while ((!frustumQueue.isEmpty() || !globalQueue.isEmpty()) && - collector.hasBudgetRemaining() && (!limitOnSize || remainingUploadSize > 0)) { + while ((!frustumQueue.isEmpty() || !globalQueue.isEmpty()) && collector.hasBudgetRemaining() && remainingUploadSize > 0) { // get the first item from the non-empty queues and see which one has higher priority. // if the priority is not infinity, then the item priority was fetched the last iteration and doesn't need updating. if (!frustumQueue.isEmpty() && Float.isInfinite(frustumPriority)) { @@ -819,6 +843,7 @@ private long submitSectionTasks(long remainingUploadSize, boolean limitOnSize, C globalPriority = PendingTaskCollector.decodePriority(globalItem) + globalPriorityBias; } + // pick the task with the higher priority, decode the section, and schedule its task if it exists RenderSection section; if (frustumPriority < globalPriority) { frustumQueue.dequeueLong(); @@ -832,62 +857,93 @@ private long submitSectionTasks(long remainingUploadSize, boolean limitOnSize, C section = this.globalTaskLists.decodeAndFetchSection(this.sectionByPosition, globalItem); } - if (section == null || section.isDisposed()) { - continue; - } - - // don't schedule tasks for sections that don't need it anymore, - // since the pending update it cleared when a task is started, this includes - // sections for which there's a currently running task. - var type = section.getPendingUpdate(); - if (type == null) { - continue; + if (section != null) { + remainingUploadSize -= submitSectionTask(collector, section); } + } + } - ChunkBuilderTask task; - if (type == ChunkUpdateType.SORT || type == ChunkUpdateType.IMPORTANT_SORT) { - task = this.createSortTask(section, this.frame); - - if (task == null) { - // when a sort task is null it means the render section has no dynamic data and - // doesn't need to be sorted. Nothing needs to be done. + private long submitImportantSectionTasks(ChunkJobCollector collector, long remainingUploadSize, DeferMode deferMode, Viewport viewport) { + var limitOnSize = deferMode != DeferMode.ZERO_FRAMES; + var it = this.importantTasks.get(deferMode).iterator(); + + while (it.hasNext() && collector.hasBudgetRemaining() && (!limitOnSize || remainingUploadSize > 0)) { + var section = it.next(); + var pendingUpdate = section.getPendingUpdate(); + if (pendingUpdate != null && pendingUpdate.getDeferMode() == deferMode) { + if (this.renderTree == null || this.renderTree.isSectionVisible(viewport, section)) { + remainingUploadSize -= submitSectionTask(collector, section, pendingUpdate); + } else { + // don't remove if simply not visible but still needs to be run continue; } - } else { - task = this.createRebuildTask(section, this.frame); - - if (task == null) { - // if the section is empty or doesn't exist submit this null-task to set the - // built flag on the render section. - // It's important to use a NoData instead of null translucency data here in - // order for it to clear the old data from the translucency sorting system. - // This doesn't apply to sorting tasks as that would result in the section being - // marked as empty just because it was scheduled to be sorted and its dynamic - // data has since been removed. In that case simply nothing is done as the - // rebuild that must have happened in the meantime includes new non-dynamic - // index data. - var result = ChunkJobResult.successfully(new ChunkBuildOutput( - section, this.frame, NoData.forEmptySection(section.getPosition()), - BuiltSectionInfo.EMPTY, Collections.emptyMap())); - this.buildResults.add(result); - - section.setTaskCancellationToken(null); - } } + it.remove(); + } - if (task != null) { - var job = this.builder.scheduleTask(task, type.isImportant(), collector::onJobFinished); - collector.addSubmittedJob(job); - remainingUploadSize -= job.getEstimatedSize(); + return remainingUploadSize; + } - section.setTaskCancellationToken(job); + private long submitSectionTask(ChunkJobCollector collector, @NotNull RenderSection section) { + // don't schedule tasks for sections that don't need it anymore, + // since the pending update it cleared when a task is started, this includes + // sections for which there's a currently running task. + var type = section.getPendingUpdate(); + if (type == null) { + return 0; + } + + return submitSectionTask(collector, section, type); + } + + private long submitSectionTask(ChunkJobCollector collector, @NotNull RenderSection section, ChunkUpdateType type) { + if (section.isDisposed()) { + return 0; + } + + ChunkBuilderTask task; + if (type == ChunkUpdateType.SORT || type == ChunkUpdateType.IMPORTANT_SORT) { + task = this.createSortTask(section, this.frame); + + if (task == null) { + // when a sort task is null it means the render section has no dynamic data and + // doesn't need to be sorted. Nothing needs to be done. + return 0; + } + } else { + task = this.createRebuildTask(section, this.frame); + + if (task == null) { + // if the section is empty or doesn't exist submit this null-task to set the + // built flag on the render section. + // It's important to use a NoData instead of null translucency data here in + // order for it to clear the old data from the translucency sorting system. + // This doesn't apply to sorting tasks as that would result in the section being + // marked as empty just because it was scheduled to be sorted and its dynamic + // data has since been removed. In that case simply nothing is done as the + // rebuild that must have happened in the meantime includes new non-dynamic + // index data. + var result = ChunkJobResult.successfully(new ChunkBuildOutput( + section, this.frame, NoData.forEmptySection(section.getPosition()), + BuiltSectionInfo.EMPTY, Collections.emptyMap())); + this.buildResults.add(result); + + section.setTaskCancellationToken(null); } + } + + var estimatedTaskSize = 0L; + if (task != null) { + var job = this.builder.scheduleTask(task, type.isImportant(), collector::onJobFinished); + collector.addSubmittedJob(job); + estimatedTaskSize = job.getEstimatedSize(); - section.setLastSubmittedFrame(this.frame); - section.clearPendingUpdate(); + section.setTaskCancellationToken(job); } - return remainingUploadSize; + section.setLastSubmittedFrame(this.frame); + section.clearPendingUpdate(); + return estimatedTaskSize; } public @Nullable ChunkBuilderMeshingTask createRebuildTask(RenderSection render, int frame) { @@ -957,13 +1013,18 @@ public int getVisibleChunkCount() { return sections; } - // TODO: this fixes very delayed tasks, but it still regresses on same-frame tasks that don't get to run in time because the frustum task collection task takes at least one (and usually only one) frame to run - // maybe intercept tasks that are scheduled in zero- or one-frame defer mode? - // collect and prioritize regardless of visibility if it's an important defer mode? private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateType type) { var current = section.getPendingUpdate(); type = ChunkUpdateType.getPromotionUpdateType(current, type); + // when the pending task type changes and it's important, add it to the list of important tasks + if (type != null && current != type) { + var deferMode = type.getDeferMode(); + if (deferMode != DeferMode.ALWAYS) { + this.importantTasks.get(deferMode).add(section); + } + } + section.setPendingUpdate(type, this.lastFrameAtTime); // if the section received a new task, mark in the task tree so an update can happen before a global cull task runs diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java index 0d1639d9d9..c477787210 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumCullTask.java @@ -1,7 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.async; import it.unimi.dsi.fastutil.longs.LongArrayList; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskListCollection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.DeferredTaskList; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.RayOcclusionSectionTree; @@ -46,7 +46,7 @@ public SectionTree getTree() { } @Override - public TaskListCollection getFrustumTaskLists() { + public DeferredTaskList getFrustumTaskLists() { return frustumTaskLists; } }; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java index 460092c0e5..439fc230d7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/FrustumTaskListsResult.java @@ -1,7 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.async; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskListCollection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.DeferredTaskList; public interface FrustumTaskListsResult { - TaskListCollection getFrustumTaskLists(); + DeferredTaskList getFrustumTaskLists(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java index 61e8f70a93..c3f6f35715 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullResult.java @@ -1,10 +1,10 @@ package net.caffeinemc.mods.sodium.client.render.chunk.async; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskListCollection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.DeferredTaskList; import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; public interface GlobalCullResult extends FrustumTaskListsResult { TaskSectionTree getTaskTree(); - TaskListCollection getGlobalTaskLists(); + DeferredTaskList getGlobalTaskLists(); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java index 0ab14c9f9d..46869001ef 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/async/GlobalCullTask.java @@ -4,7 +4,7 @@ import it.unimi.dsi.fastutil.longs.LongArrayList; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.lists.FrustumTaskCollector; -import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskListCollection; +import net.caffeinemc.mods.sodium.client.render.chunk.lists.DeferredTaskList; import net.caffeinemc.mods.sodium.client.render.chunk.lists.TaskSectionTree; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.CullType; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.OcclusionCuller; @@ -56,12 +56,12 @@ public TaskSectionTree getTaskTree() { } @Override - public TaskListCollection getFrustumTaskLists() { + public DeferredTaskList getFrustumTaskLists() { return frustumTaskLists; } @Override - public TaskListCollection getGlobalTaskLists() { + public DeferredTaskList getGlobalTaskLists() { return globalTaskLists; } }; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskListCollection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/DeferredTaskList.java similarity index 70% rename from common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskListCollection.java rename to common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/DeferredTaskList.java index 022b3dd7b7..f8209de95c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskListCollection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/DeferredTaskList.java @@ -1,21 +1,24 @@ package net.caffeinemc.mods.sodium.client.render.chunk.lists; import it.unimi.dsi.fastutil.longs.Long2ReferenceMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongCollection; import it.unimi.dsi.fastutil.longs.LongHeapPriorityQueue; -import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.minecraft.core.SectionPos; -import java.util.EnumMap; - -public class TaskListCollection extends EnumMap { +public class DeferredTaskList extends LongHeapPriorityQueue { private final long creationTime; private final boolean isFrustumTested; private final int baseOffsetX; private final int baseOffsetZ; - public TaskListCollection(Class keyType, long creationTime, boolean isFrustumTested, int baseOffsetX, int baseOffsetZ) { - super(keyType); + public static DeferredTaskList createHeapCopyOf(LongCollection copyFrom, long creationTime, boolean isFrustumTested, int baseOffsetX, int baseOffsetZ) { + return new DeferredTaskList(new LongArrayList(copyFrom), creationTime, isFrustumTested, baseOffsetX, baseOffsetZ); + } + + private DeferredTaskList(LongArrayList copyFrom, long creationTime, boolean isFrustumTested, int baseOffsetX, int baseOffsetZ) { + super(copyFrom.elements(), copyFrom.size()); this.creationTime = creationTime; this.isFrustumTested = isFrustumTested; this.baseOffsetX = baseOffsetX; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index 9f6c4a7551..eb48a88bee 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -22,7 +22,7 @@ public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisit static final float CLOSE_PROXIMITY_FACTOR = 0.6f; // penalty for being CLOSE_DISTANCE or farther away static final float INV_MAX_DISTANCE_CLOSE = CLOSE_PROXIMITY_FACTOR / CLOSE_DISTANCE; - private final LongArrayList[] pendingTasks = new LongArrayList[DeferMode.values().length]; + private final LongArrayList pendingTasks = new LongArrayList(); protected final boolean isFrustumTested; protected final int baseOffsetX, baseOffsetY, baseOffsetZ; @@ -67,7 +67,8 @@ public void visit(RenderSection section) { protected void checkForTask(RenderSection section) { ChunkUpdateType type = section.getPendingUpdate(); - if (type != null && section.getTaskCancellationToken() == null) { + // collect non-important tasks (important tasks are handled separately) + if (type != null && !type.isImportant() && section.getTaskCancellationToken() == null) { this.addPendingSection(section, type); } } @@ -82,14 +83,8 @@ protected void addPendingSection(RenderSection section, ChunkUpdateType type) { var localZ = section.getChunkZ() - this.baseOffsetZ; long taskCoordinate = (long) (localX & 0b1111111111) << 20 | (long) (localY & 0b1111111111) << 10 | (long) (localZ & 0b1111111111); - var queue = this.pendingTasks[type.getDeferMode().ordinal()]; - if (queue == null) { - queue = new LongArrayList(); - this.pendingTasks[type.getDeferMode().ordinal()] = queue; - } - // encode the priority and the section position into a single long such that all parts can be later decoded - queue.add((long) MathUtil.floatToComparableInt(priority) << 32 | taskCoordinate); + this.pendingTasks.add((long) MathUtil.floatToComparableInt(priority) << 32 | taskCoordinate); } private float getSectionPriority(RenderSection section, ChunkUpdateType type) { @@ -121,18 +116,8 @@ public static float decodePriority(long encoded) { return MathUtil.comparableIntToFloat((int) (encoded >>> 32)); } - public TaskListCollection getPendingTaskLists() { - var result = new TaskListCollection(DeferMode.class, this.creationTime, this.isFrustumTested, this.baseOffsetX, this.baseOffsetZ); - - for (var mode : DeferMode.values()) { - var list = this.pendingTasks[mode.ordinal()]; - if (list != null) { - var queue = new LongHeapPriorityQueue(list.elements(), list.size()); - result.put(mode, queue); - } - } - - return result; + public DeferredTaskList getPendingTaskLists() { + return DeferredTaskList.createHeapCopyOf(this.pendingTasks, this.creationTime, this.isFrustumTested, this.baseOffsetX, this.baseOffsetZ); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 7525330c0b..3c3c8a5770 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -38,9 +38,9 @@ public int getFrame() { public boolean isValidFor(Viewport viewport, float searchDistance) { var cameraPos = viewport.getChunkCoord(); - 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 && + 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; } @@ -113,6 +113,11 @@ private boolean isSectionPresent(int x, int y, int z) { return this.tree.getPresence(x, y, z) == Tree.PRESENT; } + public boolean isSectionVisible(Viewport viewport, RenderSection section) { + return this.isSectionPresent(section.getChunkX(), section.getChunkY(), section.getChunkZ()) && + this.isWithinFrustum(viewport, section); + } + public void traverse(VisibleSectionVisitor visitor, Viewport viewport, float distanceLimit) { this.tree.traverse(visitor, viewport, distanceLimit); } From f48974700d71f8d1b044a00f1f3ca3b0a2b8abab Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 31 Dec 2024 05:01:31 +0100 Subject: [PATCH 65/81] add region sorting to fix translucency sorting issues, tree traversal only does correct in-region sorting, not between regions --- .../render/chunk/RenderSectionManager.java | 4 +- .../lists/FallbackVisibleChunkCollector.java | 4 +- .../chunk/lists/RenderListProvider.java | 54 +++++++++++++++++++ .../lists/VisibleChunkCollectorAsync.java | 26 +++++---- .../lists/VisibleChunkCollectorSync.java | 46 +++++----------- .../render/chunk/tree/TraversableTree.java | 3 ++ 6 files changed, 89 insertions(+), 48 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 29f6861eaa..110d890f29 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -423,7 +423,7 @@ private void processRenderListUpdate(Viewport viewport) { this.renderableSectionTree.prepareForTraversal(); this.renderableSectionTree.traverse(visitor, viewport, searchDistance); - this.renderLists = visitor.createRenderLists(); + this.renderLists = visitor.createRenderLists(viewport); this.frustumTaskLists = visitor.getPendingTaskLists(); this.globalTaskLists = null; this.renderTree = null; @@ -432,7 +432,7 @@ private void processRenderListUpdate(Viewport viewport) { var visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame); bestTree.traverse(visibleCollector, viewport, this.getSearchDistance()); - this.renderLists = visibleCollector.createRenderLists(); + this.renderLists = visibleCollector.createRenderLists(viewport); var end = System.nanoTime(); var time = end - start; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java index 9eec7b4eaa..70e1d33689 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/FallbackVisibleChunkCollector.java @@ -13,8 +13,8 @@ public FallbackVisibleChunkCollector(Viewport viewport, float buildDistance, Lon this.renderListCollector = new VisibleChunkCollectorAsync(regions, frame); } - public SortedRenderLists createRenderLists() { - return this.renderListCollector.createRenderLists(); + public SortedRenderLists createRenderLists(Viewport viewport) { + return this.renderListCollector.createRenderLists(viewport); } @Override diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java new file mode 100644 index 0000000000..156e66edfa --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/RenderListProvider.java @@ -0,0 +1,54 @@ +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.region.RenderRegion; +import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; + +public interface RenderListProvider { + ObjectArrayList getUnsortedRenderLists(); + + int[] getCachedSortItems(); + + void setCachedSortItems(int[] sortItems); + + default SortedRenderLists createRenderLists(Viewport viewport) { + // sort the regions by distance to fix rare region ordering bugs + var sectionPos = viewport.getChunkCoord(); + var cameraX = sectionPos.getX() >> RenderRegion.REGION_WIDTH_SH; + var cameraY = sectionPos.getY() >> RenderRegion.REGION_HEIGHT_SH; + var cameraZ = sectionPos.getZ() >> RenderRegion.REGION_LENGTH_SH; + + var unsortedRenderLists = this.getUnsortedRenderLists(); + var size = unsortedRenderLists.size(); + + var sortItems = this.getCachedSortItems(); + if (sortItems.length < size) { + sortItems = new int[size]; + this.setCachedSortItems(sortItems); + } + + for (var i = 0; i < size; i++) { + var region = unsortedRenderLists.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(size); + for (var i = 0; i < size; i++) { + var key = sortItems[i]; + var renderList = unsortedRenderLists.get(key & 0xFFFF); + sorted.add(renderList); + } + + for (var list : sorted) { + list.sortSections(sectionPos, sortItems); + } + + return new SortedRenderLists(sorted); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java index cc82e94d33..1dc15df9ab 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorAsync.java @@ -1,22 +1,15 @@ 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.LocalSectionIndex; import net.caffeinemc.mods.sodium.client.render.chunk.occlusion.SectionTree; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegionManager; -import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; - -import java.util.ArrayDeque; -import java.util.EnumMap; -import java.util.Map; -import java.util.Queue; /** * The async visible chunk collector is passed into a section tree to collect visible chunks. */ -public class VisibleChunkCollectorAsync implements SectionTree.VisibleSectionVisitor { +public class VisibleChunkCollectorAsync implements SectionTree.VisibleSectionVisitor, RenderListProvider { private final RenderRegionManager regions; private final int frame; @@ -55,7 +48,20 @@ public void visit(int x, int y, int z) { renderList.add(sectionIndex); } - public SortedRenderLists createRenderLists() { - return new SortedRenderLists(this.sortedRenderLists); + private static int[] sortItems = new int[RenderRegion.REGION_SIZE]; + + @Override + public ObjectArrayList getUnsortedRenderLists() { + return this.sortedRenderLists; + } + + @Override + public int[] getCachedSortItems() { + return sortItems; + } + + @Override + public void setCachedSortItems(int[] sortItems) { + VisibleChunkCollectorAsync.sortItems = sortItems; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java index 6c1edffcc1..e69a647cb7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java @@ -1,6 +1,5 @@ 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.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.RenderSectionFlags; @@ -13,7 +12,7 @@ /** * The sync visible chunk collector is passed into the graph search occlusion culler to collect visible chunks. */ -public class VisibleChunkCollectorSync extends SectionTree { +public class VisibleChunkCollectorSync extends SectionTree implements RenderListProvider { private final ObjectArrayList sortedRenderLists; public VisibleChunkCollectorSync(Viewport viewport, float buildDistance, int frame, CullType cullType, Level level) { @@ -44,39 +43,18 @@ public void visit(RenderSection section) { 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 sectionPos = viewport.getChunkCoord(); - var cameraX = sectionPos.getX() >> RenderRegion.REGION_WIDTH_SH; - var cameraY = sectionPos.getY() >> RenderRegion.REGION_HEIGHT_SH; - var cameraZ = sectionPos.getZ() >> 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(size); - for (var i = 0; i < size; i++) { - var key = sortItems[i]; - var renderList = this.sortedRenderLists.get(key & 0xFFFF); - sorted.add(renderList); - } + @Override + public ObjectArrayList getUnsortedRenderLists() { + return this.sortedRenderLists; + } - for (var list : sorted) { - list.sortSections(sectionPos, sortItems); - } + @Override + public int[] getCachedSortItems() { + return sortItems; + } - return new SortedRenderLists(sorted); + @Override + public void setCachedSortItems(int[] sortItems) { + VisibleChunkCollectorSync.sortItems = sortItems; } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java index f4f3c881b1..002059efd8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/TraversableTree.java @@ -5,6 +5,9 @@ import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; import org.joml.FrustumIntersection; +/** + * A traversable tree is a tree of sections that can be traversed with a distance limit and a frustum. It traverses the sections in visual front-to-back order, so that they can be directly put into a render list. Note however that ordering regions by adding them to the list the first time one of their sections is visited does not yield the correct order. This is because the sections are traversed in visual order, not ordered by distance from the camera. + */ public class TraversableTree extends Tree { private static final int INSIDE_FRUSTUM = 0b01; private static final int INSIDE_DISTANCE = 0b10; From 652c09c9918484f144bf1b7bbd0e99def92618de Mon Sep 17 00:00:00 2001 From: douira Date: Thu, 2 Jan 2025 06:22:12 +0100 Subject: [PATCH 66/81] use small delta instead of strict equality when comparing projection matrix to avoid very long tail of camera changes after player movement --- .../mods/sodium/client/render/SodiumWorldRenderer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index 6381f1ee76..9040a9342b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -199,7 +199,7 @@ public void setupTerrain(Camera camera, } boolean cameraLocationChanged = !pos.equals(this.lastCameraPos); boolean cameraAngleChanged = pitch != this.lastCameraPitch || yaw != this.lastCameraYaw || fogDistance != this.lastFogDistance; - boolean cameraProjectionChanged = !projectionMatrix.equals(this.lastProjectionMatrix); + boolean cameraProjectionChanged = !projectionMatrix.equals(this.lastProjectionMatrix, 0.0001f); this.lastProjectionMatrix = projectionMatrix; From ba797340483f056c9bffc31a55942778b5032c17 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 3 Jan 2025 17:41:38 +0100 Subject: [PATCH 67/81] prevent estimator from corrupting its data with Infinity --- .../estimation/CategoryFactorEstimator.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java index a6e1a0b2c4..d282062aff 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java @@ -32,7 +32,7 @@ public void reset() { } public float getAPerBFactor() { - return (float) this.aSum / this.bSum; + return ((float) this.aSum) / this.bSum; } } @@ -45,19 +45,27 @@ public interface BatchEntry { } public void addBatchEntry(BatchEntry batchEntry) { + var a = batchEntry.getA(); + var b = batchEntry.getB(); + + // skip if b is 0 to prevent Infinity and NaN + if (b == 0) { + return; + } + var category = batchEntry.getCategory(); - if (this.newData.containsKey(category)) { - this.newData.get(category).addDataPoint(batchEntry.getA(), batchEntry.getB()); - } else { - var batchData = new BatchDataAggregation(); - batchData.addDataPoint(batchEntry.getA(), batchEntry.getB()); - this.newData.put(category, batchData); + var aggregation = this.newData.get(category); + if (aggregation == null) { + aggregation = new BatchDataAggregation(); + this.newData.put(category, aggregation); } + aggregation.addDataPoint(a, b); } public void flushNewData() { this.newData.forEach((category, frameData) -> { var newFactor = frameData.getAPerBFactor(); + // if there was no data it results in NaN if (Float.isNaN(newFactor)) { return; } From bf79c48684dedfc029208682c32fafb35c1b4fba Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 3 Jan 2025 17:44:47 +0100 Subject: [PATCH 68/81] fix forgotten chunk updates stemming from concurrently building a task tree and updating the section's pending tasks by storing and then later catching up the tree with the changes --- .../render/chunk/RenderSectionManager.java | 48 ++++++++++++++++++- .../render/chunk/lists/TaskSectionTree.java | 5 ++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 110d890f29..378132baf2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -1,6 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk; import com.mojang.blaze3d.systems.RenderSystem; +import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.longs.*; import it.unimi.dsi.fastutil.objects.*; import net.caffeinemc.mods.sodium.client.SodiumClientMod; @@ -13,7 +14,9 @@ import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.JobDurationEstimator; import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshResultSize; import net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation.MeshTaskSizeEstimator; -import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.*; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkBuilder; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobCollector; +import net.caffeinemc.mods.sodium.client.render.chunk.compile.executor.ChunkJobResult; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderMeshingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderSortingTask; import net.caffeinemc.mods.sodium.client.render.chunk.compile.tasks.ChunkBuilderTask; @@ -35,6 +38,7 @@ import net.caffeinemc.mods.sodium.client.render.util.RenderAsserts; import net.caffeinemc.mods.sodium.client.render.viewport.CameraTransform; import net.caffeinemc.mods.sodium.client.render.viewport.Viewport; +import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import net.caffeinemc.mods.sodium.client.util.MathUtil; import net.caffeinemc.mods.sodium.client.util.task.CancellationToken; import net.caffeinemc.mods.sodium.client.world.LevelSlice; @@ -115,6 +119,9 @@ public class RenderSectionManager { private final ExecutorService asyncCullExecutor = Executors.newSingleThreadExecutor(RenderSectionManager::makeAsyncCullThread); private final ObjectArrayList> pendingTasks = new ObjectArrayList<>(); + private GlobalCullTask pendingGlobalCullTask = null; + private final IntArrayList concurrentlySubmittedTasks = new IntArrayList(); + private SectionTree renderTree = null; private TaskSectionTree globalTaskTree = null; private final Map cullResults = new EnumMap<>(CullType.class); @@ -181,7 +188,7 @@ public void updateRenderLists(Camera camera, Viewport viewport, boolean spectato this.cullResults.remove(CullType.FRUSTUM); this.pendingTasks.removeIf(task -> { - if (task instanceof CullTask cullTask && cullTask.getCullType() == CullType.FRUSTUM) { + if (task instanceof FrustumCullTask cullTask) { cullTask.setCancelled(); return true; } @@ -219,6 +226,8 @@ private void renderSync(Camera camera, Viewport viewport, boolean spectator) { task.getResult(); } this.pendingTasks.clear(); + this.pendingGlobalCullTask = null; + this.concurrentlySubmittedTasks.clear(); var tree = new VisibleChunkCollectorSync(viewport, searchDistance, this.frame, CullType.FRUSTUM, this.level); this.occlusionCuller.findVisible(tree, viewport, searchDistance, useOcclusionCulling, CancellationToken.NEVER_CANCELLED); @@ -280,6 +289,16 @@ private SectionTree unpackTaskResults(Viewport waitingViewport) { latestTreeCullType = cullType; this.needsRenderListUpdate = true; + this.pendingGlobalCullTask = null; + + // mark changes on the global task tree if they were scheduled while the task was already running + for (int i = 0, length = this.concurrentlySubmittedTasks.size(); i < length; i += 3) { + var x = this.concurrentlySubmittedTasks.getInt(i); + var y = this.concurrentlySubmittedTasks.getInt(i + 1); + var z = this.concurrentlySubmittedTasks.getInt(i + 2); + tree.markSectionTask(x, y, z); + } + this.concurrentlySubmittedTasks.clear(); } case FrustumTaskCollectionTask collectionTask -> this.frustumTaskLists = collectionTask.getResult().getFrustumTaskLists(); @@ -346,6 +365,10 @@ private void scheduleAsyncWork(Camera camera, Viewport viewport, boolean spectat }; task.submitTo(this.asyncCullExecutor); this.pendingTasks.add(task); + + if (task instanceof GlobalCullTask globalCullTask) { + this.pendingGlobalCullTask = globalCullTask; + } } } } @@ -1031,6 +1054,15 @@ private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateT if (this.globalTaskTree != null && type != null && current == null) { this.globalTaskTree.markSectionTask(section); this.needsFrustumTaskListUpdate = true; + + // when a global cull task is already running and has already processed the section, and we mark it with a pending task, + // the section will not be marked as having a task in the then replaced global tree and the derivative frustum tree also won't have it. + // Sections that are marked with a pending task while a task that may replace the global task tree is running are a added to a list from which the new global task tree is populated once it's done. + if (this.pendingGlobalCullTask != null) { + this.concurrentlySubmittedTasks.add(section.getChunkX()); + this.concurrentlySubmittedTasks.add(section.getChunkY()); + this.concurrentlySubmittedTasks.add(section.getChunkZ()); + } } return type; @@ -1161,6 +1193,18 @@ public Collection getDebugStrings() { this.thisFrameBlockingTasks, this.nextFrameBlockingTasks, this.deferredTasks, this.buildResults.size()) ); + if (PlatformRuntimeInformation.getInstance().isDevelopmentEnvironment()) { + var meshTaskDuration = this.jobDurationEstimator.estimateJobDuration(ChunkBuilderMeshingTask.class, 1); + var sortTaskDuration = this.jobDurationEstimator.estimateJobDuration(ChunkBuilderSortingTask.class, 1); + list.add(String.format("Duration: Mesh=%dns, Sort=%dns", meshTaskDuration, sortTaskDuration)); + + var sizeEstimates = new StringBuilder(); + for (var type : MeshResultSize.SectionCategory.values()) { + sizeEstimates.append(String.format("%s=%d, ", type, this.meshTaskSizeEstimator.estimateAWithB(type, 1))); + } + list.add(String.format("Size: %s", sizeEstimates)); + } + this.sortTriggering.addDebugStrings(list); var taskSlots = new String[AsyncTaskType.VALUES.length]; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java index d9232d552b..da5eef6d3a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/TaskSectionTree.java @@ -23,6 +23,11 @@ public void markSectionTask(RenderSection section) { this.taskTreeFinalized = false; } + public void markSectionTask(int x, int y, int z) { + this.taskTree.add(x, y, z); + this.taskTreeFinalized = false; + } + @Override protected void addPendingSection(RenderSection section, ChunkUpdateType type) { super.addPendingSection(section, type); From 4df703ac4679e5641a4f33e361eebee1f672f772 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 3 Jan 2025 19:48:21 +0100 Subject: [PATCH 69/81] split important and defer mode in the chunk update type to allow for speedy scheduling of nearby section tasks --- .../client/render/chunk/ChunkUpdateType.java | 37 +++++++--- .../sodium/client/render/chunk/DeferMode.java | 6 +- .../render/chunk/RenderSectionManager.java | 68 ++++++++++--------- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java index f225da54b3..079c5649ff 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java @@ -1,23 +1,41 @@ package net.caffeinemc.mods.sodium.client.render.chunk; +import org.jetbrains.annotations.NotNull; + /** - * NOTE: Update types with not-ALWAYS defer mode should be prefixed "important". + * Important: Whether the task is scheduled immediately after its creation. Otherwise, they're scheduled through asynchronous culling that collects non-important tasks. + * Defer mode: For important tasks, how fast they are going to be executed. One or zero frame deferral only allows one or zero frames to pass before the frame blocks on the task. Always deferral allows the task to be deferred indefinitely, but if it's important it will still be put to the front of the queue. */ public enum ChunkUpdateType { - SORT(DeferMode.ALWAYS, 2), - INITIAL_BUILD(DeferMode.ALWAYS, 0), - REBUILD(DeferMode.ALWAYS, 1), - IMPORTANT_REBUILD(DeferMode.ONE_FRAME, 1), + SORT(2), + INITIAL_BUILD(0), + REBUILD(1), + IMPORTANT_REBUILD(DeferMode.ZERO_FRAMES, 1), IMPORTANT_SORT(DeferMode.ZERO_FRAMES, 2); private final DeferMode deferMode; + private final boolean important; private final float priorityValue; - ChunkUpdateType(DeferMode deferMode, float priorityValue) { + ChunkUpdateType(float priorityValue) { + this.deferMode = DeferMode.ALWAYS; + this.important = false; + this.priorityValue = priorityValue; + } + + ChunkUpdateType(@NotNull DeferMode deferMode, float priorityValue) { this.deferMode = deferMode; + this.important = true; this.priorityValue = priorityValue; } + /** + * Returns a promoted update type if the new update type is more important than the previous one. Nothing is returned if the update type is the same or less important. + * + * @param prev Previous update type + * @param next New update type + * @return Promoted update type or {@code null} if the update type is the same or less important + */ public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, ChunkUpdateType next) { if (prev == null || prev == SORT || prev == next) { return next; @@ -31,11 +49,12 @@ public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, Chunk } public boolean isImportant() { - return this.deferMode != DeferMode.ALWAYS; + return this.important; } - public DeferMode getDeferMode() { - return this.deferMode; + public DeferMode getDeferMode(boolean deferImportantRebuilds) { + // use defer mode ALWAYS if important rebuilds are configured to be always deferred + return deferImportantRebuilds && this == IMPORTANT_REBUILD ? DeferMode.ALWAYS : this.deferMode; } public float getPriorityValue() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java index 5201b5aa8c..989ffbfdd7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java @@ -1,5 +1,9 @@ package net.caffeinemc.mods.sodium.client.render.chunk; public enum DeferMode { - ALWAYS, ONE_FRAME, ZERO_FRAMES + ALWAYS, ONE_FRAME, ZERO_FRAMES; + + public boolean allowsUnlimitedUploadSize() { + return this == ZERO_FRAMES; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 378132baf2..6528577e24 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -114,7 +114,6 @@ public class RenderSectionManager { private boolean cameraChanged = false; private boolean needsFrustumTaskListUpdate = true; - private @Nullable BlockPos cameraBlockPos; private @Nullable Vector3dc cameraPosition; private final ExecutorService asyncCullExecutor = Executors.newSingleThreadExecutor(RenderSectionManager::makeAsyncCullThread); @@ -148,8 +147,9 @@ public RenderSectionManager(ClientLevel level, int renderDistance, CommandList c this.renderableSectionTree = new RemovableMultiForest(renderDistance); this.importantTasks = new EnumMap<>(DeferMode.class); - this.importantTasks.put(DeferMode.ZERO_FRAMES, new ReferenceLinkedOpenHashSet<>()); - this.importantTasks.put(DeferMode.ONE_FRAME, new ReferenceLinkedOpenHashSet<>()); + for (var deferMode : DeferMode.values()) { + this.importantTasks.put(deferMode, new ReferenceLinkedOpenHashSet<>()); + } } public void updateCameraState(Vector3dc cameraPosition, Camera camera) { @@ -167,7 +167,6 @@ public void updateCameraState(Vector3dc cameraPosition, Camera camera) { this.needsRenderListUpdate |= this.cameraChanged; this.needsFrustumTaskListUpdate |= this.needsRenderListUpdate; - this.cameraBlockPos = camera.getBlockPosition(); this.cameraPosition = cameraPosition; } @@ -791,6 +790,8 @@ private void submitSectionTasks( ChunkJobCollector importantCollector, ChunkJobCollector semiImportantCollector, ChunkJobCollector deferredCollector, long remainingUploadSize, Viewport viewport) { remainingUploadSize = submitImportantSectionTasks(importantCollector, remainingUploadSize, DeferMode.ZERO_FRAMES, viewport); remainingUploadSize = submitImportantSectionTasks(semiImportantCollector, remainingUploadSize, DeferMode.ONE_FRAME, viewport); + remainingUploadSize = submitImportantSectionTasks(deferredCollector, remainingUploadSize, DeferMode.ALWAYS, viewport); + submitDeferredSectionTasks(deferredCollector, remainingUploadSize); } @@ -887,13 +888,13 @@ private void submitDeferredSectionTasks(ChunkJobCollector collector, long remain } private long submitImportantSectionTasks(ChunkJobCollector collector, long remainingUploadSize, DeferMode deferMode, Viewport viewport) { - var limitOnSize = deferMode != DeferMode.ZERO_FRAMES; - var it = this.importantTasks.get(deferMode).iterator(); + var it = this.importantTasks.get(deferMode).iterator(); + var alwaysDeferImportantRebuilds = alwaysDeferImportantRebuilds(); - while (it.hasNext() && collector.hasBudgetRemaining() && (!limitOnSize || remainingUploadSize > 0)) { + while (it.hasNext() && collector.hasBudgetRemaining() && (deferMode.allowsUnlimitedUploadSize() || remainingUploadSize > 0)) { var section = it.next(); var pendingUpdate = section.getPendingUpdate(); - if (pendingUpdate != null && pendingUpdate.getDeferMode() == deferMode) { + if (pendingUpdate != null && pendingUpdate.getDeferMode(alwaysDeferImportantRebuilds) == deferMode) { if (this.renderTree == null || this.renderTree.isSectionVisible(viewport, section)) { remainingUploadSize -= submitSectionTask(collector, section, pendingUpdate); } else { @@ -1038,34 +1039,33 @@ public int getVisibleChunkCount() { private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateType type) { var current = section.getPendingUpdate(); - type = ChunkUpdateType.getPromotionUpdateType(current, type); + var typeChanged = ChunkUpdateType.getPromotionUpdateType(current, type); - // when the pending task type changes and it's important, add it to the list of important tasks - if (type != null && current != type) { - var deferMode = type.getDeferMode(); - if (deferMode != DeferMode.ALWAYS) { - this.importantTasks.get(deferMode).add(section); + if (typeChanged != null) { + // when the pending task type changes, and it's important, add it to the list of important tasks + if (current != type && type.isImportant()) { + this.importantTasks.get(type.getDeferMode(alwaysDeferImportantRebuilds())).add(section); } - } - section.setPendingUpdate(type, this.lastFrameAtTime); + section.setPendingUpdate(type, this.lastFrameAtTime); - // if the section received a new task, mark in the task tree so an update can happen before a global cull task runs - if (this.globalTaskTree != null && type != null && current == null) { - this.globalTaskTree.markSectionTask(section); - this.needsFrustumTaskListUpdate = true; + // if the section received a new task, mark in the task tree so an update can happen before a global cull task runs + if (this.globalTaskTree != null && current == null) { + this.globalTaskTree.markSectionTask(section); + this.needsFrustumTaskListUpdate = true; - // when a global cull task is already running and has already processed the section, and we mark it with a pending task, - // the section will not be marked as having a task in the then replaced global tree and the derivative frustum tree also won't have it. - // Sections that are marked with a pending task while a task that may replace the global task tree is running are a added to a list from which the new global task tree is populated once it's done. - if (this.pendingGlobalCullTask != null) { - this.concurrentlySubmittedTasks.add(section.getChunkX()); - this.concurrentlySubmittedTasks.add(section.getChunkY()); - this.concurrentlySubmittedTasks.add(section.getChunkZ()); + // when a global cull task is already running and has already processed the section, and we mark it with a pending task, + // the section will not be marked as having a task in the then replaced global tree and the derivative frustum tree also won't have it. + // Sections that are marked with a pending task while a task that may replace the global task tree is running are a added to a list from which the new global task tree is populated once it's done. + if (this.pendingGlobalCullTask != null) { + this.concurrentlySubmittedTasks.add(section.getChunkX()); + this.concurrentlySubmittedTasks.add(section.getChunkY()); + this.concurrentlySubmittedTasks.add(section.getChunkZ()); + } } } - return type; + return typeChanged; } public void scheduleSort(long sectionPos, boolean isDirectTrigger) { @@ -1095,7 +1095,7 @@ public void scheduleRebuild(int x, int y, int z, boolean important) { if (section != null && section.isBuilt()) { ChunkUpdateType pendingUpdate; - if (allowImportantRebuilds() && (important || this.shouldPrioritizeTask(section, NEARBY_REBUILD_DISTANCE))) { + if (important || this.shouldPrioritizeTask(section, NEARBY_REBUILD_DISTANCE)) { pendingUpdate = ChunkUpdateType.IMPORTANT_REBUILD; } else { pendingUpdate = ChunkUpdateType.REBUILD; @@ -1106,11 +1106,15 @@ public void scheduleRebuild(int x, int y, int z, boolean important) { } private boolean shouldPrioritizeTask(RenderSection section, float distance) { - return this.cameraBlockPos != null && section.getSquaredDistance(this.cameraBlockPos) < distance; + return this.cameraPosition != null && section.getSquaredDistance( + (float) this.cameraPosition.x(), + (float) this.cameraPosition.y(), + (float) this.cameraPosition.z() + ) < distance; } - private static boolean allowImportantRebuilds() { - return !SodiumClientMod.options().performance.alwaysDeferChunkUpdates; + private static boolean alwaysDeferImportantRebuilds() { + return SodiumClientMod.options().performance.alwaysDeferChunkUpdates; } private float getEffectiveRenderDistance() { From 068332e5eec44d7f6abbabdb050fc13f2a986302 Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 3 Jan 2025 19:48:58 +0100 Subject: [PATCH 70/81] rename updateCameraState to something more descriptive of its actual function --- .../mods/sodium/client/render/SodiumWorldRenderer.java | 2 +- .../mods/sodium/client/render/chunk/RenderSectionManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index 9040a9342b..6ba82627f9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -212,7 +212,7 @@ public void setupTerrain(Camera camera, this.lastFogDistance = fogDistance; - this.renderSectionManager.updateCameraState(pos, camera); + this.renderSectionManager.prepareFrame(pos); if (cameraLocationChanged) { profiler.popPush("translucent_triggering"); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 6528577e24..0767dc9e3a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -152,7 +152,7 @@ public RenderSectionManager(ClientLevel level, int renderDistance, CommandList c } } - public void updateCameraState(Vector3dc cameraPosition, Camera camera) { + public void prepareFrame(Vector3dc cameraPosition) { var now = System.nanoTime(); this.lastFrameDuration = now - this.lastFrameAtTime; this.lastFrameAtTime = now; From 2864a170cfd5c37b48eda2f4e9a5907b36aac38a Mon Sep 17 00:00:00 2001 From: douira Date: Fri, 3 Jan 2025 19:54:16 +0100 Subject: [PATCH 71/81] adjust pending task scheduling time priority factor --- .../sodium/client/render/chunk/lists/PendingTaskCollector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index eb48a88bee..ef4a617a6c 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -15,7 +15,7 @@ public class PendingTaskCollector implements OcclusionCuller.GraphOcclusionVisit // tunable parameters for the priority calculation. // each "gained" point means a reduction in the final priority score (lowest score processed first) - static final float PENDING_TIME_FACTOR = -1.0f / 500_000_000.0f; // 1 point gained per 500ms + static final float PENDING_TIME_FACTOR = -1.0f / 5_000_000_000.0f; // 1 point gained per 5s static final float WITHIN_FRUSTUM_BIAS = -3.0f; // points for being within the frustum static final float PROXIMITY_FACTOR = 3.0f; // penalty for being far away static final float CLOSE_DISTANCE = 50.0f; // distance at which another proximity bonus is applied From fc9ee4f1e56f9a7759856ded83f024cf916d8aa6 Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 4 Jan 2025 00:49:05 +0100 Subject: [PATCH 72/81] consolidate tests that check whether a render section has anything to render into one method, fix bugs with disappearing entities and not rendered newly non-empty sections by refactoring is present code in SectionTree to use a predicate that checks for section emptiness --- .../client/render/chunk/ChunkUpdateType.java | 7 ++-- .../client/render/chunk/RenderSection.java | 12 +++---- .../render/chunk/RenderSectionFlags.java | 4 +++ .../render/chunk/RenderSectionManager.java | 35 +++++++++++++------ .../lists/VisibleChunkCollectorSync.java | 2 +- .../occlusion/RayOcclusionSectionTree.java | 6 ++-- .../render/chunk/occlusion/SectionTree.java | 25 +++++++++---- .../render/chunk/region/RenderRegion.java | 4 +++ .../client/render/chunk/tree/Forest.java | 4 +++ 9 files changed, 69 insertions(+), 30 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java index 079c5649ff..ce2ed991b2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java @@ -36,8 +36,11 @@ public enum ChunkUpdateType { * @param next New update type * @return Promoted update type or {@code null} if the update type is the same or less important */ - public static ChunkUpdateType getPromotionUpdateType(ChunkUpdateType prev, ChunkUpdateType next) { - if (prev == null || prev == SORT || prev == next) { + public static ChunkUpdateType getPromotedTypeChange(ChunkUpdateType prev, ChunkUpdateType next) { + if (prev == next) { + return null; + } + if (prev == null || prev == SORT || prev == INITIAL_BUILD) { return next; } if (next == IMPORTANT_REBUILD diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java index 0091976649..faac0e1e5b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSection.java @@ -199,14 +199,6 @@ public int getOriginZ() { return this.chunkZ << 4; } - /** - * @return The squared distance from the center of this chunk in the level to the center of the block position - * given by {@param pos} - */ - public float getSquaredDistance(BlockPos pos) { - return this.getSquaredDistance(pos.getX() + 0.5f, pos.getY() + 0.5f, pos.getZ() + 0.5f); - } - /** * @return The squared distance from the center of this chunk to the given block position */ @@ -275,6 +267,10 @@ public RenderRegion getRegion() { return this.region; } + public boolean needsRender() { + return this.region.sectionNeedsRender(this.sectionIndex); + } + public void setLastVisibleSearchToken(int frame) { this.lastVisibleSearchToken = frame; } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java index 1f23664750..c52f14035a 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionFlags.java @@ -13,4 +13,8 @@ public class RenderSectionFlags { public static final int MASK_NEEDS_RENDER = MASK_HAS_BLOCK_GEOMETRY | MASK_HAS_BLOCK_ENTITIES | MASK_HAS_ANIMATED_SPRITES; public static final int NONE = 0; + + public static boolean needsRender(int flags) { + return (flags & MASK_NEEDS_RENDER) != 0; + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 0767dc9e3a..867c89e6d8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -613,9 +613,20 @@ public void tickVisibleRenders() { } } + private boolean isSectionEmpty(int x, int y, int z) { + long key = SectionPos.asLong(x, y, z); + RenderSection section = this.sectionByPosition.get(key); + + if (section == null) { + return true; + } + + return !section.needsRender(); + } + // renderTree is not necessarily frustum-filtered but that is ok since the caller makes sure to eventually also perform a frustum test on the box being tested (see EntityRendererMixin) public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { - return this.renderTree == null || this.renderTree.isBoxVisible(x1, y1, z1, x2, y2, z2); + return this.renderTree == null || this.renderTree.isBoxVisible(x1, y1, z1, x2, y2, z2, this::isSectionEmpty); } public void uploadChunks() { @@ -682,7 +693,7 @@ private boolean processChunkBuildResults(ArrayList results) { } private boolean updateSectionInfo(RenderSection render, BuiltSectionInfo info) { - if (info == null || (info.flags & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) { + if (info == null || !RenderSectionFlags.needsRender(info.flags)) { this.renderableSectionTree.remove(render); } else { this.renderableSectionTree.add(render); @@ -895,6 +906,7 @@ private long submitImportantSectionTasks(ChunkJobCollector collector, long remai var section = it.next(); var pendingUpdate = section.getPendingUpdate(); if (pendingUpdate != null && pendingUpdate.getDeferMode(alwaysDeferImportantRebuilds) == deferMode) { + // isSectionVisible includes a special case for not testing empty sections against the tree as they won't be in it if (this.renderTree == null || this.renderTree.isSectionVisible(viewport, section)) { remainingUploadSize -= submitSectionTask(collector, section, pendingUpdate); } else { @@ -1039,16 +1051,19 @@ public int getVisibleChunkCount() { private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateType type) { var current = section.getPendingUpdate(); - var typeChanged = ChunkUpdateType.getPromotionUpdateType(current, type); + type = ChunkUpdateType.getPromotedTypeChange(current, type); - if (typeChanged != null) { - // when the pending task type changes, and it's important, add it to the list of important tasks - if (current != type && type.isImportant()) { - this.importantTasks.get(type.getDeferMode(alwaysDeferImportantRebuilds())).add(section); - } + // if there was no change the upgraded type is null + if (type == null) { + return null; + } - section.setPendingUpdate(type, this.lastFrameAtTime); + section.setPendingUpdate(type, this.lastFrameAtTime); + // when the pending task type changes, and it's important, add it to the list of important tasks + if (type.isImportant()) { + this.importantTasks.get(type.getDeferMode(alwaysDeferImportantRebuilds())).add(section); + } else { // if the section received a new task, mark in the task tree so an update can happen before a global cull task runs if (this.globalTaskTree != null && current == null) { this.globalTaskTree.markSectionTask(section); @@ -1065,7 +1080,7 @@ private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateT } } - return typeChanged; + return type; } public void scheduleSort(long sectionPos, boolean isDirectTrigger) { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java index e69a647cb7..c307cd334d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/VisibleChunkCollectorSync.java @@ -36,7 +36,7 @@ public void visit(RenderSection section) { } var index = section.getSectionIndex(); - if ((region.getSectionFlags(index) & RenderSectionFlags.MASK_NEEDS_RENDER) != 0) { + if (region.sectionNeedsRender(index)) { renderList.add(index); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java index 1f0ccfda79..c5f2c75b79 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -30,13 +30,13 @@ public RayOcclusionSectionTree(Viewport viewport, float buildDistance, int frame @Override public boolean visitTestVisible(RenderSection section) { - if ((section.getRegion().getSectionFlags(section.getSectionIndex()) & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) { - this.lastSectionKnownEmpty = true; - } else { + if (section.needsRender()) { this.lastSectionKnownEmpty = false; if (this.isRayBlockedStepped(section)) { return false; } + } else { + this.lastSectionKnownEmpty = true; } return super.visitTestVisible(section); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java index 3c3c8a5770..3372fe00a7 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/SectionTree.java @@ -71,7 +71,7 @@ public void visit(RenderSection section) { // discard invisible or sections that don't need to be rendered, // only perform this test if it hasn't already been done before - if (this.lastSectionKnownEmpty || (section.getRegion().getSectionFlags(section.getSectionIndex()) & RenderSectionFlags.MASK_NEEDS_RENDER) == 0) { + if (this.lastSectionKnownEmpty || !section.needsRender()) { return; } @@ -86,7 +86,7 @@ public void prepareForTraversal() { this.tree.prepareForTraversal(); } - public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2) { + public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y2, double z2, NotInTreePredicate notInTreePredicate) { // check if there's a section at any part of the box int minX = SectionPos.posToSectionCoord(x1 - 0.5D); int minY = SectionPos.posToSectionCoord(y1 - 0.5D); @@ -96,10 +96,21 @@ public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y int maxY = SectionPos.posToSectionCoord(y2 + 0.5D); int maxZ = SectionPos.posToSectionCoord(z2 + 0.5D); + // check if any of the sections in the box are present, and then check the predicate as it's likely more expensive than fetching from the bitmask for (int x = minX; x <= maxX; x++) { for (int z = minZ; z <= maxZ; z++) { for (int y = minY; y <= maxY; y++) { - if (this.isSectionPresent(x, y, z)) { + if (this.tree.isSectionPresent(x, y, z)) { + return true; + } + } + } + } + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + for (int y = minY; y <= maxY; y++) { + if (notInTreePredicate.isEmpty(x, y, z)) { return true; } } @@ -109,12 +120,14 @@ public boolean isBoxVisible(double x1, double y1, double z1, double x2, double y return false; } - private boolean isSectionPresent(int x, int y, int z) { - return this.tree.getPresence(x, y, z) == Tree.PRESENT; + @FunctionalInterface + public interface NotInTreePredicate { + boolean isEmpty(int x, int y, int z); } public boolean isSectionVisible(Viewport viewport, RenderSection section) { - return this.isSectionPresent(section.getChunkX(), section.getChunkY(), section.getChunkZ()) && + // empty sections are not tested against the tree because the tree is not aware of them + return (!section.needsRender() || this.tree.isSectionPresent(section.getChunkX(), section.getChunkY(), section.getChunkZ())) && this.isWithinFrustum(viewport, section); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java index 8149845e74..5cd473b47b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/region/RenderRegion.java @@ -196,6 +196,10 @@ public int getSectionFlags(int id) { return this.sectionFlags[id]; } + public boolean sectionNeedsRender(int id) { + return RenderSectionFlags.needsRender(this.sectionFlags[id]); + } + /** * Returns the collection of block entities contained by this rendered chunk, which are not part of its culling * volume. These entities should always be rendered regardless of the render being visible in the frustum. diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java index 98623debe8..9fbbd3b471 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Forest.java @@ -10,4 +10,8 @@ default void add(RenderSection section) { } int getPresence(int x, int y, int z); + + default boolean isSectionPresent(int x, int y, int z) { + return this.getPresence(x, y, z) == Tree.PRESENT; + } } From 9c96385a5da09d61a0415d1125f65127000b9285 Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 4 Jan 2025 03:30:31 +0100 Subject: [PATCH 73/81] add option for changing the tri-state defer mode, rename "important" flag from MC to "playerChanged" for clarity, don't schedule tasks as important if they're no longer in range and schedule tasks during cull task even if they're important to make sure tasks that were downgraded in importance are still processed eventually --- .../client/gui/SodiumGameOptionPages.java | 11 +++++----- .../sodium/client/gui/SodiumGameOptions.java | 4 ++-- .../client/render/SodiumWorldRenderer.java | 12 +++++------ .../client/render/chunk/ChunkUpdateType.java | 5 ++--- .../sodium/client/render/chunk/DeferMode.java | 20 +++++++++++++++++-- .../render/chunk/RenderSectionManager.java | 19 +++++++----------- .../chunk/lists/PendingTaskCollector.java | 4 ++-- .../resources/assets/sodium/lang/en_us.json | 7 +++++-- 8 files changed, 48 insertions(+), 34 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java index 6a43897da0..991ad81d35 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptionPages.java @@ -14,6 +14,7 @@ import net.caffeinemc.mods.sodium.client.gui.options.storage.MinecraftOptionsStorage; import net.caffeinemc.mods.sodium.client.gui.options.storage.SodiumOptionsStorage; import net.caffeinemc.mods.sodium.client.compatibility.workarounds.Workarounds; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import net.minecraft.client.AttackIndicatorStatus; import net.minecraft.client.InactivityFpsLimit; @@ -279,12 +280,12 @@ public static OptionPage performance() { .setFlags(OptionFlag.REQUIRES_RENDERER_RELOAD) .build() ) - .add(OptionImpl.createBuilder(boolean.class, sodiumOpts) - .setName(Component.translatable("sodium.options.always_defer_chunk_updates.name")) - .setTooltip(Component.translatable("sodium.options.always_defer_chunk_updates.tooltip")) - .setControl(TickBoxControl::new) + .add(OptionImpl.createBuilder(DeferMode.class, sodiumOpts) + .setName(Component.translatable("sodium.options.defer_chunk_updates.name")) + .setTooltip(Component.translatable("sodium.options.defer_chunk_updates.tooltip")) + .setControl(option -> new CyclingControl<>(option, DeferMode.class)) .setImpact(OptionImpact.HIGH) - .setBinding((opts, value) -> opts.performance.alwaysDeferChunkUpdates = value, opts -> opts.performance.alwaysDeferChunkUpdates) + .setBinding((opts, value) -> opts.performance.chunkBuildDeferMode = value, opts -> opts.performance.chunkBuildDeferMode) .setFlags(OptionFlag.REQUIRES_RENDERER_UPDATE) .build()) .build() diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptions.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptions.java index ec681430b8..68c9e8842d 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptions.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/gui/SodiumGameOptions.java @@ -5,6 +5,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.annotations.SerializedName; import net.caffeinemc.mods.sodium.client.gui.options.TextProvider; +import net.caffeinemc.mods.sodium.client.render.chunk.DeferMode; import net.caffeinemc.mods.sodium.client.services.PlatformRuntimeInformation; import net.caffeinemc.mods.sodium.client.util.FileUtil; import net.caffeinemc.mods.sodium.client.render.chunk.translucent_sorting.SortBehavior; @@ -37,8 +38,7 @@ public static SodiumGameOptions defaults() { public static class PerformanceSettings { public int chunkBuilderThreads = 0; - @SerializedName("always_defer_chunk_updates_v2") // this will reset the option in older configs - public boolean alwaysDeferChunkUpdates = true; + public DeferMode chunkBuildDeferMode = DeferMode.ALWAYS; public boolean animateOnlyVisibleTextures = true; public boolean useEntityCulling = true; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java index 6ba82627f9..cce2cedb13 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/SodiumWorldRenderer.java @@ -516,18 +516,18 @@ public String getChunksDebugString() { /** * Schedules chunk rebuilds for all chunks in the specified block region. */ - public void scheduleRebuildForBlockArea(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean important) { - this.scheduleRebuildForChunks(minX >> 4, minY >> 4, minZ >> 4, maxX >> 4, maxY >> 4, maxZ >> 4, important); + public void scheduleRebuildForBlockArea(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean playerChanged) { + this.scheduleRebuildForChunks(minX >> 4, minY >> 4, minZ >> 4, maxX >> 4, maxY >> 4, maxZ >> 4, playerChanged); } /** * Schedules chunk rebuilds for all chunks in the specified chunk region. */ - public void scheduleRebuildForChunks(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean important) { + public void scheduleRebuildForChunks(int minX, int minY, int minZ, int maxX, int maxY, int maxZ, boolean playerChanged) { for (int chunkX = minX; chunkX <= maxX; chunkX++) { for (int chunkY = minY; chunkY <= maxY; chunkY++) { for (int chunkZ = minZ; chunkZ <= maxZ; chunkZ++) { - this.scheduleRebuildForChunk(chunkX, chunkY, chunkZ, important); + this.scheduleRebuildForChunk(chunkX, chunkY, chunkZ, playerChanged); } } } @@ -536,8 +536,8 @@ public void scheduleRebuildForChunks(int minX, int minY, int minZ, int maxX, int /** * Schedules a chunk rebuild for the render belonging to the given chunk section position. */ - public void scheduleRebuildForChunk(int x, int y, int z, boolean important) { - this.renderSectionManager.scheduleRebuild(x, y, z, important); + public void scheduleRebuildForChunk(int x, int y, int z, boolean playerChanged) { + this.renderSectionManager.scheduleRebuild(x, y, z, playerChanged); } public Collection getDebugStrings() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java index ce2ed991b2..46fc02a7aa 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/ChunkUpdateType.java @@ -55,9 +55,8 @@ public boolean isImportant() { return this.important; } - public DeferMode getDeferMode(boolean deferImportantRebuilds) { - // use defer mode ALWAYS if important rebuilds are configured to be always deferred - return deferImportantRebuilds && this == IMPORTANT_REBUILD ? DeferMode.ALWAYS : this.deferMode; + public DeferMode getDeferMode(DeferMode importantRebuildDeferMode) { + return this == IMPORTANT_REBUILD ? importantRebuildDeferMode : this.deferMode; } public float getPriorityValue() { diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java index 989ffbfdd7..978e745fe2 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/DeferMode.java @@ -1,7 +1,23 @@ package net.caffeinemc.mods.sodium.client.render.chunk; -public enum DeferMode { - ALWAYS, ONE_FRAME, ZERO_FRAMES; +import net.caffeinemc.mods.sodium.client.gui.options.TextProvider; +import net.minecraft.network.chat.Component; + +public enum DeferMode implements TextProvider { + ALWAYS("sodium.options.defer_chunk_updates.always"), + ONE_FRAME("sodium.options.defer_chunk_updates.one_frame"), + ZERO_FRAMES("sodium.options.defer_chunk_updates.zero_frames"); + + private final Component name; + + DeferMode(String name) { + this.name = Component.translatable(name); + } + + @Override + public Component getLocalizedName() { + return this.name; + } public boolean allowsUnlimitedUploadSize() { return this == ZERO_FRAMES; diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 867c89e6d8..95e5e9bacd 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -900,17 +900,17 @@ private void submitDeferredSectionTasks(ChunkJobCollector collector, long remain private long submitImportantSectionTasks(ChunkJobCollector collector, long remainingUploadSize, DeferMode deferMode, Viewport viewport) { var it = this.importantTasks.get(deferMode).iterator(); - var alwaysDeferImportantRebuilds = alwaysDeferImportantRebuilds(); + var importantRebuildDeferMode = SodiumClientMod.options().performance.chunkBuildDeferMode; while (it.hasNext() && collector.hasBudgetRemaining() && (deferMode.allowsUnlimitedUploadSize() || remainingUploadSize > 0)) { var section = it.next(); var pendingUpdate = section.getPendingUpdate(); - if (pendingUpdate != null && pendingUpdate.getDeferMode(alwaysDeferImportantRebuilds) == deferMode) { + if (pendingUpdate != null && pendingUpdate.getDeferMode(importantRebuildDeferMode) == deferMode && this.shouldPrioritizeTask(section, NEARBY_SORT_DISTANCE)) { // isSectionVisible includes a special case for not testing empty sections against the tree as they won't be in it if (this.renderTree == null || this.renderTree.isSectionVisible(viewport, section)) { remainingUploadSize -= submitSectionTask(collector, section, pendingUpdate); } else { - // don't remove if simply not visible but still needs to be run + // don't remove if simply not visible currently but still relevant continue; } } @@ -1062,7 +1062,7 @@ private ChunkUpdateType upgradePendingUpdate(RenderSection section, ChunkUpdateT // when the pending task type changes, and it's important, add it to the list of important tasks if (type.isImportant()) { - this.importantTasks.get(type.getDeferMode(alwaysDeferImportantRebuilds())).add(section); + this.importantTasks.get(type.getDeferMode(SodiumClientMod.options().performance.chunkBuildDeferMode)).add(section); } else { // if the section received a new task, mark in the task tree so an update can happen before a global cull task runs if (this.globalTaskTree != null && current == null) { @@ -1089,8 +1089,7 @@ public void scheduleSort(long sectionPos, boolean isDirectTrigger) { if (section != null) { var pendingUpdate = ChunkUpdateType.SORT; var priorityMode = SodiumClientMod.options().performance.getSortBehavior().getPriorityMode(); - if (priorityMode == PriorityMode.ALL - || priorityMode == PriorityMode.NEARBY && this.shouldPrioritizeTask(section, NEARBY_SORT_DISTANCE)) { + if (priorityMode == PriorityMode.NEARBY && this.shouldPrioritizeTask(section, NEARBY_SORT_DISTANCE) || priorityMode == PriorityMode.ALL) { pendingUpdate = ChunkUpdateType.IMPORTANT_SORT; } @@ -1100,7 +1099,7 @@ public void scheduleSort(long sectionPos, boolean isDirectTrigger) { } } - public void scheduleRebuild(int x, int y, int z, boolean important) { + public void scheduleRebuild(int x, int y, int z, boolean playerChanged) { RenderAsserts.validateCurrentThread(); this.sectionCache.invalidate(x, y, z); @@ -1110,7 +1109,7 @@ public void scheduleRebuild(int x, int y, int z, boolean important) { if (section != null && section.isBuilt()) { ChunkUpdateType pendingUpdate; - if (important || this.shouldPrioritizeTask(section, NEARBY_REBUILD_DISTANCE)) { + if (playerChanged && this.shouldPrioritizeTask(section, NEARBY_REBUILD_DISTANCE)) { pendingUpdate = ChunkUpdateType.IMPORTANT_REBUILD; } else { pendingUpdate = ChunkUpdateType.REBUILD; @@ -1128,10 +1127,6 @@ private boolean shouldPrioritizeTask(RenderSection section, float distance) { ) < distance; } - private static boolean alwaysDeferImportantRebuilds() { - return SodiumClientMod.options().performance.alwaysDeferChunkUpdates; - } - private float getEffectiveRenderDistance() { var alpha = RenderSystem.getShaderFog().alpha(); var distance = RenderSystem.getShaderFog().end(); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java index ef4a617a6c..92fa3f3f0b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/lists/PendingTaskCollector.java @@ -67,8 +67,8 @@ public void visit(RenderSection section) { protected void checkForTask(RenderSection section) { ChunkUpdateType type = section.getPendingUpdate(); - // collect non-important tasks (important tasks are handled separately) - if (type != null && !type.isImportant() && section.getTaskCancellationToken() == null) { + // collect tasks even if they're important, whether they're actually important is decided later + if (type != null && section.getTaskCancellationToken() == null) { this.addPendingSection(section, type); } } diff --git a/common/src/main/resources/assets/sodium/lang/en_us.json b/common/src/main/resources/assets/sodium/lang/en_us.json index d1d1e6b6a7..0ae812fc55 100644 --- a/common/src/main/resources/assets/sodium/lang/en_us.json +++ b/common/src/main/resources/assets/sodium/lang/en_us.json @@ -48,8 +48,11 @@ "sodium.options.use_persistent_mapping.tooltip": "For debugging only. If enabled, persistent memory mappings will be used for the staging buffer so that unnecessary memory copies can be avoided. Disabling this can be useful for narrowing down the cause of graphical corruption.\n\nRequires OpenGL 4.4 or ARB_buffer_storage.", "sodium.options.chunk_update_threads.name": "Chunk Update Threads", "sodium.options.chunk_update_threads.tooltip": "Specifies the number of threads to use for chunk building and sorting. Using more threads can speed up chunk loading and update speed, but may negatively impact frame times. The default value is usually good enough for all situations.", - "sodium.options.always_defer_chunk_updates.name": "Always Defer Chunk Updates", - "sodium.options.always_defer_chunk_updates.tooltip": "If enabled, rendering will never wait for chunk updates to finish, even if they are important. This can greatly improve frame rates in some scenarios, but it may create significant visual lag where blocks take a while to appear or disappear.", + "sodium.options.defer_chunk_updates.name": "Defer Chunk Updates", + "sodium.options.defer_chunk_updates.tooltip": "If set to \"Always\", rendering will never wait for nearby chunk updates to finish, even if they are important. This can greatly improve frame rates in some scenarios, but it may create significant visual lag where blocks take a while to appear or disappear. \"Zero Frames\" eliminates visual lag by blocking the frame until chunk updates are complete while \"One Frame\" allows at most one frame of visual lag.", + "sodium.options.defer_chunk_updates.always": "Always", + "sodium.options.defer_chunk_updates.one_frame": "One Frame", + "sodium.options.defer_chunk_updates.zero_frames": "Zero Frames", "sodium.options.sort_behavior.name": "Translucency Sorting", "sodium.options.sort_behavior.tooltip": "Enables translucency sorting. This avoids glitches in translucent blocks like water and glass when enabled and attempts to correctly present them even when the camera is in motion. This has a small performance impact on chunk loading and update speeds, but is usually not noticeable in frame rates.", "sodium.options.use_no_error_context.name": "Use No Error Context", From 6bb25a659869f8b6918735f5666c5d19473051d0 Mon Sep 17 00:00:00 2001 From: douira Date: Sat, 4 Jan 2025 03:31:17 +0100 Subject: [PATCH 74/81] update job duration factor faster to keep up with JVM warmup at first world load --- .../render/chunk/compile/estimation/JobDurationEstimator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java index d2cb6c85a7..a73b5b34ae 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java @@ -1,7 +1,7 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; public class JobDurationEstimator extends CategoryFactorEstimator> { - public static final float NEW_DATA_FACTOR = 0.01f; + public static final float NEW_DATA_FACTOR = 0.07f; private static final long INITIAL_JOB_DURATION_ESTIMATE = 5_000_000L; public JobDurationEstimator() { From 0a8cc9e2cfdd757f01ecfac2e7450245e18e0ee0 Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 5 Jan 2025 04:20:46 +0100 Subject: [PATCH 75/81] rewrite estimation code to split into 1d average and 2d linear regression, this improves scheduling accuracy and doesn't allow the estimator to get biased by fast tasks on small sections --- .../render/chunk/RenderSectionManager.java | 14 +- .../estimation/Average1DEstimator.java | 82 +++++++++++ .../estimation/CategoryFactorEstimator.java | 90 ------------ .../chunk/compile/estimation/Estimator.java | 83 +++++++++++ .../estimation/JobDurationEstimator.java | 20 ++- .../chunk/compile/estimation/JobEffort.java | 13 +- .../compile/estimation/Linear2DEstimator.java | 136 ++++++++++++++++++ .../compile/estimation/MeshResultSize.java | 11 +- .../estimation/MeshTaskSizeEstimator.java | 15 +- 9 files changed, 339 insertions(+), 125 deletions(-) create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java delete mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java create mode 100644 common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index 95e5e9bacd..f1ab39ea74 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -107,7 +107,7 @@ public class RenderSectionManager { private long lastFrameDuration = -1; private long averageFrameDuration = -1; private long lastFrameAtTime = System.nanoTime(); - private static final float AVERAGE_FRAME_DURATION_FACTOR = 0.05f; + private static final float FRAME_DURATION_UPDATE_RATIO = 0.05f; private boolean needsGraphUpdate = true; private boolean needsRenderListUpdate = true; @@ -159,7 +159,7 @@ public void prepareFrame(Vector3dc cameraPosition) { if (this.averageFrameDuration == -1) { this.averageFrameDuration = this.lastFrameDuration; } else { - this.averageFrameDuration = MathUtil.exponentialMovingAverage(this.averageFrameDuration, this.lastFrameDuration, AVERAGE_FRAME_DURATION_FACTOR); + this.averageFrameDuration = MathUtil.exponentialMovingAverage(this.averageFrameDuration, this.lastFrameDuration, FRAME_DURATION_UPDATE_RATIO); } this.averageFrameDuration = Mth.clamp(this.averageFrameDuration, 1_000_100, 100_000_000); @@ -662,7 +662,7 @@ private boolean processChunkBuildResults(ArrayList results) { var resultSize = chunkBuildOutput.getResultSize(); result.render.setLastMeshResultSize(resultSize); - this.meshTaskSizeEstimator.addBatchEntry(MeshResultSize.forSection(result.render, resultSize)); + this.meshTaskSizeEstimator.addData(MeshResultSize.forSection(result.render, resultSize)); if (chunkBuildOutput.translucentData != null) { this.sortTriggering.integrateTranslucentData(oldData, chunkBuildOutput.translucentData, this.cameraPosition, this::scheduleSort); @@ -687,7 +687,7 @@ private boolean processChunkBuildResults(ArrayList results) { result.render.setLastUploadFrame(result.submitTime); } - this.meshTaskSizeEstimator.flushNewData(); + this.meshTaskSizeEstimator.updateModels(); return touchedSectionInfo; } @@ -737,11 +737,11 @@ private ArrayList collectChunkBuildResults() { results.add(result.unwrap()); var jobEffort = result.getJobEffort(); if (jobEffort != null) { - this.jobDurationEstimator.addBatchEntry(jobEffort); + this.jobDurationEstimator.addData(jobEffort); } } - this.jobDurationEstimator.flushNewData(); + this.jobDurationEstimator.updateModels(); return results; } @@ -1214,7 +1214,7 @@ public Collection getDebugStrings() { var sizeEstimates = new StringBuilder(); for (var type : MeshResultSize.SectionCategory.values()) { - sizeEstimates.append(String.format("%s=%d, ", type, this.meshTaskSizeEstimator.estimateAWithB(type, 1))); + sizeEstimates.append(String.format("%s=%d, ", type, this.meshTaskSizeEstimator.predict(type))); } list.add(String.format("Size: %s", sizeEstimates)); } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java new file mode 100644 index 0000000000..b90709d8c7 --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java @@ -0,0 +1,82 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import net.caffeinemc.mods.sodium.client.util.MathUtil; + +public abstract class Average1DEstimator extends Estimator, Average1DEstimator.ValueBatch, Void, Long, Average1DEstimator.Average> { + private final float newDataRatio; + private final long initialEstimate; + + public Average1DEstimator(float newDataRatio, long initialEstimate) { + this.newDataRatio = newDataRatio; + this.initialEstimate = initialEstimate; + } + + public interface Value extends DataPoint { + long value(); + } + + protected static class ValueBatch implements Estimator.DataBatch> { + private long valueSum; + private long count; + + @Override + public void addDataPoint(Value input) { + this.valueSum += input.value(); + this.count++; + } + + @Override + public void reset() { + this.valueSum = 0; + this.count = 0; + } + + public float getAverage() { + return ((float) this.valueSum) / this.count; + } + } + + @Override + protected ValueBatch createNewDataBatch() { + return new ValueBatch<>(); + } + + protected static class Average implements Estimator.Model, Average> { + private final float newDataRatio; + private boolean hasRealData = false; + private float average; + + public Average(float newDataRatio, float initialValue) { + this.average = initialValue; + this.newDataRatio = newDataRatio; + } + + @Override + public Average update(ValueBatch batch) { + if (batch.count > 0) { + if (this.hasRealData) { + this.average = MathUtil.exponentialMovingAverage(this.average, batch.getAverage(), this.newDataRatio); + } else { + this.average = batch.getAverage(); + this.hasRealData = true; + } + } + + return this; + } + + @Override + public Long predict(Void input) { + return (long) this.average; + } + } + + @Override + protected Average createNewModel() { + return new Average<>(this.newDataRatio, this.initialEstimate); + } + + public Long predict(C category) { + return super.predict(category, null); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java deleted file mode 100644 index d282062aff..0000000000 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/CategoryFactorEstimator.java +++ /dev/null @@ -1,90 +0,0 @@ -package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; - -import it.unimi.dsi.fastutil.objects.Reference2FloatMap; -import it.unimi.dsi.fastutil.objects.Reference2FloatOpenHashMap; -import it.unimi.dsi.fastutil.objects.Reference2ReferenceMap; -import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; -import net.caffeinemc.mods.sodium.client.util.MathUtil; - -public class CategoryFactorEstimator { - private final Reference2FloatMap aPerB = new Reference2FloatOpenHashMap<>(); - private final Reference2ReferenceMap newData = new Reference2ReferenceOpenHashMap<>(); - private final float newDataFactor; - private final long initialAEstimate; - - public CategoryFactorEstimator(float newDataFactor, long initialAEstimate) { - this.newDataFactor = newDataFactor; - this.initialAEstimate = initialAEstimate; - } - - private static class BatchDataAggregation { - private long aSum; - private long bSum; - - public void addDataPoint(long a, long b) { - this.aSum += a; - this.bSum += b; - } - - public void reset() { - this.aSum = 0; - this.bSum = 0; - } - - public float getAPerBFactor() { - return ((float) this.aSum) / this.bSum; - } - } - - public interface BatchEntry { - C getCategory(); - - long getA(); - - long getB(); - } - - public void addBatchEntry(BatchEntry batchEntry) { - var a = batchEntry.getA(); - var b = batchEntry.getB(); - - // skip if b is 0 to prevent Infinity and NaN - if (b == 0) { - return; - } - - var category = batchEntry.getCategory(); - var aggregation = this.newData.get(category); - if (aggregation == null) { - aggregation = new BatchDataAggregation(); - this.newData.put(category, aggregation); - } - aggregation.addDataPoint(a, b); - } - - public void flushNewData() { - this.newData.forEach((category, frameData) -> { - var newFactor = frameData.getAPerBFactor(); - // if there was no data it results in NaN - if (Float.isNaN(newFactor)) { - return; - } - if (this.aPerB.containsKey(category)) { - var oldFactor = this.aPerB.getFloat(category); - var newValue = MathUtil.exponentialMovingAverage(oldFactor, newFactor, this.newDataFactor); - this.aPerB.put(category, newValue); - } else { - this.aPerB.put(category, newFactor); - } - frameData.reset(); - }); - } - - public long estimateAWithB(C category, long b) { - if (this.aPerB.containsKey(category)) { - return (long) (this.aPerB.getFloat(category) * b); - } else { - return this.initialAEstimate; - } - } -} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java new file mode 100644 index 0000000000..1d5a641ecb --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java @@ -0,0 +1,83 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import java.util.Map; + +/** + * This generic model learning class that can be used to estimate values based on a set of data points. It performs batch-wise model updates. The actual data aggregation and model updates are delegated to the implementing classes. The estimator stores multiple models in a map, one for each category. + * + * @param The type of the category key + * @param A data point contains a category and one piece of data + * @param A data batch contains multiple data points + * @param The input to the model + * @param The output of the model + * @param The model that is used to predict values + */ +public abstract class Estimator< + C, + D extends Estimator.DataPoint, + B extends Estimator.DataBatch, + I, + O, + M extends Estimator.Model> { + protected final Map models = createMap(); + protected final Map batches = createMap(); + + protected interface DataBatch { + void addDataPoint(D input); + + void reset(); + } + + protected interface DataPoint { + C category(); + } + + protected interface Model> { + M update(B batch); + + O predict(I input); + } + + protected abstract B createNewDataBatch(); + + protected abstract M createNewModel(); + + protected abstract Map createMap(); + + public void addData(D data) { + var category = data.category(); + var batch = this.batches.get(category); + if (batch == null) { + batch = this.createNewDataBatch(); + this.batches.put(category, batch); + } + batch.addDataPoint(data); + } + + private M ensureModel(C category) { + var model = this.models.get(category); + if (model == null) { + model = this.createNewModel(); + this.models.put(category, model); + } + return model; + } + + public void updateModels() { + this.batches.forEach((category, aggregator) -> { + var oldModel = this.ensureModel(category); + + // update the model and store it back if it returned a new model + var newModel = oldModel.update(aggregator); + if (newModel != oldModel) { + this.models.put(category, newModel); + } + + aggregator.reset(); + }); + } + + public O predict(C category, I input) { + return this.ensureModel(category).predict(input); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java index a73b5b34ae..0f83a902c9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobDurationEstimator.java @@ -1,14 +1,24 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; -public class JobDurationEstimator extends CategoryFactorEstimator> { - public static final float NEW_DATA_FACTOR = 0.07f; - private static final long INITIAL_JOB_DURATION_ESTIMATE = 5_000_000L; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceArrayMap; + +import java.util.Map; + +public class JobDurationEstimator extends Linear2DEstimator> { + public static final int INITIAL_SAMPLE_TARGET = 100; + public static final float NEW_DATA_RATIO = 0.05f; + private static final long INITIAL_JOB_DURATION_ESTIMATE = 5_000_000L; // 5ms public JobDurationEstimator() { - super(NEW_DATA_FACTOR, INITIAL_JOB_DURATION_ESTIMATE); + super(NEW_DATA_RATIO, INITIAL_SAMPLE_TARGET, INITIAL_JOB_DURATION_ESTIMATE); } public long estimateJobDuration(Class jobType, long effort) { - return this.estimateAWithB(jobType, effort); + return this.predict(jobType, effort); + } + + @Override + protected Map, T> createMap() { + return new Reference2ReferenceArrayMap<>(); } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java index 0802e57ccf..2a35dda343 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/JobEffort.java @@ -1,22 +1,17 @@ package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; -public record JobEffort(Class category, long duration, long effort) implements CategoryFactorEstimator.BatchEntry> { +public record JobEffort(Class category, long duration, long effort) implements Linear2DEstimator.DataPair> { public static JobEffort untilNowWithEffort(Class effortType, long start, long effort) { return new JobEffort(effortType,System.nanoTime() - start, effort); } @Override - public Class getCategory() { - return this.category; + public long x() { + return this.effort; } @Override - public long getA() { + public long y() { return this.duration; } - - @Override - public long getB() { - return this.effort; - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java new file mode 100644 index 0000000000..591de3f9ad --- /dev/null +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java @@ -0,0 +1,136 @@ +package net.caffeinemc.mods.sodium.client.render.chunk.compile.estimation; + +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +public abstract class Linear2DEstimator extends Estimator, Linear2DEstimator.LinearRegressionBatch, Long, Long, Linear2DEstimator.LinearFunction> { + private final float newDataRatio; + private final int initialSampleTarget; + private final long initialOutput; + + public Linear2DEstimator(float newDataRatio, int initialSampleTarget, long initialOutput) { + this.newDataRatio = newDataRatio; + this.initialSampleTarget = initialSampleTarget; + this.initialOutput = initialOutput; + } + + public interface DataPair extends DataPoint { + long x(); + + long y(); + } + + protected static class LinearRegressionBatch extends ObjectArrayList> implements Estimator.DataBatch> { + @Override + public void addDataPoint(DataPair input) { + this.add(input); + } + + @Override + public void reset() { + this.clear(); + } + } + + @Override + protected LinearRegressionBatch createNewDataBatch() { + return new LinearRegressionBatch<>(); + } + + protected static class LinearFunction implements Model, LinearFunction> { + // the maximum fraction of the total weight that new data can have + private final float newDataRatioInv; + // how many samples we want to have at least before we start diminishing the new data's weight + private final int initialSampleTarget; + private final long initialOutput; + + private float yIntercept; + private float slope; + + private int gatheredSamples = 0; + private float xMeanOld = 0; + private float yMeanOld = 0; + private float covarianceOld = 0; + private float varianceOld = 0; + + public LinearFunction(float newDataRatio, int initialSampleTarget, long initialOutput) { + this.newDataRatioInv = 1.0f / newDataRatio; + this.initialSampleTarget = initialSampleTarget; + this.initialOutput = initialOutput; + } + + @Override + public LinearFunction update(LinearRegressionBatch batch) { + if (batch.isEmpty()) { + return this; + } + + // condition the weight to gather at least the initial sample target, and then weight the new data with a ratio + var newDataSize = batch.size(); + var totalSamples = this.gatheredSamples + newDataSize; + float oldDataWeight; + float totalWeight; + if (totalSamples <= this.initialSampleTarget) { + totalWeight = totalSamples; + oldDataWeight = this.gatheredSamples; + this.gatheredSamples = totalSamples; + } else { + oldDataWeight = newDataSize * this.newDataRatioInv - newDataSize; + totalWeight = oldDataWeight + newDataSize; + } + + var totalWeightInv = 1.0f / totalWeight; + + // calculate the weighted mean along both axes + long xSum = 0; + long ySum = 0; + for (var data : batch) { + xSum += data.x(); + ySum += data.y(); + } + var xMean = (this.xMeanOld * oldDataWeight + xSum) * totalWeightInv; + var yMean = (this.yMeanOld * oldDataWeight + ySum) * totalWeightInv; + + // the covariance and variance are calculated from the differences to the mean + var covarianceSum = 0.0f; + var varianceSum = 0.0f; + for (var data : batch) { + var xDelta = data.x() - xMean; + var yDelta = data.y() - yMean; + covarianceSum += xDelta * yDelta; + varianceSum += xDelta * xDelta; + } + + if (varianceSum == 0) { + return this; + } + + covarianceSum += this.covarianceOld * oldDataWeight; + varianceSum += this.varianceOld * oldDataWeight; + + // negative slopes are clamped to produce a flat line if necessary + this.slope = Math.max(0, covarianceSum / varianceSum); + this.yIntercept = yMean - this.slope * xMean; + + this.xMeanOld = xMean; + this.yMeanOld = yMean; + this.covarianceOld = covarianceSum * totalWeightInv; + this.varianceOld = varianceSum * totalWeightInv; + + return this; + } + + @Override + public Long predict(Long input) { + if (this.gatheredSamples == 0) { + return this.initialOutput; + } + + return (long) (this.yIntercept + this.slope * input); + } + } + + @Override + protected LinearFunction createNewModel() { + return new LinearFunction<>(this.newDataRatio, this.initialSampleTarget, this.initialOutput); + } +} diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java index 1552630efe..e4d976bac0 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshResultSize.java @@ -2,7 +2,7 @@ import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; -public record MeshResultSize(SectionCategory category, long resultSize) implements CategoryFactorEstimator.BatchEntry { +public record MeshResultSize(SectionCategory category, long resultSize) implements Average1DEstimator.Value { public static long NO_DATA = -1; public enum SectionCategory { @@ -33,17 +33,12 @@ public static MeshResultSize forSection(RenderSection section, long resultSize) } @Override - public SectionCategory getCategory() { + public SectionCategory category() { return this.category; } @Override - public long getA() { + public long value() { return this.resultSize; } - - @Override - public long getB() { - return 1; - } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java index 21f10787ca..67809d61d8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/MeshTaskSizeEstimator.java @@ -3,11 +3,14 @@ import net.caffeinemc.mods.sodium.client.render.chunk.RenderSection; import net.caffeinemc.mods.sodium.client.render.chunk.region.RenderRegion; -public class MeshTaskSizeEstimator extends CategoryFactorEstimator { - public static final float NEW_DATA_FACTOR = 0.02f; +import java.util.EnumMap; +import java.util.Map; + +public class MeshTaskSizeEstimator extends Average1DEstimator { + public static final float NEW_DATA_RATIO = 0.02f; public MeshTaskSizeEstimator() { - super(NEW_DATA_FACTOR, RenderRegion.SECTION_BUFFER_ESTIMATE); + super(NEW_DATA_RATIO, RenderRegion.SECTION_BUFFER_ESTIMATE); } public long estimateSize(RenderSection section) { @@ -15,11 +18,11 @@ public long estimateSize(RenderSection section) { if (lastResultSize != MeshResultSize.NO_DATA) { return lastResultSize; } - return this.estimateAWithB(MeshResultSize.SectionCategory.forSection(section), 1); + return this.predict(MeshResultSize.SectionCategory.forSection(section)); } @Override - public void flushNewData() { - super.flushNewData(); + protected Map createMap() { + return new EnumMap<>(MeshResultSize.SectionCategory.class); } } From 29e8ab71d9eba81750c2cf613e4dc08e67eabce4 Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 5 Jan 2025 17:05:39 +0100 Subject: [PATCH 76/81] add correct debug presentation of the estimator models --- .../client/render/chunk/RenderSectionManager.java | 12 ++++++------ .../chunk/compile/estimation/Average1DEstimator.java | 7 +++++++ .../render/chunk/compile/estimation/Estimator.java | 8 ++++++++ .../chunk/compile/estimation/Linear2DEstimator.java | 7 +++++++ 4 files changed, 28 insertions(+), 6 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index f1ab39ea74..fbd948af4f 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -1208,15 +1208,15 @@ public Collection getDebugStrings() { ); if (PlatformRuntimeInformation.getInstance().isDevelopmentEnvironment()) { - var meshTaskDuration = this.jobDurationEstimator.estimateJobDuration(ChunkBuilderMeshingTask.class, 1); - var sortTaskDuration = this.jobDurationEstimator.estimateJobDuration(ChunkBuilderSortingTask.class, 1); - list.add(String.format("Duration: Mesh=%dns, Sort=%dns", meshTaskDuration, sortTaskDuration)); + var meshTaskParameters = this.jobDurationEstimator.toString(ChunkBuilderMeshingTask.class); + var sortTaskParameters = this.jobDurationEstimator.toString(ChunkBuilderSortingTask.class); + list.add(String.format("Duration: Mesh %s, Sort %s", meshTaskParameters, sortTaskParameters)); - var sizeEstimates = new StringBuilder(); + var sizeEstimates = new ReferenceArrayList(); for (var type : MeshResultSize.SectionCategory.values()) { - sizeEstimates.append(String.format("%s=%d, ", type, this.meshTaskSizeEstimator.predict(type))); + sizeEstimates.add(String.format("%s=%s", type, this.meshTaskSizeEstimator.toString(type))); } - list.add(String.format("Size: %s", sizeEstimates)); + list.add(String.format("Size: %s", String.join(", ", sizeEstimates))); } this.sortTriggering.addDebugStrings(list); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java index b90709d8c7..02c3a71eb8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Average1DEstimator.java @@ -2,6 +2,8 @@ import net.caffeinemc.mods.sodium.client.util.MathUtil; +import java.util.Locale; + public abstract class Average1DEstimator extends Estimator, Average1DEstimator.ValueBatch, Void, Long, Average1DEstimator.Average> { private final float newDataRatio; private final long initialEstimate; @@ -69,6 +71,11 @@ public Average update(ValueBatch batch) { public Long predict(Void input) { return (long) this.average; } + + @Override + public String toString() { + return String.format(Locale.US, "%.0f", this.average); + } } @Override diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java index 1d5a641ecb..c9abad07a8 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Estimator.java @@ -80,4 +80,12 @@ public void updateModels() { public O predict(C category, I input) { return this.ensureModel(category).predict(input); } + + public String toString(C category) { + var model = this.models.get(category); + if (model == null) { + return "-"; + } + return model.toString(); + } } diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java index 591de3f9ad..159d08f067 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/compile/estimation/Linear2DEstimator.java @@ -2,6 +2,8 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import java.util.Locale; + public abstract class Linear2DEstimator extends Estimator, Linear2DEstimator.LinearRegressionBatch, Long, Long, Linear2DEstimator.LinearFunction> { private final float newDataRatio; private final int initialSampleTarget; @@ -127,6 +129,11 @@ public Long predict(Long input) { return (long) (this.yIntercept + this.slope * input); } + + @Override + public String toString() { + return String.format(Locale.US, "s=%.2f,y=%.0f", this.slope, this.yIntercept); + } } @Override From fafede32cd7c7794de4cf8d84a6b6da01137a25b Mon Sep 17 00:00:00 2001 From: douira Date: Sun, 5 Jan 2025 18:37:15 +0100 Subject: [PATCH 77/81] improve naming of defer chunk updates option --- .../src/main/resources/assets/sodium/lang/en_us.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/src/main/resources/assets/sodium/lang/en_us.json b/common/src/main/resources/assets/sodium/lang/en_us.json index 0ae812fc55..02cbea6f41 100644 --- a/common/src/main/resources/assets/sodium/lang/en_us.json +++ b/common/src/main/resources/assets/sodium/lang/en_us.json @@ -48,11 +48,11 @@ "sodium.options.use_persistent_mapping.tooltip": "For debugging only. If enabled, persistent memory mappings will be used for the staging buffer so that unnecessary memory copies can be avoided. Disabling this can be useful for narrowing down the cause of graphical corruption.\n\nRequires OpenGL 4.4 or ARB_buffer_storage.", "sodium.options.chunk_update_threads.name": "Chunk Update Threads", "sodium.options.chunk_update_threads.tooltip": "Specifies the number of threads to use for chunk building and sorting. Using more threads can speed up chunk loading and update speed, but may negatively impact frame times. The default value is usually good enough for all situations.", - "sodium.options.defer_chunk_updates.name": "Defer Chunk Updates", - "sodium.options.defer_chunk_updates.tooltip": "If set to \"Always\", rendering will never wait for nearby chunk updates to finish, even if they are important. This can greatly improve frame rates in some scenarios, but it may create significant visual lag where blocks take a while to appear or disappear. \"Zero Frames\" eliminates visual lag by blocking the frame until chunk updates are complete while \"One Frame\" allows at most one frame of visual lag.", - "sodium.options.defer_chunk_updates.always": "Always", - "sodium.options.defer_chunk_updates.one_frame": "One Frame", - "sodium.options.defer_chunk_updates.zero_frames": "Zero Frames", + "sodium.options.defer_chunk_updates.name": "Chunk Updates", + "sodium.options.defer_chunk_updates.tooltip": "If set to \"Deferred\", rendering will never wait for nearby chunk updates to finish, even if they are important. This can greatly improve frame rates in some scenarios, but it may create significant visual lag where blocks take a while to appear or disappear. \"Immediate\" eliminates visual lag by blocking the frame until chunk updates are complete while \"Soon\" allows at most one frame of visual lag.", + "sodium.options.defer_chunk_updates.always": "Deferred", + "sodium.options.defer_chunk_updates.one_frame": "Soon", + "sodium.options.defer_chunk_updates.zero_frames": "Immediate", "sodium.options.sort_behavior.name": "Translucency Sorting", "sodium.options.sort_behavior.tooltip": "Enables translucency sorting. This avoids glitches in translucent blocks like water and glass when enabled and attempts to correctly present them even when the camera is in motion. This has a small performance impact on chunk loading and update speeds, but is usually not noticeable in frame rates.", "sodium.options.use_no_error_context.name": "Use No Error Context", From 7f046e14da00b8bf49b4e4e54dd2c8451d50d685 Mon Sep 17 00:00:00 2001 From: douira Date: Mon, 13 Jan 2025 21:00:10 +0100 Subject: [PATCH 78/81] fix max section bound for ray occlusion tree after Minecraft changed it in 1.21.2 Signed-off-by: douira --- .../client/render/chunk/occlusion/RayOcclusionSectionTree.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java index c5f2c75b79..018b93843b 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/occlusion/RayOcclusionSectionTree.java @@ -106,7 +106,7 @@ private int blockHasObstruction(int x, int y, int z) { y >>= 4; z >>= 4; - if (y < this.minSection || y >= this.maxSection) { + if (y < this.minSection || y > this.maxSection) { return Tree.OUT_OF_BOUNDS; } From 76c8eec605c8665854de551ff2b53d509f66c2fb Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 14 Jan 2025 19:12:06 +0100 Subject: [PATCH 79/81] make tree selection for rendering more robust and avoid waiting for async tasks whenever possible, only perform out of graph fallback when really out of graph Signed-off-by: douira --- .../render/chunk/RenderSectionManager.java | 90 ++++++++++++------- 1 file changed, 59 insertions(+), 31 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java index fbd948af4f..ab08a493be 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/RenderSectionManager.java @@ -302,6 +302,7 @@ private SectionTree unpackTaskResults(Viewport waitingViewport) { case FrustumTaskCollectionTask collectionTask -> this.frustumTaskLists = collectionTask.getResult().getFrustumTaskLists(); default -> { + throw new IllegalStateException("Unexpected task type: " + task); } } } @@ -415,7 +416,8 @@ private void updateFrustumTaskList(Viewport viewport) { private void processRenderListUpdate(Viewport viewport) { // 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; + SectionTree bestValidTree = null; + SectionTree bestAnyTree = null; for (var type : NARROW_TO_WIDE) { var tree = this.cullResults.get(type); if (tree == null) { @@ -424,49 +426,75 @@ private void processRenderListUpdate(Viewport viewport) { // pick the most recent and most valid tree float searchDistance = this.getSearchDistanceForCullType(type); + int treeFrame = tree.getFrame(); + if (bestAnyTree == null || treeFrame > bestAnyTree.getFrame()) { + bestAnyTree = tree; + } if (!tree.isValidFor(viewport, searchDistance)) { continue; } - if (bestTree == null || tree.getFrame() > bestTree.getFrame()) { - bestTree = tree; + if (bestValidTree == null || treeFrame > bestValidTree.getFrame()) { + bestValidTree = tree; } } + // use out-of-graph fallback if the origin section is not loaded and there's no valid tree (missing origin section, empty world) + if (bestValidTree == null && this.isOutOfGraph(viewport.getChunkCoord())) { + this.renderOutOfGraph(viewport); + return; + } + // 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); + if (bestAnyTree == null) { + var result = this.unpackTaskResults(viewport); + if (result != null) { + bestValidTree = result; + } } - // use out-of-graph fallback there's still no result because nothing was scheduled (missing origin section, empty world) - if (bestTree == null) { - var searchDistance = this.getSearchDistance(); - var visitor = new FallbackVisibleChunkCollector(viewport, searchDistance, this.sectionByPosition, this.regions, this.frame); + // use the best valid tree, or even invalid tree if necessary + if (bestValidTree != null) { + bestAnyTree = bestValidTree; + } + if (bestAnyTree == null) { + this.renderOutOfGraph(viewport); + return; + } - this.renderableSectionTree.prepareForTraversal(); - this.renderableSectionTree.traverse(visitor, viewport, searchDistance); + var start = System.nanoTime(); - this.renderLists = visitor.createRenderLists(viewport); - 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(viewport); - - 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 visibleCollector = new VisibleChunkCollectorAsync(this.regions, this.frame); + bestAnyTree.traverse(visibleCollector, viewport, this.getSearchDistance()); + this.renderLists = visibleCollector.createRenderLists(viewport); - this.renderTree = bestTree; + 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 = bestAnyTree; + } + + private void renderOutOfGraph(Viewport viewport) { + var searchDistance = this.getSearchDistance(); + var visitor = new FallbackVisibleChunkCollector(viewport, searchDistance, this.sectionByPosition, this.regions, this.frame); + + this.renderableSectionTree.prepareForTraversal(); + this.renderableSectionTree.traverse(visitor, viewport, searchDistance); + + this.renderLists = visitor.createRenderLists(viewport); + this.frustumTaskLists = visitor.getPendingTaskLists(); + this.globalTaskLists = null; + this.renderTree = null; + } + + private boolean isOutOfGraph(SectionPos pos) { + var sectionY = pos.getY(); + return this.level.getMaxSectionY() <= sectionY && sectionY <= this.level.getMaxSectionY() && !this.sectionByPosition.containsKey(pos.asLong()); } public void markGraphDirty() { From 9f96fd6aa8c93ee8b93c53e0462f91aaf8a5f964 Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 14 Jan 2025 19:13:06 +0100 Subject: [PATCH 80/81] fix concurrent modification of tree hashmap Signed-off-by: douira --- .../client/render/chunk/tree/RemovableMultiForest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java index 5065f33e57..153a76bdb6 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/RemovableMultiForest.java @@ -36,10 +36,12 @@ public void prepareForTraversal() { return; } - for (var tree : this.trees.values()) { + var it = this.trees.values().iterator(); + while (it.hasNext()) { + var tree = it.next(); tree.prepareForTraversal(); if (tree.isEmpty()) { - this.trees.remove(tree.getTreeKey()); + it.remove(); if (this.lastTree == tree) { this.lastTree = null; } From 99bd356ddf8d3eff0bdb3cf71e836bff63df4c6c Mon Sep 17 00:00:00 2001 From: douira Date: Tue, 14 Jan 2025 19:14:40 +0100 Subject: [PATCH 81/81] fix tree index calculation and out of bounds cases with multi-forests Signed-off-by: douira --- .../render/chunk/tree/BaseMultiForest.java | 30 ++++++++++++++----- .../sodium/client/render/chunk/tree/Tree.java | 6 ++-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java index 43485b8bbc..ccf62c58f9 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/BaseMultiForest.java @@ -6,7 +6,7 @@ public abstract class BaseMultiForest extends BaseForest { protected T lastTree; - public BaseMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ,float buildDistance) { + public BaseMultiForest(int baseOffsetX, int baseOffsetY, int baseOffsetZ, float buildDistance) { super(baseOffsetX, baseOffsetY, baseOffsetZ, buildDistance); this.forestDim = forestDimFromBuildDistance(buildDistance); @@ -23,11 +23,13 @@ protected int getTreeIndex(int localX, int localY, int localZ) { var treeY = localY >> 6; var treeZ = localZ >> 6; - return treeX + (treeZ * this.forestDim + treeY) * this.forestDim; - } + if (treeX < 0 || treeX >= this.forestDim || + treeY < 0 || treeY >= this.forestDim || + treeZ < 0 || treeZ >= this.forestDim) { + return Tree.OUT_OF_BOUNDS; + } - protected int getTreeIndexAbsolute(int x, int y, int z) { - return this.getTreeIndex(x - this.baseOffsetX, y - this.baseOffsetY, z - this.baseOffsetZ); + return treeX + (treeZ * this.forestDim + treeY) * this.forestDim; } @Override @@ -41,6 +43,10 @@ public void add(int x, int y, int z) { var localZ = z - this.baseOffsetZ; var treeIndex = this.getTreeIndex(localX, localY, localZ); + if (treeIndex == Tree.OUT_OF_BOUNDS) { + return; + } + var tree = this.trees[treeIndex]; if (tree == null) { @@ -59,18 +65,26 @@ public void add(int x, int y, int z) { public int getPresence(int x, int y, int z) { if (this.lastTree != null) { var result = this.lastTree.getPresence(x, y, z); - if (result != TraversableTree.OUT_OF_BOUNDS) { + if (result != Tree.OUT_OF_BOUNDS) { return result; } } - var treeIndex = this.getTreeIndexAbsolute(x, y, z); + var localX = x - this.baseOffsetX; + var localY = y - this.baseOffsetY; + var localZ = z - this.baseOffsetZ; + + var treeIndex = this.getTreeIndex(localX, localY, localZ); + if (treeIndex == Tree.OUT_OF_BOUNDS) { + return Tree.OUT_OF_BOUNDS; + } + var tree = this.trees[treeIndex]; if (tree != null) { this.lastTree = tree; return tree.getPresence(x, y, z); } - return TraversableTree.OUT_OF_BOUNDS; + return Tree.OUT_OF_BOUNDS; } protected abstract T[] makeTrees(int length); diff --git a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java index b5c9304904..9128db2721 100644 --- a/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java +++ b/common/src/main/java/net/caffeinemc/mods/sodium/client/render/chunk/tree/Tree.java @@ -1,9 +1,9 @@ package net.caffeinemc.mods.sodium.client.render.chunk.tree; public abstract class Tree { - public static final int OUT_OF_BOUNDS = 0; - public static final int NOT_PRESENT = 1; - public static final int PRESENT = 2; + public static final int OUT_OF_BOUNDS = -1; + public static final int NOT_PRESENT = 0; + public static final int PRESENT = 1; protected final long[] tree = new long[64 * 64]; protected final int offsetX, offsetY, offsetZ;