diff --git a/core/src/main/java/art/arcane/iris/generation/biome/FloatingIslandBoundarySampler.java b/core/src/main/java/art/arcane/iris/generation/biome/FloatingIslandBoundarySampler.java index 517836cfb..93e39da8f 100644 --- a/core/src/main/java/art/arcane/iris/generation/biome/FloatingIslandBoundarySampler.java +++ b/core/src/main/java/art/arcane/iris/generation/biome/FloatingIslandBoundarySampler.java @@ -48,7 +48,7 @@ public final class FloatingIslandBoundarySampler { } public @Nullable IrisBiome parent(int x, int z) { - return parentField(x, z, EDGE_TAPER_WIDTH).parent(x, z); + return source.sample(x, z); } public double edgeFade(IrisBiome parent, int x, int z) { @@ -230,10 +230,6 @@ public final class FloatingIslandBoundarySampler { build(source); } - private @Nullable IrisBiome parent(int x, int z) { - return parents[geometry.index(x - minX, z - minZ)]; - } - private int edgeDistance(IrisBiome expected, int x, int z) { int index = geometry.index(x - minX, z - minZ); if (!sameBiome(expected, parents[index]) || !viable[index]) { diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java b/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java index a7e6d5c69..157fc8fad 100644 --- a/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolder.java @@ -23,19 +23,35 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockVolume; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.iris.generation.block.BoundBlockState; import art.arcane.iris.generation.block.IrisCustomData; -import art.arcane.volmlib.util.hunk.storage.AtomicHunk; +import art.arcane.volmlib.util.hunk.storage.StorageHunk; import org.bukkit.block.data.BlockData; import org.bukkit.generator.ChunkGenerator.ChunkData; -@SuppressWarnings("ClassCanBeRecord") -public class ChunkDataHunkHolder extends AtomicHunk implements NativeBlockVolume { +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Arrays; + +/** + * Plain-array block buffer for one chunk. Stages write disjoint columns when they fan out and every hand-off + * between threads is a join, so the cells need no per-access fences. Each column tracks its highest stored y; + * a raise is a CAS so concurrent writers to one column can never lose the maximum. + */ +public class ChunkDataHunkHolder extends StorageHunk implements NativeBlockVolume, ColumnExtent { private static final BoundBlockState AIR = BoundBlockState.of("AIR"); + private static final VarHandle COLUMN_TOPS = MethodHandles.arrayElementVarHandle(int[].class); private final ChunkData chunk; + private final NativeBlockState[] data; + private final int[] columnTops; + private final int zStride; public ChunkDataHunkHolder(ChunkData chunk) { super(16, chunk.getMaxHeight() - chunk.getMinHeight(), 16); this.chunk = chunk; + this.zStride = 16 * getHeight(); + this.data = new NativeBlockState[zStride * 16]; + this.columnTops = new int[256]; + Arrays.fill(columnTops, -1); } @Override @@ -56,7 +72,16 @@ public class ChunkDataHunkHolder extends AtomicHunk implements if (t == null) { return; } - super.setRaw(x, y, z, t); + data[index(x, y, z)] = t; + int column = (z << 4) | x; + int top = (int) COLUMN_TOPS.getOpaque(columnTops, column); + while (y > top) { + int witness = (int) COLUMN_TOPS.compareAndExchange(columnTops, column, top, y); + if (witness == top) { + break; + } + top = witness; + } } @Override @@ -65,13 +90,23 @@ public class ChunkDataHunkHolder extends AtomicHunk implements return AIR.get(); } - NativeBlockState b = super.getRaw(x, y, z); + NativeBlockState b = data[index(x, y, z)]; return b != null ? b : AIR.get(); } + @Override public NativeBlockState getStoredRaw(int x, int y, int z) { - return super.getRaw(x, y, z); + return data[index(x, y, z)]; + } + + @Override + public int highestStoredY(int x, int z) { + return (int) COLUMN_TOPS.getOpaque(columnTops, (z << 4) | x); + } + + private int index(int x, int y, int z) { + return (z * zStride) + (y << 4) + x; } public void apply() { @@ -90,7 +125,7 @@ public class ChunkDataHunkHolder extends AtomicHunk implements int runStart = -1; for (int y = 0; y < height; y++) { - NativeBlockState state = super.getRaw(x, y, z); + NativeBlockState state = getStoredRaw(x, y, z); BlockData block = state == null ? null : (BlockData) state.nativeHandle(); // Custom wrappers are not real Bukkit data; write the vanilla base like the // NMS fast path (NMSBinding.applyChunkDataBlocks) does. diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnBiomeHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnBiomeHunk.java new file mode 100644 index 000000000..8872b4c07 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnBiomeHunk.java @@ -0,0 +1,112 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 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.generation.chunk; + +import art.arcane.iris.platform.bukkit.BukkitBiome; +import art.arcane.volmlib.nativelib.terrain.NativeBiome; +import art.arcane.volmlib.util.hunk.HunkMutationSupport; +import art.arcane.volmlib.util.hunk.storage.StorageHunk; +import org.bukkit.block.Biome; + +/** + * Biome output for one Bukkit chunk, read back only by the generation stages. The biome actuator writes whole + * columns, so storage stays at one entry per column until a partial column write arrives; that write switches + * the hunk to full resolution. Reads, clamping and the plains fallback match {@link LinkedTerrainChunk}. + */ +public final class ColumnBiomeHunk extends StorageHunk { + private static final int CHUNK_SIZE = 16; + + private final NativeBiome[] columns = new NativeBiome[CHUNK_SIZE * CHUNK_SIZE]; + private NativeBiome[] cells; + private volatile NativeBiome defaultBiome; + + public ColumnBiomeHunk(int height) { + super(CHUNK_SIZE, height, CHUNK_SIZE); + } + + @Override + public void set(int x1, int y1, int z1, int x2, int y2, int z2, NativeBiome biome) { + if (x1 == x2 && z1 == z2 && y1 == 0 && y2 == getHeight() - 1) { + fillColumn(x1, z1, LinkedTerrainChunk.canonicalBiome(biome)); + return; + } + + HunkMutationSupport.setRangeInclusive(this, x1, y1, z1, x2, y2, z2, biome); + } + + @Override + public void setRaw(int x, int y, int z, NativeBiome biome) { + NativeBiome[] storage = cells == null ? expand() : cells; + storage[cellIndex(x, y, z)] = LinkedTerrainChunk.canonicalBiome(biome); + } + + @Override + public NativeBiome getRaw(int x, int y, int z) { + NativeBiome[] storage = cells; + NativeBiome biome = storage == null ? columns[columnIndex(x, z)] : storage[cellIndex(x, y, z)]; + if (biome != null) { + return biome; + } + NativeBiome fallback = defaultBiome; + if (fallback == null) { + fallback = BukkitBiome.of(Biome.PLAINS); + defaultBiome = fallback; + } + return fallback; + } + + private void fillColumn(int x, int z, NativeBiome biome) { + NativeBiome[] storage = cells; + if (storage == null) { + columns[columnIndex(x, z)] = biome; + return; + } + int stride = CHUNK_SIZE * CHUNK_SIZE; + int index = columnIndex(x, z); + for (int y = 0; y < getHeight(); y++) { + storage[index] = biome; + index += stride; + } + } + + private NativeBiome[] expand() { + NativeBiome[] storage = new NativeBiome[CHUNK_SIZE * getHeight() * CHUNK_SIZE]; + int stride = CHUNK_SIZE * CHUNK_SIZE; + for (int column = 0; column < stride; column++) { + NativeBiome biome = columns[column]; + if (biome == null) { + continue; + } + for (int index = column; index < storage.length; index += stride) { + storage[index] = biome; + } + } + cells = storage; + return storage; + } + + private static int columnIndex(int x, int z) { + return ((z & (CHUNK_SIZE - 1)) * CHUNK_SIZE) + (x & (CHUNK_SIZE - 1)); + } + + private int cellIndex(int x, int y, int z) { + int clampedY = Math.max(0, Math.min(getHeight() - 1, y)); + return (clampedY * CHUNK_SIZE * CHUNK_SIZE) + columnIndex(x, z); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtent.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtent.java new file mode 100644 index 000000000..8629c1ef8 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtent.java @@ -0,0 +1,33 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 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.generation.chunk; + +import art.arcane.volmlib.util.hunk.Hunk; + +/** + * A block volume that knows, per column, the highest y it holds a value for. Every cell above that y reads as + * unwritten, so column scans can stop there without changing what they observe. + */ +public interface ColumnExtent { + int highestStoredY(int x, int z); + + static int highestStoredY(Hunk hunk, int x, int z) { + return hunk instanceof ColumnExtent extent ? extent.highestStoredY(x, z) : hunk.getHeight() - 1; + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java new file mode 100644 index 000000000..bdbf33af2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/chunk/ColumnExtentListeningHunk.java @@ -0,0 +1,37 @@ +/* + * Iris is a World Generator for Minecraft Bukkit Servers + * Copyright (c) 2026 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.generation.chunk; + +import art.arcane.volmlib.util.function.Consumer4; +import art.arcane.volmlib.util.hunk.Hunk; +import art.arcane.volmlib.util.hunk.view.ListeningHunk; + +public final class ColumnExtentListeningHunk extends ListeningHunk implements ColumnExtent { + private final Hunk source; + + public ColumnExtentListeningHunk(Hunk source, Consumer4 listener) { + super(source, listener); + this.source = source; + } + + @Override + public int highestStoredY(int x, int z) { + return ColumnExtent.highestStoredY(source, x, z); + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/chunk/LinkedTerrainChunk.java b/core/src/main/java/art/arcane/iris/generation/chunk/LinkedTerrainChunk.java index 8451bdcfe..8b6d37c41 100644 --- a/core/src/main/java/art/arcane/iris/generation/chunk/LinkedTerrainChunk.java +++ b/core/src/main/java/art/arcane/iris/generation/chunk/LinkedTerrainChunk.java @@ -130,7 +130,7 @@ public class LinkedTerrainChunk implements TerrainChunk { return (clampedY * CHUNK_SIZE + clampedZ) * CHUNK_SIZE + clampedX; } - private static NativeBiome canonicalBiome(NativeBiome biome) { + static NativeBiome canonicalBiome(NativeBiome biome) { if (biome instanceof BukkitBiome) { return biome; } diff --git a/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java b/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java index b3e7509f4..8135a3c9c 100644 --- a/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java +++ b/core/src/main/java/art/arcane/iris/generation/context/ChunkContext.java @@ -230,6 +230,10 @@ public class ChunkContext { return region; } + public boolean hasDimensionStack() { + return dimensionStackContext != null; + } + public DimensionStackLayout getDimensionStackLayout(int localX, int localZ) { if (dimensionStackContext == null) { return null; diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineHotloader.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineHotloader.java index e7f1556d8..b8202333e 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/EngineHotloader.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineHotloader.java @@ -81,7 +81,7 @@ final class EngineHotloader { previousGeneration.transitionPlan()); PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture( assembly.target, assembly.runtimeKernel, assembly.transitionPlan, engine.isStudio()); - engine.runtimeAssembly.set(assembly); + engine.threadState.setAssembly(assembly); EngineRuntime next; try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { assembly.complex = assembly.runtimeKernel.createComplex(engine, assembly.transitionPlan, false); @@ -97,7 +97,7 @@ final class EngineHotloader { biomeMaxes); next = previous.withGeneration(nextGeneration); } finally { - engine.runtimeAssembly.remove(); + engine.threadState.setAssembly(null); } Throwable retirementFailure = runCleanup(null, previousGeneration.complex()::close); if (retirementFailure != null) { diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineMode.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineMode.java index 5e3d27b61..5c5c9bda4 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/EngineMode.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineMode.java @@ -23,7 +23,7 @@ import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; import art.arcane.iris.generation.mantle.EngineMantle; -import art.arcane.iris.generation.stage.IrisBiomeActuator; +import art.arcane.iris.generation.stage.IrisDimensionStackActuator; import art.arcane.iris.generation.stage.IrisTransitionGeometryActuator; import art.arcane.iris.integration.Identifier; import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial; @@ -215,7 +215,7 @@ public interface EngineMode extends Staged { try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), generationSessionId, ctx)) { generateTerrain(x, z, blocks, biomes, multicore, ctx); recordNaturalTerrain(x, z, blocks, biomes, ctx); - IrisBiomeActuator.publishNaturalMetadata(getEngine(), x, z, biomes, ctx); + IrisDimensionStackActuator.clearHostNaturalMetadata(getEngine(), x, z, biomes.getHeight(), ctx); ctx.beginContent(); List continuations = restoreObjectContinuations(x, z); for (EngineStage i : stages) { diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineRuntimeBuilder.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineRuntimeBuilder.java index 71ec16139..79fe292cd 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/EngineRuntimeBuilder.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineRuntimeBuilder.java @@ -93,7 +93,7 @@ final class EngineRuntimeBuilder { mantleStorageDirectory, selectRuntimeKernel(kernelVersion), transitionPlan); - engine.runtimeAssembly.set(assembly); + engine.threadState.setAssembly(assembly); try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { IrisLogging.debug("Setup Engine " + assembly.cacheId); long started = M.ms(); @@ -148,7 +148,7 @@ final class EngineRuntimeBuilder { } throw new IllegalStateException("Failed to build a complete Iris engine runtime.", e); } finally { - engine.runtimeAssembly.remove(); + engine.threadState.setAssembly(null); } } @@ -174,7 +174,7 @@ final class EngineRuntimeBuilder { transitionPlan); PreparedHydrologyCacheIdentity cacheIdentity = PreparedHydrologyCacheIdentity.capture( runtimeTarget, assembly.runtimeKernel, transitionPlan, engine.isStudio()); - engine.runtimeAssembly.set(assembly); + engine.threadState.setAssembly(assembly); try (IrisContext.Scope ignored = IrisContext.open(engine, engine.getGenerationSessions().currentSessionId(), null)) { IrisLogging.debug("Setup Detached Generation Runtime " + assembly.cacheId); assembly.mantle = assembly.runtimeKernel.createMantle(engine, assembly.mantleStorageDirectory); @@ -201,7 +201,7 @@ final class EngineRuntimeBuilder { } throw new IllegalStateException("Failed to build a detached Iris generation runtime.", e); } finally { - engine.runtimeAssembly.remove(); + engine.threadState.setAssembly(null); } } diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineShutdownSequence.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineShutdownSequence.java index 76300e75d..b995fde29 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/EngineShutdownSequence.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineShutdownSequence.java @@ -264,20 +264,16 @@ final class EngineShutdownSequence { return failure; } incompleteAssemblies.add(assembly); - RuntimeAssembly previous = engine == null ? null : engine.runtimeAssembly.get(); + RuntimeAssembly previous = engine == null ? null : engine.threadState.assembly(); if (engine != null) { - engine.runtimeAssembly.set(assembly); + engine.threadState.setAssembly(assembly); } Throwable assemblyFailure; try { assemblyFailure = closeAssemblyResources(assembly); } finally { if (engine != null) { - if (previous == null) { - engine.runtimeAssembly.remove(); - } else { - engine.runtimeAssembly.set(previous); - } + engine.threadState.setAssembly(previous); } } if (assemblyFailure == null) { @@ -401,7 +397,7 @@ final class EngineShutdownSequence { engine, generationRuntime); Throwable runtimeFailure; - try (IrisEngine.GenerationRuntimeScope ignored = engine.generationRuntimeScopes.open(binding)) { + try (IrisEngine.GenerationRuntimeScope ignored = engine.threadState.open(binding)) { runtimeFailure = closeGenerationRuntime(generationRuntime, null); } catch (Throwable scopeFailure) { runtimeFailure = scopeFailure; diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/EngineThreadState.java b/core/src/main/java/art/arcane/iris/generation/runtime/EngineThreadState.java new file mode 100644 index 000000000..c9597b27f --- /dev/null +++ b/core/src/main/java/art/arcane/iris/generation/runtime/EngineThreadState.java @@ -0,0 +1,162 @@ +/* + * 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.generation.runtime; + +import art.arcane.iris.generation.runtime.EngineRuntimeBuilder.RuntimeAssembly; +import art.arcane.iris.generation.runtime.IrisEngine.BiomeEnvironmentBinding; +import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeBinding; +import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeScope; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReferenceArray; + +/** + * Every per-thread engine binding (runtime assembly, biome environment, generation runtime scope) in one frame. + * The engine getters run on generation hot paths, where a ThreadLocal lookup probes long collision chains in the + * workers' crowded thread-local maps. Frames therefore live in a table indexed by thread id and owned by their + * thread; a frame whose slot is taken by another thread falls back to a ThreadLocal. + */ +final class EngineThreadState { + private static final int SLOTS = 256; + + private final AtomicReferenceArray slots = new AtomicReferenceArray<>(SLOTS); + private final ThreadLocal overflow = new ThreadLocal<>(); + private final AtomicInteger overflowFrames = new AtomicInteger(); + + Frame current() { + Thread thread = Thread.currentThread(); + Frame frame = slots.getPlain(slot(thread)); + if (frame != null && frame.owner == thread) { + return frame; + } + return overflowFrames.get() == 0 ? null : overflow.get(); + } + + GenerationRuntimeBinding binding() { + Frame frame = current(); + return frame == null ? null : frame.binding; + } + + RuntimeAssembly assembly() { + Frame frame = current(); + return frame == null ? null : frame.assembly; + } + + void setAssembly(RuntimeAssembly assembly) { + if (assembly != null) { + frame().assembly = assembly; + return; + } + Frame frame = current(); + if (frame != null) { + frame.assembly = null; + releaseIfEmpty(frame); + } + } + + BiomeEnvironmentBinding environment() { + Frame frame = current(); + return frame == null ? null : frame.environment; + } + + void setEnvironment(BiomeEnvironmentBinding environment) { + if (environment != null) { + frame().environment = environment; + return; + } + Frame frame = current(); + if (frame != null) { + frame.environment = null; + releaseIfEmpty(frame); + } + } + + GenerationRuntimeScope open(GenerationRuntimeBinding binding) { + Thread owner = Thread.currentThread(); + Frame frame = frame(); + GenerationRuntimeBinding previous = frame.binding; + frame.binding = binding; + return new GenerationRuntimeScope(this, owner, previous, binding); + } + + void close( + Thread owner, + GenerationRuntimeBinding previous, + GenerationRuntimeBinding installed + ) { + if (Thread.currentThread() != owner) { + throw new IllegalStateException("Iris generation runtime scope closed from a different thread."); + } + Frame frame = current(); + if ((frame == null ? null : frame.binding) != installed) { + throw new IllegalStateException("Iris generation runtime scopes must close in LIFO order."); + } + if (previous != null) { + frame().binding = previous; + return; + } + if (frame != null) { + frame.binding = null; + releaseIfEmpty(frame); + } + } + + private Frame frame() { + Frame frame = current(); + if (frame != null) { + return frame; + } + Thread thread = Thread.currentThread(); + frame = new Frame(thread); + if (!slots.compareAndSet(slot(thread), null, frame)) { + frame.overflow = true; + overflow.set(frame); + overflowFrames.incrementAndGet(); + } + return frame; + } + + private void releaseIfEmpty(Frame frame) { + if (frame.assembly != null || frame.environment != null || frame.binding != null) { + return; + } + if (frame.overflow) { + overflow.remove(); + overflowFrames.decrementAndGet(); + return; + } + slots.compareAndSet(slot(frame.owner), frame, null); + } + + private static int slot(Thread thread) { + return (int) thread.threadId() & (SLOTS - 1); + } + + static final class Frame { + private final Thread owner; + private boolean overflow; + RuntimeAssembly assembly; + BiomeEnvironmentBinding environment; + GenerationRuntimeBinding binding; + + private Frame(Thread owner) { + this.owner = owner; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/GenerationRuntimeScopeState.java b/core/src/main/java/art/arcane/iris/generation/runtime/GenerationRuntimeScopeState.java deleted file mode 100644 index 38c8efc4d..000000000 --- a/core/src/main/java/art/arcane/iris/generation/runtime/GenerationRuntimeScopeState.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.generation.runtime; - -import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeBinding; -import art.arcane.iris.generation.runtime.IrisEngine.GenerationRuntimeScope; - -final class GenerationRuntimeScopeState { - private final ThreadLocal current = new ThreadLocal<>(); - - GenerationRuntimeBinding current() { - return current.get(); - } - - GenerationRuntimeScope open(GenerationRuntimeBinding binding) { - Thread owner = Thread.currentThread(); - GenerationRuntimeBinding previous = current.get(); - current.set(binding); - return new GenerationRuntimeScope(this, owner, previous, binding); - } - - void close( - Thread owner, - GenerationRuntimeBinding previous, - GenerationRuntimeBinding installed - ) { - if (Thread.currentThread() != owner) { - throw new IllegalStateException("Iris generation runtime scope closed from a different thread."); - } - if (current.get() != installed) { - throw new IllegalStateException("Iris generation runtime scopes must close in LIFO order."); - } - if (previous == null) { - current.remove(); - return; - } - current.set(previous); - } -} diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index bdce1e93b..70cdd6b7c 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -48,6 +48,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.atomics.AtomicRollingSequence; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; +import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk; import art.arcane.volmlib.util.documentation.BlockCoordinates; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.hunk.Hunk; @@ -116,13 +117,7 @@ public class IrisEngine implements Engine { final Object generationHistoryRuntimeRouterLock = new Object(); @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) - final ThreadLocal runtimeAssembly = new ThreadLocal<>(); - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - final ThreadLocal biomeEnvironmentScopes = new ThreadLocal<>(); - @Getter(AccessLevel.NONE) - @Setter(AccessLevel.NONE) - final GenerationRuntimeScopeState generationRuntimeScopes = new GenerationRuntimeScopeState(); + final EngineThreadState threadState = new EngineThreadState(); @Getter(AccessLevel.NONE) @Setter(AccessLevel.NONE) final Set detachedGenerationRuntimes = Collections.synchronizedSet( @@ -382,7 +377,7 @@ public class IrisEngine implements Engine { throw new IllegalStateException("World entry hydrology preparation could not acquire its generation session.", failure); } try (lease; - GenerationRuntimeScope runtimeScope = generationRuntimeScopes.open(binding); + GenerationRuntimeScope runtimeScope = threadState.open(binding); IrisContext.Scope context = IrisContext.open(this, lease.sessionId(), null)) { getComplex().getHydrologyRuntime().prepareChunkColumns(blockX, blockZ); } catch (Exception failure) { @@ -572,8 +567,8 @@ public class IrisEngine implements Engine { @Override public BiomeEnvironment.Scope openBiomeEnvironmentScope(BiomeEnvironment environment) { BiomeEnvironmentBinding binding = new BiomeEnvironmentBinding( - Objects.requireNonNull(environment, "environment"), biomeEnvironmentScopes.get()); - biomeEnvironmentScopes.set(binding); + Objects.requireNonNull(environment, "environment"), threadState.environment()); + threadState.setEnvironment(binding); return binding; } @@ -979,7 +974,7 @@ public class IrisEngine implements Engine { } public boolean hasGenerationRuntimeScope() { - return generationRuntimeScopes.current() != null; + return threadState.binding() != null; } private boolean usesScopedNaturalTerrain() { @@ -1058,7 +1053,7 @@ public class IrisEngine implements Engine { } public GenerationRuntimeBinding captureGenerationRuntimeBinding() { - GenerationRuntimeBinding scoped = generationRuntimeScopes.current(); + GenerationRuntimeBinding scoped = threadState.binding(); return scoped == null ? getActiveGenerationRuntimeBinding() : scoped; } @@ -1074,7 +1069,7 @@ public class IrisEngine implements Engine { if (!isGenerationRuntimeBindingLive(required)) { throw new IllegalStateException("Iris generation runtime binding is closed or no longer owned."); } - return generationRuntimeScopes.open(required); + return threadState.open(required); } } @@ -1265,75 +1260,85 @@ public class IrisEngine implements Engine { @Override public SeedManager getSeedManager() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.seedManager; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? seedManager : current.seedManager(); } @Override public IrisComplex getComplex() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null && assembly.complex != null) { return assembly.complex; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? null : current.complex(); } @Override public EngineTarget getTarget() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.target; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? publishedTarget : current.target(); } @Override public IrisData getData() { - BiomeEnvironmentBinding environment = biomeEnvironmentScopes.get(); - if (environment != null && !hasGenerationRuntimeScope()) { - return environment.environment.data(); + EngineThreadState.Frame frame = threadState.current(); + if (frame != null) { + BiomeEnvironmentBinding environment = frame.environment; + if (environment != null && frame.binding == null) { + return environment.environment.data(); + } + RuntimeAssembly assembly = frame.assembly; + if (assembly != null) { + return assembly.target.getData(); + } } - RuntimeAssembly assembly = runtimeAssembly.get(); - if (assembly != null) { - return assembly.target.getData(); - } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? publishedTarget.getData() : current.data(); } @Override public IrisDimension getDimension() { - BiomeEnvironmentBinding environment = biomeEnvironmentScopes.get(); - if (environment != null && !hasGenerationRuntimeScope()) { - return environment.environment.dimension(); + EngineThreadState.Frame frame = threadState.current(); + if (frame != null) { + BiomeEnvironmentBinding environment = frame.environment; + if (environment != null && frame.binding == null) { + return environment.environment.dimension(); + } + RuntimeAssembly assembly = frame.assembly; + if (assembly != null) { + return assembly.target.getDimension(); + } } - RuntimeAssembly assembly = runtimeAssembly.get(); - if (assembly != null) { - return assembly.target.getDimension(); - } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? publishedTarget.getDimension() : current.dimension(); } @Override public EngineMantle getMantle() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null && assembly.mantle != null) { return assembly.mantle; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? null : current.mantle(); } @Override public EngineMode getMode() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null && assembly.mode != null) { return assembly.mode; } @@ -1343,7 +1348,7 @@ public class IrisEngine implements Engine { @Override public EngineEffects getEffects() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null && assembly.effects != null) { return assembly.effects; } @@ -1353,7 +1358,7 @@ public class IrisEngine implements Engine { @Override public EngineWorldManager getWorldManager() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null && assembly.worldManager != null) { return assembly.worldManager; } @@ -1363,27 +1368,29 @@ public class IrisEngine implements Engine { @Override public UpperDimensionContext getUpperContext() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.upperContext; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? null : current.upperContext(); } @Override public DimensionStackContext getDimensionStackContext() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.dimensionStackContext; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? null : current.dimensionStackContext(); } @Override public CompletableFuture getHash32() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null && assembly.hash32 != null) { return assembly.hash32; } @@ -1465,7 +1472,7 @@ public class IrisEngine implements Engine { IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) { getEngineData().getStatistics().generatedChunk(); PrecisionStopwatch p = PrecisionStopwatch.start(); - Hunk blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t)); + Hunk blocks = new ColumnExtentListeningHunk<>(vblocks, (xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t)); if (getDimension().isDebugChunkCrossSections() && ((x >> 4) % getDimension().getDebugCrossSectionsMod() == 0 || (z >> 4) % getDimension().getDebugCrossSectionsMod() == 0)) { NativeBlockState crossSection = B.getState("CRYING_OBSIDIAN"); @@ -1593,16 +1600,21 @@ public class IrisEngine implements Engine { @Override public int getCacheID() { - RuntimeAssembly assembly = runtimeAssembly.get(); + EngineThreadState.Frame frame = threadState.current(); + RuntimeAssembly assembly = frame == null ? null : frame.assembly; if (assembly != null) { return assembly.cacheId; } - GenerationRuntime current = selectedGenerationRuntime(); + GenerationRuntime current = selectedGenerationRuntime(frame); return current == null ? -1 : current.cacheId(); } private GenerationRuntime selectedGenerationRuntime() { - GenerationRuntimeBinding scoped = generationRuntimeScopes.current(); + return selectedGenerationRuntime(threadState.current()); + } + + private GenerationRuntime selectedGenerationRuntime(EngineThreadState.Frame frame) { + GenerationRuntimeBinding scoped = frame == null ? null : frame.binding; if (scoped != null && isGenerationRuntimeBindingLive(scoped)) { return scoped.runtime; } @@ -1611,7 +1623,7 @@ public class IrisEngine implements Engine { } public GenerationKernelRegistry.Version getGenerationKernelVersion() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null) { return assembly.kernelVersion; } @@ -1619,7 +1631,7 @@ public class IrisEngine implements Engine { } public GenerationKernelRegistry.RuntimeKernel getGenerationRuntimeKernel() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null) { return assembly.runtimeKernel; } @@ -1627,7 +1639,7 @@ public class IrisEngine implements Engine { } public TransitionGenerationPlan getTransitionGenerationPlan() { - RuntimeAssembly assembly = runtimeAssembly.get(); + RuntimeAssembly assembly = threadState.assembly(); if (assembly != null) { return assembly.transitionPlan; } @@ -1751,14 +1763,14 @@ public class IrisEngine implements Engine { } public static final class GenerationRuntimeScope implements AutoCloseable { - private final GenerationRuntimeScopeState state; + private final EngineThreadState state; private final Thread owner; private final GenerationRuntimeBinding previous; private final GenerationRuntimeBinding installed; private boolean closed; GenerationRuntimeScope( - GenerationRuntimeScopeState state, + EngineThreadState state, Thread owner, GenerationRuntimeBinding previous, GenerationRuntimeBinding installed @@ -1780,7 +1792,7 @@ public class IrisEngine implements Engine { } - private final class BiomeEnvironmentBinding implements BiomeEnvironment.Scope { + final class BiomeEnvironmentBinding implements BiomeEnvironment.Scope { private final BiomeEnvironment environment; private final BiomeEnvironmentBinding previous; private final Thread owner = Thread.currentThread(); @@ -1799,14 +1811,10 @@ public class IrisEngine implements Engine { if (closed) { return; } - if (biomeEnvironmentScopes.get() != this) { + if (threadState.environment() != this) { throw new IllegalStateException("Biome environment scopes must close in reverse order."); } - if (previous == null) { - biomeEnvironmentScopes.remove(); - } else { - biomeEnvironmentScopes.set(previous); - } + threadState.setEnvironment(previous); closed = true; } } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisBiomeActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisBiomeActuator.java index 351a35ff8..0410ac36d 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisBiomeActuator.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisBiomeActuator.java @@ -27,7 +27,6 @@ import art.arcane.iris.world.history.TransitionGenerationPlan; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiomeCustom; import art.arcane.iris.generation.terrain.IrisDimension; -import art.arcane.iris.spi.IrisPlatform; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.iris.generation.context.ChunkContext; @@ -35,12 +34,7 @@ import art.arcane.iris.generation.context.ChunkedDataCache; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.documentation.BlockCoordinates; -import art.arcane.volmlib.util.mantle.runtime.Mantle; -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.MatterBiomeInject; -import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.util.List; @@ -48,8 +42,8 @@ import java.util.Objects; public class IrisBiomeActuator extends EngineAssignedActuator { private final RNG rng; - private final KMap resolvedBiomes = new KMap<>(); - private final KMap resolvedPhysicalBiomes = new KMap<>(); + private final KMap resolvedBiomes = new KMap<>(); + private final KMap resolvedPhysicalBiomes = new KMap<>(); public IrisBiomeActuator(Engine engine) { super(engine, "Biome"); @@ -64,7 +58,6 @@ public class IrisBiomeActuator extends EngineAssignedActuator { int depth = h.getDepth(); int height = h.getHeight(); Engine engine = getEngine(); - Mantle mantle = engine.getMantle().getMantle(); ChunkedDataCache biomeCache = context.getBiome(); IrisComplex complex = context.getComplex(); DimensionStackContext dimensionStackContext = engine.getDimensionStackContext(); @@ -74,7 +67,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator { int worldX = x + xf; int worldZ = z + zf; IrisBiome biome = biomeCache.get(xf, zf); - ResolvedBiome resolved = resolve(biomeKey( + NativeBiome platformBiome = resolve(biomeKey( biome, getDimension(), engine, @@ -82,8 +75,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator { 0, worldZ )); - NativeBiome platformBiome = resolved.biome(); - writeColumn(h, mantle, xf, zf, worldX, worldZ, height, resolved); + if (platformBiome != null) { + h.set(xf, 0, zf, xf, height - 1, zf, platformBiome); + } if (dimensionStackContext != null) { NativeBiome bottomBiome = platformBiome == null @@ -95,48 +89,19 @@ public class IrisBiomeActuator extends EngineAssignedActuator { worldX, worldZ, h, - mantle, engine, - new ResolvedBiome(bottomBiome, resolved.matter()), + bottomBiome, context.getDimensionStackLayout(xf, zf) ); } - writeHistoricalColumn(h, mantle, xf, zf, worldX, worldZ, height, engine, complex); + writeHistoricalColumn(h, xf, zf, worldX, worldZ, height, engine, complex); } } engine.getMetrics().getBiome().put(p.getMilliseconds()); } - public static void publishNaturalMetadata(Engine engine, int x, int z, Hunk biomes, - ChunkContext context) { - if (!context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) { - return; - } - Mantle mantle = engine.getMantle().getMantle(); - MantleChunk chunk = mantle.getChunk(x >> 4, z >> 4).use(); - try { - synchronized (chunk) { - IrisDimensionStackActuator.clearNaturalMetadata(chunk, context, biomes.getHeight()); - chunk.deleteSlices(MatterBiomeInject.class); - for (int localX = 0; localX < biomes.getWidth(); localX += 4) { - for (int localZ = 0; localZ < biomes.getDepth(); localZ += 4) { - for (int y = 0; y < biomes.getHeight(); y += 4) { - NativeBiome biome = biomes.getRaw(localX, y, localZ); - if (biome != null) { - mantle.set(x + localX, y, z + localZ, BiomeInjectMatter.get(biome.key())); - } - } - } - } - } - } finally { - chunk.release(); - } - } - private void writeHistoricalColumn( Hunk output, - Mantle mantle, int localX, int localZ, int worldX, @@ -167,12 +132,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator { applyBiomeRange( localX, localZ, - worldX, - worldZ, rangeStart, internalY - 1, output, - mantle, resolvePhysicalKey(activeKey) ); } @@ -181,33 +143,14 @@ public class IrisBiomeActuator extends EngineAssignedActuator { } } - private void writeColumn( - Hunk output, - Mantle mantle, - int localX, - int localZ, - int worldX, - int worldZ, - int height, - ResolvedBiome resolved - ) { - if (resolved.biome() != null) { - output.set(localX, 0, localZ, localX, height - 1, localZ, resolved.biome()); - } - if (mantle != null) { - mantle.set(worldX, 0, worldZ, resolved.matter()); - } - } - private void applyDimensionStackBiomes( int localX, int localZ, int worldX, int worldZ, Hunk output, - Mantle mantle, Engine engine, - ResolvedBiome bottom, + NativeBiome bottom, DimensionStackLayout layout ) { List layers = layout.layersBottomToTop(); @@ -222,18 +165,15 @@ public class IrisBiomeActuator extends EngineAssignedActuator { applyBiomeRange( localX, localZ, - worldX, - worldZ, gapMinY, gapMaxY, output, - mantle, bottom ); if (!layer.visible()) { continue; } - ResolvedBiome resolved = bottom; + NativeBiome resolved = bottom; if (layer.biome() != null) { IrisDimension dimension = layer.terrainContext().getDimension(); resolved = resolve(biomeKey( @@ -248,12 +188,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator { applyBiomeRange( localX, localZ, - worldX, - worldZ, layer.renderMinY(), layer.renderMaxY(), output, - mantle, resolved ); } @@ -262,46 +199,15 @@ public class IrisBiomeActuator extends EngineAssignedActuator { private void applyBiomeRange( int localX, int localZ, - int worldX, - int worldZ, int minimumY, int maximumY, Hunk output, - Mantle mantle, - ResolvedBiome resolved + NativeBiome biome ) { - if (minimumY > maximumY) { + if (minimumY > maximumY || biome == null) { return; } - if (mantle != null) { - clearBiomeMatterRange(mantle, worldX, worldZ, minimumY, maximumY); - } - if (resolved.biome() != null) { - output.set( - localX, - minimumY, - localZ, - localX, - maximumY, - localZ, - resolved.biome() - ); - } - if (mantle != null && resolved.matter() != null) { - mantle.set(worldX, minimumY, worldZ, resolved.matter()); - } - } - - static void clearBiomeMatterRange( - Mantle mantle, - int worldX, - int worldZ, - int minimumY, - int maximumY - ) { - for (int y = minimumY; y <= maximumY; y++) { - mantle.remove(worldX, y, worldZ, MatterBiomeInject.class); - } + output.set(localX, minimumY, localZ, localX, maximumY, localZ, biome); } private String biomeKey( @@ -319,37 +225,28 @@ public class IrisBiomeActuator extends EngineAssignedActuator { return biome.getSkyBiomeKey(rng, engine, x, y, z); } - private ResolvedBiome resolve(String key) { - ResolvedBiome cached = key == null ? null : resolvedBiomes.get(key); + private NativeBiome resolve(String key) { + NativeBiome cached = key == null ? null : resolvedBiomes.get(key); if (cached != null) { return cached; } - IrisPlatform platform = IrisPlatforms.get(); - NativeBiome biome = platform.registries().biome(key); - ResolvedBiome resolved = new ResolvedBiome( - biome, - BiomeInjectMatter.get(platform.biomeWriter().biomeIdFor(key)) - ); + NativeBiome biome = IrisPlatforms.get().registries().biome(key); if (key != null && biome != null) { - resolvedBiomes.put(key, resolved); + resolvedBiomes.put(key, biome); } - return resolved; + return biome; } - private ResolvedBiome resolvePhysicalKey(String key) { - ResolvedBiome cached = resolvedPhysicalBiomes.get(key); + private NativeBiome resolvePhysicalKey(String key) { + NativeBiome cached = resolvedPhysicalBiomes.get(key); if (cached != null) { return cached; } NativeBiome biome = IrisPlatforms.get().registries().biome(key); - ResolvedBiome resolved = new ResolvedBiome(biome, BiomeInjectMatter.get(key)); if (biome != null) { - resolvedPhysicalBiomes.put(key, resolved); + resolvedPhysicalBiomes.put(key, biome); } - return resolved; - } - - private record ResolvedBiome(NativeBiome biome, MatterBiomeInject matter) { + return biome; } } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java index fe845b704..64bf27b3e 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisCarveModifier.java @@ -40,6 +40,7 @@ import art.arcane.iris.generation.decoration.IrisProceduralBlocks; import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveAction; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; +import art.arcane.iris.generation.chunk.ColumnExtent; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; import art.arcane.iris.generation.block.B; @@ -896,12 +897,17 @@ public class IrisCarveModifier extends EngineAssignedModifier int width = output.getWidth(); int depth = output.getDepth(); int height = output.getHeight(); + ColumnExtent extent = output instanceof ColumnExtent columnExtent ? columnExtent : null; for (int localX = 0; localX < width; localX++) { for (int localZ = 0; localZ < depth; localZ++) { int worldX = blockX + localX; int worldZ = blockZ + localZ; int floor = -1; + int top = extent == null ? height - 1 : extent.highestStoredY(localX, localZ); for (int y = 1; y < height; y++) { + if (y > top) { + break; + } NativeBlockState state = output.getRaw(localX, y, localZ); if (B.isSolid(state)) { if (floor >= 0) { @@ -922,6 +928,9 @@ public class IrisCarveModifier extends EngineAssignedModifier worldX, worldZ, resolver, caveBiomes, customBiomes); decorateZone(output, zone, localX, localZ, worldX, worldZ, floorBiome, ceilingBiome); + if (extent != null) { + top = extent.highestStoredY(localX, localZ); + } } floor = -1; } diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisDimensionStackActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisDimensionStackActuator.java index 2df4baf8b..041a2af5d 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisDimensionStackActuator.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisDimensionStackActuator.java @@ -106,7 +106,21 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator chunk, ChunkContext context, int height) { + public static void clearHostNaturalMetadata(Engine engine, int x, int z, int height, ChunkContext context) { + if (!context.hasDimensionStack() || !context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) { + return; + } + MantleChunk chunk = engine.getMantle().getMantle().getChunk(x >> 4, z >> 4).use(); + try { + synchronized (chunk) { + clearNaturalMetadata(chunk, context, height); + } + } finally { + chunk.release(); + } + } + + static void clearNaturalMetadata(MantleChunk chunk, ChunkContext context, int height) { MetadataCleaner metadata = new MetadataCleaner(chunk, height); for (int x = 0; x < 16; x++) { for (int z = 0; z < 16; z++) { diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisFloatingChildBiomeModifier.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisFloatingChildBiomeModifier.java index 1d43804fc..de24fa14d 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisFloatingChildBiomeModifier.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisFloatingChildBiomeModifier.java @@ -44,7 +44,6 @@ import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.noise.CNG; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; -import art.arcane.volmlib.util.matter.slices.BiomeInjectMatter; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; @@ -431,7 +430,6 @@ public class IrisFloatingChildBiomeModifier extends EngineAssignedModifier output, int x, int z) { - for (int i = output.getHeight() - 1; i >= 0; i--) { + for (int i = Math.min(output.getHeight() - 1, ColumnExtent.highestStoredY(output, x, z)); i >= 0; i--) { NativeBlockState b = output.get(x, i, z); if (b != null) { diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java index 9ccbda4f5..67f0f0237 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/BukkitBlockState.java @@ -57,29 +57,32 @@ public final class BukkitBlockState implements NativeBlockState { // the canonical NMS state): a hit skips getAsString(), which built the full property // string on EVERY of() call — the dominant cost of the 99.9% hit case. private static final ConcurrentHashMap DATA_CACHE = new ConcurrentHashMap<>(); + private static final byte UNKNOWN = 0; + private static final byte FALSE = 1; + private static final byte TRUE = 2; private final BlockData data; private final String key; private final String namespace; - private volatile String materialKey; - private volatile Boolean air; - private volatile Boolean solid; - private volatile Boolean occluding; - private volatile Boolean fluid; - private volatile Boolean water; - private volatile Boolean waterLogged; - private volatile Boolean lit; - private volatile Boolean updatable; - private volatile Boolean foliage; - private volatile Boolean treeBlock; - private volatile Boolean foliagePlantable; - private volatile Boolean decorant; - private volatile Boolean storage; - private volatile Boolean storageChest; - private volatile Boolean ore; - private volatile Boolean deepSlate; - private volatile Boolean vineBlock; - private volatile Boolean tileEntity; + private String materialKey; + private byte air; + private byte solid; + private byte occluding; + private byte fluid; + private byte water; + private byte waterLogged; + private byte lit; + private byte updatable; + private byte foliage; + private byte treeBlock; + private byte foliagePlantable; + private byte decorant; + private byte storage; + private byte storageChest; + private byte ore; + private byte deepSlate; + private byte vineBlock; + private byte tileEntity; private BukkitBlockState(BlockData data, String key) { this.data = data; @@ -87,6 +90,10 @@ public final class BukkitBlockState implements NativeBlockState { this.namespace = BlockStateKey.namespace(key); } + private static byte flag(boolean value) { + return value ? TRUE : FALSE; + } + public static BukkitBlockState of(BlockData data) { if (data instanceof IrisCustomData custom) { return new BukkitBlockState(data, custom.getCustom().toString()); @@ -198,32 +205,32 @@ public final class BukkitBlockState implements NativeBlockState { @Override public boolean isAir() { - Boolean cached = air; - if (cached == null) { - cached = BukkitBlockResolution.isAir(data); + byte cached = air; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isAir(data)); air = cached; } - return cached; + return cached == TRUE; } @Override public boolean isSolid() { - Boolean cached = solid; - if (cached == null) { - cached = BukkitBlockResolution.isSolid(data); + byte cached = solid; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isSolid(data)); solid = cached; } - return cached; + return cached == TRUE; } @Override public boolean isOccluding() { - Boolean cached = occluding; - if (cached == null) { - cached = data.getMaterial().isOccluding(); + byte cached = occluding; + if (cached == UNKNOWN) { + cached = flag(data.getMaterial().isOccluding()); occluding = cached; } - return cached; + return cached == TRUE; } @Override @@ -248,142 +255,142 @@ public final class BukkitBlockState implements NativeBlockState { @Override public boolean isFluid() { - Boolean cached = fluid; - if (cached == null) { - cached = BukkitBlockResolution.isFluid(data); + byte cached = fluid; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isFluid(data)); fluid = cached; } - return cached; + return cached == TRUE; } @Override public boolean isWater() { - Boolean cached = water; - if (cached == null) { - cached = BukkitBlockResolution.isWater(data); + byte cached = water; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isWater(data)); water = cached; } - return cached; + return cached == TRUE; } @Override public boolean isWaterLogged() { - Boolean cached = waterLogged; - if (cached == null) { - cached = BukkitBlockResolution.isWaterLogged(data); + byte cached = waterLogged; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isWaterLogged(data)); waterLogged = cached; } - return cached; + return cached == TRUE; } @Override public boolean isLit() { - Boolean cached = lit; - if (cached == null) { - cached = BukkitBlockResolution.isLit(data); + byte cached = lit; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isLit(data)); lit = cached; } - return cached; + return cached == TRUE; } @Override public boolean isUpdatable() { - Boolean cached = updatable; - if (cached == null) { - cached = BukkitBlockResolution.isUpdatable(data); + byte cached = updatable; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isUpdatable(data)); updatable = cached; } - return cached; + return cached == TRUE; } @Override public boolean isFoliage() { - Boolean cached = foliage; - if (cached == null) { - cached = BukkitBlockResolution.isFoliage(data); + byte cached = foliage; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isFoliage(data)); foliage = cached; } - return cached; + return cached == TRUE; } @Override public boolean isTreeBlock() { - Boolean cached = treeBlock; - if (cached == null) { - cached = Tag.LOGS.isTagged(data.getMaterial()) || Tag.LEAVES.isTagged(data.getMaterial()); + byte cached = treeBlock; + if (cached == UNKNOWN) { + cached = flag(Tag.LOGS.isTagged(data.getMaterial()) || Tag.LEAVES.isTagged(data.getMaterial())); treeBlock = cached; } - return cached; + return cached == TRUE; } @Override public boolean isFoliagePlantable() { - Boolean cached = foliagePlantable; - if (cached == null) { - cached = BukkitBlockResolution.isFoliagePlantable(data); + byte cached = foliagePlantable; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isFoliagePlantable(data)); foliagePlantable = cached; } - return cached; + return cached == TRUE; } @Override public boolean isDecorant() { - Boolean cached = decorant; - if (cached == null) { - cached = BukkitBlockResolution.isDecorant(data); + byte cached = decorant; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isDecorant(data)); decorant = cached; } - return cached; + return cached == TRUE; } @Override public boolean isStorage() { - Boolean cached = storage; - if (cached == null) { - cached = BukkitBlockResolution.isStorage(data); + byte cached = storage; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isStorage(data)); storage = cached; } - return cached; + return cached == TRUE; } @Override public boolean isStorageChest() { - Boolean cached = storageChest; - if (cached == null) { - cached = BukkitBlockResolution.isStorageChest(data); + byte cached = storageChest; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isStorageChest(data)); storageChest = cached; } - return cached; + return cached == TRUE; } @Override public boolean isOre() { - Boolean cached = ore; - if (cached == null) { - cached = BukkitBlockResolution.isOre(data); + byte cached = ore; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isOre(data)); ore = cached; } - return cached; + return cached == TRUE; } @Override public boolean isDeepSlate() { - Boolean cached = deepSlate; - if (cached == null) { - cached = BukkitBlockResolution.isDeepSlate(data); + byte cached = deepSlate; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isDeepSlate(data)); deepSlate = cached; } - return cached; + return cached == TRUE; } @Override public boolean isVineBlock() { - Boolean cached = vineBlock; - if (cached == null) { - cached = BukkitBlockResolution.isVineBlock(data); + byte cached = vineBlock; + if (cached == UNKNOWN) { + cached = flag(BukkitBlockResolution.isVineBlock(data)); vineBlock = cached; } - return cached; + return cached == TRUE; } @Override @@ -398,12 +405,12 @@ public final class BukkitBlockState implements NativeBlockState { @Override public boolean hasTileEntity() { - Boolean cached = tileEntity; - if (cached == null) { - cached = INMS.get().hasTile(data.getMaterial()); + byte cached = tileEntity; + if (cached == UNKNOWN) { + cached = flag(INMS.get().hasTile(data.getMaterial())); tileEntity = cached; } - return cached; + return cached == TRUE; } @Override diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java index 9d9f53205..453c1130a 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java @@ -633,7 +633,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy return existingHolder; } - if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { + if ((y & 63) == 0 && noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) { noiseBiomeCache.clear(); } diff --git a/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java b/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java index 556dcce12..7c04a6142 100644 --- a/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java +++ b/core/src/main/java/art/arcane/iris/platform/generation/BukkitChunkGenerator.java @@ -72,7 +72,7 @@ import art.arcane.volmlib.util.bukkit.WorldIdentity; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.hunk.Hunk; -import art.arcane.iris.generation.chunk.TerrainChunkBiomeHunkView; +import art.arcane.iris.generation.chunk.ColumnBiomeHunk; import art.arcane.iris.generation.chunk.ChunkDataHunkHolder; import art.arcane.iris.generation.context.ChunkContext; import art.arcane.iris.generation.context.IrisContext; @@ -1298,12 +1298,11 @@ public class BukkitChunkGenerator extends ChunkGenerator implements PlatformChun try { Engine engine = getEngine(world); StudioGenerator selected = computeStudioGenerator(); - TerrainChunk tc = TerrainChunk.create(d); if (selected != null) { - selected.generateChunk(engine, tc, x, z); + selected.generateChunk(engine, TerrainChunk.create(d), x, z); } else { ChunkDataHunkHolder blocks = new ChunkDataHunkHolder(d); - Hunk biomes = new TerrainChunkBiomeHunkView(tc); + Hunk biomes = new ColumnBiomeHunk(d.getMaxHeight() - d.getMinHeight()); try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope = openGenerationHistoryCoordinateScope(engine, x << 4, z << 4); GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage"); diff --git a/core/src/test/java/art/arcane/iris/generation/biome/FloatingIslandBoundarySamplerTest.java b/core/src/test/java/art/arcane/iris/generation/biome/FloatingIslandBoundarySamplerTest.java index d2869793f..2faffebcb 100644 --- a/core/src/test/java/art/arcane/iris/generation/biome/FloatingIslandBoundarySamplerTest.java +++ b/core/src/test/java/art/arcane/iris/generation/biome/FloatingIslandBoundarySamplerTest.java @@ -103,20 +103,18 @@ public class FloatingIslandBoundarySamplerTest { } @Test - public void parent_cachesResolvedBiomeColumns() { + public void parent_readsOnlyTheQueriedColumnWithoutBuildingTheBoundaryField() { IrisBiome parent = new IrisBiome(); + IrisBiome other = new IrisBiome(); AtomicInteger calls = new AtomicInteger(); FloatingIslandBoundarySampler sampler = new FloatingIslandBoundarySampler((x, z) -> { calls.incrementAndGet(); - return parent; + return x == 12 && z == -9 ? parent : other; }); assertSame(parent, sampler.parent(12, -9)); - int firstReadCount = calls.get(); - assertSame(parent, sampler.parent(12, -9)); - assertEquals(firstReadCount, calls.get()); - int rawWidth = 16 + (FloatingIslandBoundarySampler.EDGE_FADE_RADIUS * 2) + 2; - assertEquals(rawWidth * rawWidth, firstReadCount); + assertSame(other, sampler.parent(13, -9)); + assertEquals(2, calls.get()); } @Test diff --git a/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java b/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java new file mode 100644 index 000000000..348aaba58 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/chunk/ChunkDataHunkHolderTest.java @@ -0,0 +1,99 @@ +package art.arcane.iris.generation.chunk; + +import art.arcane.volmlib.nativelib.terrain.NativeBlockState; +import org.bukkit.generator.ChunkGenerator; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ChunkDataHunkHolderTest { + @Test + public void columnTopTracksTheHighestStoredValueAndIgnoresNulls() { + ChunkDataHunkHolder holder = holder(64); + NativeBlockState stone = mock(NativeBlockState.class); + + assertEquals(-1, holder.highestStoredY(3, 7)); + holder.setRaw(3, 40, 7, null); + assertEquals(-1, holder.highestStoredY(3, 7)); + holder.setRaw(3, 12, 7, stone); + holder.setRaw(3, 30, 7, stone); + holder.setRaw(3, 5, 7, stone); + + assertEquals(30, holder.highestStoredY(3, 7)); + assertEquals(-1, holder.highestStoredY(7, 3)); + assertSame(stone, holder.getStoredRaw(3, 30, 7)); + assertNull(holder.getStoredRaw(3, 31, 7)); + assertEquals(30, ColumnExtent.highestStoredY(new ColumnExtentListeningHunk<>(holder, (x, y, z, state) -> { + }), 3, 7)); + } + + @Test + public void cellsKeepTheZMajorLayout() { + ChunkDataHunkHolder holder = holder(32); + List states = new ArrayList<>(); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 32; y++) { + NativeBlockState state = mock(NativeBlockState.class); + states.add(state); + holder.setRaw(x, y, z, state); + } + } + } + int index = 0; + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 32; y++) { + assertSame(states.get(index++), holder.getStoredRaw(x, y, z)); + } + } + } + } + + @Test + public void concurrentWritersNeverLoseTheColumnMaximum() throws InterruptedException { + for (int trial = 0; trial < 20; trial++) { + ChunkDataHunkHolder holder = holder(768); + NativeBlockState stone = mock(NativeBlockState.class); + int threads = 8; + CountDownLatch start = new CountDownLatch(1); + List writers = new ArrayList<>(); + for (int thread = 0; thread < threads; thread++) { + int offset = thread; + Thread writer = new Thread(() -> { + try { + start.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + for (int y = offset; y < 768; y += threads) { + holder.setRaw(9, y, 4, stone); + } + }); + writer.start(); + writers.add(writer); + } + start.countDown(); + for (Thread writer : writers) { + writer.join(); + } + assertEquals(767, holder.highestStoredY(9, 4)); + } + } + + private static ChunkDataHunkHolder holder(int height) { + ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class); + when(chunk.getMinHeight()).thenReturn(0); + when(chunk.getMaxHeight()).thenReturn(height); + return new ChunkDataHunkHolder(chunk); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/chunk/LinkedTerrainChunkTest.java b/core/src/test/java/art/arcane/iris/generation/chunk/LinkedTerrainChunkTest.java index 54c386389..bc67df495 100644 --- a/core/src/test/java/art/arcane/iris/generation/chunk/LinkedTerrainChunkTest.java +++ b/core/src/test/java/art/arcane/iris/generation/chunk/LinkedTerrainChunkTest.java @@ -18,6 +18,7 @@ import org.junit.Test; import org.mockito.MockedStatic; import java.lang.reflect.Proxy; +import java.util.Random; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -110,6 +111,48 @@ public class LinkedTerrainChunkTest { } } + @Test + public void columnBiomeHunkMatchesTheLinkedChunkView() { + NativeBiome plains = BukkitBiome.of(Biome.PLAINS); + NativeBiome desert = BukkitBiome.of(Biome.DESERT); + NativeBiome alternate = mock(NativeBiome.class); + when(alternate.nativeHandle()).thenReturn(Biome.DESERT); + NativeBiome[] palette = {plains, desert, alternate}; + for (int trial = 0; trial < 12; trial++) { + Random random = new Random(9127L + trial); + TerrainChunkBiomeHunkView expected = new TerrainChunkBiomeHunkView(chunk()); + ColumnBiomeHunk actual = new ColumnBiomeHunk(16); + int operations = 1 + random.nextInt(80); + for (int operation = 0; operation < operations; operation++) { + NativeBiome biome = palette[random.nextInt(palette.length)]; + int x = random.nextInt(16); + int z = random.nextInt(16); + int kind = random.nextInt(trial % 3 == 0 ? 1 : 3); + if (kind == 0) { + expected.set(x, 0, z, x, 15, z, biome); + actual.set(x, 0, z, x, 15, z, biome); + } else if (kind == 1) { + int minimumY = random.nextInt(16); + int maximumY = minimumY + random.nextInt(16 - minimumY); + expected.set(x, minimumY, z, x, maximumY, z, biome); + actual.set(x, minimumY, z, x, maximumY, z, biome); + } else { + int y = random.nextInt(20) - 2; + int rawX = x + (random.nextBoolean() ? 16 : 0); + expected.setRaw(rawX, y, z, biome); + actual.setRaw(rawX, y, z, biome); + } + } + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = -2; y < 18; y++) { + assertSame("biome " + x + "," + y + "," + z, expected.getRaw(x, y, z), actual.getRaw(x, y, z)); + } + } + } + } + } + private static LinkedTerrainChunk chunk() { ChunkData data = mock(ChunkData.class); when(data.getMinHeight()).thenReturn(-16); diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/EngineShutdownDrainTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/EngineShutdownDrainTest.java index 75defd7d6..093f7c1bc 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/EngineShutdownDrainTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/EngineShutdownDrainTest.java @@ -112,7 +112,7 @@ public class EngineShutdownDrainTest { doAnswer(invocation -> { Future continuation = worker.submit(() -> { try (IrisEngine.GenerationRuntimeScope ignored = fixture.engine.openGenerationRuntimeScope(binding)) { - assertSame(binding, fixture.engine.generationRuntimeScopes.current()); + assertSame(binding, fixture.engine.threadState.binding()); assertSame(fixture.runtime, fixture.engine.runtime); assertTrue(fixture.engine.getClosing().get()); assertThrows(IllegalStateException.class, () -> fixture.engine.requireRunning("start generation")); @@ -513,16 +513,16 @@ public class EngineShutdownDrainTest { assembly.mantle = mock(EngineMantle.class); assembly.ownsMantle = true; RuntimeAssembly previous = mock(RuntimeAssembly.class); - fixture.engine.runtimeAssembly.set(previous); + fixture.engine.threadState.setAssembly(previous); IllegalStateException failure = new IllegalStateException("Planner still active"); doAnswer(invocation -> { - assertSame(assembly, fixture.engine.runtimeAssembly.get()); + assertSame(assembly, fixture.engine.threadState.assembly()); throw failure; }).when(assembly.complex).close(); assertSame(failure, fixture.shutdown.closeAssembly(assembly, null)); - assertSame(previous, fixture.engine.runtimeAssembly.get()); - fixture.engine.runtimeAssembly.remove(); + assertSame(previous, fixture.engine.threadState.assembly()); + fixture.engine.threadState.setAssembly(null); assertTrue(fixture.shutdown.retainsData(detachedTarget.getData())); assertTrue(fixture.shutdown.closeDetachedTarget(detachedTarget, null) instanceof IllegalStateException); verify(detachedTarget, never()).close(); @@ -531,13 +531,13 @@ public class EngineShutdownDrainTest { verify(fixture.target, never()).close(); verifyNoInteractions(assembly.mantle); doAnswer(invocation -> { - assertSame(assembly, fixture.engine.runtimeAssembly.get()); + assertSame(assembly, fixture.engine.threadState.assembly()); return null; }).when(assembly.complex).close(); fixture.shutdown.close(); } - assertNull(fixture.engine.runtimeAssembly.get()); + assertNull(fixture.engine.threadState.assembly()); assertFalse(assembly.ownsMantle); assertFalse(fixture.shutdown.retainsData(detachedTarget.getData())); verify(assembly.mantle).close(); @@ -851,10 +851,9 @@ public class EngineShutdownDrainTest { setField(engine, "shutdownSequence", shutdown); setField(engine, "engineDataStore", mock(EngineDataStore.class)); setField(engine, "lifecycleLock", new Object()); - setField(engine, "runtimeAssembly", new ThreadLocal()); + setField(engine, "threadState", new EngineThreadState()); setField(engine, "detachedGenerationRuntimes", ConcurrentHashMap.newKeySet()); setField(engine, "retiringGenerationRuntimes", ConcurrentHashMap.newKeySet()); - setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState()); AtomicBoolean closing = new AtomicBoolean(); setField(engine, "closing", closing); when(engine.getClosing()).thenReturn(closing); diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/EngineThreadStateTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/EngineThreadStateTest.java new file mode 100644 index 000000000..e833fb4e1 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/EngineThreadStateTest.java @@ -0,0 +1,109 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.runtime.EngineRuntimeBuilder.RuntimeAssembly; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; + +public class EngineThreadStateTest { + @Test + public void bindingsShareOneFrameAndReleaseItWhenAllAreCleared() { + EngineThreadState state = new EngineThreadState(); + RuntimeAssembly assembly = mock(RuntimeAssembly.class); + IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + + assertNull(state.current()); + state.setAssembly(assembly); + IrisEngine.GenerationRuntimeScope scope = state.open(binding); + EngineThreadState.Frame frame = state.current(); + assertSame(assembly, frame.assembly); + assertSame(binding, frame.binding); + + state.setAssembly(null); + assertSame(frame, state.current()); + assertSame(binding, state.binding()); + scope.close(); + assertNull(state.current()); + } + + @Test + public void nestedScopesRestoreThePreviousBindingInOrder() { + EngineThreadState state = new EngineThreadState(); + IrisEngine.GenerationRuntimeBinding outer = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + IrisEngine.GenerationRuntimeBinding inner = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + + IrisEngine.GenerationRuntimeScope outerScope = state.open(outer); + IrisEngine.GenerationRuntimeScope innerScope = state.open(inner); + assertThrows(IllegalStateException.class, outerScope::close); + innerScope.close(); + assertSame(outer, state.binding()); + outerScope.close(); + assertNull(state.current()); + } + + @Test + public void threadsSharingATableSlotKeepSeparateFrames() throws InterruptedException { + EngineThreadState state = new EngineThreadState(); + IrisEngine.GenerationRuntimeBinding mainBinding = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + IrisEngine.GenerationRuntimeBinding otherBinding = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + IrisEngine.GenerationRuntimeScope mainScope = state.open(mainBinding); + long slot = Thread.currentThread().threadId() & 255; + AtomicReference otherSeen = new AtomicReference<>(); + AtomicReference otherAfterClose = new AtomicReference<>(otherBinding); + Thread colliding = null; + while (colliding == null) { + Thread candidate = new Thread(() -> { + IrisEngine.GenerationRuntimeScope scope = state.open(otherBinding); + otherSeen.set(state.binding()); + scope.close(); + otherAfterClose.set(state.current()); + }); + if ((candidate.threadId() & 255) == slot) { + colliding = candidate; + } + } + colliding.start(); + colliding.join(); + + assertSame(otherBinding, otherSeen.get()); + assertNull(otherAfterClose.get()); + assertSame(mainBinding, state.binding()); + mainScope.close(); + assertNull(state.current()); + } + + @Test + public void framesAreThreadConfinedAndScopesRejectForeignClose() throws InterruptedException { + EngineThreadState state = new EngineThreadState(); + IrisEngine.GenerationRuntimeBinding binding = new IrisEngine.GenerationRuntimeBinding( + mock(IrisEngine.class), mock(GenerationRuntime.class)); + IrisEngine.GenerationRuntimeScope scope = state.open(binding); + AtomicReference seen = new AtomicReference<>(binding); + AtomicReference foreignClose = new AtomicReference<>(); + Thread other = new Thread(() -> { + seen.set(state.current()); + try { + scope.close(); + } catch (Throwable failure) { + foreignClose.set(failure); + } + }); + other.start(); + other.join(); + + assertNull(seen.get()); + assertSame(IllegalStateException.class, foreignClose.get().getClass()); + scope.close(); + assertNull(state.current()); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java index 3102a557e..53d93e80c 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisCarveWallPaletteScopeTest.java @@ -103,7 +103,7 @@ public class IrisCarveWallPaletteScopeTest { IrisContext.Scope context = IrisContext.open(fixture.engine, 71L, fixture.context); MockedStatic terrain = mockStatic(TerrainMatterView.class)) { if (mode == Mode.ASSEMBLY) { - fixture.engine.runtimeAssembly.set(fixture.assembly); + fixture.engine.threadState.setAssembly(fixture.assembly); } terrain.when(() -> TerrainMatterView.iterate(eq(fixture.chunk), eq(MatterCavern.class), any())) .thenAnswer(call -> { @@ -137,7 +137,7 @@ public class IrisCarveWallPaletteScopeTest { verify(fixture.chunk).release(); assertEquals(1, fixture.dataLookups.get()); } finally { - fixture.engine.runtimeAssembly.remove(); + fixture.engine.threadState.setAssembly(null); } } assertSame(fixture.active.data(), fixture.engine.getData()); @@ -199,9 +199,7 @@ public class IrisCarveWallPaletteScopeTest { active = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle); detached = runtime(mock(IrisData.class), mock(IrisComplex.class), engineMantle); field(engine, IrisEngine.class, "lifecycleLock", new Object()); - field(engine, IrisEngine.class, "runtimeAssembly", new ThreadLocal()); - field(engine, IrisEngine.class, "biomeEnvironmentScopes", new ThreadLocal<>()); - field(engine, IrisEngine.class, "generationRuntimeScopes", new GenerationRuntimeScopeState()); + field(engine, IrisEngine.class, "threadState", new EngineThreadState()); field(engine, IrisEngine.class, "detachedGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>())); field(engine, IrisEngine.class, "retiringGenerationRuntimes", Collections.newSetFromMap(new IdentityHashMap<>())); engine.detachedGenerationRuntimes.add(detached); @@ -255,7 +253,7 @@ public class IrisCarveWallPaletteScopeTest { private IrisBiome scopedBiome(IrisBiome biome) { try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope( new IrisEngine.GenerationRuntimeBinding(engine, active))) { - assertSame(engine.runtimeAssembly.get() == null ? active.data() : assemblyData, engine.getData()); + assertSame(engine.threadState.assembly() == null ? active.data() : assemblyData, engine.getData()); } return biome; } diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineEntryHydrologyTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineEntryHydrologyTest.java index d5dd32cd3..40a8868c8 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineEntryHydrologyTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineEntryHydrologyTest.java @@ -62,7 +62,7 @@ public class IrisEngineEntryHydrologyTest { doAnswer(invocation -> { assertSame(engine, IrisContext.require().getEngine()); assertEquals(93L, IrisContext.require().getGenerationSessionId()); - assertSame(binding, engine.generationRuntimeScopes.current()); + assertSame(binding, engine.threadState.binding()); return null; }).when(hydrology).prepareChunkColumns(0, 0); AtomicReference> queued = new AtomicReference<>(); @@ -75,7 +75,7 @@ public class IrisEngineEntryHydrologyTest { verify(hydrology).prepareChunkColumns(0, 0); verify(lease).close(); assertNull(IrisContext.get()); - assertNull(engine.generationRuntimeScopes.current()); + assertNull(engine.threadState.binding()); tasks.drainBackgroundTasks("test").requireComplete("test"); } } @@ -325,9 +325,9 @@ public class IrisEngineEntryHydrologyTest { Field closing = IrisEngine.class.getDeclaredField("closing"); closing.setAccessible(true); closing.set(engine, new AtomicBoolean()); - Field scopes = IrisEngine.class.getDeclaredField("generationRuntimeScopes"); + Field scopes = IrisEngine.class.getDeclaredField("threadState"); scopes.setAccessible(true); - scopes.set(engine, new GenerationRuntimeScopeState()); + scopes.set(engine, new EngineThreadState()); Field lifecycle = IrisEngine.class.getDeclaredField("lifecycleLock"); lifecycle.setAccessible(true); lifecycle.set(engine, new Object()); diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java index 3faf566bd..011b55946 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisEngineGenerationRuntimeScopeTest.java @@ -923,9 +923,7 @@ public class IrisEngineGenerationRuntimeScopeTest { IrisEngine engine = mock(IrisEngine.class, CALLS_REAL_METHODS); setField(engine, "lifecycleLock", new Object()); setField(engine, "generationHistoryRuntimeRouterLock", new Object()); - setField(engine, "runtimeAssembly", new ThreadLocal()); - setField(engine, "biomeEnvironmentScopes", new ThreadLocal()); - setField(engine, "generationRuntimeScopes", new GenerationRuntimeScopeState()); + setField(engine, "threadState", new EngineThreadState()); Set detached = Collections.synchronizedSet( Collections.newSetFromMap(new IdentityHashMap())); setField(engine, "detachedGenerationRuntimes", detached); diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisBiomeActuatorCoordinateTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisBiomeActuatorCoordinateTest.java index 5b81665e6..61b4e4514 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisBiomeActuatorCoordinateTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisBiomeActuatorCoordinateTest.java @@ -20,7 +20,6 @@ import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.Matter; -import art.arcane.volmlib.util.matter.MatterBiomeInject; import org.junit.After; import org.junit.ClassRule; import org.junit.Test; @@ -34,13 +33,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.same; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -54,19 +53,58 @@ public class IrisBiomeActuatorCoordinateTest { } @Test - @SuppressWarnings("unchecked") - public void historicalChunkDoesNotPublishNewPersistentMetadata() { + public void historicalStackChunkDoesNotClearPersistentMetadata() { Engine engine = mock(Engine.class); ChunkContext context = mock(ChunkContext.class); IrisComplex complex = mock(IrisComplex.class); + when(context.hasDimensionStack()).thenReturn(true); when(context.getComplex()).thenReturn(complex); when(complex.allowsMantleChunkWrite(0, 0)).thenReturn(false); - IrisBiomeActuator.publishNaturalMetadata(engine, 0, 0, mock(Hunk.class), context); + IrisDimensionStackActuator.clearHostNaturalMetadata(engine, 0, 0, 16, context); verifyNoMoreInteractions(engine); } + @Test + public void unstackedChunkNeverTouchesTheMantle() { + Engine engine = mock(Engine.class); + ChunkContext context = mock(ChunkContext.class); + when(context.hasDimensionStack()).thenReturn(false); + + IrisDimensionStackActuator.clearHostNaturalMetadata(engine, 0, 0, 16, context); + + verifyNoMoreInteractions(engine); + } + + @Test + @SuppressWarnings("unchecked") + public void actuatorWritesColumnBiomesWithoutMantleMetadata() { + bindPlatform(); + IrisComplex complex = mock(IrisComplex.class); + Engine engine = engine(complex); + IrisBiomeActuator actuator = new IrisBiomeActuator(engine); + Hunk output = mock(Hunk.class); + when(output.getWidth()).thenReturn(2); + when(output.getDepth()).thenReturn(2); + when(output.getHeight()).thenReturn(8); + ChunkedDataCache biomeCache = currentBiomeCache(engine); + ChunkContext context = mock(ChunkContext.class); + when(context.getComplex()).thenReturn(complex); + when(context.getBiome()).thenReturn(biomeCache); + + actuator.onActuate(100, -200, output, false, context); + + NativeBiome plains = IrisPlatforms.get().registries().biome("minecraft:plains"); + for (int x = 0; x < 2; x++) { + for (int z = 0; z < 2; z++) { + verify(output).set(x, 0, z, x, 7, z, plains); + } + } + verifyNoInteractions(engine.getMantle().getMantle()); + verify(IrisPlatforms.get().biomeWriter(), never()).biomeIdFor(anyString()); + } + @Test @SuppressWarnings("unchecked") public void derivativeScatterUsesEachWorldColumnCoordinate() { @@ -101,19 +139,6 @@ public class IrisBiomeActuatorCoordinateTest { assertEquals(List.of("100,-200", "100,-199", "101,-200", "101,-199"), samples); } - @Test - @SuppressWarnings("unchecked") - public void stackedBiomeReplacementRemovesEveryExistingMarkerWithItsTypedSlice() { - Mantle mantle = mock(Mantle.class); - - IrisBiomeActuator.clearBiomeMatterRange(mantle, 100, -200, 4, 6); - - verify(mantle).remove(100, 4, -200, MatterBiomeInject.class); - verify(mantle).remove(100, 5, -200, MatterBiomeInject.class); - verify(mantle).remove(100, 6, -200, MatterBiomeInject.class); - verifyNoMoreInteractions(mantle); - } - @Test @SuppressWarnings("unchecked") public void transitionColumnOverlaysFrozenPhysicalKeyAfterCurrentBiome() { @@ -143,11 +168,8 @@ public class IrisBiomeActuatorCoordinateTest { verify(biomeCache).get(0, 0); verify(IrisPlatforms.get().registries()).biome("iris:old-physical"); verify(IrisPlatforms.get().biomeWriter(), never()).biomeIdFor("iris:old-physical"); - Mantle mantle = engine.getMantle().getMantle(); - verify(mantle).set(eq(100), eq(0), eq(-200), argThat(value -> { - MatterBiomeInject injection = (MatterBiomeInject) value; - return !injection.isCustom() && "iris:old-physical".equals(injection.getBiomeKey()); - })); + verify(output, times(2)).set(0, 0, 0, 0, 0, 0, IrisPlatforms.get().registries().biome("iris:old-physical")); + verifyNoInteractions(engine.getMantle().getMantle()); } @Test @@ -184,15 +206,9 @@ public class IrisBiomeActuatorCoordinateTest { verify(transitionPlan, never()).historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt()); verify(output).set(eq(0), eq(0), eq(0), eq(0), eq(3), eq(0), any(NativeBiome.class)); verify(output).set(eq(0), eq(4), eq(0), eq(0), eq(7), eq(0), any(NativeBiome.class)); - Mantle mantle = engine.getMantle().getMantle(); - verify(mantle).set(eq(100), eq(0), eq(-200), argThat(value -> { - MatterBiomeInject injection = (MatterBiomeInject) value; - return "iris:old-bottom".equals(injection.getBiomeKey()); - })); - verify(mantle).set(eq(100), eq(4), eq(-200), argThat(value -> { - MatterBiomeInject injection = (MatterBiomeInject) value; - return "iris:old-top".equals(injection.getBiomeKey()); - })); + verify(IrisPlatforms.get().registries()).biome("iris:old-bottom"); + verify(IrisPlatforms.get().registries()).biome("iris:old-top"); + verifyNoInteractions(engine.getMantle().getMantle()); } @SuppressWarnings("unchecked") diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java index 961f9e38b..9b16e3bcd 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisNaturalCaveScanTest.java @@ -12,6 +12,11 @@ import art.arcane.iris.generation.decoration.IrisDecorator; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.iris.generation.block.B; +import art.arcane.iris.generation.chunk.ChunkDataHunkHolder; +import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk; +import art.arcane.iris.spi.IrisPlatform; +import art.arcane.iris.spi.IrisPlatforms; +import org.bukkit.generator.ChunkGenerator; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; import art.arcane.volmlib.util.matter.Matter; @@ -91,6 +96,46 @@ public class IrisNaturalCaveScanTest { verify(fixture.chunk).release(); } + @Test + public void columnExtentScanMatchesFullScanWhenDecorationBuildsAboveTheColumnTop() throws Exception { + IrisPlatforms.bind(mock(IrisPlatform.class)); + try { + Fixture full = new Fixture(true); + Hunk fullOutput = full.sparseOutput(Hunk.newArrayHunk(16, 32, 16)); + List fullWrites = new ArrayList<>(); + full.modifier.decorateNaturalCaves(-32, 48, + fullOutput.listen((x, y, z, state) -> fullWrites.add(x + ":" + y + ":" + z + ":" + state.key()))); + + Fixture bounded = new Fixture(true); + ChunkGenerator.ChunkData chunkData = mock(ChunkGenerator.ChunkData.class); + doReturn(0).when(chunkData).getMinHeight(); + doReturn(32).when(chunkData).getMaxHeight(); + ChunkDataHunkHolder holder = new ChunkDataHunkHolder(chunkData); + bounded.sparseOutput(holder); + List boundedWrites = new ArrayList<>(); + bounded.modifier.decorateNaturalCaves(-32, 48, new ColumnExtentListeningHunk<>(holder, + (x, y, z, state) -> boundedWrites.add(x + ":" + y + ":" + z + ":" + state.key()))); + + assertTrue("fixture must decorate a zone above the initial column top", + full.calls.contains("floor:0:0:27:1")); + assertEquals(full.calls, bounded.calls); + assertEquals(fullWrites, boundedWrites); + assertEquals(full.markers, bounded.markers); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 32; y++) { + NativeBlockState expected = fullOutput.getRaw(x, y, z); + NativeBlockState actual = holder.getStoredRaw(x, y, z); + assertEquals("block " + x + ":" + y + ":" + z, + expected == null ? null : expected.key(), actual == null ? null : actual.key()); + } + } + } + } finally { + IrisPlatforms.unbind(); + } + } + private static void verifyScan(int width, int depth) throws Exception { Fixture fixture = new Fixture(); Hunk output = fixture.output(width, depth); @@ -169,8 +214,12 @@ public class IrisNaturalCaveScanTest { private final List calls = new ArrayList<>(); private final List markers = new ArrayList<>(); - @SuppressWarnings("unchecked") private Fixture() throws Exception { + this(false); + } + + @SuppressWarnings("unchecked") + private Fixture(boolean buildAboveTop) throws Exception { Engine engine = mock(Engine.class); EngineMantle engineMantle = mock(EngineMantle.class); Mantle mantle = mock(Mantle.class); @@ -211,6 +260,9 @@ public class IrisNaturalCaveScanTest { for (int height = 20; height <= 22; height++) { output.setRaw(x, height, z, air); } + if (buildAboveTop) { + output.setRaw(x, 31, z, stone); + } } return null; }).when(surface).decorate(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), eq(biome), any(), anyInt(), anyInt()); @@ -225,6 +277,20 @@ public class IrisNaturalCaveScanTest { }).when(roof).decorate(anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), anyInt(), any(), eq(biome), any(), anyInt(), anyInt()); } + private > H sparseOutput(H output) { + for (int x = 0; x < output.getWidth(); x++) { + for (int z = 0; z < output.getDepth(); z++) { + for (int y = 0; y < 32; y++) { + NativeBlockState state = initialState(this, y); + if (state != air) { + output.setRaw(x, y, z, state); + } + } + } + } + return output; + } + private Hunk output(int width, int depth) { Hunk output = Hunk.newArrayHunk(width, 32, depth); for (int x = 0; x < width; x++) { diff --git a/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java b/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java index 8256667a7..e5f9618f9 100644 --- a/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java +++ b/core/src/test/java/art/arcane/iris/generation/stage/IrisPerfectionModifierColumnTest.java @@ -1,6 +1,8 @@ package art.arcane.iris.generation.stage; import art.arcane.iris.generation.block.B; +import art.arcane.iris.generation.chunk.ChunkDataHunkHolder; +import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk; import art.arcane.iris.generation.concurrent.BurstExecutor; import art.arcane.iris.generation.concurrent.MultiBurst; import art.arcane.iris.generation.decoration.IrisProceduralBlocks; @@ -13,6 +15,7 @@ import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.hunk.Hunk; import art.arcane.volmlib.util.hunk.storage.ArrayHunk; import org.bukkit.block.data.BlockData; +import org.bukkit.generator.ChunkGenerator; import org.junit.Rule; import org.junit.Test; @@ -24,6 +27,7 @@ import java.util.Random; import java.util.concurrent.ExecutorService; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -55,6 +59,46 @@ public class IrisPerfectionModifierColumnTest { } } + @Test + public void columnExtentTopSearchMatchesFullHeightSearch() { + Fixture fixture = new Fixture(); + Random random = new Random(55031L); + NativeBlockState[] palette = {null, fixture.air, fixture.water, fixture.stone, + fixture.decorant, fixture.half, fixture.spike(true, false), fixture.spike(false, true)}; + for (int trial = 0; trial < 8; trial++) { + ChunkDataHunkHolder bounded = holder(64); + ChunkDataHunkHolder full = holder(64); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + int top = random.nextInt(64); + for (int y = 0; y <= top; y++) { + NativeBlockState state = y == 0 ? fixture.stone : palette[random.nextInt(palette.length)]; + bounded.setRaw(x, y, z, state); + full.setRaw(x, y, z, state); + } + } + } + fixture.modifier.onModify(0, 0, new ColumnExtentListeningHunk<>(bounded, (x, y, z, state) -> { + }), false, null); + fixture.modifier.onModify(0, 0, full.listen((x, y, z, state) -> { + }), false, null); + for (int x = 0; x < 16; x++) { + for (int z = 0; z < 16; z++) { + for (int y = 0; y < 64; y++) { + assertSame("voxel " + x + "," + y + "," + z, full.getStoredRaw(x, y, z), bounded.getStoredRaw(x, y, z)); + } + } + } + } + } + + private static ChunkDataHunkHolder holder(int height) { + ChunkGenerator.ChunkData chunk = mock(ChunkGenerator.ChunkData.class); + when(chunk.getMinHeight()).thenReturn(0); + when(chunk.getMaxHeight()).thenReturn(height); + return new ChunkDataHunkHolder(chunk); + } + @Test public void unrelatedTallColumnsAreScannedOnceDuringRepeatedCleanup() { Fixture fixture = new Fixture();