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 39bfd599..195371d5 100644 --- a/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java +++ b/src/main/java/me/cortex/voxy/client/config/VoxyConfigScreenFactory.java @@ -91,11 +91,11 @@ public class VoxyConfigScreenFactory implements ModMenuApi { .setDefaultValue(DEFAULT.maxSections) .build()); - //category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.renderDistance"), config.maxSections, 16, 2048) - // .setTooltip(Text.translatable("voxy.config.general.renderDistance.tooltip")) - // .setSaveConsumer(val -> config.renderDistance = val) - // .setDefaultValue(DEFAULT.renderDistance) - // .build()); + category.addEntry(entryBuilder.startIntField(Text.translatable("voxy.config.general.renderDistance"), config.renderDistance) + .setTooltip(Text.translatable("voxy.config.general.renderDistance.tooltip")) + .setSaveConsumer(val -> config.renderDistance = val) + .setDefaultValue(DEFAULT.renderDistance) + .build()); //category.addEntry(entryBuilder.startIntSlider(Text.translatable("voxy.config.general.compression"), config.savingCompressionLevel, 1, 21) // .setTooltip(Text.translatable("voxy.config.general.compression.tooltip")) diff --git a/src/main/java/me/cortex/voxy/client/core/DistanceTracker.java b/src/main/java/me/cortex/voxy/client/core/DistanceTracker.java index 0fd29a96..b582e6cd 100644 --- a/src/main/java/me/cortex/voxy/client/core/DistanceTracker.java +++ b/src/main/java/me/cortex/voxy/client/core/DistanceTracker.java @@ -8,8 +8,6 @@ import me.cortex.voxy.client.core.rendering.RenderTracker; import me.cortex.voxy.client.core.util.RingUtil; import net.minecraft.client.MinecraftClient; -import java.util.stream.IntStream; - //Can use ring logic // i.e. when a player moves the rings of each lod change (how it was doing in the original attempt) // also have it do directional quad culling and rebuild the chunk if needed (this shouldent happen very often) (the reason is to significantly reduce draw calls) @@ -19,12 +17,13 @@ public class DistanceTracker { private final TransitionRing2D[] loDRings; private final TransitionRing2D[] cacheLoadRings; private final TransitionRing2D[] cacheUnloadRings; + private final TransitionRing2D mostOuterNonClampedRing; private final RenderTracker tracker; private final int minYSection; private final int maxYSection; private final int renderDistance; - public DistanceTracker(RenderTracker tracker, int[] lodRingScales, int renderDistance, int cacheLoadDistance, int cacheUnloadDistance) { + public DistanceTracker(RenderTracker tracker, int[] lodRingScales, int renderDistance, int cacheDistance) { this.loDRings = new TransitionRing2D[lodRingScales.length]; this.cacheLoadRings = new TransitionRing2D[lodRingScales.length]; this.cacheUnloadRings = new TransitionRing2D[lodRingScales.length]; @@ -34,30 +33,87 @@ public class DistanceTracker { this.renderDistance = renderDistance; + boolean wasRdClamped = false; //The rings 0+ start at 64 vanilla rd, no matter what the game is set at, that is if the game is set to 32 rd // there will still be 32 chunks untill the first lod drop // if the game is set to 16, then there will be 48 chunks until the drop for (int i = 0; i < this.loDRings.length; i++) { + int scaleP = lodRingScales[i]; + boolean isTerminatingRing = ((lodRingScales[i]+2)<<(1+i) >= renderDistance)&&renderDistance>0; + if (isTerminatingRing) { + scaleP = Math.max(renderDistance >> (1+i), 1); + wasRdClamped = true; + } + int scale = scaleP; + //TODO: FIXME: check that the level shift is right when inc/dec int capRing = i; - this.loDRings[i] = new TransitionRing2D(6+i, lodRingScales[i], (x, z) -> this.dec(capRing+1, x, z), (x, z) -> this.inc(capRing+1, x, z)); + this.loDRings[i] = new TransitionRing2D((isTerminatingRing?5:6)+i, isTerminatingRing?scale<<1:scale, (x, z) -> { + if (isTerminatingRing) { + add(capRing, x, z); + } else + this.dec(capRing+1, x, z); + }, (x, z) -> { + if (isTerminatingRing) { + remove(capRing, x, z); + //remove(capRing, (x<<1), (z<<1)); + } else + this.inc(capRing+1, x, z); + }); //TODO:FIXME i think the radius is wrong and (lodRingScales[i]) needs to be (lodRingScales[i]<<1) since the transition ring (the thing above) // acts on LoD level + 1 //TODO: check this is actually working lmao and make it generate parent level lods on the exit instead of entry so it looks correct when flying backwards - this.cacheLoadRings[i] = new TransitionRing2D(5+i, (lodRingScales[i]<<1) + cacheLoadDistance, (x, z) -> { - //When entering a cache ring, trigger a mesh op and inject into cache - for (int y = this.minYSection>>capRing; y <= this.maxYSection>>capRing; y++) { - this.tracker.addCache(capRing, x, y, z); - } - }, (x, z) -> {}); - this.cacheUnloadRings[i] = new TransitionRing2D(5+i, (lodRingScales[i]<<1) + cacheUnloadDistance, (x, z) -> {}, (x, z) -> { - //When exiting the cache unload ring, tell the cache to dump whatever mesh it has cached and not add any mesh from that position - for (int y = this.minYSection>>capRing; y <= this.maxYSection>>capRing; y++) { - this.tracker.removeCache(capRing, x, y, z); + if (!isTerminatingRing) { + //TODO: COMPLETLY REDO THE CACHING SYSTEM CAUSE THE LOGIC IS COMPLETLY INCORRECT + // we want basicly 2 rings offset by an amount such that when a position is near an lod transition point + // it will be meshed (both the higher and lower quality lods), enabling semless loading + // the issue is when to uncache these methods + + + /* + this.cacheLoadRings[i] = new TransitionRing2D(5 + i, (scale << 1) + cacheDistance, (x, z) -> { + //When entering a cache ring, trigger a mesh op and inject into cache + for (int y = this.minYSection >> capRing; y <= this.maxYSection >> capRing; y++) { + this.tracker.addCache(capRing, x, y, z); + } + }, (x, z) -> { + int shift = capRing+1; + if (shift <= this.loDRings.length) { + for (int y = this.minYSection >> shift; y <= this.maxYSection >> shift; y++) { + this.tracker.removeCache(shift, x>>1, y, z>>1); + } + } + }); + this.cacheUnloadRings[i] = new TransitionRing2D(5 + i, Math.max(1, (scale << 1) + cacheDistance), (x, z) -> { + int shift = capRing+1; + if (shift <= this.loDRings.length) { + for (int y = this.minYSection >> shift; y <= this.maxYSection >> shift; y++) { + this.tracker.addCache(shift, x>>1, y, z>>1); + } + } + }, (x, z) -> { + //When exiting the cache unload ring, tell the cache to dump whatever mesh it has cached and not add any mesh from that position + for (int y = this.minYSection >> capRing; y <= this.maxYSection >> capRing; y++) { + this.tracker.removeCache(capRing, x, y, z); + } + });*/ + } + + if (isTerminatingRing) { + break; + } + } + if (!wasRdClamped) { + this.mostOuterNonClampedRing = new TransitionRing2D(5+this.loDRings.length, Math.max(renderDistance, 2048)>>this.loDRings.length, (x,z)-> + add(this.loDRings.length, x, z), (x,z)->{ + if (renderDistance > 0) { + remove(this.loDRings.length,x,z); } }); + } else { + this.mostOuterNonClampedRing = null; } } @@ -73,6 +129,19 @@ public class DistanceTracker { } } + private void add(int lvl, int x, int z) { + for (int y = this.minYSection>>lvl; y <= this.maxYSection>>lvl; y++) { + this.tracker.add(lvl, x, y, z); + } + } + + private void remove(int lvl, int x, int z) { + for (int y = this.minYSection>>lvl; y <= this.maxYSection>>lvl; y++) { + this.tracker.remove(lvl, x, y, z); + this.tracker.removeCache(lvl, x, y, z); + } + } + //How it works is there are N ring zones (one zone for each lod boundary) // the transition zone is what determines what lods are rendered etc (and it biases higher lod levels cause its easier) // the transition zone is only ever checked when the player moves 1<<(4+lodlvl) blocks, its position is set @@ -84,9 +153,14 @@ public class DistanceTracker { if (ring!=null) ring.update(x, z); } + if (this.mostOuterNonClampedRing!=null) + this.mostOuterNonClampedRing.update(x, z); + //Update in reverse order (biggest lod to smallest lod) for (int i = this.loDRings.length-1; -1 { - //Radius of chunks to enqueue - int SIZE = 128; - //Insert highest LOD level - for (int ox = -SIZE; ox <= SIZE; ox++) { - for (int oz = -SIZE; oz <= SIZE; oz++) { - this.inc(4, (x >> (5 + this.loDRings.length)) + ox, (z >> (5 + this.loDRings.length)) + oz); - } - } - - for (var ring : this.cacheLoadRings) { if (ring != null) ring.fill(x, z); @@ -131,6 +197,14 @@ public class DistanceTracker { ring.fill(x, z); } + //This is an ungodly terrible hack to make the lods load in a semi ok order + for (var ring : this.loDRings) + if (ring != null) + ring.fill(x, z); + + if (this.mostOuterNonClampedRing!=null) + this.mostOuterNonClampedRing.fill(x, z); + for (int i = this.loDRings.length - 1; 0 <= i; i--) { if (this.loDRings[i] != null) { this.loDRings[i].fill(x, z); diff --git a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java index 57316313..bc100c28 100644 --- a/src/main/java/me/cortex/voxy/client/core/VoxelCore.java +++ b/src/main/java/me/cortex/voxy/client/core/VoxelCore.java @@ -74,7 +74,7 @@ public class VoxelCore { //To get to chunk scale multiply the scale by 2, the scale is after how many chunks does the lods halve int q = VoxyConfig.CONFIG.qualityScale; //TODO: add an option for cache load and unload distance - this.distanceTracker = new DistanceTracker(this.renderTracker, new int[]{q,q,q,q}, VoxyConfig.CONFIG.renderDistance/2, 6, 6); + this.distanceTracker = new DistanceTracker(this.renderTracker, new int[]{q,q,q,q}, (VoxyConfig.CONFIG.renderDistance<0?VoxyConfig.CONFIG.renderDistance:((VoxyConfig.CONFIG.renderDistance+1)/2)), 3); System.out.println("Distance tracker initialized"); this.postProcessing = new PostProcessing(); @@ -182,6 +182,7 @@ public class VoxelCore { debug.add("Saving service tasks: " + this.world.savingService.getTaskCount()); debug.add("Render service tasks: " + this.renderGen.getTaskCount()); debug.add("Loaded cache sizes: " + Arrays.toString(this.world.getLoadedSectionCacheSizes())); + debug.add("Mesh cache count: " + this.renderGen.getMeshCacheCount()); this.renderer.addDebugData(debug); } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/GeometryManager.java b/src/main/java/me/cortex/voxy/client/core/rendering/GeometryManager.java index 9fcece60..8f77780a 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/GeometryManager.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/GeometryManager.java @@ -88,16 +88,17 @@ public class GeometryManager { long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long)SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE); meta.writeMetadata(ptr); } else { - //Add to the end of the array - id = this.sectionCount++; - this.pos2id.put(result.position, id); - this.id2pos.add(result.position); - //Create the new meta var meta = this.createMeta(result); if (meta == null) { continue; } + + //Add to the end of the array + id = this.sectionCount++; + this.pos2id.put(result.position, id); + this.id2pos.add(result.position); + this.sectionMetadata.add(meta); long ptr = UploadStream.INSTANCE.upload(this.sectionMetaBuffer, (long)SECTION_METADATA_SIZE * id, SECTION_METADATA_SIZE); meta.writeMetadata(ptr); diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/RenderTracker.java b/src/main/java/me/cortex/voxy/client/core/rendering/RenderTracker.java index 0f51fad4..123f16f3 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/RenderTracker.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/RenderTracker.java @@ -150,6 +150,15 @@ public class RenderTracker { this.renderGen.unmarkCache(lvl, x, y, z); } + public void remove(int lvl, int x, int y, int z) { + this.remove(WorldEngine.getWorldSectionId(lvl, x, y, z)); + this.renderer.enqueueResult(new BuiltSection(WorldEngine.getWorldSectionId(lvl, x, y, z))); + } + + public void add(int lvl, int x, int y, int z) { + this.put(WorldEngine.getWorldSectionId(lvl, x, y, z)); + this.renderGen.enqueueTask(lvl, x, y, z, this::shouldStillBuild); + } //Called by the world engine when a section gets dirtied diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/building/BuiltSectionMeshCache.java b/src/main/java/me/cortex/voxy/client/core/rendering/building/BuiltSectionMeshCache.java index 2e5bb41e..c0887e0f 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/building/BuiltSectionMeshCache.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/building/BuiltSectionMeshCache.java @@ -62,4 +62,8 @@ public class BuiltSectionMeshCache { } } } + + public int getCount() { + return this.renderCache.size(); + } } diff --git a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java index ef3f6032..32bf760a 100644 --- a/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java +++ b/src/main/java/me/cortex/voxy/client/core/rendering/building/RenderGenerationService.java @@ -14,6 +14,7 @@ import java.util.function.ToIntFunction; //TODO: Add a render cache public class RenderGenerationService { + public interface TaskChecker {boolean check(int lvl, int x, int y, int z);} private record BuildTask(Supplier sectionSupplier) {} @@ -82,6 +83,10 @@ public class RenderGenerationService { } } + public int getMeshCacheCount() { + return this.meshCache.getCount(); + } + //TODO: Add a priority system, higher detail sections must always be updated before lower detail // e.g. priorities NONE->lvl0 and lvl1 -> lvl0 over lvl0 -> lvl1 @@ -102,6 +107,7 @@ public class RenderGenerationService { this.enqueueTask(lvl, x, y, z, (l,x1,y1,z1)->true); } + public void enqueueTask(int lvl, int x, int y, int z, TaskChecker checker) { long ikey = WorldEngine.getWorldSectionId(lvl, x, y, z); { diff --git a/src/main/java/me/cortex/voxy/common/world/WorldSection.java b/src/main/java/me/cortex/voxy/common/world/WorldSection.java index 584a7cc7..fc9a6a3e 100644 --- a/src/main/java/me/cortex/voxy/common/world/WorldSection.java +++ b/src/main/java/me/cortex/voxy/common/world/WorldSection.java @@ -11,6 +11,7 @@ import java.util.concurrent.atomic.AtomicInteger; // holds a 32x32x32 region of detail public final class WorldSection { private static final int ARRAY_REUSE_CACHE_SIZE = 256; + //TODO: maybe just swap this to a ConcurrentLinkedDeque private static final Deque ARRAY_REUSE_CACHE = new ArrayDeque<>(1024); diff --git a/src/main/resources/assets/voxy/lang/en_us.json b/src/main/resources/assets/voxy/lang/en_us.json index 3e15c665..2cac1f8d 100644 --- a/src/main/resources/assets/voxy/lang/en_us.json +++ b/src/main/resources/assets/voxy/lang/en_us.json @@ -16,7 +16,7 @@ "voxy.config.general.maxSections": "Max Sections", "voxy.config.general.maxSections.tooltip": "The max number of sections the renderer can contain", "voxy.config.general.renderDistance": "Render Distance", - "voxy.config.general.renderDistance.tooltip": "The render distance in chunks", + "voxy.config.general.renderDistance.tooltip": "The render distance in chunks (set to -1 to disable chunk unloading)", "voxy.config.threads.ingest": "Ingest", "voxy.config.threads.ingest.tooltip": "How many threads voxy will use for ingesting new chunks",