From 40c4101becaac5172afad4f50dc3d63af1c46e03 Mon Sep 17 00:00:00 2001 From: mcrcortex <18544518+MCRcortex@users.noreply.github.com> Date: Mon, 31 Mar 2025 00:57:54 +1000 Subject: [PATCH] Added render distance slider and implementation --- .../cortex/voxy/client/config/VoxyConfig.java | 3 +- .../config/VoxyConfigScreenFactory.java | 12 ++ .../core/rendering/RenderDistanceTracker.java | 58 +++++ .../client/core/rendering/RenderService.java | 19 +- .../core/rendering/VoxyRenderSystem.java | 15 ++ .../HierarchicalOcclusionTraverser.java | 5 +- .../rendering/hierachical/NodeManager.java | 115 ++++++---- .../hierachical/SingleNodeRequest.java | 3 + .../hierachical/TestNodeManager.java | 57 +++-- .../voxy/client/core/util/RingTracker.java | 204 ++++++++++++++++++ .../voxy/client/core/util/RingUtil.java | 2 +- .../resources/assets/voxy/lang/en_us.json | 7 +- 12 files changed, 424 insertions(+), 76 deletions(-) create mode 100644 src/main/java/me/cortex/voxy/client/core/rendering/RenderDistanceTracker.java create mode 100644 src/main/java/me/cortex/voxy/client/core/util/RingTracker.java diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java index 0007addb..37ea454f 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfig.java @@ -3,7 +3,6 @@ package me.cortex.voxy.client.config; import com.google.gson.FieldNamingPolicy; import com.google.gson.Gson; import com.google.gson.GsonBuilder; -import me.cortex.voxy.client.saver.ContextSelectionSystem; import me.cortex.voxy.common.Logger; import net.fabricmc.loader.api.FabricLoader; @@ -24,7 +23,7 @@ public class VoxyConfig { public boolean enabled = true; public boolean enableRendering = true; public boolean ingestEnabled = true; - //public int renderDistance = 128; + public int sectionRenderDistance = 16; public int serviceThreads = Math.max(Runtime.getRuntime().availableProcessors()/2, 1); public float subDivisionSize = 128; public int secondaryLruCacheSize = 1024; diff --git a/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java b/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java index 8607f588..bb08d74a 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java @@ -112,6 +112,18 @@ public class VoxyConfigScreenFactory implements ModMenuApi { .setDefaultValue((int) DEFAULT.subDivisionSize) .build()); + category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.renderDistance"), config.sectionRenderDistance, 2, 64) + .setTooltip(Text.translatable("voxy.config.general.renderDistance.tooltip")) + .setSaveConsumer(val -> { + config.sectionRenderDistance = val; + var wrenderer =((IGetVoxyRenderSystem)(MinecraftClient.getInstance().worldRenderer)); + if (wrenderer != null && wrenderer.getVoxyRenderSystem() != null) { + wrenderer.getVoxyRenderSystem().setRenderDistance(val); + } + }) + .setDefaultValue(DEFAULT.sectionRenderDistance) + .build()); + //category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.lruCacheSize"), config.secondaryLruCacheSize, 16, 1<<13) // .setTooltip(Text.translatable("voxy.config.general.lruCacheSize.tooltip")) // .setSaveConsumer(val ->{if (config.secondaryLruCacheSize != val) reload(); config.secondaryLruCacheSize = val;}) diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/RenderDistanceTracker.java b/src/main/java/me/cortex/voxy/client/core/rendering/RenderDistanceTracker.java new file mode 100644 index 00000000..2a5f02d6 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/rendering/RenderDistanceTracker.java @@ -0,0 +1,58 @@ +package me.cortex.voxy.client.core.rendering; + +import me.cortex.voxy.client.core.util.RingTracker; +import me.cortex.voxy.common.world.WorldEngine; + +import java.util.function.LongConsumer; + +public class RenderDistanceTracker { + private static final int CHECK_DISTANCE_BLOCKS = 128; + private final LongConsumer addTopLevelNode; + private final LongConsumer removeTopLevelNode; + private final int processRate; + private final int minSec; + private final int maxSec; + private RingTracker tracker; + private int renderDistance; + private double posX; + private double posZ; + public RenderDistanceTracker(int rate, int minSec, int maxSec, LongConsumer addTopLevelNode, LongConsumer removeTopLevelNode) { + this.addTopLevelNode = addTopLevelNode; + this.removeTopLevelNode = removeTopLevelNode; + this.renderDistance = 2; + this.tracker = new RingTracker(this.renderDistance, 0, 0, true); + this.processRate = rate; + this.minSec = minSec; + this.maxSec = maxSec; + } + + public void setRenderDistance(int renderDistance) { + this.tracker.unload(); + this.tracker.process(Integer.MAX_VALUE, this::add, this::rem); + this.renderDistance = renderDistance; + this.tracker = new RingTracker(renderDistance, ((int)this.posX)>>9, ((int)this.posZ)>>9, true); + } + + public void setCenterAndProcess(double x, double z) { + double dx = this.posX-x; + double dz = this.posZ-z; + if (CHECK_DISTANCE_BLOCKS*CHECK_DISTANCE_BLOCKS>9, ((int)z)>>9); + } + this.tracker.process(this.processRate, this::add, this::rem); + } + + private void add(int x, int z) { + for (int y = this.minSec; y <= this.maxSec; y++) { + this.addTopLevelNode.accept(WorldEngine.getWorldSectionId(4, x, y, z)); + } + } + + private void rem(int x, int z) { + for (int y = this.minSec; y <= this.maxSec; y++) { + this.removeTopLevelNode.accept(WorldEngine.getWorldSectionId(4, x, y, z)); + } + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java index 36bace49..92de5f3a 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/RenderService.java @@ -86,18 +86,15 @@ public class RenderService, J extends Vi world.getMapper().setBiomeCallback(this.modelService::addBiome); } - private int q = -60; + public void addTopLevelNode(long pos) { + this.nodeManager.insertTopLevelNode(pos); + } + + public void removeTopLevelNode(long pos) { + this.nodeManager.removeTopLevelNode(pos); + } + public void setup(Camera camera) { - final int W = 32; - final int H = 2; - boolean SIDED = false; - for (int i = 0; i<64 && q<((W*2+1)*(W*2+1)*H)&&q++>=0;i++) { - this.nodeManager.insertTopLevelNode(WorldEngine.getWorldSectionId(4, (q%(W*2+1))-(SIDED?0:W), ((q/(W*2+1))/(W*2+1))-1, ((q/(W*2+1))%(W*2+1))-(SIDED?0:W))); - } - if (q==((W*2+1)*(W*2+1)*H)) { - q++; - Logger.info("Finished loading render distance"); - } this.modelService.tick(); } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/VoxyRenderSystem.java b/src/main/java/me/cortex/voxy/client/core/rendering/VoxyRenderSystem.java index e6a03453..178d2303 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/VoxyRenderSystem.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/VoxyRenderSystem.java @@ -13,6 +13,7 @@ import me.cortex.voxy.client.core.rendering.post.PostProcessing; import me.cortex.voxy.client.core.rendering.util.DownloadStream; import me.cortex.voxy.client.core.rendering.util.RawDownloadStream; import me.cortex.voxy.client.core.util.IrisUtil; +import me.cortex.voxy.client.core.util.RingTracker; import me.cortex.voxy.common.Logger; import me.cortex.voxy.common.thread.ServiceThreadPool; import me.cortex.voxy.common.world.WorldEngine; @@ -40,6 +41,7 @@ public class VoxyRenderSystem { private final RenderService renderer; private final PostProcessing postProcessing; private final WorldEngine worldIn; + private final RenderDistanceTracker renderDistanceTracker; public VoxyRenderSystem(WorldEngine world, ServiceThreadPool threadPool) { //Trigger the shared index buffer loading @@ -49,10 +51,23 @@ public class VoxyRenderSystem { this.worldIn = world; this.renderer = new RenderService(world, threadPool); this.postProcessing = new PostProcessing(); + + this.renderDistanceTracker = new RenderDistanceTracker(10, + MinecraftClient.getInstance().world.getBottomSectionCoord()>>5, + (MinecraftClient.getInstance().world.getTopSectionCoord()-1)>>5, + this.renderer::addTopLevelNode, + this.renderer::removeTopLevelNode); + + this.renderDistanceTracker.setRenderDistance(VoxyConfig.CONFIG.sectionRenderDistance); } + public void setRenderDistance(int renderDistance) { + this.renderDistanceTracker.setRenderDistance(renderDistance); + } public void renderSetup(Frustum frustum, Camera camera) { + this.renderDistanceTracker.setCenterAndProcess(camera.getBlockPos().getX(), camera.getBlockPos().getZ()); + this.renderer.setup(camera); PrintfDebugUtil.tick(); } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java index 0840607f..68a813fb 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/HierarchicalOcclusionTraverser.java @@ -216,8 +216,9 @@ public class HierarchicalOcclusionTraverser { //TODO: Move the first queue to a persistent list so its not updated every frame ptr = UploadStream.INSTANCE.upload(this.scratchQueueA, 0, 4L*initialQueueSize); - for (int i = 0; i < initialQueueSize; i++) { - MemoryUtil.memPutInt(ptr + 4L*i, this.nodeManager.getTopLevelNodeIds().getInt(i)); + int i = 0; + for (int node : this.nodeManager.getTopLevelNodeIds()) { + MemoryUtil.memPutInt(ptr + 4L*(i++), node); } UploadStream.INSTANCE.commit(); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java index 3516cbc9..d9499603 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/hierachical/NodeManager.java @@ -1,6 +1,5 @@ package me.cortex.voxy.client.core.rendering.hierachical; -import it.unimi.dsi.fastutil.ints.IntArrayList; import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap; @@ -87,7 +86,7 @@ public class NodeManager { private final Long2IntOpenHashMap activeSectionMap = new Long2IntOpenHashMap(); private final NodeStore nodeData; public final int maxNodeCount; - private final IntArrayList topLevelNodeIds = new IntArrayList(); + private final IntOpenHashSet topLevelNodeIds = new IntOpenHashSet(); private final LongOpenHashSet topLevelNodes = new LongOpenHashSet(); private int activeNodeRequestCount; @@ -133,24 +132,26 @@ public class NodeManager { } public void removeTopLevelNode(long pos) { + if (!this.topLevelNodes.remove(pos)) { + throw new IllegalStateException("Position not in top level map"); + } int nodeId = this.activeSectionMap.get(pos); if (nodeId == -1) { - Logger.error("Tried removing top level pos " + WorldEngine.pprintPos(pos) + " but it was not in active map, discarding!"); - return; + throw new IllegalStateException("Tried removing top level pos " + WorldEngine.pprintPos(pos) + " but it was not in active map, discarding!"); + } + if ((nodeId&NODE_TYPE_MSK)!=NODE_TYPE_REQUEST) { + int id = nodeId&NODE_ID_MSK; + if (!this.topLevelNodeIds.remove(id)) { + throw new IllegalStateException("Node id was not in top level node ids: " + nodeId + " pos: " + WorldEngine.pprintPos(pos)); + } } - //TODO: assert is top level node - - //TODO:FIXME augment topLevelNodeIds with a hashmap from node id to array index - // OR!! just ensure the list is always ordered?? maybe? idk i think hashmap is best - // since the array list might get shuffled as nodes are removed - // since need to move the entry at the end of the array to fill a hole made - - // remove from topLevelNodes aswell + //Remove the entire thing + this.recurseRemoveNode(pos); } - IntArrayList getTopLevelNodeIds() { + IntOpenHashSet getTopLevelNodeIds() { return this.topLevelNodeIds; } @@ -599,6 +600,35 @@ public class NodeManager { this._recurseRemoveNode(pos, false); } + + private void _removeRequest(int reqId, NodeChildRequest req, long pos) { + //Delete all the request data + for (int i = 0; i < 8; i++) { + if ((req.getMsk()&(1<>lvl; - return WorldEngine.getWorldSectionId(lvl, r.nextInt(bound), r.nextInt(bound), r.nextInt(bound)); + return WorldEngine.getWorldSectionId(lvl, r.nextInt(bound)+(WorldEngine.getX(top)<<4), r.nextInt(bound)+(WorldEngine.getY(top)<<4), r.nextInt(bound)+(WorldEngine.getZ(top)<<4)); } private static boolean runTest(int ITERS, int testIdx, Set> traces, boolean geoRemoval) { @@ -314,14 +317,30 @@ public class TestNodeManager { Random r = new Random(testIdx * 1234L); try { var test = new TestBase(); + LongList tops = new LongArrayList(); //Fuzzy bruteforce everything test.putTopPos(POS_A); + tops.add(POS_A); for (int i = 0; i < ITERS; i++) { - long pos = rPos(r); - int op = r.nextInt(4); + long pos = rPos(r, tops); + int op = r.nextInt(5); int extra = r.nextInt(256); boolean hasGeometry = r.nextBoolean(); - if (op == 0) { + boolean addRemTLN = r.nextInt(512) == 0; + boolean extraBool = r.nextBoolean(); + if (op == 0 && addRemTLN) { + pos = WorldEngine.getWorldSectionId(4, r.nextInt(5)-2, r.nextInt(5)-2, r.nextInt(5)-2); + boolean cont = tops.contains(pos); + if (cont&&extraBool&&tops.size()>1) { + extraBool = true; + test.remTopPos(pos); + tops.rem(pos); + } else if (!cont) { + extraBool = false; + test.putTopPos(pos); + tops.add(pos); + } + } else if (op == 0) { test.request(pos); } if (op == 1) { @@ -336,8 +355,20 @@ public class TestNodeManager { test.printNodeChanges(); test.verifyIntegrity(); } - test.childUpdate(POS_A, 0); - test.meshUpdate(POS_A, 0, 0); + for (long top : tops) { + test.remTopPos(top); + } + test.printNodeChanges(); + test.verifyIntegrity(); + if (test.nodeManager.getCurrentMaxNodeId() != -1) { + throw new IllegalStateException(); + } + if (!test.cleaner.active.isEmpty()) { + throw new IllegalStateException(); + } + if (!test.watcher.updateTypes.isEmpty()) { + throw new IllegalStateException(); + } if (test.geometryManager.memoryInUse != 0) { throw new IllegalStateException(); } diff --git a/src/main/java/me/cortex/voxy/client/core/util/RingTracker.java b/src/main/java/me/cortex/voxy/client/core/util/RingTracker.java new file mode 100644 index 00000000..0ab34ce1 --- /dev/null +++ b/src/main/java/me/cortex/voxy/client/core/util/RingTracker.java @@ -0,0 +1,204 @@ +package me.cortex.voxy.client.core.util; + +import it.unimi.dsi.fastutil.longs.Long2ByteOpenHashMap; +import me.cortex.voxy.common.Logger; + +import java.util.Random; + +//Tracks a ring and load/unload positions +// can process N of these load/unload positions +public class RingTracker { + //TODO: replace with custom map that removes elements if its mapped to 0 + private final Long2ByteOpenHashMap operations = new Long2ByteOpenHashMap(1<<13); + private final int[] boundDist; + private final int radius; + private int centerX; + private int centerZ; + + public RingTracker(int radius, int centerX, int centerZ, boolean fill) { + this.centerX = centerX; + this.centerZ = centerZ; + this.radius = radius; + this.boundDist = generateBoundingHalfCircleDistance(radius); + if (fill) { + this.fillRing(true); + } + } + + private static long pack(int x, int z) { + return Integer.toUnsignedLong(x)|(Integer.toUnsignedLong(z)<<32); + } + + private void fillRing(boolean load) { + for (int i = 0; i <= this.radius*2; i++) { + int x = this.centerX + i - this.radius; + int d = this.boundDist[i]; + for (int z = this.centerZ-d; z <= this.centerZ+d; z++) { + int res = this.operations.addTo(pack(x, z), (byte) (load?1:-1)); + if ((load&&0>>32)&0xFFFFFFFFL); + if (isAdd) { + onAdd.accept(x, z); + } else { + onRemove.accept(x, z); + } + iter.remove(); + } + return i; + } + + private int[] generateBoundingHalfCircleDistance(int radius) { + var ret = new int[radius*2+1]; + for (int i = -radius; i <= radius; i++) { + ret[i+radius] = (int)Math.sqrt(radius*radius - i*i); + } + return ret; + } + + public static void main(String[] args) { + for (int j = 0; j < 50; j++) { + Random r = new Random((j+18723)*1234); + var tracker = new RingTracker(r.nextInt(100)+1, 0, 0, true); + int R = r.nextInt(500); + for (int i = 0; i < 50_000; i++) { + int x = r.nextInt(R*2+1)-R; + int z = r.nextInt(R*2+1)-R; + tracker.moveCenter(x, z); + } + tracker.fillRing(false); + tracker.process(64, (x,z)->{ + Logger.info("Add:", x,",",z); + }, (x,z)->{ + Logger.info("Remove:", x,",",z); + }); + } + + } +} diff --git a/src/main/java/me/cortex/voxy/client/core/util/RingUtil.java b/src/main/java/me/cortex/voxy/client/core/util/RingUtil.java index 087ae147..9edc1d6d 100644 --- a/src/main/java/me/cortex/voxy/client/core/util/RingUtil.java +++ b/src/main/java/me/cortex/voxy/client/core/util/RingUtil.java @@ -65,7 +65,7 @@ public class RingUtil { public static int[] generatingBoundingCorner2D(int radius) { IntOpenHashSet points = new IntOpenHashSet(); //Do 2 pass (x and y) to generate and cover all points - for (int i = 0; i <= radius; i++) { + for (int i = 1; i <= radius; i++) { int other = (int) Math.floor(Math.sqrt(radius*radius - i*i)); //add points (x,other) and (other,x) as it covers the full spectrum points.add((i<<16)|other); diff --git a/src/main/resources/assets/voxy/lang/en_us.json b/src/main/resources/assets/voxy/lang/en_us.json index cda52dda..49c40d33 100644 --- a/src/main/resources/assets/voxy/lang/en_us.json +++ b/src/main/resources/assets/voxy/lang/en_us.json @@ -2,8 +2,6 @@ "voxy.config.title": "Voxy config", "voxy.config.general": "General", - "voxy.config.threads": "Threads", - "voxy.config.storage": "Storage", "voxy.config.general.enabled": "Enable Voxy", "voxy.config.general.enabled.tooltip": "Fully enables or disables voxy", @@ -18,5 +16,8 @@ "voxy.config.general.serviceThreads.tooltip": "Number of threads the ServiceThreadPool can use", "voxy.config.general.subDivisionSize": "Pixels^2 of subdivision size", - "voxy.config.general.subDivisionSize.tooltip": "Maximum size in pixels (squared) of screenspace AABB before subdiving to smaller LoDs (Smaller being higher quality)" + "voxy.config.general.subDivisionSize.tooltip": "Maximum size in pixels (squared) of screenspace AABB before subdiving to smaller LoDs (Smaller being higher quality)", + + "voxy.config.general.renderDistance": "Render distance", + "voxy.config.general.renderDistance.tooltip": "Render distance of voxy, each unit is equivalent to 32 chunks in vanilla (i.e. to get to vanilla render distance multiply by 32)" } \ No newline at end of file