changes :)

This commit is contained in:
Brian Neumann-Fopiano
2026-06-16 01:35:25 -04:00
parent 3cfbcbf8ed
commit 8fb627f9c1
13 changed files with 1103 additions and 76 deletions
@@ -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<net.minecraft.world.level.biome.Biome> 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<BlockState>(c, d, b);
});
MCAPalette<BlockState> global = globalCache.aquireNasty(() -> new MCAGlobalPalette<>(registry, ((CraftBlockData) AIR).getState()));
java.util.Map<CompoundTag, BlockState> innerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
java.util.Map<CompoundTag, BlockState> outerDecodeCache = new java.util.concurrent.ConcurrentHashMap<>(64);
MCAPalettedContainer<BlockState> 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<PlatformBlockState> blocks, Hunk<PlatformBiome> 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<BlockData, CompoundTag> 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;
@@ -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));
@@ -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) {
@@ -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<PlatformBlockState> blocks, Hunk<PlatformBiome> biomes) {
return false;
}
default boolean forceEvictChunk(World world, int chunkX, int chunkZ) {
return false;
}
void injectBiomesFromMantle(Chunk e, Mantle<Matter> mantle);
ItemStack applyCustomNbt(ItemStack itemStack, KMap<String, Object> customNbt) throws IllegalArgumentException;
@@ -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
@@ -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
@@ -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);
@@ -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<CompletableFuture<?>> futures;
private final Map<Chunk, Long> 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<Void> 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<Void> 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
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Long> liveLoadedChunkKeys;
private final ConcurrentHashMap<Long, Boolean> 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<PlatformBlockState> blocks = new AirDefaultAtomicHunk(16, height, 16, air);
Hunk<PlatformBiome> 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<Long> loaded = J.sfut(() -> {
Set<Long> 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<PlatformBlockState> {
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;
}
}
}
@@ -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<Matter> 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<MatterCavern> 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));
@@ -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> 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;
}
}
@@ -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;
}
@@ -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<int[]> LAST_REJECT = ThreadLocal.withInitial(() -> new int[1]);
private static final ThreadLocal<double[]> LAST_DENSITY = ThreadLocal.withInitial(() -> new double[2]);
private static final ThreadLocal<HashMap<Long, FloatingIslandSample>> CHUNK_MEMO = ThreadLocal.withInitial(HashMap::new);
private static final ThreadLocal<IdentityHashMap<CNG, Long2DoubleOpenHashMap>> 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<CNG, Long2DoubleOpenHashMap> 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;
}