From 8fb627f9c1b2d58b186a10fd8f46e28cb980ca6f Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Tue, 16 Jun 2026 01:35:25 -0400 Subject: [PATCH] changes :) --- .../iris/core/nms/v26_1_R1/NMSBinding.java | 133 +++- .../art/arcane/iris/core/IrisSettings.java | 5 + .../arcane/iris/core/gui/PregeneratorJob.java | 5 +- .../art/arcane/iris/core/nms/INMSBinding.java | 10 + .../methods/AsyncOrMedievalPregenMethod.java | 34 +- .../methods/AsyncPregenMethod.java | 6 +- .../methods/CachedPregenMethod.java | 2 +- .../methods/MedievalPregenMethod.java | 173 +++++- .../methods/SpigotDirectPregenMethod.java | 586 ++++++++++++++++++ .../iris/engine/mantle/MantleWriter.java | 39 ++ .../mantle/components/IrisCaveCarver3D.java | 138 ++++- .../components/MantleObjectComponent.java | 14 +- .../engine/object/FloatingIslandSample.java | 34 +- 13 files changed, 1103 insertions(+), 76 deletions(-) create mode 100644 core/src/main/java/art/arcane/iris/core/pregenerator/methods/SpigotDirectPregenMethod.java diff --git a/adapters/bukkit/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java b/adapters/bukkit/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java index b26fc275b..6186c0462 100644 --- a/adapters/bukkit/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java +++ b/adapters/bukkit/nms/v26_1_R1/src/main/java/art/arcane/iris/core/nms/v26_1_R1/NMSBinding.java @@ -24,6 +24,7 @@ import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.math.Vector3d; import art.arcane.volmlib.util.matter.MatterBiomeInject; +import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.util.nbt.common.mca.NBTWorld; import art.arcane.volmlib.util.nbt.mca.palette.*; import art.arcane.volmlib.util.nbt.tag.CompoundTag; @@ -325,7 +326,14 @@ public class NMSBinding implements INMSBinding { @Override public Object getBiomeBaseFromId(int id) { - return getCustomBiomeRegistry().get(id); + Object raw = getCustomBiomeRegistry().get(id); + if (raw instanceof java.util.Optional opt) { + raw = opt.orElse(null); + } + if (raw instanceof Holder holder) { + raw = holder.value(); + } + return raw; } @Override @@ -594,7 +602,23 @@ public class NMSBinding implements INMSBinding { for (World i : Bukkit.getWorlds()) { if (i.getEnvironment().equals(World.Environment.NORMAL)) { Registry registry = ((CraftWorld) i).getHandle().registryAccess().lookup(Registries.BIOME).orElse(null); - return registry.getId((net.minecraft.world.level.biome.Biome) getBiomeBase(registry, biome)); + if (registry == null) { + continue; + } + Object baseRaw = getBiomeBase(registry, biome); + net.minecraft.world.level.biome.Biome base; + if (baseRaw instanceof Holder holder) { + Object value = holder.value(); + if (!(value instanceof net.minecraft.world.level.biome.Biome resolved)) { + continue; + } + base = resolved; + } else if (baseRaw instanceof net.minecraft.world.level.biome.Biome resolved) { + base = resolved; + } else { + continue; + } + return registry.getId(base); } } @@ -700,13 +724,15 @@ public class NMSBinding implements INMSBinding { return new MCAIdMapper(c, d, b); }); MCAPalette global = globalCache.aquireNasty(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState())); + java.util.Map innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); + java.util.Map outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64); MCAPalettedContainer container = new MCAPalettedContainer<>(global, registry, - i -> ((CraftBlockData) NBTWorld.getBlockData(i)).getState(), + i -> innerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState()), i -> NBTWorld.getCompound(CraftBlockData.createData(i)), ((CraftBlockData) AIR).getState()); return new MCAWrappedPalettedContainer<>(container, i -> NBTWorld.getCompound(CraftBlockData.createData(i)), - i -> ((CraftBlockData) NBTWorld.getBlockData(i)).getState()); + i -> outerDecodeCache.computeIfAbsent(i, t -> ((CraftBlockData) NBTWorld.getBlockData(t)).getState())); } @Override @@ -788,7 +814,6 @@ public class NMSBinding implements INMSBinding { int accessMinY = access.getMinY(); int baseX = access.getPos().getMinBlockX(); int baseZ = access.getPos().getMinBlockZ(); - boolean[] dirtySections = new boolean[access.getSections().length]; ChunkDataHunkHolder holder = data instanceof ChunkDataHunkHolder chunkDataHolder ? chunkDataHolder : null; for (int z = 0; z < 16; z++) { for (int y = 0; y < height; y++) { @@ -811,8 +836,7 @@ public class NMSBinding implements INMSBinding { } BlockState state = craftBlockData.getState(); - BlockState oldState = section.states.getAndSetUnchecked(x, sectionY, z, state); - dirtySections[sectionIndex] = true; + BlockState oldState = section.setBlockState(x, sectionY, z, state, false); if (state.hasBlockEntity()) { BlockPos pos = new BlockPos(baseX + x, blockY, baseZ + z); BlockEntity entity = ((EntityBlock) state.getBlock()).newBlockEntity(pos, state); @@ -828,13 +852,102 @@ public class NMSBinding implements INMSBinding { } } - for (int i = 0; i < dirtySections.length; i++) { - if (dirtySections[i]) { - access.getSection(i).recalcBlockCounts(); + return true; + } catch (Throwable e) { + IrisLogging.reportError(e); + return false; + } + } + + @Override + public boolean writeChunkNbtDirect(NBTWorld nbtWorld, int chunkX, int chunkZ, Hunk blocks, Hunk biomes) { + try { + art.arcane.iris.util.nbt.common.mca.MCAFile mca = nbtWorld.getMCA(chunkX >> 5, chunkZ >> 5); + if (mca == null) { + return false; + } + + art.arcane.iris.util.nbt.common.mca.Chunk chunk = art.arcane.iris.util.nbt.common.mca.Chunk.newChunk(); + art.arcane.iris.util.nbt.common.mca.Chunk.injectIrisData(chunk); + + int blockHeight = blocks.getHeight(); + java.util.IdentityHashMap encodeCache = new java.util.IdentityHashMap<>(64); + for (int y = 0; y < blockHeight; y++) { + for (int z = 0; z < 16; z++) { + for (int x = 0; x < 16; x++) { + PlatformBlockState platformState = blocks.getRaw(x, y, z); + if (platformState == null) { + continue; + } + Object nativeHandle = platformState.nativeHandle(); + if (!(nativeHandle instanceof BlockData blockData)) { + continue; + } + if (blockData instanceof IrisCustomData customData) { + blockData = customData.getBase(); + } + if (blockData.getMaterial().isAir()) { + continue; + } + CompoundTag nbtState = encodeCache.get(blockData); + if (nbtState == null) { + nbtState = NBTWorld.getCompound(blockData); + if (nbtState == null) { + continue; + } + encodeCache.put(blockData, nbtState); + } + chunk.setBlockStateAt(x, y, z, nbtState, false); + } } } + int biomeHeight = biomes.getHeight(); + for (int by = 0; by < biomeHeight; by += 4) { + int quartY = by >> 2; + for (int bz = 0; bz < 16; bz += 4) { + int quartZ = bz >> 2; + for (int bx = 0; bx < 16; bx += 4) { + int quartX = bx >> 2; + PlatformBiome platformBiome = biomes.getRaw(bx, by, bz); + if (platformBiome == null) { + continue; + } + String biomeKey = platformBiome.key(); + if (biomeKey == null || biomeKey.isEmpty()) { + continue; + } + int biomeId = getBiomeBaseIdForKey(biomeKey); + if (biomeId < 0) { + continue; + } + chunk.setBiomeAt(quartX, quartY, quartZ, biomeId); + } + } + } + + chunk.cleanupPalettesAndBlockStates(); + + synchronized (mca) { + mca.setChunk(chunkX & 31, chunkZ & 31, chunk); + } + return true; + } catch (Throwable e) { + IrisLogging.warn("writeChunkNbtDirect failed at " + chunkX + "," + chunkZ + ": " + e.getClass().getSimpleName() + ": " + e.getMessage()); + e.printStackTrace(System.err); + IrisLogging.reportError(e); + return false; + } + } + + @Override + public boolean forceEvictChunk(World world, int chunkX, int chunkZ) { + try { + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return true; + } + return world.unloadChunk(chunkX, chunkZ, true); } catch (Throwable e) { IrisLogging.reportError(e); return false; 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 0cfa283d1..e069dc32a 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -150,6 +150,11 @@ public class IrisSettings { public int maxResidentTectonicPlates = 96; public int mantleBackpressureWaitMs = 25; public int mantleBackpressureTimeoutMs = 60_000; + public boolean enableSpigotDirectPregen = false; + + public boolean isEnableSpigotDirectPregen() { + return enableSpigotDirectPregen; + } public int getChunkLoadTimeoutSeconds() { return Math.max(5, Math.min(chunkLoadTimeoutSeconds, 120)); diff --git a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java index 7908ffb30..fee81f313 100644 --- a/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java +++ b/core/src/main/java/art/arcane/iris/core/gui/PregeneratorJob.java @@ -103,7 +103,7 @@ public class PregeneratorJob implements PregenListener { open(); } - var t = new Thread(() -> { + Thread t = new Thread(() -> { J.sleep(1000); this.pregenerator.start(); }, "Iris Pregenerator"); @@ -232,6 +232,9 @@ public class PregeneratorJob implements PregenListener { J.a(() -> { try { monitor.close(); + if (frame == null) { + return; + } J.sleep(3000); frame.setVisible(false); } catch (Throwable ignored) { diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java index 480f91103..84b05e282 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java @@ -27,7 +27,9 @@ import art.arcane.iris.core.nms.datapack.DataVersion; import art.arcane.iris.engine.data.chunk.TerrainChunk; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.platform.PlatformChunkGenerator; +import art.arcane.iris.spi.PlatformBiome; import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.util.nbt.common.mca.NBTWorld; import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; @@ -180,6 +182,14 @@ public interface INMSBinding { return false; } + default boolean writeChunkNbtDirect(NBTWorld nbtWorld, int chunkX, int chunkZ, Hunk blocks, Hunk biomes) { + return false; + } + + default boolean forceEvictChunk(World world, int chunkX, int chunkZ) { + return false; + } + void injectBiomesFromMantle(Chunk e, Mantle mantle); ItemStack applyCustomNbt(ItemStack itemStack, KMap customNbt) throws IllegalArgumentException; diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java index d132da96e..347664496 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncOrMedievalPregenMethod.java @@ -18,17 +18,49 @@ package art.arcane.iris.core.pregenerator.methods; +import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.pregenerator.PregenListener; import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.core.tools.IrisToolbelt; import art.arcane.volmlib.util.mantle.runtime.Mantle; import io.papermc.lib.PaperLib; +import org.bukkit.Bukkit; import org.bukkit.World; public class AsyncOrMedievalPregenMethod implements PregeneratorMethod { private final PregeneratorMethod method; public AsyncOrMedievalPregenMethod(World world, int threads) { - method = PaperLib.isPaper() ? new AsyncPregenMethod(world, threads) : new MedievalPregenMethod(world); + if (PaperLib.isPaper()) { + method = new AsyncPregenMethod(world, threads); + } else if (shouldUseSpigotDirect(world)) { + method = new SpigotDirectPregenMethod(world, threads); + } else { + method = new MedievalPregenMethod(world); + } + } + + private static boolean shouldUseSpigotDirect(World world) { + if (!IrisSettings.get().getPregen().isEnableSpigotDirectPregen()) { + return false; + } + if (IrisToolbelt.isIrisStudioWorld(world)) { + return false; + } + if (!world.isAutoSave()) { + return false; + } + try { + String name = Bukkit.getServer().getName(); + if (name != null) { + String lower = name.toLowerCase(); + if (lower.contains("spigot") || lower.contains("craftbukkit")) { + return true; + } + } + } catch (Throwable ignored) { + } + return false; } @Override 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 ca23f07b3..1dfcb8e4b 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 @@ -414,7 +414,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { static int computeFoliaRecommendedCap(int workerThreads) { int normalizedWorkers = Math.max(1, workerThreads); - int recommendedCap = normalizedWorkers * 4; + int recommendedCap = normalizedWorkers * 8; if (recommendedCap < 64) { return 64; } @@ -592,7 +592,9 @@ public class AsyncPregenMethod implements PregeneratorMethod { + ", urgent=" + urgent + ", timeout=" + timeoutSeconds + "s"); unloadAndSaveAllChunks(); - increaseWorkerThreads(); + if (workerPoolThreads > 0) { + increaseWorkerThreads(); + } } @Override diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java index 83a603c0a..834324870 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/CachedPregenMethod.java @@ -16,7 +16,7 @@ public class CachedPregenMethod implements PregeneratorMethod { public CachedPregenMethod(PregeneratorMethod method, String worldName) { this.method = method; - var cache = IrisServices.get(GlobalCacheSVC.class).get(worldName); + PregenCache cache = IrisServices.get(GlobalCacheSVC.class).get(worldName); if (cache == null) { IrisLogging.debug("Could not find existing cache for " + worldName + " creating fallback"); cache = GlobalCacheSVC.createDefault(worldName); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java index 5d960651b..798e496d4 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/MedievalPregenMethod.java @@ -18,17 +18,18 @@ package art.arcane.iris.core.pregenerator.methods; -import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.pregenerator.PregenListener; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.math.M; import art.arcane.iris.util.common.scheduling.J; import io.papermc.lib.PaperLib; -import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.World; @@ -36,16 +37,63 @@ import java.util.ArrayList; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; public class MedievalPregenMethod implements PregeneratorMethod { private final World world; private final KList> futures; private final Map lastUse; + private final int maxFutures; + private final AtomicBoolean directAsyncDisabled; + private final AtomicBoolean prefetchDisabled; + private final ExecutorService prefetchPool; + private volatile Engine cachedEngine; + private volatile boolean engineResolutionAttempted; public MedievalPregenMethod(World world) { this.world = world; futures = new KList<>(); this.lastUse = new ConcurrentHashMap<>(); + int configuredThreads = IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()); + this.maxFutures = J.isFolia() ? Math.max(2, Math.min(64, configuredThreads)) : Math.max(16, Math.min(128, configuredThreads * 4)); + this.directAsyncDisabled = new AtomicBoolean(false); + this.prefetchDisabled = new AtomicBoolean(J.isFolia()); + int prefetchThreads = J.isFolia() ? 0 : Math.max(2, Math.min(16, configuredThreads)); + this.prefetchPool = prefetchThreads > 0 ? newPrefetchPool(prefetchThreads) : null; + } + + private static ExecutorService newPrefetchPool(int threads) { + AtomicInteger counter = new AtomicInteger(); + return Executors.newFixedThreadPool(threads, r -> { + Thread t = new Thread(r, "Iris Medieval Prefetch " + counter.incrementAndGet()); + t.setDaemon(true); + return t; + }); + } + + private Engine resolveEngine() { + Engine cached = cachedEngine; + if (cached != null) { + return cached; + } + if (engineResolutionAttempted) { + return null; + } + engineResolutionAttempted = true; + try { + if (!IrisToolbelt.isIrisWorld(world)) { + return null; + } + cached = IrisToolbelt.access(world).getEngine(); + if (cached != null) { + cachedEngine = cached; + } + } catch (Throwable ignored) { + } + return cached; } private void waitForChunks() { @@ -60,7 +108,7 @@ public class MedievalPregenMethod implements PregeneratorMethod { futures.clear(); } - private void unloadAndSaveAllChunks() { + private void unloadAndSaveAllChunks(boolean saveWorld) { if (J.isFolia()) { lastUse.clear(); return; @@ -80,7 +128,9 @@ public class MedievalPregenMethod implements PregeneratorMethod { lastUse.remove(i); } } - world.save(); + if (saveWorld) { + world.save(); + } }).get(); } catch (Throwable e) { e.printStackTrace(); @@ -89,17 +139,21 @@ public class MedievalPregenMethod implements PregeneratorMethod { @Override public void init() { - unloadAndSaveAllChunks(); + unloadAndSaveAllChunks(false); } @Override public void close() { - unloadAndSaveAllChunks(); + waitForChunks(); + if (prefetchPool != null) { + prefetchPool.shutdownNow(); + } + unloadAndSaveAllChunks(true); } @Override public void save() { - unloadAndSaveAllChunks(); + unloadAndSaveAllChunks(false); } @Override @@ -119,15 +173,12 @@ public class MedievalPregenMethod implements PregeneratorMethod { @Override public void generateChunk(int x, int z, PregenListener listener) { - if (futures.size() > IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism())) { + if (futures.size() >= maxFutures) { waitForChunks(); } listener.onChunkGenerating(x, z); if (J.isFolia()) { - // Folia: a synchronous world.getChunkAt() runs off the owning region - // thread and fails the TickThread check. Drive the chunk system - // asynchronously instead, which is region-safe. futures.add(PaperLib.getChunkAtAsync(world, x, z, true).thenAccept(c -> { if (c != null) { lastUse.put(c, M.ms()); @@ -138,13 +189,99 @@ public class MedievalPregenMethod implements PregeneratorMethod { return; } - futures.add(J.sfut(() -> { - world.getChunkAt(x, z); - Chunk c = Bukkit.getWorld(world.getUID()).getChunkAt(x, z); - lastUse.put(c, M.ms()); - listener.onChunkGenerated(x, z); - listener.onChunkCleaned(x, z); - })); + futures.add(scheduleChunkLoad(x, z, listener)); + } + + private CompletableFuture scheduleChunkLoad(int x, int z, PregenListener listener) { + if (prefetchDisabled.get() || prefetchPool == null) { + return runChunkLoad(x, z, listener); + } + + Engine engine = resolveEngine(); + if (engine == null) { + return runChunkLoad(x, z, listener); + } + + CompletableFuture aggregate = new CompletableFuture<>(); + try { + prefetchPool.submit(() -> { + try { + prefetchMantle(engine, x, z); + } catch (Throwable e) { + if (prefetchDisabled.compareAndSet(false, true)) { + IrisLogging.warn("Mantle prefetch failed at chunk " + x + "," + z + "; disabling prefetch for this pregen."); + IrisLogging.reportError(e); + } + } + + CompletableFuture chunkFuture = runChunkLoad(x, z, listener); + chunkFuture.whenComplete((r, err) -> { + if (err != null) { + aggregate.completeExceptionally(err); + } else { + aggregate.complete(null); + } + }); + }); + } catch (Throwable rejected) { + if (prefetchDisabled.compareAndSet(false, true)) { + IrisLogging.warn("Mantle prefetch pool rejected work; disabling prefetch."); + } + return runChunkLoad(x, z, listener); + } + return aggregate; + } + + private void prefetchMantle(Engine engine, int chunkX, int chunkZ) { + if (engine == null || engine.isClosing()) { + return; + } + if (!engine.getDimension().isUseMantle()) { + return; + } + + ChunkContext context = new ChunkContext(chunkX, chunkZ, engine.getComplex()); + engine.generateMatter(chunkX, chunkZ, true, context); + } + + private CompletableFuture runChunkLoad(int x, int z, PregenListener listener) { + return directAsyncDisabled.get() ? generateChunkSync(x, z, listener) : generateChunkDirectAsync(x, z, listener); + } + + private CompletableFuture generateChunkDirectAsync(int x, int z, PregenListener listener) { + CompletableFuture future = new CompletableFuture<>(); + + J.a(() -> { + try { + loadChunk(x, z, listener); + future.complete(null); + } catch (Throwable error) { + if (directAsyncDisabled.compareAndSet(false, true)) { + IrisLogging.warn("Direct async Spigot pregen chunk load failed at " + x + "," + z + "; falling back to sync chunk loads."); + IrisLogging.reportError(error); + } + + try { + generateChunkSync(x, z, listener).get(); + future.complete(null); + } catch (Throwable fallbackError) { + future.completeExceptionally(fallbackError); + } + } + }); + + return future; + } + + private CompletableFuture generateChunkSync(int x, int z, PregenListener listener) { + return J.sfut(() -> loadChunk(x, z, listener)); + } + + private void loadChunk(int x, int z, PregenListener listener) { + Chunk chunk = world.getChunkAt(x, z); + lastUse.put(chunk, M.ms()); + listener.onChunkGenerated(x, z); + listener.onChunkCleaned(x, z); } @Override diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/SpigotDirectPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/SpigotDirectPregenMethod.java new file mode 100644 index 000000000..2d0073f50 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/SpigotDirectPregenMethod.java @@ -0,0 +1,586 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2022 Arcane Arts (Volmit Software) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package art.arcane.iris.core.pregenerator.methods; + +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.nms.INMS; +import art.arcane.iris.core.pregenerator.PregenListener; +import art.arcane.iris.core.pregenerator.PregeneratorMethod; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.spi.IrisLogging; +import art.arcane.iris.spi.PlatformBiome; +import art.arcane.iris.spi.PlatformBlockState; +import art.arcane.iris.spi.IrisPlatforms; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.common.scheduling.J; +import art.arcane.iris.util.nbt.common.mca.NBTWorld; +import art.arcane.iris.util.project.hunk.Hunk; +import art.arcane.iris.util.project.hunk.storage.AtomicHunk; +import art.arcane.volmlib.util.mantle.runtime.Mantle; +import art.arcane.volmlib.util.math.M; +import org.bukkit.Chunk; +import org.bukkit.World; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +public class SpigotDirectPregenMethod implements PregeneratorMethod { + private static final int ADAPTIVE_TIMEOUT_STEP = 3; + private static final int ADAPTIVE_RECOVERY_INTERVAL = 8; + private static final long PERMIT_WAIT_NOTIFY_MS = 500L; + + private final World world; + private final int threads; + private final int timeoutSeconds; + private final int timeoutWarnIntervalMs; + private final int maxResidentTectonicPlates; + private final int mantleBackpressureWaitMs; + private final int mantleBackpressureTimeoutMs; + + private final Semaphore semaphore; + private final AtomicInteger adaptiveInFlightLimit; + private final int adaptiveMinInFlightLimit; + private final AtomicInteger timeoutStreak = new AtomicInteger(); + private final AtomicLong lastTimeoutLogAt = new AtomicLong(0L); + private final AtomicInteger suppressedTimeoutLogs = new AtomicInteger(); + private final AtomicLong lastAdaptiveLogAt = new AtomicLong(0L); + private final AtomicInteger inFlight = new AtomicInteger(); + private final AtomicLong submitted = new AtomicLong(); + private final AtomicLong completed = new AtomicLong(); + private final AtomicLong failed = new AtomicLong(); + private final AtomicLong lastProgressAt = new AtomicLong(M.ms()); + private final Object permitMonitor = new Object(); + + private final Set liveLoadedChunkKeys; + private final ConcurrentHashMap chunksWrittenDirect; + private final AtomicBoolean writePathDisabled; + private volatile Engine cachedEngine; + private volatile Mantle cachedMantle; + private volatile NBTWorld nbtWorld; + private volatile MedievalPregenMethod fallback; + + public SpigotDirectPregenMethod(World world, int threads) { + this.world = world; + int configured = Math.max(1, threads); + this.threads = Math.max(8, Math.min(32, configured)); + this.semaphore = new Semaphore(this.threads, true); + this.adaptiveInFlightLimit = new AtomicInteger(this.threads); + this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4))); + + IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); + this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds(); + this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs(); + this.maxResidentTectonicPlates = pregen.getMaxResidentTectonicPlates(); + this.mantleBackpressureWaitMs = pregen.getMantleBackpressureWaitMs(); + this.mantleBackpressureTimeoutMs = pregen.getMantleBackpressureTimeoutMs(); + + this.liveLoadedChunkKeys = ConcurrentHashMap.newKeySet(); + this.chunksWrittenDirect = new ConcurrentHashMap<>(); + this.writePathDisabled = new AtomicBoolean(false); + } + + @Override + public void init() { + try { + this.nbtWorld = new NBTWorld(world.getWorldFolder()); + } catch (Throwable e) { + IrisLogging.error("SpigotDirect pregen could not open NBTWorld for " + world.getName() + "; disabling direct write path."); + IrisLogging.reportError(e); + writePathDisabled.set(true); + } + + snapshotLoadedChunks(); + + IrisLogging.info("SpigotDirect pregen init: world=" + world.getName() + + ", threads=" + threads + + ", adaptiveLimit=" + adaptiveInFlightLimit.get() + + ", initialLoadedChunks=" + liveLoadedChunkKeys.size() + + ", maxResidentTectonicPlates=" + maxResidentTectonicPlates + + ", timeout=" + timeoutSeconds + "s"); + } + + @Override + public void close() { + semaphore.acquireUninterruptibly(threads); + if (nbtWorld != null) { + try { + nbtWorld.flushNow(); + nbtWorld.close(); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + evictWrittenChunksFromServer(); + if (fallback != null) { + try { + fallback.close(); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + + @Override + public void save() { + if (nbtWorld != null) { + try { + nbtWorld.save(); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + if (fallback != null) { + try { + fallback.save(); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + } + + @Override + public boolean supportsRegions(int x, int z, PregenListener listener) { + return false; + } + + @Override + public void generateRegion(int x, int z, PregenListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public String getMethod(int x, int z) { + return "SpigotDirect"; + } + + @Override + public Mantle getMantle() { + if (IrisToolbelt.isIrisWorld(world)) { + return IrisToolbelt.access(world).getEngine().getMantle().getMantle(); + } + return null; + } + + @Override + public void generateChunk(int x, int z, PregenListener listener) { + listener.onChunkGenerating(x, z); + enforceMantleBudget(); + + long key = chunkKey(x, z); + if (writePathDisabled.get() || nbtWorld == null || liveLoadedChunkKeys.contains(key) || world.isChunkLoaded(x, z)) { + ensureFallback().generateChunk(x, z, listener); + return; + } + + Engine engine = resolveEngine(); + if (engine == null || !engine.getDimension().isUseMantle()) { + ensureFallback().generateChunk(x, z, listener); + return; + } + + try { + synchronized (permitMonitor) { + while (inFlight.get() >= adaptiveInFlightLimit.get()) { + permitMonitor.wait(PERMIT_WAIT_NOTIFY_MS); + } + } + while (!semaphore.tryAcquire(1, TimeUnit.SECONDS)) { + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + markSubmitted(); + MultiBurst.burst.lazy(() -> runDirect(engine, x, z, listener)); + } + + private void runDirect(Engine engine, int x, int z, PregenListener listener) { + boolean success = false; + try { + int worldMinY = world.getMinHeight(); + int worldMaxY = world.getMaxHeight(); + int height = worldMaxY - worldMinY; + + PlatformBlockState air = IrisPlatforms.get().registries().air(); + Hunk blocks = new AirDefaultAtomicHunk(16, height, 16, air); + Hunk biomes = Hunk.newSynchronizedArrayHunk(16, height, 16); + + try { + engine.generate(x << 4, z << 4, blocks, biomes, false); + } catch (Throwable e) { + handleFailure(x, z, e); + return; + } + + boolean wrote; + try { + wrote = INMS.get().writeChunkNbtDirect(nbtWorld, x, z, blocks, biomes); + } catch (Throwable e) { + handleFailure(x, z, e); + return; + } + + if (!wrote) { + if (writePathDisabled.compareAndSet(false, true)) { + IrisLogging.warn("SpigotDirect NBT write returned false at chunk " + x + "," + z + + "; disabling direct path. Subsequent chunks will route through MedievalPregenMethod on the iterator thread."); + } + listener.onChunkGenerated(x, z); + listener.onChunkCleaned(x, z); + success = true; + return; + } + + chunksWrittenDirect.put(chunkKey(x, z), Boolean.TRUE); + + try { + cleanupMantleChunk(x, z); + } catch (Throwable ignored) { + } + + listener.onChunkGenerated(x, z); + listener.onChunkCleaned(x, z); + success = true; + } catch (Throwable e) { + handleFailure(x, z, e); + } finally { + markFinished(success); + semaphore.release(); + } + } + + private void handleFailure(int x, int z, Throwable error) { + IrisLogging.warn("SpigotDirect pregen failure at chunk " + x + "," + z + ". " + metricsSnapshot()); + IrisLogging.reportError(error); + if (error != null) { + error.printStackTrace(System.err); + } + if (writePathDisabled.compareAndSet(false, true)) { + IrisLogging.warn("SpigotDirect: disabling direct write path after first failure. Subsequent chunks route through MedievalPregenMethod on the iterator thread."); + } + onTimeout(x, z); + } + + private MedievalPregenMethod ensureFallback() { + MedievalPregenMethod existing = fallback; + if (existing != null) { + return existing; + } + synchronized (this) { + existing = fallback; + if (existing == null) { + existing = new MedievalPregenMethod(world); + existing.init(); + fallback = existing; + } + } + return existing; + } + + private void snapshotLoadedChunks() { + try { + Set loaded = J.sfut(() -> { + Set keys = new HashSet<>(); + if (world == null) { + return keys; + } + for (Chunk loadedChunk : world.getLoadedChunks()) { + keys.add(chunkKey(loadedChunk.getX(), loadedChunk.getZ())); + } + return keys; + }).get(); + if (loaded != null) { + liveLoadedChunkKeys.addAll(loaded); + } + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private void evictWrittenChunksFromServer() { + if (chunksWrittenDirect.isEmpty()) { + return; + } + try { + J.sfut(() -> { + int evicted = 0; + for (Long key : chunksWrittenDirect.keySet()) { + int x = keyX(key); + int z = keyZ(key); + if (!world.isChunkLoaded(x, z)) { + continue; + } + if (INMS.get().forceEvictChunk(world, x, z)) { + evicted++; + } + } + if (evicted > 0) { + IrisLogging.info("SpigotDirect: force-evicted " + evicted + " chunks loaded mid-pregen so the server reloads them from disk."); + } + }).get(); + } catch (Throwable e) { + IrisLogging.reportError(e); + } + } + + private static long chunkKey(int x, int z) { + return (((long) x) << 32) | (z & 0xffffffffL); + } + + private static int keyX(long key) { + return (int) (key >> 32); + } + + private static int keyZ(long key) { + return (int) (key & 0xffffffffL); + } + + private void onTimeout(int x, int z) { + int streak = timeoutStreak.incrementAndGet(); + if (streak % ADAPTIVE_TIMEOUT_STEP == 0) { + lowerAdaptiveInFlightLimit(); + } + + long now = M.ms(); + long last = lastTimeoutLogAt.get(); + if (now - last < timeoutWarnIntervalMs || !lastTimeoutLogAt.compareAndSet(last, now)) { + suppressedTimeoutLogs.incrementAndGet(); + return; + } + + int suppressed = suppressedTimeoutLogs.getAndSet(0); + String suppressedText = suppressed <= 0 ? "" : " suppressed=" + suppressed; + IrisLogging.warn("SpigotDirect pregen failure cluster at " + x + "," + z + + " adaptiveLimit=" + adaptiveInFlightLimit.get() + + suppressedText + " " + metricsSnapshot()); + } + + private void onSuccess() { + int streak = timeoutStreak.get(); + if (streak > 0) { + int newStreak = Math.max(0, streak - 2); + timeoutStreak.compareAndSet(streak, newStreak); + if (newStreak > 0) { + return; + } + } + + if ((completed.get() & (ADAPTIVE_RECOVERY_INTERVAL - 1)) == 0L) { + raiseAdaptiveInFlightLimit(); + } + } + + private void lowerAdaptiveInFlightLimit() { + while (true) { + int current = adaptiveInFlightLimit.get(); + if (current <= adaptiveMinInFlightLimit) { + return; + } + int next = Math.max(adaptiveMinInFlightLimit, current - 1); + if (adaptiveInFlightLimit.compareAndSet(current, next)) { + logAdaptiveLimit("decrease", next); + notifyPermitWaiters(); + return; + } + } + } + + private void raiseAdaptiveInFlightLimit() { + while (true) { + int current = adaptiveInFlightLimit.get(); + if (current >= threads) { + return; + } + int deficit = threads - current; + int step = deficit > (threads / 2) ? Math.max(2, threads / 8) : 1; + int next = Math.min(threads, current + step); + if (adaptiveInFlightLimit.compareAndSet(current, next)) { + logAdaptiveLimit("increase", next); + notifyPermitWaiters(); + return; + } + } + } + + private void logAdaptiveLimit(String mode, int value) { + long now = M.ms(); + long last = lastAdaptiveLogAt.get(); + if (now - last < 5000L) { + return; + } + if (lastAdaptiveLogAt.compareAndSet(last, now)) { + IrisLogging.info("SpigotDirect pregen adaptive limit " + mode + " -> " + value + " (" + metricsSnapshot() + ")"); + } + } + + private String metricsSnapshot() { + long stalledFor = Math.max(0L, M.ms() - lastProgressAt.get()); + return "world=" + world.getName() + + " permits=" + semaphore.availablePermits() + "/" + threads + + " adaptiveLimit=" + adaptiveInFlightLimit.get() + + " inFlight=" + inFlight.get() + + " submitted=" + submitted.get() + + " completed=" + completed.get() + + " failed=" + failed.get() + + " stalledForMs=" + stalledFor; + } + + private void markSubmitted() { + submitted.incrementAndGet(); + inFlight.incrementAndGet(); + } + + private void markFinished(boolean success) { + if (success) { + completed.incrementAndGet(); + onSuccess(); + } else { + failed.incrementAndGet(); + } + + lastProgressAt.set(M.ms()); + int after = inFlight.decrementAndGet(); + if (after < 0) { + inFlight.compareAndSet(after, 0); + } + notifyPermitWaiters(); + } + + private void notifyPermitWaiters() { + synchronized (permitMonitor) { + permitMonitor.notifyAll(); + } + } + + private void cleanupMantleChunk(int x, int z) { + Engine engine = resolveEngine(); + if (engine != null) { + try { + engine.getMantle().forceCleanupChunk(x, z); + } catch (Throwable ignored) { + } + } + } + + private Engine resolveEngine() { + Engine cached = cachedEngine; + if (cached != null) { + return cached; + } + + if (!IrisToolbelt.isIrisWorld(world)) { + return null; + } + + try { + Engine resolved = IrisToolbelt.access(world).getEngine(); + if (resolved != null) { + cachedEngine = resolved; + } + return resolved; + } catch (Throwable ignored) { + return null; + } + } + + private Mantle resolveMantle() { + Mantle cached = cachedMantle; + if (cached != null) { + return cached; + } + Mantle resolved = getMantle(); + if (resolved != null) { + cachedMantle = resolved; + } + return resolved; + } + + private void enforceMantleBudget() { + int cap = maxResidentTectonicPlates; + if (cap <= 0) { + return; + } + + Mantle mantle = resolveMantle(); + if (mantle == null) { + return; + } + + int hardCap = cap * 2; + if (mantle.getLoadedRegionCount() <= hardCap) { + return; + } + + long waitStart = M.ms(); + long lastLog = 0L; + while (mantle.getLoadedRegionCount() > hardCap) { + mantle.trim(0L, 0); + int freed = mantle.unloadTectonicPlate(0); + int resident = mantle.getLoadedRegionCount(); + if (resident <= hardCap) { + break; + } + + long elapsed = M.ms() - waitStart; + if (elapsed >= mantleBackpressureTimeoutMs) { + IrisLogging.warn("SpigotDirect mantle backpressure exceeded " + mantleBackpressureTimeoutMs + "ms with " + resident + + " tectonic plates resident (hard cap " + hardCap + "); proceeding to avoid deadlock. " + + "Raise pregen.maxResidentTectonicPlates if this persists. " + metricsSnapshot()); + return; + } + + long logNow = M.ms(); + if (logNow - lastLog >= 5_000L) { + lastLog = logNow; + IrisLogging.warn("SpigotDirect mantle backpressure: " + resident + " tectonic plates resident (hard cap " + hardCap + + "), freed " + freed + " last pass, waited " + elapsed + "ms."); + } + + try { + Thread.sleep(mantleBackpressureWaitMs); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + + private static final class AirDefaultAtomicHunk extends AtomicHunk { + private final PlatformBlockState airDefault; + + AirDefaultAtomicHunk(int w, int h, int d, PlatformBlockState airDefault) { + super(w, h, d); + this.airDefault = airDefault; + } + + @Override + public PlatformBlockState getRaw(int x, int y, int z) { + PlatformBlockState v = super.getRaw(x, y, z); + return v != null ? v : airDefault; + } + } +} 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 875cf0639..4681bc1a7 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 @@ -41,6 +41,7 @@ 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.iris.util.project.noise.CNG; import art.arcane.iris.util.common.scheduling.J; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; @@ -299,6 +300,44 @@ public class MantleWriter implements IObjectPlacer, AutoCloseable { return getData(x, y, z, MatterCavern.class) != null; } + public byte[] getCarvedColumn(int x, int z, int height) { + int cappedHeight = Math.min(Math.max(height, 0), mantle.getWorldHeight()); + byte[] carvedColumn = new byte[cappedHeight]; + if (cappedHeight <= 0) { + return carvedColumn; + } + + MantleChunk chunk = acquireChunk(x >> 4, z >> 4); + if (chunk == null) { + return carvedColumn; + } + + int localX = x & 15; + int localZ = z & 15; + int lastSection = (cappedHeight - 1) >> 4; + for (int section = 0; section <= lastSection; section++) { + if (!chunk.exists(section)) { + continue; + } + + Matter matter = chunk.get(section); + if (matter == null || !matter.hasSlice(MatterCavern.class)) { + continue; + } + + MatterSlice slice = matter.getSlice(MatterCavern.class); + int sectionBaseY = section << 4; + int sectionMaxY = Math.min(cappedHeight, sectionBaseY + 16); + for (int y = sectionBaseY; y < sectionMaxY; y++) { + if (slice.get(localX, y & 15, localZ) != null) { + carvedColumn[y] = 1; + } + } + } + + return carvedColumn; + } + @Override public boolean isSolid(int x, int y, int z) { return B.isSolid(get(x, y, z)); 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 b48018255..f0f1d2429 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 @@ -41,10 +41,10 @@ public class IrisCaveCarver3D { private static final byte LIQUID_AIR = 0; private static final byte LIQUID_LAVA = 2; private static final byte LIQUID_FORCED_AIR = 3; - private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 32; + private static final int ADAPTIVE_MIN_PLANE_COLUMNS = 16; private static final int ADAPTIVE_DEEP_SAMPLE_STEP = 8; private static final int ADAPTIVE_DEEP_SURFACE_MARGIN = 12; - private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.25D; + private static final double ADAPTIVE_LOCAL_RANGE_SCALE = 0.125D; private static final double ADAPTIVE_DEEP_MARGIN_BOOST = 0.015D; private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(Scratch::new); @@ -1167,6 +1167,7 @@ public class IrisCaveCarver3D { for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) { int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16); adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpModules( + scratch, x, y, z0 + sampleLocalZ, @@ -1603,11 +1604,13 @@ public class IrisCaveCarver3D { } private boolean classifyDensityPointWarpOnly(int x, int y, int z, double thresholdLimit) { + Scratch scratch = SCRATCH.get(); int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); - double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); - double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); @@ -1637,11 +1640,13 @@ public class IrisCaveCarver3D { return classifyDensityPointWarpOnly(x, y, z, thresholdLimit); } + Scratch scratch = SCRATCH.get(); int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); - double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); - double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); @@ -1735,11 +1740,13 @@ public class IrisCaveCarver3D { return true; } + Scratch scratch = SCRATCH.get(); int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); - double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); - double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); @@ -1771,6 +1778,10 @@ public class IrisCaveCarver3D { int[] adaptivePlaneSampleBounds, int axisCells ) { + Scratch scratch = SCRATCH.get(); + prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisCells + 1); + int[] adaptiveCellX = scratch.adaptiveCellX; + int[] adaptiveCellZ = scratch.adaptiveCellZ; int minSampleX = axisCells; int maxSampleX = 0; int minSampleZ = axisCells; @@ -1778,10 +1789,8 @@ public class IrisCaveCarver3D { for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { int columnIndex = planeColumnIndices[planeIndex]; - int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); - int localZ = columnIndex & 15; - int sampleX = Math.min(localX / adaptiveSampleStep, axisCells - 1); - int sampleZ = Math.min(localZ / adaptiveSampleStep, axisCells - 1); + int sampleX = adaptiveCellX[columnIndex]; + int sampleZ = adaptiveCellZ[columnIndex]; if (sampleX < minSampleX) { minSampleX = sampleX; } @@ -1813,20 +1822,20 @@ public class IrisCaveCarver3D { double[] adaptivePlanePrediction, double[] adaptivePlaneAmbiguity ) { + Scratch scratch = SCRATCH.get(); + prepareAdaptiveGeometry(scratch, adaptiveSampleStep, axisCells, axisSamples); + int[] adaptiveCellZ = scratch.adaptiveCellZ; + int[] adaptiveRow0 = scratch.adaptiveRow0; + int[] adaptiveRow1 = scratch.adaptiveRow1; + double[] adaptiveTx = scratch.adaptiveTx; + double[] adaptiveTz = scratch.adaptiveTz; for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { int columnIndex = planeColumnIndices[planeIndex]; - int localX = PowerOfTwoCoordinates.unpackLocal16X(columnIndex); - int localZ = columnIndex & 15; - int cellX = Math.min(localX / adaptiveSampleStep, axisCells - 1); - int cellZ = Math.min(localZ / adaptiveSampleStep, axisCells - 1); - int x0 = cellX * adaptiveSampleStep; - int z0 = cellZ * adaptiveSampleStep; - int x1 = Math.min(x0 + adaptiveSampleStep, 16); - int z1 = Math.min(z0 + adaptiveSampleStep, 16); - double tx = x1 == x0 ? 0D : (localX - x0) / (double) (x1 - x0); - double tz = z1 == z0 ? 0D : (localZ - z0) / (double) (z1 - z0); - int row0 = cellX * axisSamples; - int row1 = (cellX + 1) * axisSamples; + int cellZ = adaptiveCellZ[columnIndex]; + int row0 = adaptiveRow0[columnIndex]; + int row1 = adaptiveRow1[columnIndex]; + double tx = adaptiveTx[columnIndex]; + double tz = adaptiveTz[columnIndex]; double d00 = adaptivePlaneDensity[row0 + cellZ]; double d01 = adaptivePlaneDensity[row0 + cellZ + 1]; double d10 = adaptivePlaneDensity[row1 + cellZ]; @@ -1840,6 +1849,32 @@ public class IrisCaveCarver3D { } } + private void prepareAdaptiveGeometry(Scratch scratch, int adaptiveSampleStep, int axisCells, int axisSamples) { + if (scratch.adaptiveGeometryStep == adaptiveSampleStep && scratch.adaptiveGeometryAxisCells == axisCells) { + return; + } + + for (int columnIndex = 0; columnIndex < 256; columnIndex++) { + int localX = columnIndex >> 4; + int localZ = columnIndex & 15; + int cellX = Math.min(localX / adaptiveSampleStep, axisCells - 1); + int cellZ = Math.min(localZ / adaptiveSampleStep, axisCells - 1); + int x0 = cellX * adaptiveSampleStep; + int z0 = cellZ * adaptiveSampleStep; + int x1 = Math.min(x0 + adaptiveSampleStep, 16); + int z1 = Math.min(z0 + adaptiveSampleStep, 16); + scratch.adaptiveCellX[columnIndex] = cellX; + scratch.adaptiveCellZ[columnIndex] = cellZ; + scratch.adaptiveRow0[columnIndex] = cellX * axisSamples; + scratch.adaptiveRow1[columnIndex] = (cellX + 1) * axisSamples; + scratch.adaptiveTx[columnIndex] = x1 == x0 ? 0D : (localX - x0) / (double) (x1 - x0); + scratch.adaptiveTz[columnIndex] = z1 == z0 ? 0D : (localZ - z0) / (double) (z1 - z0); + } + + scratch.adaptiveGeometryStep = adaptiveSampleStep; + scratch.adaptiveGeometryAxisCells = axisCells; + } + private double sampleDensityNoWarpNoModules(int x, int y, int z) { double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight; density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight; @@ -1870,11 +1905,13 @@ public class IrisCaveCarver3D { } private double sampleDensityWarpOnly(int x, int y, int z) { + Scratch scratch = SCRATCH.get(); int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); - double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); - double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); @@ -1891,15 +1928,16 @@ public class IrisCaveCarver3D { } ModuleState[] localModules = scratch.activeModules; - return sampleDensityWarpModules(x, y, z, localModules, activeModuleCount); + return sampleDensityWarpModules(scratch, x, y, z, localModules, activeModuleCount); } - private double sampleDensityWarpModules(int x, int y, int z, ModuleState[] localModules, int activeModuleCount) { + private double sampleDensityWarpModules(Scratch scratch, int x, int y, int z, ModuleState[] localModules, int activeModuleCount) { int sx = snapWarp(x); int sy = snapWarp(y); int sz = snapWarp(z); - double warpA = warpDensity.noiseFastSigned3D(sx, sy, sz); - double warpB = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); + int warpSlot = prepareWarpSample(scratch, sx, sy, sz); + double warpA = scratch.warpCacheA[warpSlot]; + double warpB = scratch.warpCacheB[warpSlot]; double warpedX = x + (warpA * warpStrength); double warpedY = y + (warpB * warpStrength); double warpedZ = z + ((warpA - warpB) * 0.5D * warpStrength); @@ -1918,6 +1956,32 @@ public class IrisCaveCarver3D { return density * inverseNormalization; } + private int prepareWarpSample(Scratch scratch, int sx, int sy, int sz) { + int slot = mixWarpKey(sx, sy, sz) & (scratch.warpCacheX.length - 1); + if (scratch.warpCacheSet[slot] + && scratch.warpCacheX[slot] == sx + && scratch.warpCacheY[slot] == sy + && scratch.warpCacheZ[slot] == sz) { + return slot; + } + + scratch.warpCacheSet[slot] = true; + scratch.warpCacheX[slot] = sx; + scratch.warpCacheY[slot] = sy; + scratch.warpCacheZ[slot] = sz; + scratch.warpCacheA[slot] = warpDensity.noiseFastSigned3D(sx, sy, sz); + scratch.warpCacheB[slot] = warpDensity.noiseFastSigned3D(sx + 31.37D, sy - 17.21D, sz + 23.91D); + return slot; + } + + private static int mixWarpKey(int sx, int sy, int sz) { + int hash = sx * 73428767; + hash ^= sy * 91227153; + hash ^= sz * 43828939; + hash ^= hash >>> 16; + return hash; + } + private int prepareActiveModules(Scratch scratch, int y) { ModuleState[] configuredModules = modules; int configuredCount = configuredModules.length; @@ -2127,6 +2191,18 @@ public class IrisCaveCarver3D { private final double[] adaptivePlanePrediction = new double[256]; private final double[] adaptivePlaneAmbiguity = new double[256]; private final int[] adaptivePlaneSampleBounds = new int[4]; + private final int[] adaptiveCellX = new int[256]; + private final int[] adaptiveCellZ = new int[256]; + private final int[] adaptiveRow0 = new int[256]; + private final int[] adaptiveRow1 = new int[256]; + private final double[] adaptiveTx = new double[256]; + private final double[] adaptiveTz = new double[256]; + private final boolean[] warpCacheSet = new boolean[256]; + private final int[] warpCacheX = new int[256]; + private final int[] warpCacheY = new int[256]; + private final int[] warpCacheZ = new int[256]; + private final double[] warpCacheA = new double[256]; + private final double[] warpCacheB = new double[256]; private final int[] tileIndices = new int[4]; private final int[] tileLocalX = new int[4]; private final int[] tileLocalZ = new int[4]; @@ -2138,6 +2214,8 @@ public class IrisCaveCarver3D { private MatterCavern[] matterByY = new MatterCavern[0]; private Matter[] sectionMatter = new Matter[0]; private MatterSlice[] sectionSlices = new MatterSlice[0]; + private int adaptiveGeometryStep = -1; + private int adaptiveGeometryAxisCells = -1; private boolean fullWeightsInitialized; } } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java index b75ab13a8..7ebb02bec 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleObjectComponent.java @@ -556,12 +556,11 @@ public class MantleObjectComponent extends IrisMantleComponent { int zz = rng.i(z, z + 15); int surfaceObjectExclusionDepth = resolveSurfaceObjectExclusionDepth(surfaceObjectExclusionBaseDepth, v, objectPlacement); int surfaceObjectExclusionRadius = resolveSurfaceObjectExclusionRadius(v, objectPlacement); - boolean overCave = surfaceObjectExclusionDepth > 0 && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius, surfaceExposureCache); - int id = rng.i(0, Integer.MAX_VALUE); IrisObjectPlacement effectivePlacement = resolveEffectivePlacement(objectPlacement, v); - if (effectivePlacement.getMode() == ObjectPlaceMode.FLOATING) { - overCave = false; - } + boolean overCave = effectivePlacement.getMode() != ObjectPlaceMode.FLOATING + && surfaceObjectExclusionDepth > 0 + && hasSurfaceCarveExposure(writer, surfaceHeightLookup, xx, zz, surfaceObjectExclusionDepth, surfaceObjectExclusionRadius, surfaceExposureCache); + int id = rng.i(0, Integer.MAX_VALUE); IObjectPlacer placePlacer = golden ? new GoldenDebugPlacer(writer, scope + "/" + v.getLoadKey()) : writer; if (golden) { IrisLogging.info("Goldendebug object attempt: chunk=" + chunkX + "," + chunkZ @@ -1834,10 +1833,7 @@ public class MantleObjectComponent extends IrisMantleComponent { return carvedColumn; } - carvedColumn = new byte[height]; - for (int y = 0; y < height; y++) { - carvedColumn[y] = (byte) (writer.isCarved(x, y, z) ? 1 : 0); - } + carvedColumn = writer.getCarvedColumn(x, z, height); carvedColumns.put(columnKey, carvedColumn); return carvedColumn; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java index ee43c326f..031caf94f 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java +++ b/core/src/main/java/art/arcane/iris/engine/object/FloatingIslandSample.java @@ -23,9 +23,11 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.collection.KList; +import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap; import java.util.Arrays; import java.util.HashMap; +import java.util.IdentityHashMap; import java.util.concurrent.atomic.AtomicBoolean; public final class FloatingIslandSample { @@ -50,6 +52,7 @@ public final class FloatingIslandSample { private static final ThreadLocal LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]); private static final ThreadLocal LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]); private static final ThreadLocal> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new); + private static final ThreadLocal> FOOTPRINT_MEMO = ThreadLocal.withInitial(IdentityHashMap::new); private static final AtomicBoolean NULL_CNG_WARNED = new AtomicBoolean(false); public static int getLastReject() { @@ -69,6 +72,7 @@ public final class FloatingIslandSample { public static void clearChunkMemo() { CHUNK_MEMO.get().clear(); + FOOTPRINT_MEMO.get().clear(); } public static FloatingIslandSample sampleMemoized(IrisBiome parent, int wx, int wz, int chunkHeight, long baseSeed, IrisData data, Engine engine) { @@ -272,8 +276,7 @@ public final class FloatingIslandSample { warnNullCng("footprintStyle", parent); return reject(REJECT_NO_SEED); } - double footprintValue = footprintCng.noise(wx, wz); - double signed = (Math.max(0, Math.min(1, footprintValue)) * 2.0) - 1.0; + double signed = footprintSigned(footprintCng, wx, wz); double threshold = Math.max(0, Math.min(1, entry.getFootprintThreshold())); double signedCut = (threshold * 2.0) - 1.0; @@ -558,8 +561,7 @@ public final class FloatingIslandSample { if (dx == 0 && dz == 0) { continue; } - double footprintValue = footprintCng.noise(wx + dx, wz + dz); - double signed = (Math.max(0, Math.min(1, footprintValue)) * 2.0) - 1.0; + double signed = footprintSigned(footprintCng, wx + dx, wz + dz); if (signed <= signedCut) { continue; } @@ -609,6 +611,30 @@ public final class FloatingIslandSample { return signedFromUnit(layerFoot) > signedCut; } + private static double footprintSigned(CNG footprintCng, int wx, int wz) { + IdentityHashMap memoByCng = FOOTPRINT_MEMO.get(); + Long2DoubleOpenHashMap memo = memoByCng.get(footprintCng); + if (memo == null) { + memo = new Long2DoubleOpenHashMap(512); + memo.defaultReturnValue(Double.NaN); + memoByCng.put(footprintCng, memo); + } + + long key = columnKey(wx, wz); + double cached = memo.get(key); + if (!Double.isNaN(cached)) { + return cached; + } + + double signed = signedFromUnit(footprintCng.noise(wx, wz)); + memo.put(key, signed); + return signed; + } + + private static long columnKey(int wx, int wz) { + return ((long) wx << 32) ^ (wz & 0xFFFFFFFFL); + } + private static double signedFromUnit(double value) { return (Math.max(0, Math.min(1, value)) * 2.0) - 1.0; }