From bf207b70628a793aab25ff75fb97897852690a3a Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Mon, 23 Feb 2026 19:04:19 -0500 Subject: [PATCH] speed pass --- core/plugins/Iris/cache/instance | 1 + core/src/main/java/art/arcane/iris/Iris.java | 43 +- .../iris/core/ExternalDataPackPipeline.java | 3 + .../iris/core/IrisHotPathMetricsMode.java | 7 - .../art/arcane/iris/core/IrisSettings.java | 17 +- .../core/StructureNbtJigsawPoolRewriter.java | 198 +++++++ .../core/pregenerator/IrisPregenerator.java | 48 +- .../iris/core/pregenerator/PregenTask.java | 88 +++ .../core/pregenerator/PregeneratorMethod.java | 4 + .../methods/AsyncPregenMethod.java | 128 ++++- .../arcane/iris/core/tools/IrisCreator.java | 200 +++++++ .../art/arcane/iris/engine/IrisComplex.java | 78 --- .../art/arcane/iris/engine/IrisEngine.java | 1 + .../arcane/iris/engine/IrisWorldManager.java | 18 +- .../actuator/IrisTerrainNormalActuator.java | 89 +++ .../arcane/iris/engine/framework/Engine.java | 2 +- .../iris/engine/mantle/MantleWriter.java | 27 + .../mantle/components/IrisCaveCarver3D.java | 542 ++++++++++++++---- .../components/MantleCarvingComponent.java | 352 ++++++++++-- .../engine/modifier/IrisCarveModifier.java | 216 +++++-- .../engine/platform/BukkitChunkGenerator.java | 4 - .../arcane/iris/util/common/scheduling/J.java | 54 +- .../util/project/context/ChunkContext.java | 100 ++-- .../arcane/iris/util/project/noise/CNG.java | 55 +- ...xternalDataPackPipelineNbtRewriteTest.java | 93 +++ .../PregenTaskInterleavedTraversalTest.java | 54 ++ .../AsyncPregenMethodConcurrencyCapTest.java | 32 ++ .../IrisCaveCarver3DNearParityTest.java | 268 +++++++++ .../MantleCarvingComponentTop2BlendTest.java | 143 +++++ .../IrisCarveModifierZoneParityTest.java | 186 ++++++ ...risDimensionCarvingResolverParityTest.java | 34 ++ .../context/ChunkContextPrefillPlanTest.java | 148 +++++ .../project/noise/CNGFastPathParityTest.java | 100 ++++ .../core/nms/v1_21_R7/IrisChunkGenerator.java | 12 +- 34 files changed, 2895 insertions(+), 450 deletions(-) create mode 100644 core/plugins/Iris/cache/instance delete mode 100644 core/src/main/java/art/arcane/iris/core/IrisHotPathMetricsMode.java create mode 100644 core/src/main/java/art/arcane/iris/core/StructureNbtJigsawPoolRewriter.java create mode 100644 core/src/test/java/art/arcane/iris/core/ExternalDataPackPipelineNbtRewriteTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/pregenerator/PregenTaskInterleavedTraversalTest.java create mode 100644 core/src/test/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethodConcurrencyCapTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java create mode 100644 core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierZoneParityTest.java create mode 100644 core/src/test/java/art/arcane/iris/util/project/context/ChunkContextPrefillPlanTest.java create mode 100644 core/src/test/java/art/arcane/iris/util/project/noise/CNGFastPathParityTest.java diff --git a/core/plugins/Iris/cache/instance b/core/plugins/Iris/cache/instance new file mode 100644 index 000000000..db2642a95 --- /dev/null +++ b/core/plugins/Iris/cache/instance @@ -0,0 +1 @@ +2117487583 \ No newline at end of file diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index 1bc67a408..61960cbef 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -97,6 +97,7 @@ public class Iris extends VolmitPlugin implements Listener { private static Thread shutdownHook; private static File settingsFile; private static final String PENDING_WORLD_DELETE_FILE = "pending-world-deletes.txt"; + private static final StackWalker DEBUG_STACK_WALKER = StackWalker.getInstance(); private static final Map stagedRuntimeGenerators = new ConcurrentHashMap<>(); private static final Map stagedRuntimeBiomeProviders = new ConcurrentHashMap<>(); @@ -303,21 +304,37 @@ public class Iris extends VolmitPlugin implements Listener { return; } + StackWalker.StackFrame frame = null; try { - throw new RuntimeException(); - } catch (Throwable e) { - try { - String[] cc = e.getStackTrace()[1].getClassName().split("\\Q.\\E"); - - if (cc.length > 5) { - debug(cc[3] + "/" + cc[4] + "/" + cc[cc.length - 1], e.getStackTrace()[1].getLineNumber(), string); - } else { - debug(cc[3] + "/" + cc[4], e.getStackTrace()[1].getLineNumber(), string); - } - } catch (Throwable ex) { - debug("Origin", -1, string); - } + frame = DEBUG_STACK_WALKER.walk(stream -> stream.skip(1).findFirst().orElse(null)); + } catch (Throwable ignored) { } + + if (frame == null) { + debug("Origin", -1, string); + return; + } + + String className = frame.getClassName(); + String[] cc = className == null ? new String[0] : className.split("\\Q.\\E"); + int line = frame.getLineNumber(); + + if (cc.length > 5) { + debug(cc[3] + "/" + cc[4] + "/" + cc[cc.length - 1], line, string); + return; + } + + if (cc.length > 4) { + debug(cc[3] + "/" + cc[4], line, string); + return; + } + + if (cc.length > 0) { + debug(cc[cc.length - 1], line, string); + return; + } + + debug("Origin", line, string); } public static void debug(String category, int line, String string) { diff --git a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java b/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java index 753c167c9..1bf6cc4c4 100644 --- a/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java +++ b/core/src/main/java/art/arcane/iris/core/ExternalDataPackPipeline.java @@ -1324,6 +1324,9 @@ public final class ExternalDataPackPipeline { writtenPaths.add(outputRelativePath); byte[] outputBytes = inputAsset.bytes(); + if (projectedEntry.type() == ProjectedEntryType.STRUCTURE_NBT && !remappedKeys.isEmpty()) { + outputBytes = StructureNbtJigsawPoolRewriter.rewrite(outputBytes, remappedKeys); + } if (projectedEntry.type() == ProjectedEntryType.STRUCTURE || projectedEntry.type() == ProjectedEntryType.STRUCTURE_SET || projectedEntry.type() == ProjectedEntryType.CONFIGURED_FEATURE diff --git a/core/src/main/java/art/arcane/iris/core/IrisHotPathMetricsMode.java b/core/src/main/java/art/arcane/iris/core/IrisHotPathMetricsMode.java deleted file mode 100644 index f0d409175..000000000 --- a/core/src/main/java/art/arcane/iris/core/IrisHotPathMetricsMode.java +++ /dev/null @@ -1,7 +0,0 @@ -package art.arcane.iris.core; - -public enum IrisHotPathMetricsMode { - SAMPLED, - EXACT, - DISABLED -} diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index 14cc02cb9..d223f8f07 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -153,8 +153,6 @@ public class IrisSettings { public boolean useTicketQueue = true; public IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO; public IrisPaperLikeBackendMode paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO; - public IrisHotPathMetricsMode hotPathMetricsMode = IrisHotPathMetricsMode.SAMPLED; - public int hotPathMetricsSampleStride = 1024; public int maxConcurrency = 256; public int paperLikeMaxConcurrency = 96; public int foliaMaxConcurrency = 32; @@ -191,20 +189,6 @@ public class IrisSettings { return paperLikeBackendMode; } - public IrisHotPathMetricsMode getHotPathMetricsMode() { - if (hotPathMetricsMode == null) { - return IrisHotPathMetricsMode.SAMPLED; - } - - return hotPathMetricsMode; - } - - public int getHotPathMetricsSampleStride() { - int stride = Math.max(1, Math.min(hotPathMetricsSampleStride, 65_536)); - int normalized = Integer.highestOneBit(stride); - return normalized <= 0 ? 1 : normalized; - } - public int getSaveIntervalMs() { return Math.max(5_000, Math.min(saveIntervalMs, 900_000)); } @@ -315,6 +299,7 @@ public class IrisSettings { public boolean studio = true; public boolean openVSCode = true; public boolean disableTimeAndWeather = true; + public boolean enableEntitySpawning = false; public boolean autoStartDefaultStudio = false; } diff --git a/core/src/main/java/art/arcane/iris/core/StructureNbtJigsawPoolRewriter.java b/core/src/main/java/art/arcane/iris/core/StructureNbtJigsawPoolRewriter.java new file mode 100644 index 000000000..7842354b7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/StructureNbtJigsawPoolRewriter.java @@ -0,0 +1,198 @@ +package art.arcane.iris.core; + +import art.arcane.volmlib.util.nbt.io.NBTDeserializer; +import art.arcane.volmlib.util.nbt.io.NBTSerializer; +import art.arcane.volmlib.util.nbt.io.NamedTag; +import art.arcane.volmlib.util.nbt.tag.ByteTag; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.tag.IntTag; +import art.arcane.volmlib.util.nbt.tag.ListTag; +import art.arcane.volmlib.util.nbt.tag.NumberTag; +import art.arcane.volmlib.util.nbt.tag.ShortTag; +import art.arcane.volmlib.util.nbt.tag.Tag; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +final class StructureNbtJigsawPoolRewriter { + private StructureNbtJigsawPoolRewriter() { + } + + static byte[] rewrite(byte[] bytes, Map remappedKeys) { + if (bytes == null || bytes.length == 0 || remappedKeys == null || remappedKeys.isEmpty()) { + return bytes; + } + + try { + NbtReadResult readResult = readNamedTagWithCompression(bytes); + Tag rootTag = readResult.namedTag().getTag(); + if (!(rootTag instanceof CompoundTag compoundTag)) { + return bytes; + } + + boolean rewritten = rewriteJigsawPoolReferences(compoundTag, remappedKeys); + if (!rewritten) { + return bytes; + } + + return writeNamedTag(readResult.namedTag(), readResult.compressed()); + } catch (Throwable ignored) { + return bytes; + } + } + + private static boolean rewriteJigsawPoolReferences(CompoundTag root, Map remappedKeys) { + ListTag palette = root.getListTag("palette"); + ListTag blocks = root.getListTag("blocks"); + if (palette == null || blocks == null || palette.size() <= 0 || blocks.size() <= 0) { + return false; + } + + Set jigsawStates = new HashSet<>(); + for (int paletteIndex = 0; paletteIndex < palette.size(); paletteIndex++) { + Object paletteRaw = palette.get(paletteIndex); + if (!(paletteRaw instanceof CompoundTag paletteEntry)) { + continue; + } + String blockName = paletteEntry.getString("Name"); + if ("minecraft:jigsaw".equalsIgnoreCase(blockName)) { + jigsawStates.add(paletteIndex); + } + } + + if (jigsawStates.isEmpty()) { + return false; + } + + boolean rewritten = false; + for (Object blockRaw : blocks.getValue()) { + if (!(blockRaw instanceof CompoundTag blockTag)) { + continue; + } + + Integer stateIndex = tagToInt(blockTag.get("state")); + if (stateIndex == null || !jigsawStates.contains(stateIndex)) { + continue; + } + + CompoundTag blockNbt = blockTag.getCompoundTag("nbt"); + if (blockNbt == null || blockNbt.size() <= 0) { + continue; + } + + String poolValue = blockNbt.getString("pool"); + if (poolValue == null || poolValue.isBlank()) { + continue; + } + + String normalizedPool = normalizeResourceKey(poolValue); + if (normalizedPool == null || normalizedPool.isBlank()) { + continue; + } + + String remappedPool = remappedKeys.get(normalizedPool); + if (remappedPool == null || remappedPool.isBlank()) { + continue; + } + + blockNbt.putString("pool", remappedPool); + rewritten = true; + } + + return rewritten; + } + + private static Integer tagToInt(Tag tag) { + if (tag == null) { + return null; + } + if (tag instanceof IntTag intTag) { + return intTag.asInt(); + } + if (tag instanceof ShortTag shortTag) { + return (int) shortTag.asShort(); + } + if (tag instanceof ByteTag byteTag) { + return (int) byteTag.asByte(); + } + if (tag instanceof NumberTag numberTag) { + Number value = numberTag.getValue(); + if (value != null) { + return value.intValue(); + } + } + Object value = tag.getValue(); + if (value instanceof Number number) { + return number.intValue(); + } + return null; + } + + private static String normalizeResourceKey(String value) { + if (value == null) { + return null; + } + + String normalized = value.trim(); + if (normalized.isEmpty()) { + return ""; + } + if (normalized.charAt(0) == '#') { + normalized = normalized.substring(1); + } + + String namespace = "minecraft"; + String path = normalized; + int separator = normalized.indexOf(':'); + if (separator >= 0) { + namespace = normalized.substring(0, separator).trim().toLowerCase(); + path = normalized.substring(separator + 1).trim(); + } + + if (path.startsWith("worldgen/template_pool/")) { + path = path.substring("worldgen/template_pool/".length()); + } + path = path.replace('\\', '/'); + while (path.startsWith("/")) { + path = path.substring(1); + } + while (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + if (path.isEmpty()) { + return ""; + } + + return namespace + ":" + path; + } + + private static NbtReadResult readNamedTagWithCompression(byte[] bytes) throws IOException { + IOException primary = null; + try { + NamedTag uncompressed = new NBTDeserializer(false).fromStream(new ByteArrayInputStream(bytes)); + return new NbtReadResult(uncompressed, false); + } catch (IOException e) { + primary = e; + } + + try { + NamedTag compressed = new NBTDeserializer(true).fromStream(new ByteArrayInputStream(bytes)); + return new NbtReadResult(compressed, true); + } catch (IOException e) { + if (primary != null) { + e.addSuppressed(primary); + } + throw e; + } + } + + private static byte[] writeNamedTag(NamedTag namedTag, boolean compressed) throws IOException { + return new NBTSerializer(compressed).toBytes(namedTag); + } + + private record NbtReadResult(NamedTag namedTag, boolean compressed) { + } +} diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java index bb3c009a6..8d33e2aac 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java @@ -173,9 +173,13 @@ public class IrisPregenerator { init(); ticker.start(); checkRegions(); - var p = PrecisionStopwatch.start(); + PrecisionStopwatch p = PrecisionStopwatch.start(); task.iterateRegions((x, z) -> visitRegion(x, z, true)); - task.iterateRegions((x, z) -> visitRegion(x, z, false)); + if (generator.isAsyncChunkMode()) { + visitChunksInterleaved(); + } else { + task.iterateRegions((x, z) -> visitRegion(x, z, false)); + } Iris.info("Pregen took " + Form.duration((long) p.getMilliseconds())); shutdown(); if (benchmarking == null) { @@ -260,6 +264,46 @@ public class IrisPregenerator { generator.supportsRegions(x, z, listener); } + private void visitChunksInterleaved() { + task.iterateAllChunksInterleaved((regionX, regionZ, chunkX, chunkZ, firstChunkInRegion, lastChunkInRegion) -> { + while (paused.get() && !shutdown.get()) { + J.sleep(50); + } + + Position2 regionPos = new Position2(regionX, regionZ); + if (shutdown.get()) { + if (!generatedRegions.contains(regionPos)) { + listener.onRegionSkipped(regionX, regionZ); + generatedRegions.add(regionPos); + } + return false; + } + + if (generatedRegions.contains(regionPos)) { + return true; + } + + if (firstChunkInRegion) { + currentGeneratorMethod.set(generator.getMethod(regionX, regionZ)); + listener.onRegionGenerating(regionX, regionZ); + } + + generator.generateChunk(chunkX, chunkZ, listener); + + if (lastChunkInRegion) { + listener.onRegionGenerated(regionX, regionZ); + if (saveLatch.flip()) { + listener.onSaving(); + generator.save(); + } + generatedRegions.add(regionPos); + checkRegions(); + } + + return true; + }); + } + public void pause() { paused.set(true); } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java index c38bb8dfd..a37073a38 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregenTask.java @@ -97,6 +97,54 @@ public class PregenTask { iterateRegions(((rX, rZ) -> iterateChunks(rX, rZ, s))); } + public void iterateAllChunksInterleaved(InterleavedChunkSpiraled spiraled) { + if (spiraled == null) { + return; + } + + KList cursors = new KList<>(); + iterateRegions((regionX, regionZ) -> { + KList chunks = new KList<>(); + iterateChunks(regionX, regionZ, (chunkX, chunkZ) -> chunks.add(new Position2(chunkX, chunkZ))); + if (!chunks.isEmpty()) { + cursors.add(new RegionChunkCursor(regionX, regionZ, chunks)); + } + }); + + boolean hasProgress = true; + while (hasProgress) { + hasProgress = false; + for (RegionChunkCursor cursor : cursors) { + if (!cursor.hasNext()) { + continue; + } + + hasProgress = true; + Position2 chunk = cursor.next(); + if (chunk == null) { + continue; + } + + boolean shouldContinue = spiraled.on( + cursor.getRegionX(), + cursor.getRegionZ(), + chunk.getX(), + chunk.getZ(), + cursor.getIndex() == 1, + !cursor.hasNext() + ); + if (!shouldContinue) { + return; + } + } + } + } + + @FunctionalInterface + public interface InterleavedChunkSpiraled { + boolean on(int regionX, int regionZ, int chunkX, int chunkZ, boolean firstChunkInRegion, boolean lastChunkInRegion); + } + private class Bounds { private Bound chunk = null; private Bound region = null; @@ -147,4 +195,44 @@ public class PregenTask { throw new IllegalStateException("This Position2 may not be modified"); } } + + private static final class RegionChunkCursor { + private final int regionX; + private final int regionZ; + private final KList chunks; + private int index; + + private RegionChunkCursor(int regionX, int regionZ, KList chunks) { + this.regionX = regionX; + this.regionZ = regionZ; + this.chunks = chunks; + this.index = 0; + } + + private boolean hasNext() { + return index < chunks.size(); + } + + private Position2 next() { + if (!hasNext()) { + return null; + } + + Position2 value = chunks.get(index); + index++; + return value; + } + + private int getRegionX() { + return regionX; + } + + private int getRegionZ() { + return regionZ; + } + + private int getIndex() { + return index; + } + } } diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/PregeneratorMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/PregeneratorMethod.java index 3bab6d252..3dd17bbe2 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/PregeneratorMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/PregeneratorMethod.java @@ -59,6 +59,10 @@ public interface PregeneratorMethod { */ String getMethod(int x, int z); + default boolean isAsyncChunkMode() { + return false; + } + /** * Called to generate a region. Execute sync, if multicore internally, wait * for the task to complete diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index 5880a9969..8f9767666 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -55,6 +55,10 @@ public class AsyncPregenMethod implements PregeneratorMethod { private final boolean foliaRuntime; private final String backendMode; private final int workerPoolThreads; + private final int runtimeCpuThreads; + private final int effectiveWorkerThreads; + private final int recommendedRuntimeConcurrencyCap; + private final int configuredMaxConcurrency; private final Executor executor; private final Semaphore semaphore; private final int threads; @@ -86,6 +90,10 @@ public class AsyncPregenMethod implements PregeneratorMethod { IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); this.runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(pregen); this.foliaRuntime = runtimeSchedulerMode == IrisRuntimeSchedulerMode.FOLIA; + int detectedWorkerPoolThreads = resolveWorkerPoolThreads(); + int detectedCpuThreads = Math.max(1, Runtime.getRuntime().availableProcessors()); + int configuredWorldGenThreads = Math.max(1, IrisSettings.get().getConcurrency().getWorldGenThreads()); + int workerThreadsForCap = Math.max(detectedCpuThreads, Math.max(configuredWorldGenThreads, Math.max(1, detectedWorkerPoolThreads))); if (foliaRuntime) { this.paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO; this.backendMode = "folia-region"; @@ -100,14 +108,19 @@ public class AsyncPregenMethod implements PregeneratorMethod { this.backendMode = "paper-ticket"; } } - int configuredThreads = pregen.getMaxConcurrency(); - if (foliaRuntime) { - configuredThreads = Math.min(configuredThreads, pregen.getFoliaMaxConcurrency()); - } else { - configuredThreads = Math.min(configuredThreads, resolvePaperLikeConcurrencyCap(pregen.getPaperLikeMaxConcurrency())); - } + int configuredThreads = applyRuntimeConcurrencyCap( + pregen.getMaxConcurrency(), + foliaRuntime, + workerThreadsForCap + ); + this.configuredMaxConcurrency = Math.max(1, pregen.getMaxConcurrency()); this.threads = Math.max(1, configuredThreads); - this.workerPoolThreads = resolveWorkerPoolThreads(); + this.workerPoolThreads = detectedWorkerPoolThreads; + this.runtimeCpuThreads = detectedCpuThreads; + this.effectiveWorkerThreads = workerThreadsForCap; + this.recommendedRuntimeConcurrencyCap = foliaRuntime + ? computeFoliaRecommendedCap(workerThreadsForCap) + : computePaperLikeRecommendedCap(workerThreadsForCap); this.semaphore = new Semaphore(this.threads, true); this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds(); this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs(); @@ -267,8 +280,40 @@ public class AsyncPregenMethod implements PregeneratorMethod { } } - private int resolvePaperLikeConcurrencyCap(int configuredCap) { - return Math.max(8, configuredCap); + static int computePaperLikeRecommendedCap(int workerThreads) { + int normalizedWorkers = Math.max(1, workerThreads); + int recommendedCap = normalizedWorkers * 2; + if (recommendedCap < 8) { + return 8; + } + + if (recommendedCap > 96) { + return 96; + } + + return recommendedCap; + } + + static int computeFoliaRecommendedCap(int workerThreads) { + int normalizedWorkers = Math.max(1, workerThreads); + int recommendedCap = normalizedWorkers * 4; + if (recommendedCap < 64) { + return 64; + } + + if (recommendedCap > 192) { + return 192; + } + + return recommendedCap; + } + + static int applyRuntimeConcurrencyCap(int maxConcurrency, boolean foliaRuntime, int workerThreads) { + int normalizedMaxConcurrency = Math.max(1, maxConcurrency); + int recommendedCap = foliaRuntime + ? computeFoliaRecommendedCap(workerThreads) + : computePaperLikeRecommendedCap(workerThreads); + return Math.min(normalizedMaxConcurrency, recommendedCap); } private String metricsSnapshot() { @@ -365,6 +410,10 @@ public class AsyncPregenMethod implements PregeneratorMethod { + ", threads=" + threads + ", adaptiveLimit=" + adaptiveInFlightLimit.get() + ", workerPoolThreads=" + workerPoolThreads + + ", cpuThreads=" + runtimeCpuThreads + + ", effectiveWorkerThreads=" + effectiveWorkerThreads + + ", maxConcurrency=" + configuredMaxConcurrency + + ", recommendedCap=" + recommendedRuntimeConcurrencyCap + ", urgent=" + urgent + ", timeout=" + timeoutSeconds + "s"); unloadAndSaveAllChunks(); @@ -376,6 +425,11 @@ public class AsyncPregenMethod implements PregeneratorMethod { return "Async"; } + @Override + public boolean isAsyncChunkMode() { + return true; + } + @Override public void close() { semaphore.acquireUninterruptibly(threads); @@ -492,35 +546,47 @@ public class AsyncPregenMethod implements PregeneratorMethod { private class FoliaRegionExecutor implements Executor { @Override public void generate(int x, int z, PregenListener listener) { + try { + PaperLib.getChunkAtAsync(world, x, z, true, urgent) + .orTimeout(timeoutSeconds, TimeUnit.SECONDS) + .whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable)); + return; + } catch (Throwable ignored) { + } + if (!J.runRegion(world, x, z, () -> PaperLib.getChunkAtAsync(world, x, z, true, urgent) .orTimeout(timeoutSeconds, TimeUnit.SECONDS) - .whenComplete((chunk, throwable) -> { - boolean success = false; - try { - if (throwable != null) { - onChunkFutureFailure(x, z, throwable); - return; - } - - listener.onChunkGenerated(x, z); - listener.onChunkCleaned(x, z); - if (chunk != null) { - lastUse.put(chunk, M.ms()); - } - success = true; - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - } finally { - markFinished(success); - semaphore.release(); - } - }))) { + .whenComplete((chunk, throwable) -> completeFoliaChunk(x, z, listener, chunk, throwable)))) { markFinished(false); semaphore.release(); Iris.warn("Failed to schedule Folia region pregen task at " + x + "," + z + ". " + metricsSnapshot()); } } + + private void completeFoliaChunk(int x, int z, PregenListener listener, Chunk chunk, Throwable throwable) { + boolean success = false; + try { + if (throwable != null) { + onChunkFutureFailure(x, z, throwable); + return; + } + + if (chunk == null) { + return; + } + + listener.onChunkGenerated(x, z); + listener.onChunkCleaned(x, z); + lastUse.put(chunk, M.ms()); + success = true; + } catch (Throwable e) { + Iris.reportError(e); + e.printStackTrace(); + } finally { + markFinished(success); + semaphore.release(); + } + } } private class ServiceExecutor implements Executor { diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index 0f19f8dd3..6dc9f33ab 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -56,8 +56,13 @@ import org.bukkit.entity.Player; import java.io.File; import java.io.IOException; import java.lang.reflect.Field; +import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -74,6 +79,9 @@ import static art.arcane.iris.util.common.misc.ServerProperties.BUKKIT_YML; @Data @Accessors(fluent = true, chain = true) public class IrisCreator { + private static final int STUDIO_PREWARM_RADIUS_CHUNKS = 1; + private static final Duration STUDIO_PREWARM_TIMEOUT = Duration.ofSeconds(45L); + /** * Specify an area to pregenerate during creation */ @@ -254,6 +262,7 @@ public class IrisCreator { if (studioEntryLocation == null) { sender.sendMessage(C.YELLOW + "Studio opened, but entry location could not be resolved safely."); } else { + prewarmStudioEntryChunks(world, studioEntryLocation, STUDIO_PREWARM_RADIUS_CHUNKS, STUDIO_PREWARM_TIMEOUT); CompletableFuture teleportFuture = PaperLib.teleportAsync(senderPlayer, studioEntryLocation); if (teleportFuture != null) { teleportFuture.thenAccept(success -> { @@ -497,6 +506,197 @@ public class IrisCreator { return true; } + private void prewarmStudioEntryChunks(World world, Location entry, int radiusChunks, Duration timeout) throws IrisException { + if (world == null || entry == null) { + throw new IrisException("Studio prewarm failed: world or entry location is null."); + } + + int centerChunkX = entry.getBlockX() >> 4; + int centerChunkZ = entry.getBlockZ() >> 4; + List chunkTargets = resolveStudioPrewarmTargets(centerChunkX, centerChunkZ, radiusChunks); + if (chunkTargets.isEmpty()) { + throw new IrisException("Studio prewarm failed: no target chunks were resolved."); + } + + int loadedBefore = 0; + Map> futures = new LinkedHashMap<>(); + for (StudioChunkCoordinate coordinate : chunkTargets) { + if (world.isChunkLoaded(coordinate.getX(), coordinate.getZ())) { + loadedBefore++; + } + + CompletableFuture chunkFuture = PaperLib.getChunkAtAsync(world, coordinate.getX(), coordinate.getZ(), true); + if (chunkFuture == null) { + throw new IrisException("Studio prewarm failed: async chunk future was null for " + coordinate + "."); + } + + futures.put(coordinate, chunkFuture); + } + + int total = chunkTargets.size(); + int completed = 0; + Set remaining = new LinkedHashSet<>(chunkTargets); + long startNanos = System.nanoTime(); + long timeoutNanos = Math.max(1L, timeout.toNanos()); + reportStudioProgress(0.88D, "Prewarming entry chunks (0/" + total + ")"); + + while (!remaining.isEmpty()) { + long elapsedNanos = System.nanoTime() - startNanos; + if (elapsedNanos >= timeoutNanos) { + StudioPrewarmDiagnostics diagnostics = buildStudioPrewarmDiagnostics(world, chunkTargets, remaining, loadedBefore, elapsedNanos); + throw new IrisException("Studio prewarm timed out: " + diagnostics.toMessage()); + } + + boolean progressed = false; + List completedCoordinates = new ArrayList<>(); + for (StudioChunkCoordinate coordinate : remaining) { + CompletableFuture chunkFuture = futures.get(coordinate); + if (chunkFuture == null || !chunkFuture.isDone()) { + continue; + } + + try { + Chunk loadedChunk = chunkFuture.get(); + if (loadedChunk == null) { + throw new IrisException("Studio prewarm failed: chunk " + coordinate + " resolved to null."); + } + } catch (IrisException e) { + throw e; + } catch (Throwable e) { + throw new IrisException("Studio prewarm failed while loading chunk " + coordinate + ".", e); + } + + completedCoordinates.add(coordinate); + progressed = true; + } + + if (!completedCoordinates.isEmpty()) { + for (StudioChunkCoordinate completedCoordinate : completedCoordinates) { + remaining.remove(completedCoordinate); + } + + completed += completedCoordinates.size(); + double ratio = (double) completed / (double) total; + reportStudioProgress(0.88D + (0.04D * ratio), "Prewarming entry chunks (" + completed + "/" + total + ")"); + } + + if (!progressed) { + J.sleep(20); + } + } + + long elapsedNanos = System.nanoTime() - startNanos; + StudioPrewarmDiagnostics diagnostics = buildStudioPrewarmDiagnostics(world, chunkTargets, new LinkedHashSet<>(), loadedBefore, elapsedNanos); + Iris.info("Studio prewarm complete: " + diagnostics.toMessage()); + } + + private StudioPrewarmDiagnostics buildStudioPrewarmDiagnostics( + World world, + List chunkTargets, + Set timedOutChunks, + int loadedBefore, + long elapsedNanos + ) { + int loadedAfter = 0; + for (StudioChunkCoordinate coordinate : chunkTargets) { + if (world.isChunkLoaded(coordinate.getX(), coordinate.getZ())) { + loadedAfter++; + } + } + + int generatedDuring = Math.max(0, loadedAfter - loadedBefore); + List timedOut = new ArrayList<>(); + for (StudioChunkCoordinate timedOutChunk : timedOutChunks) { + timedOut.add(timedOutChunk.toString()); + } + + long elapsedMs = TimeUnit.NANOSECONDS.toMillis(Math.max(0L, elapsedNanos)); + return new StudioPrewarmDiagnostics(elapsedMs, loadedBefore, loadedAfter, generatedDuring, timedOut); + } + + private List resolveStudioPrewarmTargets(int centerChunkX, int centerChunkZ, int radiusChunks) { + int safeRadius = Math.max(0, radiusChunks); + List targets = new ArrayList<>(); + targets.add(new StudioChunkCoordinate(centerChunkX, centerChunkZ)); + + for (int x = -safeRadius; x <= safeRadius; x++) { + for (int z = -safeRadius; z <= safeRadius; z++) { + if (x == 0 && z == 0) { + continue; + } + + targets.add(new StudioChunkCoordinate(centerChunkX + x, centerChunkZ + z)); + } + } + + return targets; + } + + private static final class StudioChunkCoordinate { + private final int x; + private final int z; + + private StudioChunkCoordinate(int x, int z) { + this.x = x; + this.z = z; + } + + private int getX() { + return x; + } + + private int getZ() { + return z; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + + if (!(other instanceof StudioChunkCoordinate coordinate)) { + return false; + } + + return x == coordinate.x && z == coordinate.z; + } + + @Override + public int hashCode() { + return 31 * x + z; + } + + @Override + public String toString() { + return x + "," + z; + } + } + + private static final class StudioPrewarmDiagnostics { + private final long elapsedMs; + private final int loadedBefore; + private final int loadedAfter; + private final int generatedDuring; + private final List timedOutChunks; + + private StudioPrewarmDiagnostics(long elapsedMs, int loadedBefore, int loadedAfter, int generatedDuring, List timedOutChunks) { + this.elapsedMs = elapsedMs; + this.loadedBefore = loadedBefore; + this.loadedAfter = loadedAfter; + this.generatedDuring = generatedDuring; + this.timedOutChunks = new ArrayList<>(timedOutChunks); + } + + private String toMessage() { + return "elapsedMs=" + elapsedMs + + ", loadedBefore=" + loadedBefore + + ", loadedAfter=" + loadedAfter + + ", generatedDuring=" + generatedDuring + + ", timedOut=" + timedOutChunks; + } + } + private static boolean containsCreateWorldUnsupportedOperation(Throwable throwable) { Throwable cursor = throwable; while (cursor != null) { diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index b628278d0..465d27ff2 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -19,7 +19,6 @@ package art.arcane.iris.engine; import art.arcane.iris.Iris; -import art.arcane.iris.core.IrisHotPathMetricsMode; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.Cache; @@ -50,8 +49,6 @@ import java.util.*; public class IrisComplex implements DataProvider { private static final BlockData AIR = Material.AIR.createBlockData(); private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); - private static final int HOT_PATH_METRICS_FLUSH_SIZE = 64; - private static final ThreadLocal HOT_PATH_METRICS = ThreadLocal.withInitial(HotPathMetricsState::new); private RNG rng; private double fluidHeight; private IrisData data; @@ -324,11 +321,6 @@ public class IrisComplex implements DataProvider { return 0; } - IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); - IrisHotPathMetricsMode metricsMode = pregen.getHotPathMetricsMode(); - HotPathMetricsState metricsState = metricsMode == IrisHotPathMetricsMode.DISABLED ? null : HOT_PATH_METRICS.get(); - boolean sampleMetrics = metricsState != null && metricsState.shouldSample(metricsMode, pregen.getHotPathMetricsSampleStride()); - long interpolateStartNanos = sampleMetrics ? System.nanoTime() : 0L; CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64); IdentityHashMap cachedBounds = generatorBounds.get(interpolator); IdentityHashMap localBounds = new IdentityHashMap<>(8); @@ -350,22 +342,15 @@ public class IrisComplex implements DataProvider { return ZERO_NOISE_BOUNDS; }); - if (sampleMetrics) { - metricsState.recordInterpolate(engine, System.nanoTime() - interpolateStartNanos); - } double hi = sampledBounds.max(); double lo = sampledBounds.min(); - long generatorStartNanos = sampleMetrics ? System.nanoTime() : 0L; double d = 0; for (IrisGenerator i : generators) { d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945)); } - if (sampleMetrics) { - metricsState.recordGenerator(engine, System.nanoTime() - generatorStartNanos); - } return d / generators.size(); } @@ -636,69 +621,6 @@ public class IrisComplex implements DataProvider { } } - private static class HotPathMetricsState { - private long callCounter; - private long interpolateNanos; - private int interpolateSamples; - private long generatorNanos; - private int generatorSamples; - - private boolean shouldSample(IrisHotPathMetricsMode mode, int sampleStride) { - if (mode == IrisHotPathMetricsMode.EXACT) { - return true; - } - - long current = callCounter++; - return (current & (sampleStride - 1L)) == 0L; - } - - private void recordInterpolate(Engine engine, long nanos) { - if (nanos < 0L) { - return; - } - - interpolateNanos += nanos; - interpolateSamples++; - if (interpolateSamples >= HOT_PATH_METRICS_FLUSH_SIZE) { - flushInterpolate(engine); - } - } - - private void recordGenerator(Engine engine, long nanos) { - if (nanos < 0L) { - return; - } - - generatorNanos += nanos; - generatorSamples++; - if (generatorSamples >= HOT_PATH_METRICS_FLUSH_SIZE) { - flushGenerator(engine); - } - } - - private void flushInterpolate(Engine engine) { - if (interpolateSamples <= 0) { - return; - } - - double averageMs = (interpolateNanos / (double) interpolateSamples) / 1_000_000D; - engine.getMetrics().getNoiseHeightInterpolate().put(averageMs); - interpolateNanos = 0L; - interpolateSamples = 0; - } - - private void flushGenerator(Engine engine) { - if (generatorSamples <= 0) { - return; - } - - double averageMs = (generatorNanos / (double) generatorSamples) / 1_000_000D; - engine.getMetrics().getNoiseHeightGenerator().put(averageMs); - generatorNanos = 0L; - generatorSamples = 0; - } - } - public void close() { } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 6629df689..c03660ed1 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -708,4 +708,5 @@ public class IrisEngine implements Engine { } return true; } + } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java index 3e5a86845..c50b09c96 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisWorldManager.java @@ -301,13 +301,17 @@ public class IrisWorldManager extends EngineAssignedWorldManager { Chunk chunk = world.getChunkAt(chunkX, chunkZ); if (IrisSettings.get().getWorld().isPostLoadBlockUpdates()) { - if (J.isFolia() && !getMantle().isChunkLoaded(chunkX, chunkZ)) { + if (!getMantle().isChunkLoaded(chunkX, chunkZ)) { warmupMantleChunkAsync(chunkX, chunkZ); return; } getEngine().updateChunk(chunk); } + if (!isEntitySpawningEnabledForCurrentWorld()) { + return; + } + if (!IrisSettings.get().getWorld().isMarkerEntitySpawningSystem()) { return; } @@ -585,6 +589,10 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return; } + if (!isEntitySpawningEnabledForCurrentWorld()) { + return; + } + IrisComplex complex = getEngine().getComplex(); if (complex == null) { return; @@ -680,6 +688,14 @@ public class IrisWorldManager extends EngineAssignedWorldManager { return (initial ? s.getInitialSpawns() : s.getSpawns()).stream(); } + private boolean isEntitySpawningEnabledForCurrentWorld() { + if (!getEngine().isStudio()) { + return true; + } + + return IrisSettings.get().getStudio().isEnableEntitySpawning(); + } + private KList spawnRandomly(List types) { KList rarityTypes = new KList<>(); int totalRarity = 0; diff --git a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java index a54b6d39b..c6c186e7a 100644 --- a/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java +++ b/core/src/main/java/art/arcane/iris/engine/actuator/IrisTerrainNormalActuator.java @@ -23,6 +23,7 @@ import art.arcane.iris.engine.framework.EngineAssignedActuator; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; import art.arcane.volmlib.util.collection.KList; +import art.arcane.iris.util.project.context.ChunkedDataCache; import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.iris.util.project.hunk.Hunk; @@ -74,6 +75,11 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator */ @BlockCoordinates public void terrainSliver(int x, int z, int xf, Hunk h, ChunkContext context) { + terrainSliverOptimized(x, z, xf, h, context); + } + + @BlockCoordinates + private void terrainSliverLegacy(int x, int z, int xf, Hunk h, ChunkContext context) { int zf, realX, realZ, hf, he; IrisBiome biome; IrisRegion region; @@ -159,4 +165,87 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator } } } + + @BlockCoordinates + private void terrainSliverOptimized(int x, int z, int xf, Hunk h, ChunkContext context) { + int chunkHeight = h.getHeight(); + int chunkDepth = h.getDepth(); + int fluidHeight = getDimension().getFluidHeight(); + boolean bedrockEnabled = getDimension().isBedrock(); + ChunkedDataCache biomeCache = context.getBiome(); + ChunkedDataCache regionCache = context.getRegion(); + ChunkedDataCache heightCache = context.getHeight(); + ChunkedDataCache fluidCache = context.getFluid(); + ChunkedDataCache rockCache = context.getRock(); + int realX = xf + x; + + for (int zf = 0; zf < chunkDepth; zf++) { + int realZ = zf + z; + IrisBiome biome = biomeCache.get(xf, zf); + IrisRegion region = regionCache.get(xf, zf); + int he = (int) Math.round(Math.min(chunkHeight, heightCache.get(xf, zf))); + int hf = Math.round(Math.max(Math.min(chunkHeight, fluidHeight), he)); + if (hf < 0) { + continue; + } + + int topY = Math.min(hf, chunkHeight - 1); + BlockData fluid = fluidCache.get(xf, zf); + BlockData rock = rockCache.get(xf, zf); + KList blocks = null; + KList fblocks = null; + + for (int i = topY; i >= 0; i--) { + if (i == 0 && bedrockEnabled) { + h.set(xf, i, zf, BEDROCK); + lastBedrock = i; + continue; + } + + BlockData ore = biome.generateOres(realX, i, realZ, rng, getData(), true); + ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), true) : ore; + ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), true) : ore; + if (ore != null) { + h.set(xf, i, zf, ore); + continue; + } + + if (i > he && i <= hf) { + int fdepth = hf - i; + if (fblocks == null) { + fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, getData()); + } + + if (fblocks.hasIndex(fdepth)) { + h.set(xf, i, zf, fblocks.get(fdepth)); + } else { + h.set(xf, i, zf, fluid); + } + continue; + } + + if (i <= he) { + int depth = he - i; + if (blocks == null) { + blocks = biome.generateLayers(getDimension(), realX, realZ, rng, he, he, getData(), getComplex()); + } + + if (blocks.hasIndex(depth)) { + h.set(xf, i, zf, blocks.get(depth)); + continue; + } + + ore = biome.generateOres(realX, i, realZ, rng, getData(), false); + ore = ore == null ? region.generateOres(realX, i, realZ, rng, getData(), false) : ore; + ore = ore == null ? getDimension().generateOres(realX, i, realZ, rng, getData(), false) : ore; + + if (ore != null) { + h.set(xf, i, zf, ore); + } else { + h.set(xf, i, zf, rock); + } + } + } + } + } } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index 82d5455b3..22c0247da 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -318,7 +318,7 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat } var mantle = getMantle().getMantle(); if (!mantle.isLoaded(c)) { - var msg = "Mantle Chunk " + c.getX() + c.getX() + " is not loaded"; + var msg = "Mantle Chunk " + c.getX() + "," + c.getZ() + " is not loaded"; if (W.getStack().getCallerClass().equals(ChunkUpdater.class)) Iris.warn(msg); else Iris.debug(msg); return; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java index 1971814de..893156ec3 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/MantleWriter.java @@ -170,6 +170,33 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { matter.slice(matter.getClass(t)).set(x & 15, y & 15, z & 15, t); } + public boolean setDataIfAbsent(int x, int y, int z, MatterCavern value) { + if (value == null) { + return false; + } + + int cx = x >> 4; + int cz = z >> 4; + + if (y < 0 || y >= mantle.getWorldHeight()) { + return false; + } + + MantleChunk chunk = acquireChunk(cx, cz); + if (chunk == null) { + return false; + } + + Matter matter = chunk.getOrCreate(y >> 4); + MatterCavern existing = matter.slice(MatterCavern.class).get(x & 15, y & 15, z & 15); + if (existing != null) { + return false; + } + + matter.slice(MatterCavern.class).set(x & 15, y & 15, z & 15, value); + return true; + } + public T getData(int x, int y, int z, Class type) { int cx = x >> 4; int cz = z >> 4; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index 7ec701d7e..054e31ac6 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -25,8 +25,11 @@ import art.arcane.iris.engine.object.IrisCaveFieldModule; import art.arcane.iris.engine.object.IrisCaveProfile; import art.arcane.iris.engine.object.IrisRange; import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.util.ArrayList; @@ -48,7 +51,7 @@ public class IrisCaveCarver3D { private final CNG surfaceBreakDensity; private final RNG thresholdRng; private final ModuleState[] modules; - private final double normalization; + private final double inverseNormalization; private final MatterCavern carveAir; private final MatterCavern carveLava; private final MatterCavern carveForcedAir; @@ -89,7 +92,8 @@ public class IrisCaveCarver3D { } this.modules = moduleStates.toArray(new ModuleState[0]); - normalization = weight <= 0 ? 1 : weight; + double normalization = weight <= 0 ? 1 : weight; + inverseNormalization = 1D / normalization; hasModules = modules.length > 0; } @@ -99,7 +103,7 @@ public class IrisCaveCarver3D { Arrays.fill(scratch.fullWeights, 1D); scratch.fullWeightsInitialized = true; } - return carve(writer, chunkX, chunkZ, scratch.fullWeights, 0D, 0D, null); + return carve(writer, chunkX, chunkZ, scratch.fullWeights, 0D, 0D, null, null); } public int carve( @@ -110,7 +114,7 @@ public class IrisCaveCarver3D { double minWeight, double thresholdPenalty ) { - return carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, null); + return carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, null, null); } public int carve( @@ -121,6 +125,19 @@ public class IrisCaveCarver3D { double minWeight, double thresholdPenalty, IrisRange worldYRange + ) { + return carve(writer, chunkX, chunkZ, columnWeights, minWeight, thresholdPenalty, worldYRange, null); + } + + public int carve( + MantleWriter writer, + int chunkX, + int chunkZ, + double[] columnWeights, + double minWeight, + double thresholdPenalty, + IrisRange worldYRange, + int[] precomputedSurfaceHeights ) { PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start(); try { @@ -150,27 +167,38 @@ public class IrisCaveCarver3D { int surfaceBreakDepth = Math.max(0, profile.getSurfaceBreakDepth()); double surfaceBreakNoiseThreshold = profile.getSurfaceBreakNoiseThreshold(); double surfaceBreakThresholdBoost = Math.max(0, profile.getSurfaceBreakThresholdBoost()); - int waterMinDepthBelowSurface = Math.max(0, profile.getWaterMinDepthBelowSurface()); - boolean waterRequiresFloor = profile.isWaterRequiresFloor(); boolean allowSurfaceBreak = profile.isAllowSurfaceBreak(); if (maxY < minY) { return 0; } + MantleChunk chunk = writer.acquireChunk(chunkX, chunkZ); + if (chunk == null) { + return 0; + } + int x0 = chunkX << 4; int z0 = chunkZ << 4; - int[] columnSurface = scratch.columnSurface; int[] columnMaxY = scratch.columnMaxY; int[] surfaceBreakFloorY = scratch.surfaceBreakFloorY; boolean[] surfaceBreakColumn = scratch.surfaceBreakColumn; double[] columnThreshold = scratch.columnThreshold; + double[] clampedWeights = scratch.clampedColumnWeights; + double[] verticalEdgeFade = prepareVerticalEdgeFadeTable(scratch, minY, maxY); + MatterCavern[] matterByY = prepareMatterByYTable(scratch, minY, maxY); + prepareSectionCaches(scratch, minY, maxY); for (int lx = 0; lx < 16; lx++) { int x = x0 + lx; for (int lz = 0; lz < 16; lz++) { int z = z0 + lz; int index = (lx << 4) | lz; - int columnSurfaceY = engine.getHeight(x, z); + int columnSurfaceY; + if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > index) { + columnSurfaceY = precomputedSurfaceHeights[index]; + } else { + columnSurfaceY = engine.getHeight(x, z); + } int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance)); boolean breakColumn = allowSurfaceBreak && signed(surfaceBreakDensity.noiseFast2D(x, z)) >= surfaceBreakNoiseThreshold; @@ -178,30 +206,30 @@ public class IrisCaveCarver3D { ? Math.min(maxY, Math.max(minY, columnSurfaceY)) : clearanceTopY; - columnSurface[index] = columnSurfaceY; columnMaxY[index] = columnTopY; surfaceBreakFloorY[index] = Math.max(minY, columnSurfaceY - surfaceBreakDepth); surfaceBreakColumn[index] = breakColumn; columnThreshold[index] = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias(); + clampedWeights[index] = clampColumnWeight(columnWeights[index]); } } - int carved = carvePass( - writer, + int latticeStep = Math.max(2, sampleStep); + int carved = carvePassLattice( + chunk, x0, z0, minY, maxY, - sampleStep, + latticeStep, surfaceBreakThresholdBoost, - waterMinDepthBelowSurface, - waterRequiresFloor, - columnSurface, columnMaxY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, - columnWeights, + clampedWeights, + verticalEdgeFade, + matterByY, resolvedMinWeight, resolvedThresholdPenalty, 0D, @@ -211,27 +239,71 @@ public class IrisCaveCarver3D { int minCarveCells = Math.max(0, profile.getMinCarveCells()); double recoveryThresholdBoost = Math.max(0, profile.getRecoveryThresholdBoost()); if (carved < minCarveCells && recoveryThresholdBoost > 0D) { - carved += carvePass( - writer, + carved += carvePassLattice( + chunk, + x0, + z0, + minY, + maxY, + latticeStep, + surfaceBreakThresholdBoost, + columnMaxY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + resolvedMinWeight, + resolvedThresholdPenalty, + recoveryThresholdBoost, + true + ); + } + + if (carved == 0 && hasFallbackCandidates(columnMaxY, clampedWeights, minY, resolvedMinWeight)) { + carved += carvePassFallback( + chunk, x0, z0, minY, maxY, sampleStep, surfaceBreakThresholdBoost, - waterMinDepthBelowSurface, - waterRequiresFloor, - columnSurface, columnMaxY, surfaceBreakFloorY, surfaceBreakColumn, columnThreshold, - columnWeights, + clampedWeights, + verticalEdgeFade, + matterByY, resolvedMinWeight, resolvedThresholdPenalty, - recoveryThresholdBoost, - true + 0D, + false ); + if (carved < minCarveCells && recoveryThresholdBoost > 0D) { + carved += carvePassFallback( + chunk, + x0, + z0, + minY, + maxY, + sampleStep, + surfaceBreakThresholdBoost, + columnMaxY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + resolvedMinWeight, + resolvedThresholdPenalty, + recoveryThresholdBoost, + true + ); + } } return carved; @@ -240,35 +312,174 @@ public class IrisCaveCarver3D { } } - private int carvePass( - MantleWriter writer, + private int carvePassLattice( + MantleChunk chunk, int x0, int z0, int minY, int maxY, - int sampleStep, + int latticeStep, double surfaceBreakThresholdBoost, - int waterMinDepthBelowSurface, - boolean waterRequiresFloor, - int[] columnSurface, int[] columnMaxY, int[] surfaceBreakFloorY, boolean[] surfaceBreakColumn, double[] columnThreshold, - double[] columnWeights, + double[] clampedWeights, + double[] verticalEdgeFade, + MatterCavern[] matterByY, double minWeight, double thresholdPenalty, double thresholdBoost, boolean skipExistingCarved ) { int carved = 0; + Scratch scratch = SCRATCH.get(); + double[] passThreshold = scratch.passThreshold; + int[] tileIndices = scratch.tileIndices; + int[] tileLocalX = scratch.tileLocalX; + int[] tileLocalZ = scratch.tileLocalZ; + int[] tileTopY = scratch.tileTopY; + + for (int index = 0; index < 256; index++) { + double columnWeight = clampedWeights[index]; + if (columnWeight <= minWeight || columnMaxY[index] < minY) { + passThreshold[index] = Double.NaN; + continue; + } + passThreshold[index] = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty); + } + + for (int lx = 0; lx < 16; lx += 2) { + int x = x0 + lx; + int lx1 = lx + 1; + for (int lz = 0; lz < 16; lz += 2) { + int z = z0 + lz; + int lz1 = lz + 1; + int activeColumns = 0; + + int index00 = (lx << 4) | lz; + if (!Double.isNaN(passThreshold[index00])) { + tileIndices[activeColumns] = index00; + tileLocalX[activeColumns] = lx; + tileLocalZ[activeColumns] = lz; + tileTopY[activeColumns] = columnMaxY[index00]; + activeColumns++; + } + + int index01 = (lx << 4) | lz1; + if (!Double.isNaN(passThreshold[index01])) { + tileIndices[activeColumns] = index01; + tileLocalX[activeColumns] = lx; + tileLocalZ[activeColumns] = lz1; + tileTopY[activeColumns] = columnMaxY[index01]; + activeColumns++; + } + + int index10 = (lx1 << 4) | lz; + if (!Double.isNaN(passThreshold[index10])) { + tileIndices[activeColumns] = index10; + tileLocalX[activeColumns] = lx1; + tileLocalZ[activeColumns] = lz; + tileTopY[activeColumns] = columnMaxY[index10]; + activeColumns++; + } + + int index11 = (lx1 << 4) | lz1; + if (!Double.isNaN(passThreshold[index11])) { + tileIndices[activeColumns] = index11; + tileLocalX[activeColumns] = lx1; + tileLocalZ[activeColumns] = lz1; + tileTopY[activeColumns] = columnMaxY[index11]; + activeColumns++; + } + + if (activeColumns == 0) { + continue; + } + + int tileMaxY = minY; + for (int columnIndex = 0; columnIndex < activeColumns; columnIndex++) { + if (tileTopY[columnIndex] > tileMaxY) { + tileMaxY = tileTopY[columnIndex]; + } + } + if (tileMaxY < minY) { + continue; + } + + for (int y = minY; y <= tileMaxY; y += latticeStep) { + double density = sampleDensityOptimized(x, y, z); + int stampMaxY = Math.min(maxY, y + 1); + for (int yy = y; yy <= stampMaxY; yy++) { + MatterCavern matter = matterByY[yy - minY]; + MatterSlice cavernSlice = resolveCavernSlice(scratch, chunk, yy >> 4); + int localY = yy & 15; + int fadeIndex = yy - minY; + for (int columnIndex = 0; columnIndex < activeColumns; columnIndex++) { + if (yy > tileTopY[columnIndex]) { + continue; + } + + int index = tileIndices[columnIndex]; + double localThreshold = passThreshold[index]; + if (surfaceBreakColumn[index] && yy >= surfaceBreakFloorY[index]) { + localThreshold += surfaceBreakThresholdBoost; + } + localThreshold -= verticalEdgeFade[fadeIndex]; + if (density > localThreshold) { + continue; + } + + int localX = tileLocalX[columnIndex]; + int localZ = tileLocalZ[columnIndex]; + if (skipExistingCarved) { + if (cavernSlice.get(localX, localY, localZ) == null) { + cavernSlice.set(localX, localY, localZ, matter); + carved++; + } + continue; + } + + cavernSlice.set(localX, localY, localZ, matter); + carved++; + } + } + } + } + } + + return carved; + } + + private int carvePassFallback( + MantleChunk chunk, + int x0, + int z0, + int minY, + int maxY, + int sampleStep, + double surfaceBreakThresholdBoost, + int[] columnMaxY, + int[] surfaceBreakFloorY, + boolean[] surfaceBreakColumn, + double[] columnThreshold, + double[] clampedWeights, + double[] verticalEdgeFade, + MatterCavern[] matterByY, + double minWeight, + double thresholdPenalty, + double thresholdBoost, + boolean skipExistingCarved + ) { + int carved = 0; + Scratch scratch = SCRATCH.get(); for (int lx = 0; lx < 16; lx++) { int x = x0 + lx; for (int lz = 0; lz < 16; lz++) { int z = z0 + lz; int index = (lx << 4) | lz; - double columnWeight = clampColumnWeight(columnWeights[index]); + double columnWeight = clampedWeights[index]; if (columnWeight <= minWeight) { continue; } @@ -280,7 +491,6 @@ public class IrisCaveCarver3D { boolean breakColumn = surfaceBreakColumn[index]; int breakFloorY = surfaceBreakFloorY[index]; - int surfaceY = columnSurface[index]; double threshold = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty); for (int y = minY; y <= columnTopY; y += sampleStep) { @@ -289,18 +499,25 @@ public class IrisCaveCarver3D { localThreshold += surfaceBreakThresholdBoost; } - localThreshold = applyVerticalEdgeFade(localThreshold, y, minY, maxY); - if (sampleDensity(x, y, z) > localThreshold) { + localThreshold -= verticalEdgeFade[y - minY]; + if (sampleDensityOptimized(x, y, z) > localThreshold) { continue; } int carveMaxY = Math.min(columnTopY, y + sampleStep - 1); for (int yy = y; yy <= carveMaxY; yy++) { - if (skipExistingCarved && writer.isCarved(x, yy, z)) { + MatterCavern matter = matterByY[yy - minY]; + MatterSlice cavernSlice = resolveCavernSlice(scratch, chunk, yy >> 4); + int localY = yy & 15; + if (skipExistingCarved) { + if (cavernSlice.get(lx, localY, lz) == null) { + cavernSlice.set(lx, localY, lz, matter); + carved++; + } continue; } - writer.setData(x, yy, z, resolveMatter(x, yy, z, surfaceY, localThreshold, waterMinDepthBelowSurface, waterRequiresFloor)); + cavernSlice.set(lx, localY, lz, matter); carved++; } } @@ -310,85 +527,163 @@ public class IrisCaveCarver3D { return carved; } - private double applyVerticalEdgeFade(double threshold, int y, int minY, int maxY) { - int fadeRange = Math.max(0, profile.getVerticalEdgeFade()); - if (fadeRange <= 0 || maxY <= minY) { - return threshold; - } + private boolean hasFallbackCandidates(int[] columnMaxY, double[] clampedWeights, int minY, double minWeight) { + for (int index = 0; index < 256; index++) { + if (clampedWeights[index] <= minWeight) { + continue; + } - int floorDistance = y - minY; - int ceilingDistance = maxY - y; - int edgeDistance = Math.min(floorDistance, ceilingDistance); - if (edgeDistance >= fadeRange) { - return threshold; - } - - double t = Math.max(0D, Math.min(1D, edgeDistance / (double) fadeRange)); - double smooth = t * t * (3D - (2D * t)); - double fadeStrength = Math.max(0D, profile.getVerticalEdgeFadeStrength()); - return threshold - ((1D - smooth) * fadeStrength); - } - - private double sampleDensity(int x, int y, int z) { - if (!hasWarp && !hasModules) { - double density = signed(baseDensity.noiseFast3D(x, y, z)) * baseWeight; - density += signed(detailDensity.noiseFast3D(x, y, z)) * detailWeight; - return density / normalization; - } - - double warpedX = x; - double warpedY = y; - double warpedZ = z; - if (hasWarp) { - double warpA = signed(warpDensity.noiseFast3D(x, y, z)); - double warpB = signed(warpDensity.noiseFast3D(x + 31.37D, y - 17.21D, z + 23.91D)); - double offsetX = warpA * warpStrength; - double offsetY = warpB * warpStrength; - double offsetZ = (warpA - warpB) * 0.5D * warpStrength; - warpedX += offsetX; - warpedY += offsetY; - warpedZ += offsetZ; - } - - double density = signed(baseDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * baseWeight; - density += signed(detailDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * detailWeight; - - if (hasModules) { - for (int moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) { - ModuleState module = modules[moduleIndex]; - if (y < module.minY || y > module.maxY) { - continue; - } - - double moduleDensity = signed(module.density.noiseFast3D(warpedX, warpedY, warpedZ)) - module.threshold; - if (module.invert) { - moduleDensity = -moduleDensity; - } - - density += moduleDensity * module.weight; + if (columnMaxY[index] >= minY) { + return true; } } - return density / normalization; + return false; } - private MatterCavern resolveMatter(int x, int y, int z, int surfaceY, double localThreshold, int waterMinDepthBelowSurface, boolean waterRequiresFloor) { + private double sampleDensityOptimized(int x, int y, int z) { + if (!hasWarp) { + if (!hasModules) { + return sampleDensityNoWarpNoModules(x, y, z); + } + + return sampleDensityNoWarpModules(x, y, z); + } + + if (!hasModules) { + return sampleDensityWarpOnly(x, y, z); + } + + return sampleDensityWarpModules(x, y, z); + } + + private double sampleDensityNoWarpNoModules(int x, int y, int z) { + double density = signed(baseDensity.noiseFast3D(x, y, z)) * baseWeight; + density += signed(detailDensity.noiseFast3D(x, y, z)) * detailWeight; + return density * inverseNormalization; + } + + private double sampleDensityNoWarpModules(int x, int y, int z) { + double density = signed(baseDensity.noiseFast3D(x, y, z)) * baseWeight; + density += signed(detailDensity.noiseFast3D(x, y, z)) * detailWeight; + for (int moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) { + ModuleState module = modules[moduleIndex]; + if (y < module.minY || y > module.maxY) { + continue; + } + + double moduleDensity = signed(module.density.noiseFast3D(x, y, z)) - module.threshold; + if (module.invert) { + moduleDensity = -moduleDensity; + } + + density += moduleDensity * module.weight; + } + + return density * inverseNormalization; + } + + private double sampleDensityWarpOnly(int x, int y, int z) { + double warpA = signed(warpDensity.noiseFast3D(x, y, z)); + double warpB = signed(warpDensity.noiseFast3D(x + 31.37D, y - 17.21D, z + 23.91D)); + double warpedX = x + (warpA * warpStrength); + double warpedY = y + (warpB * warpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); + double density = signed(baseDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * baseWeight; + density += signed(detailDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * detailWeight; + return density * inverseNormalization; + } + + private double sampleDensityWarpModules(int x, int y, int z) { + double warpA = signed(warpDensity.noiseFast3D(x, y, z)); + double warpB = signed(warpDensity.noiseFast3D(x + 31.37D, y - 17.21D, z + 23.91D)); + double warpedX = x + (warpA * warpStrength); + double warpedY = y + (warpB * warpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); + double density = signed(baseDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * baseWeight; + density += signed(detailDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * detailWeight; + for (int moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) { + ModuleState module = modules[moduleIndex]; + if (y < module.minY || y > module.maxY) { + continue; + } + + double moduleDensity = signed(module.density.noiseFast3D(warpedX, warpedY, warpedZ)) - module.threshold; + if (module.invert) { + moduleDensity = -moduleDensity; + } + + density += moduleDensity * module.weight; + } + + return density * inverseNormalization; + } + + private MatterSlice resolveCavernSlice(Scratch scratch, MantleChunk chunk, int sectionIndex) { + @SuppressWarnings("unchecked") + MatterSlice cachedSlice = (MatterSlice) scratch.sectionSlices[sectionIndex]; + if (cachedSlice != null) { + return cachedSlice; + } + + Matter sectionMatter = scratch.sectionMatter[sectionIndex]; + if (sectionMatter == null) { + sectionMatter = chunk.getOrCreate(sectionIndex); + scratch.sectionMatter[sectionIndex] = sectionMatter; + } + + MatterSlice resolvedSlice = sectionMatter.slice(MatterCavern.class); + scratch.sectionSlices[sectionIndex] = resolvedSlice; + return resolvedSlice; + } + + private MatterCavern[] prepareMatterByYTable(Scratch scratch, int minY, int maxY) { + int size = Math.max(0, maxY - minY + 1); + if (scratch.matterByY.length < size) { + scratch.matterByY = new MatterCavern[size]; + } + + MatterCavern[] matterByY = scratch.matterByY; + boolean allowLava = profile.isAllowLava(); + boolean allowWater = profile.isAllowWater(); int lavaHeight = engine.getDimension().getCaveLavaHeight(); int fluidHeight = engine.getDimension().getFluidHeight(); - if (profile.isAllowLava() && y <= lavaHeight) { - return carveLava; + for (int y = minY; y <= maxY; y++) { + int offset = y - minY; + if (allowLava && y <= lavaHeight) { + matterByY[offset] = carveLava; + continue; + } + if (allowWater && y <= fluidHeight) { + matterByY[offset] = carveAir; + continue; + } + if (!allowLava && y <= lavaHeight) { + matterByY[offset] = carveForcedAir; + continue; + } + + matterByY[offset] = carveAir; } - if (profile.isAllowWater() && y <= fluidHeight) { - return carveAir; + return matterByY; + } + + private void prepareSectionCaches(Scratch scratch, int minY, int maxY) { + int minSection = Math.max(0, minY >> 4); + int maxSection = Math.max(minSection, maxY >> 4); + int requiredSections = maxSection + 1; + if (scratch.sectionMatter.length < requiredSections) { + scratch.sectionMatter = new Matter[requiredSections]; + scratch.sectionSlices = new MatterSlice[requiredSections]; + return; } - if (!profile.isAllowLava() && y <= lavaHeight) { - return carveForcedAir; + for (int section = minSection; section <= maxSection; section++) { + scratch.sectionMatter[section] = null; + scratch.sectionSlices[section] = null; } - - return carveAir; } private double clampColumnWeight(double weight) { @@ -411,6 +706,38 @@ public class IrisCaveCarver3D { return (value * 2D) - 1D; } + private double[] prepareVerticalEdgeFadeTable(Scratch scratch, int minY, int maxY) { + int size = Math.max(0, maxY - minY + 1); + if (scratch.verticalEdgeFade.length < size) { + scratch.verticalEdgeFade = new double[size]; + } + + double[] verticalEdgeFade = scratch.verticalEdgeFade; + int fadeRange = Math.max(0, profile.getVerticalEdgeFade()); + double fadeStrength = Math.max(0D, profile.getVerticalEdgeFadeStrength()); + if (size <= 0 || fadeRange <= 0 || maxY <= minY || fadeStrength <= 0D) { + Arrays.fill(verticalEdgeFade, 0, size, 0D); + return verticalEdgeFade; + } + + for (int y = minY; y <= maxY; y++) { + int floorDistance = y - minY; + int ceilingDistance = maxY - y; + int edgeDistance = Math.min(floorDistance, ceilingDistance); + int offsetIndex = y - minY; + if (edgeDistance >= fadeRange) { + verticalEdgeFade[offsetIndex] = 0D; + continue; + } + + double t = Math.max(0D, Math.min(1D, edgeDistance / (double) fadeRange)); + double smooth = t * t * (3D - (2D * t)); + verticalEdgeFade[offsetIndex] = (1D - smooth) * fadeStrength; + } + + return verticalEdgeFade; + } + private static final class ModuleState { private final CNG density; private final int minY; @@ -431,12 +758,21 @@ public class IrisCaveCarver3D { } private static final class Scratch { - private final int[] columnSurface = new int[256]; private final int[] columnMaxY = new int[256]; private final int[] surfaceBreakFloorY = new int[256]; private final boolean[] surfaceBreakColumn = new boolean[256]; private final double[] columnThreshold = new double[256]; + private final double[] passThreshold = new double[256]; private final double[] fullWeights = new double[256]; + private final double[] clampedColumnWeights = new double[256]; + private final int[] tileIndices = new int[4]; + private final int[] tileLocalX = new int[4]; + private final int[] tileLocalZ = new int[4]; + private final int[] tileTopY = new int[4]; + private double[] verticalEdgeFade = new double[0]; + private MatterCavern[] matterByY = new MatterCavern[0]; + private Matter[] sectionMatter = new Matter[0]; + private MatterSlice[] sectionSlices = new MatterSlice[0]; private boolean fullWeightsInitialized; } } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index 9c89d8ea3..ce72b911b 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -36,7 +36,7 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import java.util.ArrayList; -import java.util.Comparator; +import java.util.Arrays; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; @@ -45,15 +45,20 @@ import java.util.Map; public class MantleCarvingComponent extends IrisMantleComponent { private static final int CHUNK_SIZE = 16; private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE; + private static final int TILE_SIZE = 2; + private static final int TILE_COUNT = CHUNK_SIZE / TILE_SIZE; + private static final int TILE_AREA = TILE_COUNT * TILE_COUNT; private static final int BLEND_RADIUS = 3; private static final int FIELD_SIZE = CHUNK_SIZE + (BLEND_RADIUS * 2); private static final double MIN_WEIGHT = 0.08D; private static final double THRESHOLD_PENALTY = 0.24D; + private static final int MAX_BLENDED_PROFILE_PASSES = 2; private static final int KERNEL_WIDTH = (BLEND_RADIUS * 2) + 1; private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH; private static final int[] KERNEL_DX = new int[KERNEL_SIZE]; private static final int[] KERNEL_DZ = new int[KERNEL_SIZE]; private static final double[] KERNEL_WEIGHT = new double[KERNEL_SIZE]; + private static final ThreadLocal BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new); private final Map profileCarvers = new IdentityHashMap<>(); @@ -78,32 +83,39 @@ public class MantleCarvingComponent extends IrisMantleComponent { public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(FIELD_SIZE * FIELD_SIZE); + BlendScratch blendScratch = BLEND_SCRATCH.get(); + int[] chunkSurfaceHeights = prepareChunkSurfaceHeights(x, z, context, blendScratch.chunkSurfaceHeights); PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); List weightedProfiles = resolveWeightedProfiles(x, z, resolverState, caveBiomeCache); getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); for (WeightedProfile weightedProfile : weightedProfiles) { - carveProfile(weightedProfile, writer, x, z); + carveProfile(weightedProfile, writer, x, z, chunkSurfaceHeights); } } @ChunkCoordinates - private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz) { + private void carveProfile(WeightedProfile weightedProfile, MantleWriter writer, int cx, int cz, int[] chunkSurfaceHeights) { IrisCaveCarver3D carver = getCarver(weightedProfile.profile); - carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange); + carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange, chunkSurfaceHeights); } private List resolveWeightedProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { - IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ, resolverState, caveBiomeCache); - Map profileWeights = new IdentityHashMap<>(); - IrisCaveProfile[] columnProfiles = new IrisCaveProfile[KERNEL_SIZE]; - double[] columnProfileWeights = new double[KERNEL_SIZE]; + BlendScratch blendScratch = BLEND_SCRATCH.get(); + IrisCaveProfile[] profileField = blendScratch.profileField; + Map tileProfileWeights = blendScratch.tileProfileWeights; + IdentityHashMap activeProfiles = blendScratch.activeProfiles; + IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles; + double[] kernelProfileWeights = blendScratch.kernelProfileWeights; + activeProfiles.clear(); + fillProfileField(profileField, chunkX, chunkZ, resolverState, caveBiomeCache); - for (int localX = 0; localX < CHUNK_SIZE; localX++) { - for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + for (int tileX = 0; tileX < TILE_COUNT; tileX++) { + for (int tileZ = 0; tileZ < TILE_COUNT; tileZ++) { int profileCount = 0; - int columnIndex = (localX << 4) | localZ; - int centerX = localX + BLEND_RADIUS; - int centerZ = localZ + BLEND_RADIUS; + int sampleLocalX = (tileX * TILE_SIZE) + 1; + int sampleLocalZ = (tileZ * TILE_SIZE) + 1; + int centerX = sampleLocalX + BLEND_RADIUS; + int centerZ = sampleLocalZ + BLEND_RADIUS; double totalKernelWeight = 0D; for (int kernelIndex = 0; kernelIndex < KERNEL_SIZE; kernelIndex++) { @@ -115,12 +127,12 @@ public class MantleCarvingComponent extends IrisMantleComponent { } double kernelWeight = KERNEL_WEIGHT[kernelIndex]; - int existingIndex = findProfileIndex(columnProfiles, profileCount, profile); + int existingIndex = findProfileIndex(kernelProfiles, profileCount, profile); if (existingIndex >= 0) { - columnProfileWeights[existingIndex] += kernelWeight; + kernelProfileWeights[existingIndex] += kernelWeight; } else { - columnProfiles[profileCount] = profile; - columnProfileWeights[profileCount] = kernelWeight; + kernelProfiles[profileCount] = profile; + kernelProfileWeights[profileCount] = kernelWeight; profileCount++; } totalKernelWeight += kernelWeight; @@ -130,25 +142,50 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } + IrisCaveProfile dominantProfile = null; + double dominantKernelWeight = Double.NEGATIVE_INFINITY; for (int profileIndex = 0; profileIndex < profileCount; profileIndex++) { - IrisCaveProfile profile = columnProfiles[profileIndex]; - double normalizedWeight = columnProfileWeights[profileIndex] / totalKernelWeight; - double[] weights = profileWeights.computeIfAbsent(profile, key -> new double[CHUNK_AREA]); - weights[columnIndex] = normalizedWeight; - columnProfiles[profileIndex] = null; - columnProfileWeights[profileIndex] = 0D; + IrisCaveProfile profile = kernelProfiles[profileIndex]; + double kernelWeight = kernelProfileWeights[profileIndex]; + if (kernelWeight > dominantKernelWeight) { + dominantProfile = profile; + dominantKernelWeight = kernelWeight; + } else if (kernelWeight == dominantKernelWeight + && profileSortKey(profile) < profileSortKey(dominantProfile)) { + dominantProfile = profile; + } + kernelProfiles[profileIndex] = null; + kernelProfileWeights[profileIndex] = 0D; } + + if (dominantProfile == null) { + continue; + } + + int tileIndex = tileIndex(tileX, tileZ); + double dominantWeight = clampWeight(dominantKernelWeight / totalKernelWeight); + double[] tileWeights = tileProfileWeights.get(dominantProfile); + if (tileWeights == null) { + tileWeights = new double[TILE_AREA]; + tileProfileWeights.put(dominantProfile, tileWeights); + } else if (!activeProfiles.containsKey(dominantProfile)) { + Arrays.fill(tileWeights, 0D); + } + activeProfiles.put(dominantProfile, Boolean.TRUE); + tileWeights[tileIndex] = dominantWeight; } } - List weightedProfiles = new ArrayList<>(); - for (Map.Entry entry : profileWeights.entrySet()) { - IrisCaveProfile profile = entry.getKey(); - double[] weights = entry.getValue(); + List tileWeightedProfiles = new ArrayList<>(); + for (IrisCaveProfile profile : activeProfiles.keySet()) { + double[] tileWeights = tileProfileWeights.get(profile); + if (tileWeights == null) { + continue; + } + double totalWeight = 0D; double maxWeight = 0D; - - for (double weight : weights) { + for (double weight : tileWeights) { totalWeight += weight; if (weight > maxWeight) { maxWeight = weight; @@ -159,22 +196,27 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } - double averageWeight = totalWeight / CHUNK_AREA; - weightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null)); + double averageWeight = totalWeight / TILE_AREA; + tileWeightedProfiles.add(new WeightedProfile(profile, tileWeights, averageWeight, null)); } - weightedProfiles.sort(Comparator.comparingDouble(WeightedProfile::averageWeight)); - weightedProfiles.addAll(0, resolveDimensionCarvingProfiles(chunkX, chunkZ, resolverState)); - return weightedProfiles; + List boundedTileProfiles = limitAndMergeBlendedProfiles(tileWeightedProfiles, MAX_BLENDED_PROFILE_PASSES, TILE_AREA); + List blendedProfiles = expandTileWeightedProfiles(boundedTileProfiles); + List resolvedProfiles = resolveDimensionCarvingProfiles(chunkX, chunkZ, resolverState, blendScratch); + resolvedProfiles.addAll(blendedProfiles); + return resolvedProfiles; } - private List resolveDimensionCarvingProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) { + private List resolveDimensionCarvingProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, BlendScratch blendScratch) { List weightedProfiles = new ArrayList<>(); List entries = getDimension().getCarving(); if (entries == null || entries.isEmpty()) { return weightedProfiles; } + Map dimensionTilePlans = blendScratch.dimensionTilePlans; + dimensionTilePlans.clear(); + for (IrisDimensionCarvingEntry entry : entries) { if (entry == null || !entry.isEnabled()) { continue; @@ -185,41 +227,93 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } - Map rootProfileWeights = new IdentityHashMap<>(); + IrisDimensionCarvingEntry[] tilePlan = dimensionTilePlans.computeIfAbsent(entry, key -> new IrisDimensionCarvingEntry[TILE_AREA]); + buildDimensionTilePlan(tilePlan, chunkX, chunkZ, entry, resolverState); + + Map rootProfileTileWeights = new IdentityHashMap<>(); IrisRange worldYRange = entry.getWorldYRange(); - for (int localX = 0; localX < CHUNK_SIZE; localX++) { - for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { - int worldX = (chunkX << 4) + localX; - int worldZ = (chunkZ << 4) + localZ; - int columnIndex = (localX << 4) | localZ; - IrisDimensionCarvingEntry resolvedEntry = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState); - IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState); - if (resolvedBiome == null) { - continue; - } - - IrisCaveProfile profile = resolvedBiome.getCaveProfile(); - if (!isProfileEnabled(profile)) { - continue; - } - - double[] weights = rootProfileWeights.computeIfAbsent(profile, key -> new double[CHUNK_AREA]); - weights[columnIndex] = 1D; + for (int tileIndex = 0; tileIndex < TILE_AREA; tileIndex++) { + IrisDimensionCarvingEntry resolvedEntry = tilePlan[tileIndex]; + IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState); + if (resolvedBiome == null) { + continue; } + + IrisCaveProfile profile = resolvedBiome.getCaveProfile(); + if (!isProfileEnabled(profile)) { + continue; + } + + double[] tileWeights = rootProfileTileWeights.computeIfAbsent(profile, key -> new double[TILE_AREA]); + tileWeights[tileIndex] = 1D; } - List> profileEntries = new ArrayList<>(rootProfileWeights.entrySet()); + List> profileEntries = new ArrayList<>(rootProfileTileWeights.entrySet()); profileEntries.sort((a, b) -> Integer.compare(a.getKey().hashCode(), b.getKey().hashCode())); for (Map.Entry profileEntry : profileEntries) { - weightedProfiles.add(new WeightedProfile(profileEntry.getKey(), profileEntry.getValue(), -1D, worldYRange)); + double[] columnWeights = expandTileWeightsToColumns(profileEntry.getValue()); + weightedProfiles.add(new WeightedProfile(profileEntry.getKey(), columnWeights, -1D, worldYRange)); } } return weightedProfiles; } - private IrisCaveProfile[] buildProfileField(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { - IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE]; + private void buildDimensionTilePlan(IrisDimensionCarvingEntry[] tilePlan, int chunkX, int chunkZ, IrisDimensionCarvingEntry entry, IrisDimensionCarvingResolver.State resolverState) { + for (int tileX = 0; tileX < TILE_COUNT; tileX++) { + int worldX = (chunkX << 4) + (tileX * TILE_SIZE); + for (int tileZ = 0; tileZ < TILE_COUNT; tileZ++) { + int worldZ = (chunkZ << 4) + (tileZ * TILE_SIZE); + int tileIndex = tileIndex(tileX, tileZ); + tilePlan[tileIndex] = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState); + } + } + } + + private List expandTileWeightedProfiles(List tileWeightedProfiles) { + List expandedProfiles = new ArrayList<>(tileWeightedProfiles.size()); + for (WeightedProfile tileWeightedProfile : tileWeightedProfiles) { + double[] columnWeights = expandTileWeightsToColumns(tileWeightedProfile.columnWeights); + double averageWeight = computeAverageWeight(columnWeights, CHUNK_AREA); + expandedProfiles.add(new WeightedProfile(tileWeightedProfile.profile, columnWeights, averageWeight, tileWeightedProfile.worldYRange)); + } + expandedProfiles.sort(MantleCarvingComponent::compareByCarveOrder); + return expandedProfiles; + } + + private static double[] expandTileWeightsToColumns(double[] tileWeights) { + double[] columnWeights = new double[CHUNK_AREA]; + if (tileWeights == null || tileWeights.length == 0) { + return columnWeights; + } + + for (int tileX = 0; tileX < TILE_COUNT; tileX++) { + int columnX = tileX * TILE_SIZE; + int columnX2 = columnX + 1; + for (int tileZ = 0; tileZ < TILE_COUNT; tileZ++) { + int tileIndex = tileIndex(tileX, tileZ); + double weight = tileWeights[tileIndex]; + if (weight <= 0D) { + continue; + } + + int columnZ = tileZ * TILE_SIZE; + int columnZ2 = columnZ + 1; + columnWeights[(columnX << 4) | columnZ] = weight; + columnWeights[(columnX << 4) | columnZ2] = weight; + columnWeights[(columnX2 << 4) | columnZ] = weight; + columnWeights[(columnX2 << 4) | columnZ2] = weight; + } + } + + return columnWeights; + } + + private static int tileIndex(int tileX, int tileZ) { + return (tileX * TILE_COUNT) + tileZ; + } + + private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { int startX = (chunkX << 4) - BLEND_RADIUS; int startZ = (chunkZ << 4) - BLEND_RADIUS; @@ -230,8 +324,6 @@ public class MantleCarvingComponent extends IrisMantleComponent { profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ, resolverState, caveBiomeCache); } } - - return profileField; } private int findProfileIndex(IrisCaveProfile[] profiles, int size, IrisCaveProfile profile) { @@ -308,6 +400,136 @@ public class MantleCarvingComponent extends IrisMantleComponent { return 0; } + private int[] prepareChunkSurfaceHeights(int chunkX, int chunkZ, ChunkContext context, int[] scratch) { + int[] surfaceHeights = scratch; + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + boolean useContextHeight = context != null + && context.getHeight() != null + && context.getX() == baseX + && context.getZ() == baseZ; + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + int worldX = baseX + localX; + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int worldZ = baseZ + localZ; + int columnIndex = (localX << 4) | localZ; + if (useContextHeight) { + Double cachedHeight = context.getHeight().get(localX, localZ); + if (cachedHeight != null) { + surfaceHeights[columnIndex] = (int) Math.round(cachedHeight); + continue; + } + } + surfaceHeights[columnIndex] = getEngineMantle().getEngine().getHeight(worldX, worldZ); + } + } + return surfaceHeights; + } + + private static List limitAndMergeBlendedProfiles(List blendedProfiles, int maxProfiles) { + return limitAndMergeBlendedProfiles(blendedProfiles, maxProfiles, CHUNK_AREA); + } + + private static List limitAndMergeBlendedProfiles(List blendedProfiles, int maxProfiles, int areaSize) { + if (blendedProfiles == null || blendedProfiles.isEmpty()) { + return new ArrayList<>(); + } + + int clampedLimit = Math.max(1, maxProfiles); + List rankedProfiles = new ArrayList<>(blendedProfiles); + rankedProfiles.sort(MantleCarvingComponent::compareBySelectionRank); + List keptProfiles = new ArrayList<>(); + int keptCount = Math.min(clampedLimit, rankedProfiles.size()); + for (int index = 0; index < keptCount; index++) { + keptProfiles.add(rankedProfiles.get(index)); + } + + if (rankedProfiles.size() > keptCount) { + for (int columnIndex = 0; columnIndex < areaSize; columnIndex++) { + int dominantIndex = 0; + double dominantWeight = Double.NEGATIVE_INFINITY; + for (int keptIndex = 0; keptIndex < keptProfiles.size(); keptIndex++) { + double keptWeight = keptProfiles.get(keptIndex).columnWeights[columnIndex]; + if (keptWeight > dominantWeight) { + dominantWeight = keptWeight; + dominantIndex = keptIndex; + } + } + + double droppedWeight = 0D; + for (int droppedIndex = keptCount; droppedIndex < rankedProfiles.size(); droppedIndex++) { + droppedWeight += rankedProfiles.get(droppedIndex).columnWeights[columnIndex]; + } + if (droppedWeight <= 0D) { + continue; + } + + WeightedProfile dominantProfile = keptProfiles.get(dominantIndex); + double mergedWeight = dominantProfile.columnWeights[columnIndex] + droppedWeight; + dominantProfile.columnWeights[columnIndex] = clampWeight(mergedWeight); + } + } + + List mergedProfiles = new ArrayList<>(); + for (WeightedProfile keptProfile : keptProfiles) { + double averageWeight = computeAverageWeight(keptProfile.columnWeights, areaSize); + mergedProfiles.add(new WeightedProfile(keptProfile.profile, keptProfile.columnWeights, averageWeight, keptProfile.worldYRange)); + } + mergedProfiles.sort(MantleCarvingComponent::compareByCarveOrder); + return mergedProfiles; + } + + private static int compareBySelectionRank(WeightedProfile a, WeightedProfile b) { + int weightOrder = Double.compare(b.averageWeight, a.averageWeight); + if (weightOrder != 0) { + return weightOrder; + } + return Integer.compare(profileSortKey(a.profile), profileSortKey(b.profile)); + } + + private static int compareByCarveOrder(WeightedProfile a, WeightedProfile b) { + int weightOrder = Double.compare(a.averageWeight, b.averageWeight); + if (weightOrder != 0) { + return weightOrder; + } + return Integer.compare(profileSortKey(a.profile), profileSortKey(b.profile)); + } + + private static int profileSortKey(IrisCaveProfile profile) { + if (profile == null) { + return 0; + } + return profile.hashCode(); + } + + private static double computeAverageWeight(double[] weights) { + return computeAverageWeight(weights, CHUNK_AREA); + } + + private static double computeAverageWeight(double[] weights, int areaSize) { + if (weights == null || weights.length == 0) { + return 0D; + } + double sum = 0D; + for (double weight : weights) { + sum += weight; + } + return sum / Math.max(1, areaSize); + } + + private static double clampWeight(double value) { + if (Double.isNaN(value) || Double.isInfinite(value)) { + return 0D; + } + if (value <= 0D) { + return 0D; + } + if (value >= 1D) { + return 1D; + } + return value; + } + private static final class WeightedProfile { private final IrisCaveProfile profile; private final double[] columnWeights; @@ -325,4 +547,14 @@ public class MantleCarvingComponent extends IrisMantleComponent { return averageWeight; } } + + private static final class BlendScratch { + private final IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE]; + private final IrisCaveProfile[] kernelProfiles = new IrisCaveProfile[KERNEL_SIZE]; + private final double[] kernelProfileWeights = new double[KERNEL_SIZE]; + private final IdentityHashMap tileProfileWeights = new IdentityHashMap<>(); + private final IdentityHashMap dimensionTilePlans = new IdentityHashMap<>(); + private final IdentityHashMap activeProfiles = new IdentityHashMap<>(); + private final int[] chunkSurfaceHeights = new int[CHUNK_AREA]; + } } diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index 9fb35c43b..39c5fadbe 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -46,8 +46,11 @@ import org.bukkit.Material; import org.bukkit.block.data.BlockData; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; public class IrisCarveModifier extends EngineAssignedModifier { + private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(CarveScratch::new); private final RNG rng; private final BlockData AIR = Material.CAVE_AIR.createBlockData(); private final BlockData LAVA = Material.LAVA.createBlockData(); @@ -67,9 +70,12 @@ public class IrisCarveModifier extends EngineAssignedModifier { MantleChunk mc = mantle.getChunk(x, z).use(); IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(2048); - int[][] columnHeights = new int[256][]; - int[] columnHeightSizes = new int[256]; - PackedWallBuffer walls = new PackedWallBuffer(512); + CarveScratch scratch = SCRATCH.get(); + scratch.reset(); + PackedWallBuffer walls = scratch.walls; + ColumnMask[] columnMasks = scratch.columnMasks; + Map customBiomeCache = scratch.customBiomeCache; + try { PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); mc.iterate(MatterCavern.class, (xx, yy, zz, c) -> { @@ -90,7 +96,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { return; } - appendColumnHeight(columnHeights, columnHeightSizes, columnIndex, yy); + columnMasks[columnIndex].add(yy); if (rz < 15 && mc.get(xx, yy, zz + 1, MatterCavern.class) == null) { walls.put(rx, yy, rz + 1, c); @@ -131,9 +137,10 @@ public class IrisCarveModifier extends EngineAssignedModifier { walls.forEach((rx, yy, rz, cavern) -> { int worldX = rx + (x << 4); int worldZ = rz + (z << 4); - IrisBiome biome = cavern.getCustomBiome().isEmpty() + String customBiome = cavern.getCustomBiome(); + IrisBiome biome = customBiome.isEmpty() ? resolveCaveBiome(caveBiomeCache, worldX, yy, worldZ, resolverState) - : getEngine().getData().getBiomeLoader().load(cavern.getCustomBiome()); + : resolveCustomBiome(customBiomeCache, customBiome); if (biome != null) { biome.setInferredType(InferredType.CAVE); @@ -146,43 +153,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { }); for (int columnIndex = 0; columnIndex < 256; columnIndex++) { - int size = columnHeightSizes[columnIndex]; - if (size <= 0) { - continue; - } - - int[] heights = columnHeights[columnIndex]; - Arrays.sort(heights, 0, size); - int rx = columnIndex >> 4; - int rz = columnIndex & 15; - CaveZone zone = new CaveZone(); - zone.setFloor(heights[0]); - int buf = heights[0] - 1; - - for (int heightIndex = 0; heightIndex < size; heightIndex++) { - int y = heights[heightIndex]; - if (y < 0 || y > getEngine().getHeight()) { - continue; - } - - if (y == buf + 1) { - buf = y; - zone.ceiling = buf; - } else if (zone.isValid(getEngine())) { - processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState, caveBiomeCache); - zone = new CaveZone(); - zone.setFloor(y); - buf = y; - } else { - zone = new CaveZone(); - zone.setFloor(y); - buf = y; - } - } - - if (zone.isValid(getEngine())) { - processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState, caveBiomeCache); - } + processColumnFromMask(output, mc, mantle, columnMasks[columnIndex], columnIndex, x, z, resolverState, caveBiomeCache); } } finally { getEngine().getMetrics().getCarveApply().put(applyStopwatch.getMilliseconds()); @@ -193,6 +164,60 @@ public class IrisCarveModifier extends EngineAssignedModifier { } } + private void processColumnFromMask( + Hunk output, + MantleChunk mc, + Mantle mantle, + ColumnMask columnMask, + int columnIndex, + int chunkX, + int chunkZ, + IrisDimensionCarvingResolver.State resolverState, + Long2ObjectOpenHashMap caveBiomeCache + ) { + if (columnMask == null || columnMask.isEmpty()) { + return; + } + + int firstHeight = columnMask.nextSetBit(0); + if (firstHeight < 0) { + return; + } + + int rx = columnIndex >> 4; + int rz = columnIndex & 15; + int worldX = rx + (chunkX << 4); + int worldZ = rz + (chunkZ << 4); + CaveZone zone = new CaveZone(); + zone.setFloor(firstHeight); + int buf = firstHeight - 1; + int y = firstHeight; + + while (y >= 0) { + if (y >= 0 && y <= getEngine().getHeight()) { + if (y == buf + 1) { + buf = y; + zone.ceiling = buf; + } else if (zone.isValid(getEngine())) { + processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache); + zone = new CaveZone(); + zone.setFloor(y); + buf = y; + } else { + zone = new CaveZone(); + zone.setFloor(y); + buf = y; + } + } + + y = columnMask.nextSetBit(y + 1); + } + + if (zone.isValid(getEngine())) { + processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState, caveBiomeCache); + } + } + private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { int center = (zone.floor + zone.ceiling) / 2; String customBiome = ""; @@ -303,20 +328,14 @@ public class IrisCarveModifier extends EngineAssignedModifier { return resolvedBiome; } - private void appendColumnHeight(int[][] heights, int[] sizes, int columnIndex, int y) { - int[] column = heights[columnIndex]; - int size = sizes[columnIndex]; - if (column == null) { - column = new int[8]; - heights[columnIndex] = column; - } else if (size >= column.length) { - int nextSize = column.length << 1; - column = Arrays.copyOf(column, nextSize); - heights[columnIndex] = column; + private IrisBiome resolveCustomBiome(Map customBiomeCache, String customBiome) { + if (customBiomeCache.containsKey(customBiome)) { + return customBiomeCache.get(customBiome); } - column[size] = y; - sizes[columnIndex] = size + 1; + IrisBiome loaded = getEngine().getData().getBiomeLoader().load(customBiome); + customBiomeCache.put(customBiome, loaded); + return loaded; } private static final class PackedWallBuffer { @@ -384,6 +403,12 @@ public class IrisCarveModifier extends EngineAssignedModifier { } } + private void clear() { + Arrays.fill(keys, EMPTY_KEY); + Arrays.fill(values, null); + size = 0; + } + private void resize() { int[] oldKeys = keys; MatterCavern[] oldValues = values; @@ -443,6 +468,87 @@ public class IrisCarveModifier extends EngineAssignedModifier { } } + private static final class CarveScratch { + private final ColumnMask[] columnMasks = new ColumnMask[256]; + private final PackedWallBuffer walls = new PackedWallBuffer(512); + private final Map customBiomeCache = new HashMap<>(); + + private CarveScratch() { + for (int index = 0; index < columnMasks.length; index++) { + columnMasks[index] = new ColumnMask(); + } + } + + private void reset() { + for (int index = 0; index < columnMasks.length; index++) { + columnMasks[index].clear(); + } + walls.clear(); + customBiomeCache.clear(); + } + } + + private static final class ColumnMask { + private long[] words = new long[8]; + private int maxWord = -1; + + private void add(int y) { + if (y < 0) { + return; + } + + int wordIndex = y >> 6; + if (wordIndex >= words.length) { + words = Arrays.copyOf(words, Math.max(words.length << 1, wordIndex + 1)); + } + + words[wordIndex] |= 1L << (y & 63); + if (wordIndex > maxWord) { + maxWord = wordIndex; + } + } + + private int nextSetBit(int fromBit) { + if (maxWord < 0) { + return -1; + } + + int startBit = Math.max(0, fromBit); + int wordIndex = startBit >> 6; + if (wordIndex > maxWord) { + return -1; + } + + long word = words[wordIndex] & (-1L << (startBit & 63)); + while (true) { + if (word != 0L) { + return (wordIndex << 6) + Long.numberOfTrailingZeros(word); + } + + wordIndex++; + if (wordIndex > maxWord) { + return -1; + } + word = words[wordIndex]; + } + } + + private boolean isEmpty() { + return maxWord < 0; + } + + private void clear() { + if (maxWord < 0) { + return; + } + + for (int index = 0; index <= maxWord; index++) { + words[index] = 0L; + } + maxWord = -1; + } + } + @FunctionalInterface private interface PackedWallConsumer { void accept(int x, int y, int z, MatterCavern cavern); diff --git a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java index 406a8a880..66ab73b42 100644 --- a/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/platform/BukkitChunkGenerator.java @@ -579,10 +579,6 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun public void generateNoise(@NotNull WorldInfo world, @NotNull Random random, int x, int z, @NotNull ChunkGenerator.ChunkData d) { try { Engine engine = getEngine(world); - World realWorld = engine.getWorld().realWorld(); - if (realWorld != null && IrisToolbelt.isWorldMaintenanceActive(realWorld)) { - return; - } computeStudioGenerator(); TerrainChunk tc = TerrainChunk.create(d); this.world.bind(world); diff --git a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java index fd7c86b9b..677942041 100644 --- a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java @@ -321,12 +321,23 @@ public class J { return; } - if (!runGlobalImmediate(r)) { - try { - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r); - } catch (UnsupportedOperationException e) { - throw new IllegalStateException("Failed to schedule sync task (Folia scheduler unavailable, BukkitScheduler unsupported).", e); + if (isFolia()) { + if (runGlobalImmediate(r)) { + return; } + + throw new IllegalStateException("Failed to schedule sync task on Folia runtime."); + } + + try { + Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r); + } catch (UnsupportedOperationException e) { + FoliaScheduler.forceFoliaThreading(Bukkit.getServer()); + if (runGlobalImmediate(r)) { + return; + } + + throw new IllegalStateException("Failed to schedule sync task (Folia scheduler unavailable, BukkitScheduler unsupported).", e); } } @@ -397,10 +408,28 @@ public class J { return; } - try { - if (!runGlobalDelayed(r, delay)) { - Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r, delay); + if (isFolia()) { + if (runGlobalDelayed(r, delay)) { + return; } + + a(() -> { + if (sleep(ticksToMilliseconds(delay))) { + s(r); + } + }); + return; + } + + try { + Bukkit.getScheduler().scheduleSyncDelayedTask(Iris.instance, r, delay); + } catch (UnsupportedOperationException e) { + FoliaScheduler.forceFoliaThreading(Bukkit.getServer()); + if (runGlobalDelayed(r, delay)) { + return; + } + + throw new IllegalStateException("Failed to schedule delayed sync task (Folia scheduler unavailable, BukkitScheduler unsupported).", e); } catch (Throwable e) { Iris.reportError(e); } @@ -551,6 +580,11 @@ public class J { return false; } + if (isPrimaryThread()) { + runnable.run(); + return true; + } + return FoliaScheduler.runGlobal(Iris.instance, runnable); } @@ -559,6 +593,10 @@ public class J { return false; } + if (delayTicks <= 0) { + return runGlobalImmediate(runnable); + } + return FoliaScheduler.runGlobal(Iris.instance, runnable, Math.max(0, delayTicks)); } diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java index 2cdf6508f..0b9fbcffc 100644 --- a/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java @@ -1,19 +1,18 @@ package art.arcane.iris.util.project.context; -import art.arcane.iris.core.IrisHotPathMetricsMode; -import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.Iris; import art.arcane.iris.engine.IrisComplex; import art.arcane.iris.engine.framework.EngineMetrics; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.volmlib.util.atomics.AtomicRollingSequence; +import art.arcane.iris.util.common.parallel.MultiBurst; import org.bukkit.block.data.BlockData; -import java.util.IdentityHashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; public class ChunkContext { - private static final int PREFILL_METRICS_FLUSH_SIZE = 64; - private static final ThreadLocal PREFILL_METRICS = ThreadLocal.withInitial(PrefillMetricsState::new); private final int x; private final int z; private final ChunkedDataCache height; @@ -47,41 +46,45 @@ public class ChunkContext { if (cache) { PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan; - PrefillMetricsState metricsState = PREFILL_METRICS.get(); - IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); - IrisHotPathMetricsMode metricsMode = pregen.getHotPathMetricsMode(); - boolean sampleMetrics = metricsMode != IrisHotPathMetricsMode.DISABLED - && metricsState.shouldSample(metricsMode, pregen.getHotPathMetricsSampleStride()); - long totalStartNanos = sampleMetrics ? System.nanoTime() : 0L; + boolean capturePrefillMetric = metrics != null; + long totalStartNanos = capturePrefillMetric ? System.nanoTime() : 0L; + List fillTasks = new ArrayList<>(6); if (resolvedPlan.height) { - fill(height, metrics == null ? null : metrics.getContextPrefillHeight(), sampleMetrics, metricsState); + fillTasks.add(new PrefillFillTask(height)); } if (resolvedPlan.biome) { - fill(biome, metrics == null ? null : metrics.getContextPrefillBiome(), sampleMetrics, metricsState); + fillTasks.add(new PrefillFillTask(biome)); } if (resolvedPlan.rock) { - fill(rock, metrics == null ? null : metrics.getContextPrefillRock(), sampleMetrics, metricsState); + fillTasks.add(new PrefillFillTask(rock)); } if (resolvedPlan.fluid) { - fill(fluid, metrics == null ? null : metrics.getContextPrefillFluid(), sampleMetrics, metricsState); + fillTasks.add(new PrefillFillTask(fluid)); } if (resolvedPlan.region) { - fill(region, metrics == null ? null : metrics.getContextPrefillRegion(), sampleMetrics, metricsState); + fillTasks.add(new PrefillFillTask(region)); } if (resolvedPlan.cave) { - fill(cave, metrics == null ? null : metrics.getContextPrefillCave(), sampleMetrics, metricsState); + fillTasks.add(new PrefillFillTask(cave)); } - if (metrics != null && sampleMetrics) { - metricsState.record(metrics.getContextPrefill(), System.nanoTime() - totalStartNanos); - } - } - } - private void fill(ChunkedDataCache dataCache, AtomicRollingSequence metrics, boolean sampleMetrics, PrefillMetricsState metricsState) { - long startNanos = sampleMetrics ? System.nanoTime() : 0L; - dataCache.fill(); - if (metrics != null && sampleMetrics) { - metricsState.record(metrics, System.nanoTime() - startNanos); + if (fillTasks.size() <= 1 || Iris.instance == null) { + for (PrefillFillTask fillTask : fillTasks) { + fillTask.run(); + } + } else { + List> futures = new ArrayList<>(fillTasks.size()); + for (PrefillFillTask fillTask : fillTasks) { + futures.add(CompletableFuture.runAsync(fillTask, MultiBurst.burst)); + } + for (CompletableFuture future : futures) { + future.join(); + } + } + + if (capturePrefillMetric) { + metrics.getContextPrefill().put((System.nanoTime() - totalStartNanos) / 1_000_000D); + } } } @@ -139,43 +142,16 @@ public class ChunkContext { } } - private static final class PrefillMetricsState { - private long callCounter; - private final IdentityHashMap buckets = new IdentityHashMap<>(); + private static final class PrefillFillTask implements Runnable { + private final ChunkedDataCache dataCache; - private boolean shouldSample(IrisHotPathMetricsMode mode, int sampleStride) { - if (mode == IrisHotPathMetricsMode.EXACT) { - return true; - } - - long current = callCounter++; - return (current & (sampleStride - 1L)) == 0L; + private PrefillFillTask(ChunkedDataCache dataCache) { + this.dataCache = dataCache; } - private void record(AtomicRollingSequence sequence, long nanos) { - if (sequence == null || nanos < 0L) { - return; - } - - MetricBucket bucket = buckets.get(sequence); - if (bucket == null) { - bucket = new MetricBucket(); - buckets.put(sequence, bucket); - } - - bucket.nanos += nanos; - bucket.samples++; - if (bucket.samples >= PREFILL_METRICS_FLUSH_SIZE) { - double averageMs = (bucket.nanos / (double) bucket.samples) / 1_000_000D; - sequence.put(averageMs); - bucket.nanos = 0L; - bucket.samples = 0; - } + @Override + public void run() { + dataCache.fill(); } } - - private static final class MetricBucket { - private long nanos; - private int samples; - } } diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java index 3a3f6b8c5..2471d1efa 100644 --- a/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java @@ -73,6 +73,8 @@ public class CNG { private double power; private NoiseStyle leakStyle; private ProceduralStream customGenerator; + private transient boolean identityPostFastPath; + private transient boolean fastPathStateDirty = true; public CNG(RNG random) { this(random, 1); @@ -112,6 +114,8 @@ public class CNG { if (generator instanceof OctaveNoise) { ((OctaveNoise) generator).setOctaves(octaves); } + + refreshFastPathState(); } public static CNG signature(RNG rng) { @@ -304,6 +308,7 @@ public class CNG { public CNG bake() { bakedScale *= scale; scale = 1; + markFastPathStateDirty(); return this; } @@ -313,6 +318,7 @@ public class CNG { } children.add(c); + markFastPathStateDirty(); return this; } @@ -323,32 +329,38 @@ public class CNG { public CNG fractureWith(CNG c, double scale) { fracture = c; fscale = scale; + markFastPathStateDirty(); return this; } public CNG scale(double c) { scale = c; + markFastPathStateDirty(); return this; } public CNG patch(double c) { patch = c; + markFastPathStateDirty(); return this; } public CNG up(double c) { up = c; + markFastPathStateDirty(); return this; } public CNG down(double c) { down = c; + markFastPathStateDirty(); return this; } public CNG injectWith(NoiseInjector i) { injector = i == null ? ADD : i; injectorMode = resolveInjectorMode(injector); + markFastPathStateDirty(); return this; } @@ -665,7 +677,7 @@ public class CNG { return generator.noise(x * scl, 0D, 0D) * opacity; } - double fx = x + ((fracture.noise(x) - 0.5D) * fscale); + double fx = x + ((fracture.noiseFast1D(x) - 0.5D) * fscale); return generator.noise(fx * scl, 0D, 0D) * opacity; } @@ -676,8 +688,8 @@ public class CNG { return generator.noise(x * scl, z * scl, 0D) * opacity; } - double fx = x + ((fracture.noise(x, z) - 0.5D) * fscale); - double fz = z + ((fracture.noise(z, x) - 0.5D) * fscale); + double fx = x + ((fracture.noiseFast2D(x, z) - 0.5D) * fscale); + double fz = z + ((fracture.noiseFast2D(z, x) - 0.5D) * fscale); return generator.noise(fx * scl, fz * scl, 0D) * opacity; } @@ -688,9 +700,9 @@ public class CNG { return generator.noise(x * scl, y * scl, z * scl) * opacity; } - double fx = x + ((fracture.noise(x, y, z) - 0.5D) * fscale); - double fy = y + ((fracture.noise(y, x) - 0.5D) * fscale); - double fz = z + ((fracture.noise(z, x, y) - 0.5D) * fscale); + double fx = x + ((fracture.noiseFast3D(x, y, z) - 0.5D) * fscale); + double fy = y + ((fracture.noiseFast2D(y, x) - 0.5D) * fscale); + double fz = z + ((fracture.noiseFast3D(z, x, y) - 0.5D) * fscale); return generator.noise(fx * scl, fy * scl, fz * scl) * opacity; } @@ -913,6 +925,10 @@ public class CNG { return cache.get((int) x, (int) z); } + if (isIdentityPostFastPath()) { + return getNoise(x, z); + } + return applyPost(getNoise(x, z), x, z); } @@ -921,11 +937,16 @@ public class CNG { } public double noiseFast3D(double x, double y, double z) { + if (isIdentityPostFastPath()) { + return getNoise(x, y, z); + } + return applyPost(getNoise(x, y, z), x, y, z); } public CNG pow(double power) { this.power = power; + markFastPathStateDirty(); return this; } @@ -942,6 +963,28 @@ public class CNG { return generator != null && generator.isStatic(); } + private boolean isIdentityPostFastPath() { + if (fastPathStateDirty) { + refreshFastPathState(); + } + + return identityPostFastPath; + } + + private void markFastPathStateDirty() { + fastPathStateDirty = true; + } + + private void refreshFastPathState() { + identityPostFastPath = power == 1D + && children == null + && fracture == null + && down == 0D + && up == 0D + && patch == 1D; + fastPathStateDirty = false; + } + private enum InjectorMode { ADD, SRC_SUBTRACT, diff --git a/core/src/test/java/art/arcane/iris/core/ExternalDataPackPipelineNbtRewriteTest.java b/core/src/test/java/art/arcane/iris/core/ExternalDataPackPipelineNbtRewriteTest.java new file mode 100644 index 000000000..9101cf23a --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/ExternalDataPackPipelineNbtRewriteTest.java @@ -0,0 +1,93 @@ +package art.arcane.iris.core; + +import art.arcane.volmlib.util.nbt.io.NBTDeserializer; +import art.arcane.volmlib.util.nbt.io.NBTSerializer; +import art.arcane.volmlib.util.nbt.io.NamedTag; +import art.arcane.volmlib.util.nbt.tag.CompoundTag; +import art.arcane.volmlib.util.nbt.tag.IntTag; +import art.arcane.volmlib.util.nbt.tag.ListTag; +import art.arcane.volmlib.util.nbt.tag.Tag; +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class ExternalDataPackPipelineNbtRewriteTest { + @Test + public void rewritesOnlyJigsawPoolReferencesForCompressedAndUncompressedNbt() throws Exception { + for (boolean compressed : new boolean[]{false, true}) { + byte[] source = encodeStructureNbt(compressed, true); + Map remapped = new HashMap<>(); + remapped.put("minecraft:witch_hut/foundation", "iris_external_1:witch_hut/foundation"); + + byte[] rewritten = invokeRewrite(source, remapped); + CompoundTag root = decodeRoot(rewritten, compressed); + ListTag blocks = root.getListTag("blocks"); + + CompoundTag jigsawBlock = (CompoundTag) blocks.get(0); + CompoundTag nonJigsawBlock = (CompoundTag) blocks.get(1); + assertEquals("iris_external_1:witch_hut/foundation", jigsawBlock.getCompoundTag("nbt").getString("pool")); + assertEquals("minecraft:witch_hut/foundation", nonJigsawBlock.getCompoundTag("nbt").getString("pool")); + } + } + + @Test + public void nonJigsawPayloadIsLeftUnchanged() throws Exception { + byte[] source = encodeStructureNbt(false, false); + Map remapped = new HashMap<>(); + remapped.put("minecraft:witch_hut/foundation", "iris_external_1:witch_hut/foundation"); + + byte[] rewritten = invokeRewrite(source, remapped); + assertArrayEquals(source, rewritten); + } + + private byte[] invokeRewrite(byte[] input, Map remappedKeys) { + return StructureNbtJigsawPoolRewriter.rewrite(input, remappedKeys); + } + + private byte[] encodeStructureNbt(boolean compressed, boolean includeJigsaw) throws Exception { + CompoundTag root = new CompoundTag(); + ListTag palette = new ListTag<>(CompoundTag.class); + + CompoundTag firstPalette = new CompoundTag(); + firstPalette.putString("Name", includeJigsaw ? "minecraft:jigsaw" : "minecraft:stone"); + palette.add(firstPalette); + + CompoundTag secondPalette = new CompoundTag(); + secondPalette.putString("Name", "minecraft:stone"); + palette.add(secondPalette); + root.put("palette", palette); + + ListTag blocks = new ListTag<>(CompoundTag.class); + blocks.add(blockTag(0, "minecraft:witch_hut/foundation")); + blocks.add(blockTag(1, "minecraft:witch_hut/foundation")); + root.put("blocks", blocks); + + NamedTag named = new NamedTag("test", root); + return new NBTSerializer(compressed).toBytes(named); + } + + private CompoundTag blockTag(int state, String pool) { + CompoundTag block = new CompoundTag(); + block.putInt("state", state); + CompoundTag nbt = new CompoundTag(); + nbt.putString("pool", pool); + block.put("nbt", nbt); + ListTag pos = new ListTag<>(IntTag.class); + pos.add(new IntTag(0)); + pos.add(new IntTag(0)); + pos.add(new IntTag(0)); + block.put("pos", pos); + return block; + } + + private CompoundTag decodeRoot(byte[] bytes, boolean compressed) throws Exception { + NamedTag namedTag = new NBTDeserializer(compressed).fromStream(new ByteArrayInputStream(bytes)); + Tag rootTag = namedTag.getTag(); + return (CompoundTag) rootTag; + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/PregenTaskInterleavedTraversalTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenTaskInterleavedTraversalTest.java new file mode 100644 index 000000000..700b7ffd4 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/PregenTaskInterleavedTraversalTest.java @@ -0,0 +1,54 @@ +package art.arcane.iris.core.pregenerator; + +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.Position2; +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +public class PregenTaskInterleavedTraversalTest { + @Test + public void interleavedTraversalIsDeterministicAndComplete() { + PregenTask task = PregenTask.builder() + .center(new Position2(0, 0)) + .radiusX(1024) + .radiusZ(1024) + .build(); + + KList baseline = new KList<>(); + task.iterateAllChunks((x, z) -> baseline.add(asKey(x, z))); + + KList firstInterleaved = new KList<>(); + task.iterateAllChunksInterleaved((regionX, regionZ, chunkX, chunkZ, firstChunkInRegion, lastChunkInRegion) -> { + firstInterleaved.add(asKey(chunkX, chunkZ)); + return true; + }); + + KList secondInterleaved = new KList<>(); + task.iterateAllChunksInterleaved((regionX, regionZ, chunkX, chunkZ, firstChunkInRegion, lastChunkInRegion) -> { + secondInterleaved.add(asKey(chunkX, chunkZ)); + return true; + }); + + assertEquals(baseline.size(), firstInterleaved.size()); + assertEquals(firstInterleaved, secondInterleaved); + assertEquals(asSet(baseline), asSet(firstInterleaved)); + } + + private Set asSet(KList values) { + Set set = new HashSet<>(); + for (Long value : values) { + set.add(value); + } + return set; + } + + private long asKey(int x, int z) { + long high = (long) x << 32; + long low = z & 0xFFFFFFFFL; + return high | low; + } +} diff --git a/core/src/test/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethodConcurrencyCapTest.java b/core/src/test/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethodConcurrencyCapTest.java new file mode 100644 index 000000000..072a23acd --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethodConcurrencyCapTest.java @@ -0,0 +1,32 @@ +package art.arcane.iris.core.pregenerator.methods; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class AsyncPregenMethodConcurrencyCapTest { + @Test + public void paperLikeRecommendedCapTracksWorkerThreads() { + assertEquals(8, AsyncPregenMethod.computePaperLikeRecommendedCap(1)); + assertEquals(8, AsyncPregenMethod.computePaperLikeRecommendedCap(4)); + assertEquals(24, AsyncPregenMethod.computePaperLikeRecommendedCap(12)); + assertEquals(96, AsyncPregenMethod.computePaperLikeRecommendedCap(80)); + } + + @Test + public void foliaRecommendedCapTracksWorkerThreads() { + assertEquals(64, AsyncPregenMethod.computeFoliaRecommendedCap(1)); + assertEquals(64, AsyncPregenMethod.computeFoliaRecommendedCap(12)); + assertEquals(80, AsyncPregenMethod.computeFoliaRecommendedCap(20)); + assertEquals(192, AsyncPregenMethod.computeFoliaRecommendedCap(80)); + } + + @Test + public void runtimeCapUsesGlobalCeilingAndWorkerRecommendation() { + assertEquals(80, AsyncPregenMethod.applyRuntimeConcurrencyCap(256, true, 20)); + assertEquals(12, AsyncPregenMethod.applyRuntimeConcurrencyCap(12, true, 20)); + assertEquals(64, AsyncPregenMethod.applyRuntimeConcurrencyCap(256, true, 8)); + assertEquals(16, AsyncPregenMethod.applyRuntimeConcurrencyCap(256, false, 8)); + assertEquals(20, AsyncPregenMethod.applyRuntimeConcurrencyCap(20, false, 40)); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java new file mode 100644 index 000000000..1c43f179f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java @@ -0,0 +1,268 @@ +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.EngineMetrics; +import art.arcane.iris.engine.framework.SeedManager; +import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.IrisCaveProfile; +import art.arcane.iris.engine.object.IrisDimension; +import art.arcane.iris.engine.object.IrisGeneratorStyle; +import art.arcane.iris.engine.object.IrisRange; +import art.arcane.iris.engine.object.IrisStyledRange; +import art.arcane.iris.engine.object.IrisWorld; +import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.matter.Matter; +import art.arcane.volmlib.util.matter.MatterCavern; +import art.arcane.volmlib.util.matter.MatterSlice; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.block.data.BlockData; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class IrisCaveCarver3DNearParityTest { + @BeforeClass + public static void setupBukkit() { + if (Bukkit.getServer() != null) { + return; + } + + Server server = mock(Server.class); + BlockData emptyBlockData = mock(BlockData.class); + doReturn(Logger.getLogger("IrisTest")).when(server).getLogger(); + doReturn("IrisTestServer").when(server).getName(); + doReturn("1.0").when(server).getVersion(); + doReturn("1.0").when(server).getBukkitVersion(); + doReturn(emptyBlockData).when(server).createBlockData(any(Material.class)); + doReturn(emptyBlockData).when(server).createBlockData(anyString()); + Bukkit.setServer(server); + } + + @Test + public void carvedCellDistributionStableAcrossEquivalentCarvers() { + Engine engine = createEngine(128, 92); + + IrisCaveCarver3D firstCarver = new IrisCaveCarver3D(engine, createProfile()); + WriterCapture firstCapture = createWriterCapture(128); + int firstCarved = firstCarver.carve(firstCapture.writer, 7, -3); + + IrisCaveCarver3D secondCarver = new IrisCaveCarver3D(engine, createProfile()); + WriterCapture secondCapture = createWriterCapture(128); + int secondCarved = secondCarver.carve(secondCapture.writer, 7, -3); + + assertTrue(firstCarved > 0); + assertEquals(firstCarved, secondCarved); + assertEquals(firstCapture.carvedCells, secondCapture.carvedCells); + } + + @Test + public void latticePathCarvesChunkEdgesAndRespectsWorldHeightClipping() { + Engine engine = createEngine(48, 46); + IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, createProfile()); + WriterCapture capture = createWriterCapture(48); + double[] columnWeights = new double[256]; + Arrays.fill(columnWeights, 1D); + int[] precomputedSurfaceHeights = new int[256]; + Arrays.fill(precomputedSurfaceHeights, 46); + + int carved = carver.carve(capture.writer, 0, 0, columnWeights, 0D, 0D, new IrisRange(0D, 80D), precomputedSurfaceHeights); + + assertTrue(carved > 0); + assertTrue(hasX(capture.carvedCells, 14)); + assertTrue(hasX(capture.carvedCells, 15)); + assertTrue(hasZ(capture.carvedCells, 14)); + assertTrue(hasZ(capture.carvedCells, 15)); + assertTrue(maxY(capture.carvedCells) <= 47); + assertTrue(minY(capture.carvedCells) >= 0); + } + + private Engine createEngine(int worldHeight, int sampledHeight) { + Engine engine = mock(Engine.class); + IrisData data = mock(IrisData.class); + IrisDimension dimension = mock(IrisDimension.class); + SeedManager seedManager = new SeedManager(942_337_445L); + EngineMetrics metrics = new EngineMetrics(16); + IrisWorld world = IrisWorld.builder().minHeight(0).maxHeight(worldHeight).build(); + + doReturn(data).when(engine).getData(); + doReturn(dimension).when(engine).getDimension(); + doReturn(seedManager).when(engine).getSeedManager(); + doReturn(metrics).when(engine).getMetrics(); + doReturn(world).when(engine).getWorld(); + doReturn(sampledHeight).when(engine).getHeight(anyInt(), anyInt()); + + doReturn(18).when(dimension).getCaveLavaHeight(); + doReturn(64).when(dimension).getFluidHeight(); + + return engine; + } + + private IrisCaveProfile createProfile() { + IrisCaveProfile profile = new IrisCaveProfile(); + profile.setEnabled(true); + profile.setVerticalRange(new IrisRange(0D, 120D)); + profile.setVerticalEdgeFade(14); + profile.setVerticalEdgeFadeStrength(0.21D); + profile.setBaseDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.07D)); + profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.17D)); + profile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.12D)); + profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.09D)); + profile.setBaseWeight(1D); + profile.setDetailWeight(0.48D); + profile.setWarpStrength(0.37D); + profile.setDensityThreshold(new IrisStyledRange(1D, 1D, new IrisGeneratorStyle(NoiseStyle.FLAT))); + profile.setThresholdBias(0D); + profile.setSampleStep(2); + profile.setMinCarveCells(0); + profile.setRecoveryThresholdBoost(0D); + profile.setSurfaceClearance(5); + profile.setAllowSurfaceBreak(true); + profile.setSurfaceBreakNoiseThreshold(0.16D); + profile.setSurfaceBreakDepth(12); + profile.setSurfaceBreakThresholdBoost(0.17D); + profile.setAllowWater(true); + profile.setWaterMinDepthBelowSurface(8); + profile.setWaterRequiresFloor(false); + profile.setAllowLava(true); + return profile; + } + + private WriterCapture createWriterCapture(int worldHeight) { + MantleWriter writer = mock(MantleWriter.class); + @SuppressWarnings("unchecked") + Mantle mantle = mock(Mantle.class); + @SuppressWarnings("unchecked") + MantleChunk chunk = mock(MantleChunk.class); + Map sections = new HashMap<>(); + Map> sectionCells = new HashMap<>(); + Set carvedCells = new HashSet<>(); + + doReturn(mantle).when(writer).getMantle(); + doReturn(worldHeight).when(mantle).getWorldHeight(); + doReturn(chunk).when(writer).acquireChunk(anyInt(), anyInt()); + doAnswer(invocation -> { + int sectionIndex = invocation.getArgument(0); + Matter section = sections.get(sectionIndex); + if (section != null) { + return section; + } + + Matter created = createSection(sectionIndex, sectionCells, carvedCells); + sections.put(sectionIndex, created); + return created; + }).when(chunk).getOrCreate(anyInt()); + + return new WriterCapture(writer, carvedCells); + } + + private Matter createSection(int sectionIndex, Map> sectionCells, Set carvedCells) { + Matter matter = mock(Matter.class); + @SuppressWarnings("unchecked") + MatterSlice slice = mock(MatterSlice.class); + Map localCells = sectionCells.computeIfAbsent(sectionIndex, key -> new HashMap<>()); + + doReturn(slice).when(matter).slice(MatterCavern.class); + doAnswer(invocation -> { + int localX = invocation.getArgument(0); + int localY = invocation.getArgument(1); + int localZ = invocation.getArgument(2); + return localCells.get(packLocal(localX, localY, localZ)); + }).when(slice).get(anyInt(), anyInt(), anyInt()); + doAnswer(invocation -> { + int localX = invocation.getArgument(0); + int localY = invocation.getArgument(1); + int localZ = invocation.getArgument(2); + MatterCavern value = invocation.getArgument(3); + localCells.put(packLocal(localX, localY, localZ), value); + int worldY = (sectionIndex << 4) + localY; + carvedCells.add(cellKey(localX, worldY, localZ)); + return null; + }).when(slice).set(anyInt(), anyInt(), anyInt(), any(MatterCavern.class)); + + return matter; + } + + private int packLocal(int x, int y, int z) { + return (x << 8) | (y << 4) | z; + } + + private String cellKey(int x, int y, int z) { + return x + ":" + y + ":" + z; + } + + private boolean hasX(Set carvedCells, int x) { + for (String cell : carvedCells) { + String[] split = cell.split(":"); + if (Integer.parseInt(split[0]) == x) { + return true; + } + } + + return false; + } + + private boolean hasZ(Set carvedCells, int z) { + for (String cell : carvedCells) { + String[] split = cell.split(":"); + if (Integer.parseInt(split[2]) == z) { + return true; + } + } + + return false; + } + + private int maxY(Set carvedCells) { + int max = Integer.MIN_VALUE; + for (String cell : carvedCells) { + String[] split = cell.split(":"); + int y = Integer.parseInt(split[1]); + if (y > max) { + max = y; + } + } + return max; + } + + private int minY(Set carvedCells) { + int min = Integer.MAX_VALUE; + for (String cell : carvedCells) { + String[] split = cell.split(":"); + int y = Integer.parseInt(split[1]); + if (y < min) { + min = y; + } + } + return min; + } + + private static final class WriterCapture { + private final MantleWriter writer; + private final Set carvedCells; + + private WriterCapture(MantleWriter writer, Set carvedCells) { + this.writer = writer; + this.carvedCells = carvedCells; + } + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java new file mode 100644 index 000000000..74c5a4791 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java @@ -0,0 +1,143 @@ +package art.arcane.iris.engine.mantle.components; + +import art.arcane.iris.engine.object.IrisCaveProfile; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +public class MantleCarvingComponentTop2BlendTest { + private static Constructor weightedProfileConstructor; + private static Method limitMethod; + private static Method expandTileMethod; + private static Field profileField; + private static Field columnWeightsField; + + @BeforeClass + public static void setup() throws Exception { + Class weightedProfileClass = Class.forName("art.arcane.iris.engine.mantle.components.MantleCarvingComponent$WeightedProfile"); + weightedProfileConstructor = weightedProfileClass.getDeclaredConstructor(IrisCaveProfile.class, double[].class, double.class, Class.forName("art.arcane.iris.engine.object.IrisRange")); + weightedProfileConstructor.setAccessible(true); + limitMethod = MantleCarvingComponent.class.getDeclaredMethod("limitAndMergeBlendedProfiles", List.class, int.class, int.class); + limitMethod.setAccessible(true); + expandTileMethod = MantleCarvingComponent.class.getDeclaredMethod("expandTileWeightsToColumns", double[].class); + expandTileMethod.setAccessible(true); + profileField = weightedProfileClass.getDeclaredField("profile"); + profileField.setAccessible(true); + columnWeightsField = weightedProfileClass.getDeclaredField("columnWeights"); + columnWeightsField.setAccessible(true); + } + + @Test + public void topTwoProfilesAreKeptAndDroppedWeightsAreMergedIntoDominantColumns() throws Exception { + WeightedInput input = createWeightedProfiles(); + List limited = invokeLimit(input.weightedProfiles(), 2); + assertEquals(2, limited.size()); + + Map byProfile = extractWeightsByProfile(limited); + IrisCaveProfile first = input.profiles().first(); + IrisCaveProfile second = input.profiles().second(); + + assertEquals(1.0D, byProfile.get(first)[1], 0D); + assertEquals(1.0D, byProfile.get(second)[0], 0D); + } + + @Test + public void topTwoMergeIsDeterministicAcrossRuns() throws Exception { + WeightedInput firstInput = createWeightedProfiles(); + WeightedInput secondInput = createWeightedProfiles(); + List first = invokeLimit(firstInput.weightedProfiles(), 2); + List second = invokeLimit(secondInput.weightedProfiles(), 2); + + Map firstByProfile = extractWeightsByProfile(first); + Map secondByProfile = extractWeightsByProfile(second); + + assertEquals(firstByProfile.get(firstInput.profiles().first())[0], secondByProfile.get(secondInput.profiles().first())[0], 0D); + assertEquals(firstByProfile.get(firstInput.profiles().first())[1], secondByProfile.get(secondInput.profiles().first())[1], 0D); + assertEquals(firstByProfile.get(firstInput.profiles().second())[0], secondByProfile.get(secondInput.profiles().second())[0], 0D); + assertEquals(firstByProfile.get(firstInput.profiles().second())[1], secondByProfile.get(secondInput.profiles().second())[1], 0D); + } + + @Test + public void tileWeightsExpandIntoFourColumnsPerTile() throws Exception { + double[] tileWeights = new double[64]; + tileWeights[0] = 0.42D; + tileWeights[9] = 0.73D; + double[] expanded = invokeExpand(tileWeights); + + assertEquals(0.42D, expanded[(0 << 4) | 0], 0D); + assertEquals(0.42D, expanded[(0 << 4) | 1], 0D); + assertEquals(0.42D, expanded[(1 << 4) | 0], 0D); + assertEquals(0.42D, expanded[(1 << 4) | 1], 0D); + + assertEquals(0.73D, expanded[(2 << 4) | 2], 0D); + assertEquals(0.73D, expanded[(2 << 4) | 3], 0D); + assertEquals(0.73D, expanded[(3 << 4) | 2], 0D); + assertEquals(0.73D, expanded[(3 << 4) | 3], 0D); + } + + private WeightedInput createWeightedProfiles() throws Exception { + IrisCaveProfile first = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.31D); + IrisCaveProfile second = new IrisCaveProfile().setEnabled(true).setBaseWeight(1.17D); + IrisCaveProfile third = new IrisCaveProfile().setEnabled(true).setBaseWeight(0.93D); + Profiles profiles = new Profiles(first, second, third); + + double[] firstWeights = new double[64]; + firstWeights[0] = 0.2D; + firstWeights[1] = 0.8D; + + double[] secondWeights = new double[64]; + secondWeights[0] = 0.7D; + secondWeights[1] = 0.1D; + + double[] thirdWeights = new double[64]; + thirdWeights[0] = 0.3D; + thirdWeights[1] = 0.4D; + + List weighted = new ArrayList<>(); + weighted.add(weightedProfileConstructor.newInstance(first, firstWeights, average(firstWeights), null)); + weighted.add(weightedProfileConstructor.newInstance(second, secondWeights, average(secondWeights), null)); + weighted.add(weightedProfileConstructor.newInstance(third, thirdWeights, average(thirdWeights), null)); + return new WeightedInput(weighted, profiles); + } + + private List invokeLimit(List weightedProfiles, int limit) throws Exception { + return (List) limitMethod.invoke(null, weightedProfiles, limit, 64); + } + + private double[] invokeExpand(double[] tileWeights) throws Exception { + return (double[]) expandTileMethod.invoke(null, (Object) tileWeights); + } + + private Map extractWeightsByProfile(List weightedProfiles) throws Exception { + Map byProfile = new IdentityHashMap<>(); + for (Object weightedProfile : weightedProfiles) { + IrisCaveProfile profile = (IrisCaveProfile) profileField.get(weightedProfile); + double[] weights = (double[]) columnWeightsField.get(weightedProfile); + byProfile.put(profile, weights); + } + return byProfile; + } + + private double average(double[] weights) { + double total = 0D; + for (double weight : weights) { + total += weight; + } + return total / weights.length; + } + + private record Profiles(IrisCaveProfile first, IrisCaveProfile second, IrisCaveProfile third) { + } + + private record WeightedInput(List weightedProfiles, Profiles profiles) { + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierZoneParityTest.java b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierZoneParityTest.java new file mode 100644 index 000000000..22d63d1d5 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/modifier/IrisCarveModifierZoneParityTest.java @@ -0,0 +1,186 @@ +package art.arcane.iris.engine.modifier; + +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import static org.junit.Assert.assertEquals; + +public class IrisCarveModifierZoneParityTest { + private static Constructor columnMaskConstructor; + private static Method addMethod; + private static Method nextSetBitMethod; + private static Method clearMethod; + + @BeforeClass + public static void setup() throws Exception { + Class columnMaskClass = Class.forName("art.arcane.iris.engine.modifier.IrisCarveModifier$ColumnMask"); + columnMaskConstructor = columnMaskClass.getDeclaredConstructor(); + addMethod = columnMaskClass.getDeclaredMethod("add", int.class); + nextSetBitMethod = columnMaskClass.getDeclaredMethod("nextSetBit", int.class); + clearMethod = columnMaskClass.getDeclaredMethod("clear"); + columnMaskConstructor.setAccessible(true); + addMethod.setAccessible(true); + nextSetBitMethod.setAccessible(true); + clearMethod.setAccessible(true); + } + + @Test + public void randomColumnZonesMatchLegacySortedResolver() throws Exception { + Object columnMask = columnMaskConstructor.newInstance(); + Random random = new Random(913_447L); + int maxHeight = 320; + + for (int scenario = 0; scenario < 400; scenario++) { + clearMethod.invoke(columnMask); + + int sampleSize = 1 + random.nextInt(180); + Set uniqueHeights = new HashSet<>(); + while (uniqueHeights.size() < sampleSize) { + uniqueHeights.add(random.nextInt(480) - 80); + } + + int[] heights = toIntArray(uniqueHeights); + for (int index = 0; index < heights.length; index++) { + addMethod.invoke(columnMask, heights[index]); + } + + List expectedZones = legacyZones(heights, maxHeight); + List actualZones = bitsetZones(columnMask, maxHeight); + assertEquals("scenario=" + scenario, expectedZones, actualZones); + } + } + + @Test + public void edgeColumnsMatchLegacySortedResolver() throws Exception { + Object columnMask = columnMaskConstructor.newInstance(); + int maxHeight = 320; + int[][] scenarios = new int[][]{ + {-10, -1, 0, 1, 2, 5, 6, 9, 10, 11, 12, 200, 201, 205}, + {300, 301, 302, 304, 305, 307, 308, 309, 310, 400, 401}, + {0, 2, 4, 6, 8, 10, 12}, + {10, 11, 12, 13, 14, 15, 16, 17} + }; + + for (int scenario = 0; scenario < scenarios.length; scenario++) { + clearMethod.invoke(columnMask); + int[] heights = Arrays.copyOf(scenarios[scenario], scenarios[scenario].length); + for (int index = 0; index < heights.length; index++) { + addMethod.invoke(columnMask, heights[index]); + } + + List expectedZones = legacyZones(heights, maxHeight); + List actualZones = bitsetZones(columnMask, maxHeight); + assertEquals("edge-scenario=" + scenario, expectedZones, actualZones); + } + } + + private int[] toIntArray(Set values) { + int[] array = new int[values.size()]; + int index = 0; + for (Integer value : values) { + array[index++] = value; + } + return array; + } + + private List legacyZones(int[] heights, int maxHeight) { + List zones = new ArrayList<>(); + if (heights.length == 0) { + return zones; + } + + int[] sorted = Arrays.copyOf(heights, heights.length); + Arrays.sort(sorted); + int floor = sorted[0]; + int ceiling = -1; + int buf = sorted[0] - 1; + for (int index = 0; index < sorted.length; index++) { + int y = sorted[index]; + if (y < 0 || y > maxHeight) { + continue; + } + + if (y == buf + 1) { + buf = y; + ceiling = buf; + } else if (isValidZone(floor, ceiling, maxHeight)) { + zones.add(zoneKey(floor, ceiling)); + floor = y; + ceiling = -1; + buf = y; + } else { + floor = y; + ceiling = -1; + buf = y; + } + } + + if (isValidZone(floor, ceiling, maxHeight)) { + zones.add(zoneKey(floor, ceiling)); + } + + return zones; + } + + private List bitsetZones(Object columnMask, int maxHeight) throws Exception { + List zones = new ArrayList<>(); + int firstHeight = nextSetBit(columnMask, 0); + if (firstHeight < 0) { + return zones; + } + + int floor = firstHeight; + int ceiling = -1; + int buf = firstHeight - 1; + int y = firstHeight; + while (y >= 0) { + if (y >= 0 && y <= maxHeight) { + if (y == buf + 1) { + buf = y; + ceiling = buf; + } else if (isValidZone(floor, ceiling, maxHeight)) { + zones.add(zoneKey(floor, ceiling)); + floor = y; + ceiling = -1; + buf = y; + } else { + floor = y; + ceiling = -1; + buf = y; + } + } + + y = nextSetBit(columnMask, y + 1); + } + + if (isValidZone(floor, ceiling, maxHeight)) { + zones.add(zoneKey(floor, ceiling)); + } + + return zones; + } + + private int nextSetBit(Object columnMask, int fromBit) throws Exception { + return (Integer) nextSetBitMethod.invoke(columnMask, fromBit); + } + + private boolean isValidZone(int floor, int ceiling, int maxHeight) { + return floor < ceiling + && floor >= 0 + && ceiling <= maxHeight + && ((ceiling - floor) - 1) > 0; + } + + private String zoneKey(int floor, int ceiling) { + return floor + ":" + ceiling; + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java index a98c385bf..f55092a9a 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java @@ -103,6 +103,27 @@ public class IrisDimensionCarvingResolverParityTest { } } + @Test + public void tileAnchoredChunkPlanResolutionIsStableAcrossRepeatedBuilds() { + Fixture fixture = createMixedDepthFixture(); + IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); + IrisDimensionCarvingEntry root = legacyResolveRootEntry(fixture.engine, 80); + + for (int chunkX = -24; chunkX <= 24; chunkX += 6) { + for (int chunkZ = -24; chunkZ <= 24; chunkZ += 6) { + IrisDimensionCarvingEntry[] firstPlan = buildTilePlan(fixture.engine, root, chunkX, chunkZ, state); + IrisDimensionCarvingEntry[] secondPlan = buildTilePlan(fixture.engine, root, chunkX, chunkZ, state); + for (int tileIndex = 0; tileIndex < firstPlan.length; tileIndex++) { + assertSame( + "tile plan mismatch at chunkX=" + chunkX + " chunkZ=" + chunkZ + " tileIndex=" + tileIndex, + firstPlan[tileIndex], + secondPlan[tileIndex] + ); + } + } + } + } + private Fixture createFixture() { IrisBiome rootLowBiome = mock(IrisBiome.class); IrisBiome rootHighBiome = mock(IrisBiome.class); @@ -251,6 +272,19 @@ public class IrisDimensionCarvingResolverParityTest { return new Fixture(engine); } + private IrisDimensionCarvingEntry[] buildTilePlan(Engine engine, IrisDimensionCarvingEntry rootEntry, int chunkX, int chunkZ, IrisDimensionCarvingResolver.State state) { + IrisDimensionCarvingEntry[] plan = new IrisDimensionCarvingEntry[64]; + for (int tileX = 0; tileX < 8; tileX++) { + int worldX = (chunkX << 4) + (tileX << 1); + for (int tileZ = 0; tileZ < 8; tileZ++) { + int worldZ = (chunkZ << 4) + (tileZ << 1); + int tileIndex = (tileX * 8) + tileZ; + plan[tileIndex] = IrisDimensionCarvingResolver.resolveFromRoot(engine, rootEntry, worldX, worldZ, state); + } + } + return plan; + } + private IrisDimensionCarvingEntry buildEntry(String id, String biome, IrisRange worldRange, int depth, List children) { IrisDimensionCarvingEntry entry = new IrisDimensionCarvingEntry(); entry.setId(id); diff --git a/core/src/test/java/art/arcane/iris/util/project/context/ChunkContextPrefillPlanTest.java b/core/src/test/java/art/arcane/iris/util/project/context/ChunkContextPrefillPlanTest.java new file mode 100644 index 000000000..ca9120866 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/context/ChunkContextPrefillPlanTest.java @@ -0,0 +1,148 @@ +package art.arcane.iris.util.project.context; + +import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.engine.object.IrisRegion; +import art.arcane.iris.util.project.stream.ProceduralStream; +import org.bukkit.block.data.BlockData; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class ChunkContextPrefillPlanTest { + @Test + public void noCavePrefillSkipsCaveCacheFill() { + AtomicInteger caveCalls = new AtomicInteger(); + AtomicInteger heightCalls = new AtomicInteger(); + AtomicInteger biomeCalls = new AtomicInteger(); + AtomicInteger rockCalls = new AtomicInteger(); + AtomicInteger fluidCalls = new AtomicInteger(); + AtomicInteger regionCalls = new AtomicInteger(); + ChunkContext context = createContext( + ChunkContext.PrefillPlan.NO_CAVE, + caveCalls, + heightCalls, + biomeCalls, + rockCalls, + fluidCalls, + regionCalls + ); + + assertEquals(256, heightCalls.get()); + assertEquals(256, biomeCalls.get()); + assertEquals(256, rockCalls.get()); + assertEquals(256, fluidCalls.get()); + assertEquals(256, regionCalls.get()); + assertEquals(0, caveCalls.get()); + + assertEquals(34051D, context.getHeight().get(2, 3), 0D); + context.getCave().get(2, 3); + context.getCave().get(2, 3); + assertEquals(1, caveCalls.get()); + } + + @Test + public void allPrefillIncludesCaveCacheFill() { + AtomicInteger caveCalls = new AtomicInteger(); + AtomicInteger heightCalls = new AtomicInteger(); + AtomicInteger biomeCalls = new AtomicInteger(); + AtomicInteger rockCalls = new AtomicInteger(); + AtomicInteger fluidCalls = new AtomicInteger(); + AtomicInteger regionCalls = new AtomicInteger(); + ChunkContext context = createContext( + ChunkContext.PrefillPlan.ALL, + caveCalls, + heightCalls, + biomeCalls, + rockCalls, + fluidCalls, + regionCalls + ); + + assertEquals(256, heightCalls.get()); + assertEquals(256, biomeCalls.get()); + assertEquals(256, rockCalls.get()); + assertEquals(256, fluidCalls.get()); + assertEquals(256, regionCalls.get()); + assertEquals(256, caveCalls.get()); + + context.getCave().get(1, 1); + assertEquals(256, caveCalls.get()); + } + + private ChunkContext createContext( + ChunkContext.PrefillPlan prefillPlan, + AtomicInteger caveCalls, + AtomicInteger heightCalls, + AtomicInteger biomeCalls, + AtomicInteger rockCalls, + AtomicInteger fluidCalls, + AtomicInteger regionCalls + ) { + IrisComplex complex = mock(IrisComplex.class); + + @SuppressWarnings("unchecked") + ProceduralStream heightStream = mock(ProceduralStream.class); + doAnswer(invocation -> { + heightCalls.incrementAndGet(); + double worldX = invocation.getArgument(0); + double worldZ = invocation.getArgument(1); + return (worldX * 1000D) + worldZ; + }).when(heightStream).get(anyDouble(), anyDouble()); + + @SuppressWarnings("unchecked") + ProceduralStream biomeStream = mock(ProceduralStream.class); + IrisBiome biome = mock(IrisBiome.class); + doAnswer(invocation -> { + biomeCalls.incrementAndGet(); + return biome; + }).when(biomeStream).get(anyDouble(), anyDouble()); + + @SuppressWarnings("unchecked") + ProceduralStream caveStream = mock(ProceduralStream.class); + IrisBiome caveBiome = mock(IrisBiome.class); + doAnswer(invocation -> { + caveCalls.incrementAndGet(); + return caveBiome; + }).when(caveStream).get(anyDouble(), anyDouble()); + + @SuppressWarnings("unchecked") + ProceduralStream rockStream = mock(ProceduralStream.class); + BlockData rock = mock(BlockData.class); + doAnswer(invocation -> { + rockCalls.incrementAndGet(); + return rock; + }).when(rockStream).get(anyDouble(), anyDouble()); + + @SuppressWarnings("unchecked") + ProceduralStream fluidStream = mock(ProceduralStream.class); + BlockData fluid = mock(BlockData.class); + doAnswer(invocation -> { + fluidCalls.incrementAndGet(); + return fluid; + }).when(fluidStream).get(anyDouble(), anyDouble()); + + @SuppressWarnings("unchecked") + ProceduralStream regionStream = mock(ProceduralStream.class); + IrisRegion region = mock(IrisRegion.class); + doAnswer(invocation -> { + regionCalls.incrementAndGet(); + return region; + }).when(regionStream).get(anyDouble(), anyDouble()); + + doReturn(heightStream).when(complex).getHeightStream(); + doReturn(biomeStream).when(complex).getTrueBiomeStream(); + doReturn(caveStream).when(complex).getCaveBiomeStream(); + doReturn(rockStream).when(complex).getRockStream(); + doReturn(fluidStream).when(complex).getFluidStream(); + doReturn(regionStream).when(complex).getRegionStream(); + + return new ChunkContext(32, 48, complex, true, prefillPlan, null); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/CNGFastPathParityTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/CNGFastPathParityTest.java new file mode 100644 index 000000000..183c89167 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/CNGFastPathParityTest.java @@ -0,0 +1,100 @@ +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class CNGFastPathParityTest { + @Test + public void identityFastPathMatchesLegacyAcrossSeedAndCoordinateGrid() { + for (long seed = 3L; seed <= 11L; seed++) { + CNG generator = createIdentityGenerator(seed); + assertFastPathParity("identity-seed-" + seed, generator); + } + } + + @Test + public void transformedGeneratorsMatchLegacyAcrossSeedAndCoordinateGrid() { + for (long seed = 21L; seed <= 27L; seed++) { + List generators = createTransformedGenerators(seed); + for (int index = 0; index < generators.size(); index++) { + assertFastPathParity("transformed-seed-" + seed + "-case-" + index, generators.get(index)); + } + } + } + + private void assertFastPathParity(String label, CNG generator) { + for (int x = -320; x <= 320; x += 19) { + for (int z = -320; z <= 320; z += 23) { + double expected = generator.noise(x, z); + double actual = generator.noiseFast2D(x, z); + assertEquals(label + " 2D x=" + x + " z=" + z, expected, actual, 1.0E-12D); + } + } + + for (int x = -128; x <= 128; x += 17) { + for (int y = -96; y <= 96; y += 13) { + for (int z = -128; z <= 128; z += 19) { + double expected = generator.noise(x, y, z); + double actual = generator.noiseFast3D(x, y, z); + assertEquals(label + " 3D x=" + x + " y=" + y + " z=" + z, expected, actual, 1.0E-12D); + } + } + } + } + + private CNG createIdentityGenerator(long seed) { + DeterministicNoiseGenerator generator = new DeterministicNoiseGenerator(0.31D + (seed * 0.01D)); + return new CNG(new RNG(seed), generator, 1D, 1).bake(); + } + + private List createTransformedGenerators(long seed) { + List generators = new ArrayList<>(); + + CNG powerTransformed = createIdentityGenerator(seed).pow(1.27D); + generators.add(powerTransformed); + + CNG offsetTransformed = createIdentityGenerator(seed + 1L).up(0.08D).down(0.03D).patch(0.91D); + generators.add(offsetTransformed); + + CNG fractured = createIdentityGenerator(seed + 2L).fractureWith(createIdentityGenerator(seed + 300L), 12.5D); + generators.add(fractured); + + CNG withChildren = createIdentityGenerator(seed + 3L); + withChildren.child(createIdentityGenerator(seed + 400L)); + withChildren.child(createIdentityGenerator(seed + 401L)); + generators.add(withChildren); + + return generators; + } + + private static class DeterministicNoiseGenerator implements NoiseGenerator { + private final double offset; + + private DeterministicNoiseGenerator(double offset) { + this.offset = offset; + } + + @Override + public double noise(double x) { + double angle = (x * 0.011D) + offset; + return 0.2D + (((Math.sin(angle) + 1D) * 0.5D) * 0.6D); + } + + @Override + public double noise(double x, double z) { + double angle = (x * 0.013D) + (z * 0.017D) + offset; + return 0.2D + (((Math.sin(angle) + 1D) * 0.5D) * 0.6D); + } + + @Override + public double noise(double x, double y, double z) { + double angle = (x * 0.007D) + (y * 0.015D) + (z * 0.019D) + offset; + return 0.2D + (((Math.sin(angle) + 1D) * 0.5D) * 0.6D); + } + } +} diff --git a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java index 174513d20..be7605629 100644 --- a/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java +++ b/nms/v1_21_R7/src/main/java/art/arcane/iris/core/nms/v1_21_R7/IrisChunkGenerator.java @@ -45,12 +45,14 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; public class IrisChunkGenerator extends CustomChunkGenerator { private static final WrappedField BIOME_SOURCE; private static final WrappedReturningMethod SET_HEIGHT; private static final int EXTERNAL_FOUNDATION_MAX_DEPTH = 96; + private static final Set loggedExternalStructureFingerprintKeys = ConcurrentHashMap.newKeySet(); private final ChunkGenerator delegate; private final Engine engine; private volatile Registry cachedStructureRegistry; @@ -389,9 +391,13 @@ public class IrisChunkGenerator extends CustomChunkGenerator { } String normalized = structureKey.toLowerCase(Locale.ROOT); - return "minecraft:ancient_city".equals(normalized) - || "minecraft:mineshaft".equals(normalized) - || "minecraft:mineshaft_mesa".equals(normalized); + if (!"minecraft:ancient_city".equals(normalized) + && !"minecraft:mineshaft".equals(normalized) + && !"minecraft:mineshaft_mesa".equals(normalized)) { + return false; + } + + return loggedExternalStructureFingerprintKeys.add(normalized); } private static void logExternalStructureFingerprint(String structureKey, StructureStart start) {