Pregen throughput work

- Object source plans are packed per destination and built once
- Source plans retire once every destination in reach consumed them
- Rotated block states and property variants are memoized
- Generation history syncs to disk from a background group commit
- History syncs at world save, shutdown, pregen end and promotion
- History capture reuses per-column biome and mantle reads
- Semantic summaries are built only when read
- Hydrology plans the whole pregen area nearest first
- Chunks without hydrology skip composition and cave cell lookups
- Server-thread stand-in hydrology answers are never cached
- Neighbour drafts stay inside the planned pregen area
- Terrain3D caches are lock-free
- Cave density lattice samples are shared between neighbouring chunks
- The eager carve window is 27 by 27 chunks with on-demand terrain beyond it
- Cave cells are copied once per chunk for the carve stage
- Terrain columns are painted z-major with per-level ore bands
- Deposit clumps that cannot place are rejected before building
- Dead biome inject writes are gone
- Chunk output buffers track each column top
- Engine getters use a thread-id frame table
- Pregen keeps region border chunks until neighbouring regions finish
- Mantle reads take plate pins so saves cannot close plates in use
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-29 00:37:46 -04:00
commit 85849693b6
180 files changed
+9102 -1964

No files matched your search

@@ -0,0 +1,84 @@
package art.arcane.iris.platform.bukkit.nms.v26_2_R1;
import art.arcane.volmlib.nativelib.v26_2_R1.terrain.NativeTerrainAccessImpl;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.bukkit.craftbukkit.CraftWorld;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsChunkRetentionTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void retentionRequestsNothingBeyondTheOuterRingOfAFullRequest() {
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
assertEquals(ChunkLevel.RADIUS_AROUND_FULL_CHUNK, binding.fullChunkDependencyRadius());
assertEquals(ChunkLevel.MAX_LEVEL, ChunkHolderManager.MAX_TICKET_LEVEL);
assertEquals(ChunkLevel.generationStatus(ChunkLevel.FULL_CHUNK_LEVEL + binding.fullChunkDependencyRadius()),
ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
assertEquals(ChunkStatus.STRUCTURE_STARTS, ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
}
@Test
public void retainAndReleaseUseOnePermanentNonPersistentTicketAtTheMaximumLevel() throws Exception {
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
CraftWorld world = mock(CraftWorld.class);
ServerLevel level = mock(ServerLevel.class);
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
ChunkHolderManager manager = mock(ChunkHolderManager.class);
Field managerField = ChunkTaskScheduler.class.getField("chunkHolderManager");
managerField.setAccessible(true);
managerField.set(scheduler, manager);
when(world.getHandle()).thenReturn(level);
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
when(manager.addTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
.thenReturn(true);
when(manager.removeTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
.thenReturn(true);
assertTrue(binding.retainChunk(world, 3, -4));
ArgumentCaptor<TicketType> type = ArgumentCaptor.forClass(TicketType.class);
verify(manager).addTicketAtLevel(type.capture(), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
assertEquals(TicketType.NO_TIMEOUT, type.getValue().timeout());
assertTrue(type.getValue().doesLoad());
assertFalse(type.getValue().doesSimulate());
assertFalse(type.getValue().persist());
assertTrue(binding.releaseChunk(world, 3, -4));
verify(manager).removeTicketAtLevel(same(type.getValue()), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
binding.retainChunk(world, 3, -4);
ArgumentCaptor<TicketType> again = ArgumentCaptor.forClass(TicketType.class);
verify(manager, times(2)).addTicketAtLevel(again.capture(), eq(3), eq(-4),
eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
assertSame(type.getValue(), again.getValue());
}
}
@@ -0,0 +1,84 @@
package art.arcane.iris.platform.bukkit.nms.v26_3_R1;
import art.arcane.volmlib.nativelib.v26_3_R1.terrain.NativeTerrainAccessImpl;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkHolderManager;
import ca.spottedleaf.moonrise.patches.chunk_system.scheduling.ChunkTaskScheduler;
import net.minecraft.SharedConstants;
import net.minecraft.server.Bootstrap;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.world.level.chunk.status.ChunkStatus;
import org.bukkit.craftbukkit.CraftWorld;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class NmsChunkRetentionTest {
@BeforeClass
public static void bootstrapMinecraftRegistries() {
SharedConstants.tryDetectVersion();
Bootstrap.bootStrap();
}
@Test
public void retentionRequestsNothingBeyondTheOuterRingOfAFullRequest() {
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
assertEquals(ChunkLevel.RADIUS_AROUND_FULL_CHUNK, binding.fullChunkDependencyRadius());
assertEquals(ChunkLevel.MAX_LEVEL, ChunkHolderManager.MAX_TICKET_LEVEL);
assertEquals(ChunkLevel.generationStatus(ChunkLevel.FULL_CHUNK_LEVEL + binding.fullChunkDependencyRadius()),
ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
assertEquals(ChunkStatus.STRUCTURE_STARTS, ChunkLevel.generationStatus(ChunkHolderManager.MAX_TICKET_LEVEL));
}
@Test
public void retainAndReleaseUseOnePermanentNonPersistentTicketAtTheMaximumLevel() throws Exception {
NativeTerrainAccessImpl binding = mock(NativeTerrainAccessImpl.class, CALLS_REAL_METHODS);
CraftWorld world = mock(CraftWorld.class);
ServerLevel level = mock(ServerLevel.class);
ChunkTaskScheduler scheduler = mock(ChunkTaskScheduler.class);
ChunkHolderManager manager = mock(ChunkHolderManager.class);
Field managerField = ChunkTaskScheduler.class.getField("chunkHolderManager");
managerField.setAccessible(true);
managerField.set(scheduler, manager);
when(world.getHandle()).thenReturn(level);
when(level.moonrise$getChunkTaskScheduler()).thenReturn(scheduler);
when(manager.addTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
.thenReturn(true);
when(manager.removeTicketAtLevel(any(TicketType.class), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull()))
.thenReturn(true);
assertTrue(binding.retainChunk(world, 3, -4));
ArgumentCaptor<TicketType> type = ArgumentCaptor.forClass(TicketType.class);
verify(manager).addTicketAtLevel(type.capture(), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
assertEquals(TicketType.NO_TIMEOUT, type.getValue().timeout());
assertTrue(type.getValue().doesLoad());
assertFalse(type.getValue().doesSimulate());
assertFalse(type.getValue().persist());
assertTrue(binding.releaseChunk(world, 3, -4));
verify(manager).removeTicketAtLevel(same(type.getValue()), eq(3), eq(-4), eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
binding.retainChunk(world, 3, -4);
ArgumentCaptor<TicketType> again = ArgumentCaptor.forClass(TicketType.class);
verify(manager, times(2)).addTicketAtLevel(again.capture(), eq(3), eq(-4),
eq(ChunkHolderManager.MAX_TICKET_LEVEL), isNull());
assertSame(type.getValue(), again.getValue());
}
}
@@ -29,6 +29,7 @@ import java.util.function.LongSupplier;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.generation.runtime.DimensionStackContext;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.DimensionTerrainContext;
@@ -338,7 +339,12 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
return cached;
}
}
H resolved = resolveStructureBiome(engine, quartX, quartY, quartZ, sampler);
H resolved;
try {
resolved = ProvisionalSampling.memoizable(() -> resolveStructureBiome(engine, quartX, quartY, quartZ, sampler));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
if (cacheable) {
cache.put(runtimeIdentity, key, resolved);
}
@@ -388,8 +394,13 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
return cached;
}
}
H resolved = resolveVisibleBiome(
engine, quartX, quartY, quartZ, sampler, null);
H resolved;
try {
resolved = ProvisionalSampling.memoizable(() -> resolveVisibleBiome(
engine, quartX, quartY, quartZ, sampler, null));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
if (cacheable) {
cache.put(runtimeIdentity, key, resolved);
}
@@ -531,7 +542,12 @@ final class ModdedBiomePolicy<H, S> implements NativeModdedBiomePolicy<H, S> {
if (cached != null) {
return cached;
}
H resolved = resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler);
H resolved;
try {
resolved = ProvisionalSampling.memoizable(() -> resolveSurfaceStructureBiome(engine, quartX, quartZ, sampler));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
cache.put(runtimeIdentity, key, resolved);
return resolved;
}
@@ -245,7 +245,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
return true;
}
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
List<PreparedMarkerSpawn> markers = markerSystemEnabled()
? prepareMarkerSpawns(level, chunkX, chunkZ, chunk) : List.of();
@@ -380,7 +380,7 @@ public final class ModdedWorldManager implements EngineWorldManager {
return;
}
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
if (markerSystemEnabled()) {
spawnPreparedMarkers(level, prepareMarkerSpawns(level, chunkX, chunkZ, chunk), false);
@@ -116,6 +116,11 @@ public final class ModdedPregenMethod implements PregeneratorMethod {
}
ModdedIrisLog.info("Iris modded pregen done: dim={} completed={} peakInFlight={} finalLimit={}",
runtime.worldIdentity(), completed.get(), inFlightPeak.get(), adaptiveLimit.get());
try {
engine.syncGenerationHistory();
} catch (RuntimeException e) {
ModdedIrisLog.error("Iris modded pregen could not force generation history to stable storage", e);
}
if (deferFinalSaveIfRequested()) {
return;
}
@@ -188,7 +188,7 @@ public final class ModdedChunkUpdateService implements ModdedTickableService {
return;
}
MantleChunk<Matter> chunk = mantle.getChunk(chunkX, chunkZ).use();
MantleChunk<Matter> chunk = mantle.useChunk(chunkX, chunkZ);
try {
chunk.raiseFlagUnchecked(MantleFlag.ETCHED, () -> {
chunk.raiseFlagUnchecked(MantleFlag.TILE, () -> runTilePass(engine, level, chunkX, chunkZ, chunk));
@@ -70,8 +70,7 @@ public class ModdedSavedBiomeConsumersTest {
when(engine.getWorld()).thenReturn(mock(IrisWorld.class));
when(engineMantle.getMantle()).thenReturn(mantle);
when(mantle.isChunkLoaded(2, -1)).thenReturn(true);
when(mantle.getChunk(2, -1)).thenReturn(chunk);
when(chunk.use()).thenReturn(chunk);
when(mantle.useChunk(2, -1)).thenReturn(chunk);
BiomeEnvironment environment = environment();
when(engine.getSurfaceBiomeEnvironment(40, -8))
.thenThrow(new SavedBiomeUnavailableException("Loading", true))
@@ -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]) {
@@ -24,7 +24,7 @@ import art.arcane.iris.generation.decoration.IrisDecorator;
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositVariant;
import art.arcane.iris.generation.decoration.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds;
import art.arcane.iris.generation.decoration.IrisOreBands;
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
import art.arcane.iris.generation.hydrology.IrisRiverPolicy;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
@@ -120,8 +120,8 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
private final transient IrisBiomeLayerHeightCache layerSeaHeightGenerators = new IrisBiomeLayerHeightCache();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> surfaceOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> undergroundOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> surfaceOreBandsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> undergroundOreBandsCache = new AtomicCache<>();
private final transient AtomicCache<KSet<String>> surfaceOreReplaceableBlockData = new AtomicCache<>();
private final transient AtomicCache<EnumMap<IrisDecorationPart, IrisDecorator[]>> decoratorBuckets = new AtomicCache<>();
private final transient AtomicCache<Biome> derivativeResolved = new AtomicCache<>();
@@ -338,8 +338,8 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
this.ores = ores == null ? new KList<>() : ores;
surfaceOreCache.reset();
undergroundOreCache.reset();
surfaceOreBoundsCache.reset();
undergroundOreBoundsCache.reset();
surfaceOreBandsCache.reset();
undergroundOreBandsCache.reset();
}
public IrisBiome setSurfaceOreReplaceableBlocks(KList<String> surfaceOreReplaceableBlocks) {
@@ -348,20 +348,12 @@ public class IrisBiome extends IrisRegistrant implements Rarity {
return this;
}
public KList<IrisOreGenerator> getSurfaceOreGenerators() {
return IrisBiomeOres.getSurfaceOreGenerators(this);
public IrisOreBands getSurfaceOreBands() {
return IrisBiomeOres.getSurfaceOreBands(this);
}
public KList<IrisOreGenerator> getUndergroundOreGenerators() {
return IrisBiomeOres.getUndergroundOreGenerators(this);
}
public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() {
return IrisBiomeOres.getSurfaceOreGeneratorBounds(this);
}
public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() {
return IrisBiomeOres.getUndergroundOreGeneratorBounds(this);
public IrisOreBands getUndergroundOreBands() {
return IrisBiomeOres.getUndergroundOreBands(this);
}
public Biome getDerivative() {
@@ -19,7 +19,7 @@
package art.arcane.iris.generation.biome;
import art.arcane.iris.generation.decoration.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds;
import art.arcane.iris.generation.decoration.IrisOreBands;
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
import art.arcane.iris.pack.loading.IrisData;
@@ -73,30 +73,22 @@ final class IrisBiomeOres {
.contains(IrisProceduralBlocks.materialKey(state));
}
static KList<IrisOreGenerator> getSurfaceOreGenerators(IrisBiome biome) {
return getOres(biome, true);
}
static KList<IrisOreGenerator> getUndergroundOreGenerators(IrisBiome biome) {
return getOres(biome, false);
}
static IrisOreGeneratorBounds getSurfaceOreGeneratorBounds(IrisBiome biome) {
static IrisOreBands getSurfaceOreBands(IrisBiome biome) {
// getIfPresent fast path: aquire allocates a capturing lambda even on a hit, and this
// runs per column on the terrain hot path.
IrisOreGeneratorBounds cached = biome.getSurfaceOreBoundsCache().getIfPresent();
IrisOreBands cached = biome.getSurfaceOreBandsCache().getIfPresent();
if (cached != null) {
return cached;
}
return biome.getSurfaceOreBoundsCache().aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres(biome)));
return biome.getSurfaceOreBandsCache().aquire(() -> IrisOreBands.of(getSurfaceOres(biome)));
}
static IrisOreGeneratorBounds getUndergroundOreGeneratorBounds(IrisBiome biome) {
IrisOreGeneratorBounds cached = biome.getUndergroundOreBoundsCache().getIfPresent();
static IrisOreBands getUndergroundOreBands(IrisBiome biome) {
IrisOreBands cached = biome.getUndergroundOreBandsCache().getIfPresent();
if (cached != null) {
return cached;
}
return biome.getUndergroundOreBoundsCache().aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres(biome)));
return biome.getUndergroundOreBandsCache().aquire(() -> IrisOreBands.of(getUndergroundOres(biome)));
}
private static KList<IrisOreGenerator> getSurfaceOres(IrisBiome biome) {
@@ -47,6 +47,7 @@ import lombok.Setter;
import lombok.experimental.Accessors;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicReferenceArray;
@Snippet("biome-palette")
@Accessors(chain = true)
@@ -64,7 +65,8 @@ public class IrisBiomePaletteLayer {
new LazyBoundedCache<>(LAYER_GENERATOR_CACHE_SIZE);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient AtomicReference<CachedLayerGenerator> recentLayerGenerator = new AtomicReference<>();
private final transient AtomicReferenceArray<CachedLayerGenerator> recentLayerGenerators =
new AtomicReferenceArray<>(LAYER_GENERATOR_CACHE_SIZE);
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
private final transient LazyBoundedCache<LayerGeneratorKey, CNG> heightGenerators =
@@ -145,7 +147,8 @@ public class IrisBiomePaletteLayer {
public CNG getLayerGenerator(RNG parent, int signature, IrisData data) {
Engine engine = data == null ? null : data.getEngine();
long generatorSeed = parent.getSeed() + signature + minHeight + maxHeight + getBlockData(data).size();
CachedLayerGenerator recent = recentLayerGenerator.get();
int slot = (int) generatorSeed & (LAYER_GENERATOR_CACHE_SIZE - 1);
CachedLayerGenerator recent = recentLayerGenerators.getAcquire(slot);
if (recent != null && recent.key.matches(data, engine, generatorSeed)) {
return recent.generator;
}
@@ -154,7 +157,7 @@ public class IrisBiomePaletteLayer {
CNG generator = layerGenerators.computeIfAbsent(key,
ignored -> style.create(new RNG(generatorSeed), data, engine));
if (generator != null) {
recentLayerGenerator.set(new CachedLayerGenerator(key, generator));
recentLayerGenerators.setRelease(slot, new CachedLayerGenerator(key, generator));
}
return generator;
}
@@ -0,0 +1,128 @@
package art.arcane.iris.generation.cache;
import art.arcane.volmlib.util.cache.CacheKey;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.function.Consumer;
/**
* A bounded, lock-free, lossy long-keyed cache for memoized values. Keys hash into 4-way sets and a full set
* evicts with CLOCK (second chance), so reads never write shared state except to set a reference bit once.
* Concurrent inserts of the same key may both compute; the first published value wins.
*/
public final class ConcurrentClockCache<V> {
private static final int WAYS = 4;
private final AtomicReferenceArray<Entry<V>> slots;
private final long sets;
public ConcurrentClockCache(int capacity) {
if (capacity <= 0) {
throw new IllegalArgumentException("Cache capacity must be positive");
}
int setCount = Math.ceilDiv(capacity, WAYS);
sets = setCount;
slots = new AtomicReferenceArray<>(Math.multiplyExact(setCount, WAYS));
}
public int capacity() {
return slots.length();
}
public V get(long key) {
long hash = CacheKey.mix(key);
int base = base(hash);
for (int way = 0; way < WAYS; way++) {
Entry<V> entry = slots.getAcquire(base + way);
if (entry != null && entry.key == key) {
entry.touch();
return entry.value;
}
}
return null;
}
public V putIfAbsent(long key, V value) {
Objects.requireNonNull(value, "Cached value");
long hash = CacheKey.mix(key);
int base = base(hash);
int hand = (int) (hash >>> 60) & (WAYS - 1);
while (true) {
int empty = -1;
for (int way = 0; way < WAYS; way++) {
Entry<V> entry = slots.getAcquire(base + way);
if (entry == null) {
if (empty < 0) {
empty = way;
}
} else if (entry.key == key) {
entry.touch();
return entry.value;
}
}
Entry<V> created = new Entry<>(key, value);
if (empty >= 0) {
if (slots.compareAndSet(base + empty, null, created)) {
return value;
}
continue;
}
int victim = -1;
Entry<V> victimEntry = null;
for (int step = 0; step < WAYS * 2 && victim < 0; step++) {
int way = (hand + step) & (WAYS - 1);
Entry<V> entry = slots.getAcquire(base + way);
if (entry == null || !entry.referenced) {
victim = way;
victimEntry = entry;
} else {
entry.referenced = false;
}
}
if (victim < 0) {
victim = hand;
victimEntry = slots.getAcquire(base + victim);
}
if (slots.compareAndSet(base + victim, victimEntry, created)) {
return value;
}
}
}
public void forEach(Consumer<? super V> action) {
for (int index = 0; index < slots.length(); index++) {
Entry<V> entry = slots.getAcquire(index);
if (entry != null) {
action.accept(entry.value);
}
}
}
public void clear() {
for (int index = 0; index < slots.length(); index++) {
slots.setRelease(index, null);
}
}
private int base(long hash) {
return (int) (((hash & 0xffffffffL) * sets) >>> 32) * WAYS;
}
private static final class Entry<V> {
private final long key;
private final V value;
private boolean referenced;
private Entry(long key, V value) {
this.key = key;
this.value = value;
}
private void touch() {
if (!referenced) {
referenced = true;
}
}
}
}
@@ -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<NativeBlockState> 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<NativeBlockState> 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<NativeBlockState> 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<NativeBlockState> 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<NativeBlockState> 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.
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<NativeBiome> {
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);
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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;
}
}
@@ -0,0 +1,67 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.chunk;
import art.arcane.volmlib.util.hunk.Hunk;
public final class ColumnExtentListeningHunk<T> implements Hunk<T>, ColumnExtent {
private final Hunk<T> source;
private final Listener<T> listener;
public ColumnExtentListeningHunk(Hunk<T> source, Listener<T> listener) {
this.source = source;
this.listener = listener;
}
@Override
public void setRaw(int x, int y, int z, T t) {
listener.onWrite(x, y, z, t);
source.setRaw(x, y, z, t);
}
@Override
public T getRaw(int x, int y, int z) {
return source.getRaw(x, y, z);
}
@Override
public int getWidth() {
return source.getWidth();
}
@Override
public int getHeight() {
return source.getHeight();
}
@Override
public int getDepth() {
return source.getDepth();
}
@Override
public int highestStoredY(int x, int z) {
return ColumnExtent.highestStoredY(source, x, z);
}
@FunctionalInterface
public interface Listener<T> {
void onWrite(int x, int y, int z, T value);
}
}
@@ -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;
}
@@ -8,6 +8,7 @@ import art.arcane.iris.generation.runtime.EngineMetrics;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.biome.FloatingIslandBoundarySampler;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
import art.arcane.iris.world.history.FloatingBiomeOverlay;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
@@ -23,6 +24,7 @@ public class ChunkContext {
private boolean naturalTerrain = true;
private Hunk<NativeBiome> terrainBiomes;
private FloatingBiomeOverlay floatingBiomes;
private CaveTerrainSnapshot caveTerrain;
private final int x;
private final int z;
private final IrisComplex complex;
@@ -157,6 +159,14 @@ public class ChunkContext {
return floatingBiomes;
}
public CaveTerrainSnapshot getCaveTerrain() {
return caveTerrain;
}
public void setCaveTerrain(CaveTerrainSnapshot caveTerrain) {
this.caveTerrain = caveTerrain;
}
public void setNaturalBiome(int localX, int y, int localZ, NativeBiome biome) {
if (terrainBiomes != null) {
terrainBiomes.setRaw(localX, y, localZ, biome);
@@ -230,6 +240,10 @@ public class ChunkContext {
return region;
}
public boolean hasDimensionStack() {
return dimensionStackContext != null;
}
public DimensionStackLayout getDimensionStackLayout(int localX, int localZ) {
if (dimensionStackContext == null) {
return null;
@@ -53,6 +53,8 @@ import lombok.experimental.Accessors;
@Description("Creates ore & other block deposits underground")
@Data
public class IrisDepositGenerator {
private static final int FOOTPRINT_MAXIMUM_SIZE = 64;
private static final ThreadLocal<FootprintCells> FOOTPRINT_CELLS = ThreadLocal.withInitial(FootprintCells::new);
private static final long FNV_OFFSET_BASIS = 0xcbf29ce484222325L;
private static final long FNV_PRIME = 0x100000001b3L;
@@ -267,6 +269,47 @@ public class IrisDepositGenerator {
return new IrisObject(1, 1, 1);
}
KSet<BlockPosition> cells = new KSet<>();
vanillaEllipsoidCells(rng, size, (x, y, z) -> cells.add(new BlockPosition(x, y, z)));
return objectFromCells(cells, rng, rdata);
}
/**
* Advances {@code rng} exactly as {@link #getClump(Engine, RNG, IrisData)} would and reports the clump's width
* and lowest block offset without building it, so a caller can reject the clump first and replay the build
* only for clumps it keeps. Returns null without drawing when the shape has no footprint path.
*/
public ClumpFootprint sampleClumpFootprint(RNG rng, IrisData rdata) {
if (shape != IrisDepositShape.VANILLA_ELLIPSOID || minSize > FOOTPRINT_MAXIMUM_SIZE
|| maxSize > FOOTPRINT_MAXIMUM_SIZE) {
return null;
}
int paletteSize = getBlockData(rdata).size();
if (paletteSize == 0) {
return null;
}
int size = rng.i(minSize, maxSize + 1);
if (size <= 0) {
return ClumpFootprint.EMPTY;
}
FootprintCells cells = FOOTPRINT_CELLS.get();
cells.reset();
vanillaEllipsoidCells(rng, size, cells);
if (cells.count == 0) {
return ClumpFootprint.EMPTY;
}
for (int cell = 0; cell < cells.count; cell++) {
rng.i(0, paletteSize);
}
return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, false);
}
/**
* Emits the clump's cells sphere by sphere in x, y, z order, duplicates included. The squared y and z
* offsets are computed once per sphere row; the sums keep the per-cell evaluation order, so every cell
* test is bit-identical to evaluating {@code nx * nx + ny * ny + nz * nz} in place.
*/
static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) {
float angle = rng.nextFloat() * (float) Math.PI;
float reach = size / 8F;
double startX = Math.sin(angle) * reach;
@@ -309,7 +352,8 @@ public class IrisDepositGenerator {
}
}
KSet<BlockPosition> cells = new KSet<>();
double[] squaredY = new double[16];
double[] squaredZ = new double[16];
for (int i = 0; i < size; i++) {
double radius = nodes[i * 4 + 3];
if (radius < 0D) {
@@ -324,28 +368,42 @@ public class IrisDepositGenerator {
int maxY = Math.max((int) Math.floor(centerY + radius), minY);
int minZ = (int) Math.floor(centerZ - radius);
int maxZ = Math.max((int) Math.floor(centerZ + radius), minZ);
int spanY = maxY - minY + 1;
int spanZ = maxZ - minZ + 1;
if (spanY > squaredY.length) {
squaredY = new double[spanY];
}
if (spanZ > squaredZ.length) {
squaredZ = new double[spanZ];
}
for (int y = 0; y < spanY; y++) {
double ny = (minY + y + 0.5D - centerY) / radius;
squaredY[y] = ny * ny;
}
for (int z = 0; z < spanZ; z++) {
double nz = (minZ + z + 0.5D - centerZ) / radius;
squaredZ[z] = nz * nz;
}
for (int x = minX; x <= maxX; x++) {
double nx = (x + 0.5D - centerX) / radius;
if (nx * nx >= 1D) {
double squaredX = nx * nx;
if (squaredX >= 1D) {
continue;
}
for (int y = minY; y <= maxY; y++) {
double ny = (y + 0.5D - centerY) / radius;
if (nx * nx + ny * ny >= 1D) {
for (int y = 0; y < spanY; y++) {
double squaredXY = squaredX + squaredY[y];
if (squaredXY >= 1D) {
continue;
}
for (int z = minZ; z <= maxZ; z++) {
double nz = (z + 0.5D - centerZ) / radius;
if (nx * nx + ny * ny + nz * nz < 1D) {
cells.add(new BlockPosition(x, y, z));
for (int z = 0; z < spanZ; z++) {
if (squaredXY + squaredZ[z] < 1D) {
sink.add(x, minY + y, minZ + z);
}
}
}
}
}
return objectFromCells(cells, rng, rdata);
}
IrisObject generateVanillaScattered(RNG rng, IrisData rdata, int size) {
@@ -564,4 +622,52 @@ public class IrisDepositGenerator {
record ClumpCacheKey(long depositSeed, int minSize, int maxSize) {
}
@FunctionalInterface
interface CellSink {
void add(int x, int y, int z);
}
public record ClumpFootprint(int width, int minY, boolean empty) {
static final ClumpFootprint EMPTY = new ClumpFootprint(1, 0, true);
}
private static final class FootprintCells implements CellSink {
private static final int OFFSET = 32;
private static final int AXIS = 64;
private final long[] occupied = new long[AXIS * AXIS * AXIS / 64];
private final int[] touched = new int[AXIS * AXIS * AXIS / 64];
private int touchedCount;
private int count;
private int extentX;
private int minY;
private void reset() {
for (int index = 0; index < touchedCount; index++) {
occupied[touched[index]] = 0L;
}
touchedCount = 0;
count = 0;
extentX = 0;
minY = Integer.MAX_VALUE;
}
@Override
public void add(int x, int y, int z) {
int index = ((x + OFFSET) * AXIS + (y + OFFSET)) * AXIS + (z + OFFSET);
int word = index >>> 6;
long bit = 1L << (index & 63);
long current = occupied[word];
if ((current & bit) != 0L) {
return;
}
if (current == 0L) {
touched[touchedCount++] = word;
}
occupied[word] = current | bit;
count++;
extentX = Math.max(extentX, Math.abs(x));
minY = Math.min(minY, y);
}
}
}
@@ -0,0 +1,99 @@
package art.arcane.iris.generation.decoration;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.math.RNG;
import java.util.Arrays;
/**
* One ore list (a biome's, region's or dimension's surface or underground ores) indexed by Y. Each Y
* holds the generators whose range contains it, in list order, so resolving a block tries exactly
* the generators that could place there and still returns the first one that does.
*/
public final class IrisOreBands {
public static final IrisOreBands EMPTY = new IrisOreBands(new IrisOreGenerator[0], 0, -1, null);
private static final int MAXIMUM_INDEXED_HEIGHT = 8192;
private final IrisOreGenerator[] generators;
private final int minY;
private final int maxY;
private final IrisOreGenerator[][] byY;
private IrisOreBands(IrisOreGenerator[] generators, int minY, int maxY, IrisOreGenerator[][] byY) {
this.generators = generators;
this.minY = minY;
this.maxY = maxY;
this.byY = byY;
}
public static IrisOreBands of(KList<IrisOreGenerator> ores) {
if (ores == null || ores.isEmpty()) {
return EMPTY;
}
IrisOreGenerator[] generators = ores.toArray(new IrisOreGenerator[0]);
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (IrisOreGenerator generator : generators) {
IrisRange range = generator.getRange();
min = Math.min(min, range.getMin());
max = Math.max(max, range.getMax());
}
if (!(min <= max)) {
return EMPTY;
}
int minY = (int) Math.ceil(min);
int maxY = (int) Math.floor(max);
if (minY > maxY) {
return EMPTY;
}
long height = (long) maxY - minY + 1L;
return new IrisOreBands(generators, minY, maxY,
height > MAXIMUM_INDEXED_HEIGHT ? null : index(generators, minY, (int) height));
}
private static IrisOreGenerator[][] index(IrisOreGenerator[] generators, int minY, int height) {
IrisOreGenerator[][] byY = new IrisOreGenerator[height][];
IrisOreGenerator[] scratch = new IrisOreGenerator[generators.length];
IrisOreGenerator[] previous = null;
for (int offset = 0; offset < height; offset++) {
int y = minY + offset;
int count = 0;
for (IrisOreGenerator generator : generators) {
if (generator.getRange().contains(y)) {
scratch[count++] = generator;
}
}
IrisOreGenerator[] candidates = Arrays.copyOf(scratch, count);
byY[offset] = previous != null && Arrays.equals(previous, candidates) ? previous : candidates;
previous = byY[offset];
}
return byY;
}
public boolean hasOres() {
return minY <= maxY;
}
public boolean contains(int y) {
return y >= minY && y <= maxY;
}
/**
* The ore placed at a Y inside {@link #contains(int)}, or null when no generator places one.
*/
public NativeBlockState generate(int x, int y, int z, RNG rng, IrisData data) {
IrisOreGenerator[] candidates = byY == null ? generators : byY[y - minY];
for (IrisOreGenerator candidate : candidates) {
NativeBlockState ore = candidate.generate(x, y, z, rng, data);
if (ore != null) {
return ore;
}
}
return null;
}
}
@@ -55,10 +55,10 @@ public class IrisOreGenerator {
@Description("Vertical band (min, max) this ore can generate in, in engine-local Y where 0 is the bottom of the dimension, not world Y.")
private IrisRange range = new IrisRange(30, 80);
private transient AtomicCache<CNG> chanceCache = new AtomicCache<>();
private transient AtomicCache<OreChance> chanceCache = new AtomicCache<>();
public void warm(RNG rng, IrisData data) {
chanceCache.aquire(() -> chanceStyle.create(rng, data));
chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data)));
palette.getLayerGenerator(rng, data);
}
@@ -71,12 +71,30 @@ public class IrisOreGenerator {
return null;
}
CNG chance = chanceCache.aquire(() -> chanceStyle.create(rng, data));
OreChance chance = chanceCache.getIfPresent();
if (chance == null) {
chance = chanceCache.aquire(() -> OreChance.of(chanceStyle.create(rng, data)));
}
if (chance.noise(x, y, z) > threshold) {
if (chance.sample(x, y, z) > threshold) {
return null;
}
return palette.get(rng, x, y, z, data);
}
/**
* The chance field, with its single value resolved once when it is constant (a FLAT style),
* which bands such as a full deepslate layer sample at every block.
*/
private record OreChance(CNG noise, boolean constant, double value) {
private static OreChance of(CNG noise) {
boolean constant = noise.isConstant();
return new OreChance(noise, constant, constant ? noise.noise(0D, 0D, 0D) : 0D);
}
private double sample(int x, int y, int z) {
return constant ? value : noise.noise(x, y, z);
}
}
}
@@ -1,45 +0,0 @@
package art.arcane.iris.generation.decoration;
import art.arcane.iris.pack.value.IrisRange;
import art.arcane.volmlib.util.collection.KList;
public final class IrisOreGeneratorBounds {
public static final IrisOreGeneratorBounds EMPTY = new IrisOreGeneratorBounds(false, 0D, 0D);
private final boolean hasOres;
private final double min;
private final double max;
private IrisOreGeneratorBounds(boolean hasOres, double min, double max) {
this.hasOres = hasOres;
this.min = min;
this.max = max;
}
public static IrisOreGeneratorBounds of(KList<IrisOreGenerator> ores) {
if (ores == null || ores.isEmpty()) {
return EMPTY;
}
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
int oreCount = ores.size();
for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) {
IrisOreGenerator oreGenerator = ores.get(oreIndex);
IrisRange range = oreGenerator.getRange();
min = Math.min(min, range.getMin());
max = Math.max(max, range.getMax());
}
return new IrisOreGeneratorBounds(true, min, max);
}
public boolean hasOres() {
return hasOres;
}
public boolean contains(int y) {
return hasOres && y >= min && y <= max;
}
}
@@ -0,0 +1,64 @@
package art.arcane.iris.generation.hydrology;
/**
* The chunks a planned tile publishes at least one footprint column into. A chunk outside every
* relevant tile's occupancy composes to no hydrology at all, so it can be answered without the
* tiles being resident.
*/
final class HydrologyChunkOccupancy {
static final HydrologyChunkOccupancy EMPTY = new HydrologyChunkOccupancy(0, 0, 0, 0, new long[0]);
private final int minimumChunkX;
private final int minimumChunkZ;
private final int width;
private final int depth;
private final long[] bits;
private HydrologyChunkOccupancy(int minimumChunkX, int minimumChunkZ, int width, int depth, long[] bits) {
this.minimumChunkX = minimumChunkX;
this.minimumChunkZ = minimumChunkZ;
this.width = width;
this.depth = depth;
this.bits = bits;
}
static HydrologyChunkOccupancy of(RiverFootprint footprint) {
if (footprint == null) {
return EMPTY;
}
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (long packed : footprint.columns().keySet()) {
int chunkX = RiverFootprint.unpackX(packed) >> 4;
int chunkZ = RiverFootprint.unpackZ(packed) >> 4;
minimumX = Math.min(minimumX, chunkX);
minimumZ = Math.min(minimumZ, chunkZ);
maximumX = Math.max(maximumX, chunkX);
maximumZ = Math.max(maximumZ, chunkZ);
}
if (minimumX > maximumX) {
return EMPTY;
}
int width = maximumX - minimumX + 1;
int depth = maximumZ - minimumZ + 1;
long[] bits = new long[Math.toIntExact(Math.ceilDiv(Math.multiplyExact((long) width, depth), 64L))];
for (long packed : footprint.columns().keySet()) {
int index = ((RiverFootprint.unpackZ(packed) >> 4) - minimumZ) * width
+ (RiverFootprint.unpackX(packed) >> 4) - minimumX;
bits[index >>> 6] |= 1L << index;
}
return new HydrologyChunkOccupancy(minimumX, minimumZ, width, depth, bits);
}
boolean occupies(int chunkX, int chunkZ) {
int localX = chunkX - minimumChunkX;
int localZ = chunkZ - minimumChunkZ;
if (localX < 0 || localZ < 0 || localX >= width || localZ >= depth) {
return false;
}
int index = localZ * width + localX;
return (bits[index >>> 6] & 1L << index) != 0L;
}
}
@@ -28,6 +28,8 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinWorkerThread;
import java.util.function.Function;
import java.util.function.Predicate;
public final class HydrologyPlanner {
static final int ROUTING_CONTEXT_CACHE_SIZE = 64;
@@ -64,6 +66,8 @@ public final class HydrologyPlanner {
final HydrologySegmentBuilder segments;
final HydrologyFeatureSitePlanner featureSites;
final HydrologyRegionalPlanner regional;
private volatile Predicate<HydrologyTileKey> earlyOwnerScope;
private volatile Function<HydrologyTileKey, HydrologyTile> plannedTiles;
public HydrologyPlanner(long worldSeed, HydrologyPlannerSettings settings, HydrologyTerrainSampler sampler) {
this(
@@ -244,6 +248,23 @@ public final class HydrologyPlanner {
return List.copyOf(unique);
}
/**
* Limits the lower-rank neighbour drafts an owner starts before its admission demands them to the
* tiles the scope accepts; null starts all of them. A neighbour that no admission demands and
* nothing plans is a draft computed for nothing, and so are the early neighbours it starts itself.
*/
void limitEarlyOwners(Predicate<HydrologyTileKey> scope) {
earlyOwnerScope = scope;
}
/**
* Where the plans of tiles already planned are found. An owner draft that left the owner cache is
* restored from its tile's plan when a neighbour demands it again, instead of being drafted twice.
*/
void usePlannedTiles(Function<HydrologyTileKey, HydrologyTile> source) {
plannedTiles = source;
}
void clearOwnerDrafts() {
resolvedOwners.invalidateAll();
regional.clear();
@@ -289,6 +310,9 @@ public final class HydrologyPlanner {
return local;
}
CrossTileResolvedOwner cached = resolvedOwners.getIfPresent(key);
if (cached == null) {
cached = plannedOwner(key);
}
if (cached != null) {
context.remember(key, cached);
return cached;
@@ -335,6 +359,17 @@ public final class HydrologyPlanner {
}
}
private CrossTileResolvedOwner plannedOwner(HydrologyTileKey key) {
Function<HydrologyTileKey, HydrologyTile> source = plannedTiles;
HydrologyTile tile = source == null ? null : source.apply(key);
if (tile == null || tile.resolvedOwner() == null) {
return null;
}
reuseResolvedTile(tile);
CrossTileResolvedOwner restored = resolvedOwners.getIfPresent(key);
return restored == null ? tile.resolvedOwner() : restored;
}
List<CrossTileResolvedOwner> resolveLowerRankOwners(
List<HydrologyTileKey> candidateKeys,
CrossTileResolutionContext context,
@@ -751,6 +786,7 @@ public final class HydrologyPlanner {
return;
}
int radius = settings.crossTileColorPeriod() - 1;
Predicate<HydrologyTileKey> scope = earlyOwnerScope;
ArrayList<HydrologyTileKey> candidates = new ArrayList<>();
for (long tileZ = (long) ownerKey.tileZ() - radius; tileZ <= (long) ownerKey.tileZ() + radius; tileZ++) {
for (long tileX = (long) ownerKey.tileX() - radius; tileX <= (long) ownerKey.tileX() + radius; tileX++) {
@@ -759,7 +795,8 @@ public final class HydrologyPlanner {
continue;
}
HydrologyTileKey key = new HydrologyTileKey((int) tileX, (int) tileZ);
if (crossTile.ownerColorRank(key) >= ownerRank || preparedOwners.containsKey(key)
if (crossTile.ownerColorRank(key) >= ownerRank || scope != null && !scope.test(key)
|| preparedOwners.containsKey(key)
|| resolvedOwners.getIfPresent(key) != null || resolvingOwners.containsKey(key)) {
continue;
}
@@ -2,6 +2,7 @@ package art.arcane.iris.generation.hydrology;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.volmlib.util.cache.CacheKey;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
@@ -9,7 +10,7 @@ import com.github.benmanes.caffeine.cache.Caffeine;
import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
@@ -36,6 +37,13 @@ public final class HydrologyTileCache implements AutoCloseable {
private static final int MAXIMUM_COMPOSED_CHUNKS = 256;
private static final int CHUNK_SIZE = 16;
private static final int CHUNK_COLUMN_COUNT = CHUNK_SIZE * CHUNK_SIZE;
private static final int LOCAL_CHUNK_SLOTS = 16;
private static final byte AROUND_UNKNOWN = 0;
private static final byte AROUND_EMPTY = 1;
private static final byte AROUND_PUBLISHED = 2;
private static final int MAXIMUM_PREGENERATION_TILES = 4096;
private static final int PREGENERATION_HALO_BLOCKS = 512;
private static final HydrologyColumnSample[] NO_COLUMNS = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
private static final Cache<SharedTileKey, HydrologyTile> SHARED_TILES = Caffeine.newBuilder()
.maximumWeight(HydrologyCacheBudget.sharedStudioBytes())
.weigher((SharedTileKey key, HydrologyTile tile) -> HydrologyCacheWeights.bounded(
@@ -48,17 +56,18 @@ public final class HydrologyTileCache implements AutoCloseable {
private final int publicationRadius;
private final int maximumEntries;
private final Cache<HydrologyTileKey, HydrologyTile> tiles;
private final ConcurrentHashMap<HydrologyTileKey, HydrologyChunkOccupancy> occupancies;
private final Cache<Long, ChunkColumns> composedChunks;
private final Cache<HydrologyTileKey, List<HydrologyDiagnosticCandidate>> diagnostics;
private final AtomicLong cacheEpoch;
private final AtomicBoolean closed;
private final ThreadLocal<LocalChunkColumns> localChunkColumns;
private final ThreadLocal<LocalChunks> localChunks;
private final Executor prefetchExecutor;
private final int prefetchSlots;
private final ConcurrentLinkedQueue<HydrologyTileKey> prefetchQueue;
private final Set<HydrologyTileKey> queuedPrefetches;
private final LinkedHashSet<ChunkCoordinate> queriedChunks;
private final Set<CacheLoadKey<HydrologyTileKey>> demandedPlans;
private final AtomicBoolean prefetchActive;
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, CompletableFuture<HydrologyTile>> planning;
private final ConcurrentHashMap<CacheLoadKey<HydrologyTileKey>, PendingLoad<HydrologyTile>> loading;
private final ConcurrentHashMap<CacheLoadKey<Long>, PendingLoad<ChunkColumns>> composing;
@@ -72,6 +81,8 @@ public final class HydrologyTileCache implements AutoCloseable {
private volatile Runnable terrainPreparation;
private PregenerationScope pregenerationScope;
private int demandBatches;
private int activePrefetches;
private boolean pumping;
public HydrologyTileCache(HydrologyPlanner planner) {
this(planner, DEFAULT_MAXIMUM_ENTRIES, null, null, null);
@@ -112,6 +123,7 @@ public final class HydrologyTileCache implements AutoCloseable {
this.tileSize = planner.settings().routing().tileSize();
this.publicationRadius = planner.settings().publicationRadius();
this.prefetchExecutor = prefetchExecutor;
this.prefetchSlots = prefetchSlots(HydrologyPlanningAdmission.maximumRoots());
this.waitingForbidden = waitingForbidden;
this.sharedCacheScope = sharedCacheScope;
this.persistentStore = null;
@@ -128,7 +140,7 @@ public final class HydrologyTileCache implements AutoCloseable {
this.queuedPrefetches = ConcurrentHashMap.newKeySet();
this.queriedChunks = new LinkedHashSet<>();
this.demandedPlans = new HashSet<>();
this.prefetchActive = new AtomicBoolean();
this.occupancies = new ConcurrentHashMap<>();
HydrologyCacheBudget budget = HydrologyCacheBudget.runtime();
this.tiles = Caffeine.newBuilder()
.maximumWeight(budget.tileBytes())
@@ -147,8 +159,27 @@ public final class HydrologyTileCache implements AutoCloseable {
.build();
this.cacheEpoch = new AtomicLong();
this.closed = new AtomicBoolean();
this.localChunkColumns = new ThreadLocal<>();
this.localChunks = ThreadLocal.withInitial(LocalChunks::new);
this.neighbourPrefetchEnabled = true;
planner.usePlannedTiles(this::plannedTile);
}
/** A tile already planned, resident or in the prepared-plan store, loaded without planning it. */
private HydrologyTile plannedTile(HydrologyTileKey key) {
HydrologyTile resident = tiles.getIfPresent(key);
if (resident != null) {
return resident;
}
PreparedHydrologyTileStore store = persistentStore;
return store == null ? null : store.load(key).orElse(null);
}
/**
* Speculative roots run beside each other but leave one root permit free, so a chunk that needs a
* tile nobody queued yet does not wait behind a full row of prefetches.
*/
static int prefetchSlots(int maximumRoots) {
return Math.max(1, maximumRoots - 1);
}
/**
@@ -317,9 +348,14 @@ public final class HydrologyTileCache implements AutoCloseable {
try {
present = tiles.getIfPresent(key);
HydrologyTile tile = present == null ? planRequired(key, epoch) : present;
HydrologyChunkOccupancy occupancy = occupancies.get(key);
if (occupancy == null) {
occupancy = HydrologyChunkOccupancy.of(tile.footprint());
}
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
tiles.put(key, tile);
occupancies.putIfAbsent(key, occupancy);
}
}
owned.future().complete(tile);
@@ -355,9 +391,20 @@ public final class HydrologyTileCache implements AutoCloseable {
SHARED_TILES.invalidate(sharedKey);
}
}
if (attempt == 0L && store != null) {
HydrologyTile persisted = store.load(key).orElse(null);
if (persisted != null) {
planner.reuseResolvedTile(persisted);
if (sharedKey != null) {
SHARED_TILES.put(sharedKey, persisted);
}
IrisLogging.debug("Loaded persisted prepared hydrology tile %d,%d", key.tileX(), key.tileZ());
return persisted;
}
}
try (HydrologyPlanningAdmission.Permit ignored = HydrologyPlanningAdmission.acquireRoot(
closed::get)) {
return planAdmitted(key, planningEpoch, sharedKey, store, attempt == 0L);
return planAdmitted(key, planningEpoch, sharedKey, store);
}
} catch (RuntimeException failure) {
if (failure instanceof CancellationException || interrupted(failure)) {
@@ -407,18 +454,9 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
/** Plans the tile under a root permit; a persisted plan is restored before any permit is taken. */
private HydrologyTile planAdmitted(HydrologyTileKey key, long planningEpoch,
SharedTileKey sharedKey, PreparedHydrologyTileStore store,
boolean reusePrepared) {
if (store != null && reusePrepared) {
HydrologyTile persisted = store.load(key).orElse(null);
if (persisted != null) {
planner.reuseResolvedTile(persisted);
SHARED_TILES.put(sharedKey, persisted);
IrisLogging.debug("Loaded persisted prepared hydrology tile %d,%d", key.tileX(), key.tileZ());
return persisted;
}
}
SharedTileKey sharedKey, PreparedHydrologyTileStore store) {
prepareTerrain();
HydrologyTile planned = planner.plan(key);
synchronized (publicationLock) {
@@ -470,10 +508,12 @@ public final class HydrologyTileCache implements AutoCloseable {
}
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
LocalChunkColumns local = localChunkColumns.get();
if (local != null && local.epoch() == cacheEpoch.get()
&& local.columns().chunkX() == chunkX && local.columns().chunkZ() == chunkZ) {
return HydrologyColumnSnapshot.ready(local.columns().columnAt(blockX, blockZ).orElse(null));
ChunkColumns local = localChunks(cacheEpoch.get()).get(chunkX, chunkZ);
if (local != null) {
return HydrologyColumnSnapshot.ready(local.columnAt(blockX, blockZ).orElse(null));
}
if (knownEmpty(chunkX, chunkZ)) {
return HydrologyColumnSnapshot.ready(null);
}
ChunkColumns composed = composedChunks.getIfPresent(CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ)));
if (composed != null) {
@@ -485,6 +525,7 @@ public final class HydrologyTileCache implements AutoCloseable {
HydrologyTile tile = tiles.getIfPresent(key);
if (tile == null) {
requestUnplanned(chunkX, chunkZ);
ProvisionalSampling.mark();
return HydrologyColumnSnapshot.unavailable();
}
snapshots.add(tile);
@@ -497,18 +538,56 @@ public final class HydrologyTileCache implements AutoCloseable {
}
/**
* Whether every tile the column's chunk composes from is already planned. A caller that must not
* wait (the server thread answering a height or biome query) uses this before sampling: when the
* answer is false the missing tiles are handed to the prefetch executor so a later query finds
* them, and nothing is planned on the caller.
* Whether the column's chunk can be answered without planning: every tile it composes from is
* already planned. A caller that must not wait (the server thread answering a height or biome
* query) uses this before sampling: when the answer is false the missing tiles are handed to the
* prefetch executor so a later query finds them, nothing is planned on the caller, and the caller
* is marked as answering provisionally.
*/
public boolean isPlanned(int blockX, int blockZ) {
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
if (composedChunks.getIfPresent(CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ))) != null) {
if (knownEmpty(chunkX, chunkZ)
|| composedChunks.getIfPresent(CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ))) != null) {
return true;
}
return !requestUnplanned(chunkX, chunkZ);
if (requestUnplanned(chunkX, chunkZ)) {
ProvisionalSampling.mark();
return false;
}
return true;
}
/**
* Whether any planned footprint column lies in the chunk or one of its eight neighbours, which
* decides whether the chunk can hold published hydrology at all. Tiles are planned like a sample
* would plan them, and each thread remembers its recent final answers.
*/
public boolean hasColumnsAround(int chunkX, int chunkZ) {
long epoch = cacheEpoch.get();
LocalChunks local = localChunks(epoch);
int slot = LocalChunks.slot(chunkX, chunkZ);
long key = RiverFootprint.pack(chunkX, chunkZ);
if (local.around[slot] != AROUND_UNKNOWN && local.aroundKeys[slot] == key) {
return local.around[slot] == AROUND_PUBLISHED;
}
boolean published = publishesAround(chunkX, chunkZ);
if (!waitsForbidden() && local.epoch == epoch) {
local.aroundKeys[slot] = key;
local.around[slot] = published ? AROUND_PUBLISHED : AROUND_EMPTY;
}
return published;
}
private boolean publishesAround(int chunkX, int chunkZ) {
for (int offsetZ = -1; offsetZ <= 1; offsetZ++) {
for (int offsetX = -1; offsetX <= 1; offsetX++) {
if (!chunkColumns((chunkX + offsetX) * CHUNK_SIZE, (chunkZ + offsetZ) * CHUNK_SIZE).empty()) {
return true;
}
}
}
return false;
}
/**
@@ -567,6 +646,7 @@ public final class HydrologyTileCache implements AutoCloseable {
cacheEpoch.incrementAndGet();
planner.clearOwnerDrafts();
tiles.invalidateAll();
occupancies.clear();
composedChunks.invalidateAll();
diagnostics.invalidateAll();
composing.forEach((key, load) -> {
@@ -581,7 +661,7 @@ public final class HydrologyTileCache implements AutoCloseable {
demandedPlans.clear();
demandBatches = 0;
}
localChunkColumns.remove();
localChunks.remove();
for (CompletableFuture<ChunkColumns> future : cancelledQueries) {
future.completeExceptionally(new CancellationException("Hydrology query was invalidated."));
}
@@ -600,20 +680,28 @@ public final class HydrologyTileCache implements AutoCloseable {
neighbourPrefetchEnabled = enabled;
}
/**
* Queues every tile the area's chunks compose from, nearest the centre first and at most
* {@link #MAXIMUM_PREGENERATION_TILES} of them, so planning runs ahead of the generation front and
* each plan lands in the prepared-plan store. Speculation stays inside the area plus a halo that
* covers generation reaching past its edge.
*/
public PregenerationScope preparePregeneration(PregenerationArea area) {
Objects.requireNonNull(area, "area");
long reach = (long) PREGENERATION_HALO_BLOCKS + publicationRadius;
TileBounds bounds = new TileBounds(
tileCoordinate(Math.subtractExact(area.minimumBlockX(), reach), tileSize),
tileCoordinate(Math.subtractExact(area.minimumBlockZ(), reach), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockX(), reach), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockZ(), reach), tileSize));
TileBounds required = new TileBounds(
tileCoordinate(Math.subtractExact(area.minimumBlockX(), publicationRadius), tileSize),
tileCoordinate(Math.subtractExact(area.minimumBlockZ(), publicationRadius), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockX(), publicationRadius), tileSize),
tileCoordinate(Math.addExact(area.maximumBlockZ(), publicationRadius), tileSize));
List<HydrologyTileKey> keys = prefetchAreaKeys(
Math.max(area.minimumBlockX(), (long) area.centerBlockX() - tileSize),
Math.max(area.minimumBlockZ(), (long) area.centerBlockZ() - tileSize),
Math.min(area.maximumBlockX(), (long) area.centerBlockX() + tileSize),
Math.min(area.maximumBlockZ(), (long) area.centerBlockZ() + tileSize),
area.centerBlockX(), area.centerBlockZ());
PregenerationScope scope = new PregenerationScope(bounds);
List<HydrologyTileKey> keys = nearestFirst(required, tileCoordinate(area.centerBlockX(), tileSize),
tileCoordinate(area.centerBlockZ(), tileSize), MAXIMUM_PREGENERATION_TILES);
PregenerationScope scope = new PregenerationScope(bounds, required);
try {
enqueuePrefetchArea(keys, scope);
return scope;
@@ -713,47 +801,109 @@ public final class HydrologyTileCache implements AutoCloseable {
&& tile.tileSize() == tileSize;
}
/**
* The chunk's composed columns. A chunk no planned tile publishes into is answered from the
* occupancy index alone, without composition or the tiles being resident; the last chunks each
* thread resolved answer repeated lookups without touching any shared structure.
*/
private ChunkColumns chunkColumns(int blockX, int blockZ) {
requireOpen();
int chunkX = Math.floorDiv(blockX, CHUNK_SIZE);
int chunkZ = Math.floorDiv(blockZ, CHUNK_SIZE);
long epoch = cacheEpoch.get();
LocalChunkColumns local = localChunkColumns.get();
if (local != null && local.epoch() == epoch
&& local.columns().chunkX() == chunkX
&& local.columns().chunkZ() == chunkZ) {
return local.columns();
LocalChunks local = localChunks(epoch);
ChunkColumns cached = local.get(chunkX, chunkZ);
if (cached != null) {
return cached;
}
long packedChunk = CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ));
if (waitsForbidden()) {
ChunkColumns present = composedChunks.getIfPresent(packedChunk);
if (present != null) {
return present;
TileBounds relevant = chunkTiles(chunkX, chunkZ);
ChunkColumns columns;
if (knownEmpty(chunkX, chunkZ, relevant)) {
columns = local.put(ChunkColumns.empty(chunkX, chunkZ));
} else if (waitsForbidden()) {
columns = nonblockingChunkColumns(chunkX, chunkZ, epoch);
if (columns == null) {
ProvisionalSampling.mark();
columns = ChunkColumns.empty(chunkX, chunkZ);
} else {
local.put(columns);
}
ArrayList<HydrologyTileKey> keys = relevantKeys(chunkX, chunkZ);
ArrayList<HydrologyTile> snapshots = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
HydrologyTile tile = tiles.getIfPresent(key);
if (tile == null) {
requestChunk(chunkX, chunkZ);
return ChunkColumns.empty(chunkX, chunkZ);
}
snapshots.add(tile);
}
ChunkColumns columns = composeChunkColumns(chunkX, chunkZ, keys, snapshots);
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
composedChunks.put(packedChunk, columns);
}
}
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
return columns;
} else {
long packedChunk = CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ));
columns = local.put(loadChunkColumns(new CacheLoadKey<>(epoch, packedChunk), chunkX, chunkZ));
}
if (!relevant.equals(local.prefetched)) {
local.prefetched = relevant;
prefetchNeighbours(relevant);
}
ChunkColumns columns = loadChunkColumns(new CacheLoadKey<>(epoch, packedChunk), chunkX, chunkZ);
localChunkColumns.set(new LocalChunkColumns(epoch, columns));
return columns;
}
/** The chunk's columns from resident tiles only, or null after handing its missing tiles to the planner. */
private ChunkColumns nonblockingChunkColumns(int chunkX, int chunkZ, long epoch) {
long packedChunk = CacheKey.mix(RiverFootprint.pack(chunkX, chunkZ));
ChunkColumns present = composedChunks.getIfPresent(packedChunk);
if (present != null) {
return present;
}
ArrayList<HydrologyTileKey> keys = relevantKeys(chunkX, chunkZ);
ArrayList<HydrologyTile> snapshots = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
HydrologyTile tile = tiles.getIfPresent(key);
if (tile == null) {
requestChunk(chunkX, chunkZ);
return null;
}
snapshots.add(tile);
}
ChunkColumns columns = composeChunkColumns(chunkX, chunkZ, keys, snapshots);
synchronized (publicationLock) {
if (!closed.get() && cacheEpoch.get() == epoch) {
composedChunks.put(packedChunk, columns);
}
}
return columns;
}
private LocalChunks localChunks(long epoch) {
LocalChunks local = localChunks.get();
if (local.epoch != epoch) {
Arrays.fill(local.slots, null);
Arrays.fill(local.around, AROUND_UNKNOWN);
local.prefetched = null;
local.epoch = epoch;
}
return local;
}
/** The tiles a chunk's columns compose from: every tile within the publication radius of the chunk. */
private TileBounds chunkTiles(int chunkX, int chunkZ) {
long minimumBlockX = (long) chunkX * CHUNK_SIZE;
long minimumBlockZ = (long) chunkZ * CHUNK_SIZE;
return new TileBounds(
tileCoordinate(minimumBlockX - publicationRadius, tileSize),
tileCoordinate(minimumBlockZ - publicationRadius, tileSize),
tileCoordinate(minimumBlockX + CHUNK_SIZE - 1L + publicationRadius, tileSize),
tileCoordinate(minimumBlockZ + CHUNK_SIZE - 1L + publicationRadius, tileSize));
}
private boolean knownEmpty(int chunkX, int chunkZ) {
return knownEmpty(chunkX, chunkZ, chunkTiles(chunkX, chunkZ));
}
/** Whether every tile the chunk composes from is planned and none of them publishes into it. */
private boolean knownEmpty(int chunkX, int chunkZ, TileBounds relevant) {
for (int tileZ = relevant.minimumZ(); tileZ <= relevant.maximumZ(); tileZ++) {
for (int tileX = relevant.minimumX(); tileX <= relevant.maximumX(); tileX++) {
HydrologyChunkOccupancy occupancy = occupancies.get(new HydrologyTileKey(tileX, tileZ));
if (occupancy == null || occupancy.occupies(chunkX, chunkZ)) {
return false;
}
}
}
return true;
}
private ChunkColumns loadChunkColumns(CacheLoadKey<Long> loadKey, int chunkX, int chunkZ) {
long epoch = loadKey.epoch();
long packedChunk = loadKey.key();
@@ -801,19 +951,12 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
/** The tiles a chunk's columns compose from: every tile within the publication radius of the chunk. */
private ArrayList<HydrologyTileKey> relevantKeys(int chunkX, int chunkZ) {
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) minimumBlockX + CHUNK_SIZE - 1L + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) minimumBlockZ + CHUNK_SIZE - 1L + publicationRadius, tileSize);
ArrayList<HydrologyTileKey> relevantKeys = new ArrayList<>(
Math.multiplyExact(maximumTileX - minimumTileX + 1, maximumTileZ - minimumTileZ + 1)
);
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
TileBounds relevant = chunkTiles(chunkX, chunkZ);
ArrayList<HydrologyTileKey> relevantKeys = new ArrayList<>(Math.multiplyExact(
relevant.maximumX() - relevant.minimumX() + 1, relevant.maximumZ() - relevant.minimumZ() + 1));
for (int tileZ = relevant.minimumZ(); tileZ <= relevant.maximumZ(); tileZ++) {
for (int tileX = relevant.minimumX(); tileX <= relevant.maximumX(); tileX++) {
relevantKeys.add(new HydrologyTileKey(tileX, tileZ));
}
}
@@ -821,11 +964,17 @@ public final class HydrologyTileCache implements AutoCloseable {
}
private ChunkColumns composeChunkColumns(int chunkX, int chunkZ) {
if (knownEmpty(chunkX, chunkZ)) {
return ChunkColumns.empty(chunkX, chunkZ);
}
ArrayList<HydrologyTileKey> relevantKeys = relevantKeys(chunkX, chunkZ);
return composeChunkColumns(chunkX, chunkZ, relevantKeys, tiles(relevantKeys));
}
private ChunkColumns composeChunkColumnsInline(int chunkX, int chunkZ, long epoch) {
if (knownEmpty(chunkX, chunkZ)) {
return ChunkColumns.empty(chunkX, chunkZ);
}
ArrayList<HydrologyTileKey> keys = relevantKeys(chunkX, chunkZ);
ArrayList<HydrologyTile> snapshots = new ArrayList<>(keys.size());
for (HydrologyTileKey key : keys) {
@@ -839,41 +988,43 @@ public final class HydrologyTileCache implements AutoCloseable {
List<HydrologyTile> relevantTiles) {
int minimumBlockX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumBlockZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
int minimumTileX = relevantKeys.getFirst().tileX();
int maximumTileX = relevantKeys.getLast().tileX();
int minimumTileZ = relevantKeys.getFirst().tileZ();
int maximumTileZ = relevantKeys.getLast().tileZ();
prefetchNeighbours(minimumTileX, maximumTileX, minimumTileZ, maximumTileZ);
if (knownEmpty(chunkX, chunkZ)) {
return ChunkColumns.empty(chunkX, chunkZ);
}
HydrologyColumnSample[] columns = new HydrologyColumnSample[CHUNK_COLUMN_COUNT];
boolean published = false;
for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) {
for (int localX = 0; localX < CHUNK_SIZE; localX++) {
int blockX = minimumBlockX + localX;
int blockZ = minimumBlockZ + localZ;
columns[localZ * CHUNK_SIZE + localX] = composeColumn(
HydrologyColumnSample column = composeColumn(
blockX,
blockZ,
relevantKeys,
relevantTiles
)
.orElse(null);
columns[localZ * CHUNK_SIZE + localX] = column;
published |= column != null;
}
}
return new ChunkColumns(chunkX, chunkZ, columns);
return published ? new ChunkColumns(chunkX, chunkZ, columns) : ChunkColumns.empty(chunkX, chunkZ);
}
/**
* Plans the ring of tiles around the ones a chunk just needed, on the prefetch executor, so a
* generation front walking outward finds its next tiles already planned instead of stalling on
* a cold plan. Tiles already cached or already being prefetched are skipped; a tile a chunk
* needs before its prefetch finishes simply joins that computation.
* Plans the ring of tiles around the ones a chunk needs, on the prefetch executor, so a generation
* front walking outward finds its next tiles already planned instead of stalling on a cold plan.
* Tiles already planned or already being prefetched are skipped; a tile a chunk needs before its
* prefetch finishes simply joins that computation.
*/
private void prefetchNeighbours(int minimumTileX, int maximumTileX, int minimumTileZ, int maximumTileZ) {
private void prefetchNeighbours(TileBounds relevant) {
if (prefetchExecutor == null || !neighbourPrefetchEnabled) {
return;
}
for (int tileZ = minimumTileZ - 1; tileZ <= maximumTileZ + 1; tileZ++) {
for (int tileX = minimumTileX - 1; tileX <= maximumTileX + 1; tileX++) {
if (tileX >= minimumTileX && tileX <= maximumTileX && tileZ >= minimumTileZ && tileZ <= maximumTileZ) {
for (int tileZ = relevant.minimumZ() - 1; tileZ <= relevant.maximumZ() + 1; tileZ++) {
for (int tileX = relevant.minimumX() - 1; tileX <= relevant.maximumX() + 1; tileX++) {
if (tileX >= relevant.minimumX() && tileX <= relevant.maximumX()
&& tileZ >= relevant.minimumZ() && tileZ <= relevant.maximumZ()) {
continue;
}
prefetch(new HydrologyTileKey(tileX, tileZ));
@@ -886,33 +1037,51 @@ public final class HydrologyTileCache implements AutoCloseable {
if (prefetchExecutor == null) {
return;
}
enqueuePrefetchArea(prefetchAreaKeys(minimumBlockX, minimumBlockZ, maximumBlockX, maximumBlockZ,
centreBlockX, centreBlockZ), null);
TileBounds area = new TileBounds(
tileCoordinate((long) minimumBlockX - publicationRadius, tileSize),
tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize),
tileCoordinate((long) maximumBlockX + publicationRadius, tileSize),
tileCoordinate((long) maximumBlockZ + publicationRadius, tileSize));
enqueuePrefetchArea(nearestFirst(area, tileCoordinate(centreBlockX, tileSize),
tileCoordinate(centreBlockZ, tileSize), Integer.MAX_VALUE), null);
}
private List<HydrologyTileKey> prefetchAreaKeys(long minimumBlockX, long minimumBlockZ, long maximumBlockX,
long maximumBlockZ, int centreBlockX, int centreBlockZ) {
int minimumTileX = tileCoordinate((long) minimumBlockX - publicationRadius, tileSize);
int maximumTileX = tileCoordinate((long) maximumBlockX + publicationRadius, tileSize);
int minimumTileZ = tileCoordinate((long) minimumBlockZ - publicationRadius, tileSize);
int maximumTileZ = tileCoordinate((long) maximumBlockZ + publicationRadius, tileSize);
int centreTileX = tileCoordinate(centreBlockX, tileSize);
int centreTileZ = tileCoordinate(centreBlockZ, tileSize);
/** Tiles of the bounds in rings around the centre tile, each ring in row order, up to the limit. */
static List<HydrologyTileKey> nearestFirst(TileBounds bounds, int centreX, int centreZ, int limit) {
ArrayList<HydrologyTileKey> keys = new ArrayList<>();
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
keys.add(new HydrologyTileKey(tileX, tileZ));
long rings = Math.max(
Math.max((long) centreX - bounds.minimumX(), (long) bounds.maximumX() - centreX),
Math.max((long) centreZ - bounds.minimumZ(), (long) bounds.maximumZ() - centreZ));
for (long ring = 0L; ring <= rings && keys.size() < limit; ring++) {
long firstZ = Math.max(bounds.minimumZ(), centreZ - ring);
long lastZ = Math.min(bounds.maximumZ(), centreZ + ring);
for (long tileZ = firstZ; tileZ <= lastZ && keys.size() < limit; tileZ++) {
if (tileZ == centreZ - ring || tileZ == centreZ + ring) {
long firstX = Math.max(bounds.minimumX(), centreX - ring);
long lastX = Math.min(bounds.maximumX(), centreX + ring);
for (long tileX = firstX; tileX <= lastX && keys.size() < limit; tileX++) {
keys.add(new HydrologyTileKey((int) tileX, (int) tileZ));
}
continue;
}
long westX = centreX - ring;
long eastX = centreX + ring;
if (westX >= bounds.minimumX() && westX <= bounds.maximumX()) {
keys.add(new HydrologyTileKey((int) westX, (int) tileZ));
}
if (eastX >= bounds.minimumX() && eastX <= bounds.maximumX() && keys.size() < limit) {
keys.add(new HydrologyTileKey((int) eastX, (int) tileZ));
}
}
}
keys.sort(Comparator.comparingInt((HydrologyTileKey key) -> Math.max(Math.abs(key.tileX() - centreTileX), Math.abs(key.tileZ() - centreTileZ)))
.thenComparingInt(HydrologyTileKey::tileZ)
.thenComparingInt(HydrologyTileKey::tileX));
return keys;
}
private void enqueuePrefetchArea(List<HydrologyTileKey> keys, PregenerationScope scope) {
Set<HydrologyTileKey> area = scope != null ? new HashSet<>(keys) : Set.of();
tiles.cleanUp();
if (persistentStore == null) {
tiles.cleanUp();
}
synchronized (prefetchQueue) {
if (closed.get()) {
if (scope != null) {
@@ -924,6 +1093,7 @@ public final class HydrologyTileCache implements AutoCloseable {
scope.previousNeighbourPrefetch = pregenerationScope == null
? neighbourPrefetchEnabled : pregenerationScope.previousNeighbourPrefetch;
pregenerationScope = scope;
planner.limitEarlyOwners(scope.required::contains);
prefetchQueue.removeIf(key -> {
if (!area.contains(key)) {
queuedPrefetches.remove(key);
@@ -937,7 +1107,7 @@ public final class HydrologyTileCache implements AutoCloseable {
return;
}
for (HydrologyTileKey key : keys) {
if (!enqueuePrefetch(key) && speculativeEntryCount() >= maximumEntries) {
if (!enqueuePrefetch(key) && speculationFull()) {
break;
}
}
@@ -947,12 +1117,14 @@ public final class HydrologyTileCache implements AutoCloseable {
private boolean prefetch(HydrologyTileKey key) {
if (closed.get()
|| tiles.getIfPresent(key) != null
|| occupancies.containsKey(key)
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|| queuedPrefetches.contains(key)) {
return false;
}
tiles.cleanUp();
if (persistentStore == null) {
tiles.cleanUp();
}
synchronized (prefetchQueue) {
if (!enqueuePrefetch(key)) {
return false;
@@ -965,10 +1137,10 @@ public final class HydrologyTileCache implements AutoCloseable {
private boolean enqueuePrefetch(HydrologyTileKey key) {
if (closed.get()
|| pregenerationScope != null && !pregenerationScope.bounds.contains(key)
|| tiles.getIfPresent(key) != null
|| occupancies.containsKey(key)
|| planning.containsKey(new CacheLoadKey<>(cacheEpoch.get(), key))
|| queuedPrefetches.contains(key)
|| speculativeEntryCount() >= maximumEntries) {
|| speculationFull()) {
return false;
}
queuedPrefetches.add(key);
@@ -976,6 +1148,15 @@ public final class HydrologyTileCache implements AutoCloseable {
return true;
}
/**
* Without a prepared-plan store a speculative tile lives only in memory, so speculation stops at
* the tile budget; with one every plan is persisted and its occupancy kept, so it never has to
* stay resident.
*/
private boolean speculationFull() {
return persistentStore == null && speculativeEntryCount() >= maximumEntries;
}
private int speculativeEntryCount() {
long entries = tiles.estimatedSize();
long budget = HydrologyCacheBudget.runtime().tileBytes();
@@ -993,57 +1174,80 @@ public final class HydrologyTileCache implements AutoCloseable {
return Math.max(0L, budget - weight) / expectedWeight;
}
/**
* Starts queued work while nothing is demanded: a nonblocking chunk query on its own, otherwise
* up to {@link #prefetchSlots} speculative roots at once. One caller pumps at a time and loops, so
* work that completes on the calling thread resumes the loop instead of recursing into it.
*/
private void pumpPrefetch() {
synchronized (prefetchQueue) {
if (prefetchExecutor == null || closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()
|| !prefetchActive.compareAndSet(false, true)) {
if (pumping) {
return;
}
pumping = true;
}
scheduleNextPrefetch();
}
private void scheduleNextPrefetch() {
CacheLoadKey<HydrologyTileKey> loadKey = null;
ChunkCoordinate chunk;
long epoch;
synchronized (prefetchQueue) {
if (closed.get() || demandBatches != 0 || !demandedPlans.isEmpty()) {
prefetchActive.set(false);
return;
}
epoch = cacheEpoch.get();
chunk = pollQuery();
if (chunk != null) {
demandBatches++;
} else {
HydrologyTileKey key = pollPrefetch();
if (key == null) {
prefetchActive.set(false);
return;
}
loadKey = new CacheLoadKey<>(epoch, key);
}
}
if (chunk != null) {
composeAsync(chunk, epoch).whenComplete((columns, failure) -> {
boolean idle = false;
try {
while (!idle) {
ChunkCoordinate chunk = null;
CacheLoadKey<HydrologyTileKey> loadKey = null;
long epoch;
synchronized (prefetchQueue) {
if (epoch == cacheEpoch.get()) {
queriedChunks.remove(chunk);
epoch = cacheEpoch.get();
if (prefetchExecutor != null && !closed.get() && demandBatches == 0 && demandedPlans.isEmpty()) {
chunk = pollQuery();
if (chunk != null) {
demandBatches++;
} else if (activePrefetches < prefetchSlots) {
HydrologyTileKey key = pollPrefetch(epoch);
if (key != null) {
activePrefetches++;
loadKey = new CacheLoadKey<>(epoch, key);
}
}
}
if (chunk == null && loadKey == null) {
pumping = false;
idle = true;
}
}
finishDemandBatch(epoch);
scheduleNextPrefetch();
});
} else {
planAsync(loadKey).whenComplete((tile, failure) -> scheduleNextPrefetch());
if (chunk != null) {
startQuery(chunk, epoch);
} else if (loadKey != null) {
planAsync(loadKey).whenComplete((tile, failure) -> finishPrefetch());
}
}
} finally {
if (!idle) {
synchronized (prefetchQueue) {
pumping = false;
}
}
}
}
private void startQuery(ChunkCoordinate chunk, long epoch) {
composeAsync(chunk, epoch).whenComplete((columns, failure) -> {
synchronized (prefetchQueue) {
if (epoch == cacheEpoch.get()) {
queriedChunks.remove(chunk);
}
}
finishDemandBatch(epoch);
});
}
private void finishPrefetch() {
synchronized (prefetchQueue) {
activePrefetches--;
}
pumpPrefetch();
}
private ChunkCoordinate pollQuery() {
while (!queriedChunks.isEmpty()) {
ChunkCoordinate chunk = queriedChunks.getFirst();
if (composedChunks.getIfPresent(chunk.packed()) == null) {
if (composedChunks.getIfPresent(chunk.packed()) == null && !knownEmpty(chunk.x(), chunk.z())) {
return chunk;
}
queriedChunks.removeFirst();
@@ -1105,13 +1309,15 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
private HydrologyTileKey pollPrefetch() {
tiles.cleanUp();
long weight = tiles.policy().eviction().orElseThrow().weightedSize().orElse(0L);
if (speculativeCapacity(tiles.estimatedSize(), HydrologyCacheBudget.runtime().tileBytes(), weight) == 0L) {
prefetchQueue.clear();
queuedPrefetches.clear();
return null;
private HydrologyTileKey pollPrefetch(long epoch) {
if (persistentStore == null) {
tiles.cleanUp();
long weight = tiles.policy().eviction().orElseThrow().weightedSize().orElse(0L);
if (speculativeCapacity(tiles.estimatedSize(), HydrologyCacheBudget.runtime().tileBytes(), weight) == 0L) {
prefetchQueue.clear();
queuedPrefetches.clear();
return null;
}
}
HydrologyTileKey key;
while ((key = prefetchQueue.poll()) != null) {
@@ -1119,7 +1325,8 @@ public final class HydrologyTileCache implements AutoCloseable {
continue;
}
if ((pregenerationScope == null || pregenerationScope.bounds.contains(key))
&& tiles.getIfPresent(key) == null) {
&& !occupancies.containsKey(key)
&& !planning.containsKey(new CacheLoadKey<>(epoch, key))) {
return key;
}
}
@@ -1424,12 +1631,19 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
/**
* Speculative planning during a pregeneration: tiles inside {@code bounds} may be planned ahead of
* generation, and owner drafts start their lower-rank neighbours early only inside {@code required},
* the tiles the area composes from.
*/
public final class PregenerationScope implements AutoCloseable {
private final TileBounds bounds;
private final TileBounds required;
private boolean previousNeighbourPrefetch;
private PregenerationScope(TileBounds bounds) {
private PregenerationScope(TileBounds bounds, TileBounds required) {
this.bounds = bounds;
this.required = required;
}
@Override
@@ -1439,6 +1653,7 @@ public final class HydrologyTileCache implements AutoCloseable {
return;
}
pregenerationScope = null;
planner.limitEarlyOwners(null);
neighbourPrefetchEnabled = previousNeighbourPrefetch;
prefetchQueue.clear();
queuedPrefetches.clear();
@@ -1449,7 +1664,7 @@ public final class HydrologyTileCache implements AutoCloseable {
private record PendingTile(int index, CompletableFuture<HydrologyTile> future) {
}
private record TileBounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
record TileBounds(int minimumX, int minimumZ, int maximumX, int maximumZ) {
private boolean contains(HydrologyTileKey key) {
return key.tileX() >= minimumX && key.tileX() <= maximumX
&& key.tileZ() >= minimumZ && key.tileZ() <= maximumZ;
@@ -1458,6 +1673,9 @@ public final class HydrologyTileCache implements AutoCloseable {
private record ChunkColumns(int chunkX, int chunkZ, HydrologyColumnSample[] columns) {
private long estimatedBytes() {
if (empty()) {
return 64L;
}
long bytes = 64L + 8L * columns.length;
for (HydrologyColumnSample column : columns) {
if (column != null) {
@@ -1467,9 +1685,16 @@ public final class HydrologyTileCache implements AutoCloseable {
return bytes;
}
/** A chunk with no hydrology at all, the answer for a caller that may not wait for its plan. */
/**
* A chunk with no hydrology at all: one no planned tile publishes into, or the answer for a
* caller that may not wait for its plan.
*/
private static ChunkColumns empty(int chunkX, int chunkZ) {
return new ChunkColumns(chunkX, chunkZ, new HydrologyColumnSample[CHUNK_COLUMN_COUNT]);
return new ChunkColumns(chunkX, chunkZ, NO_COLUMNS);
}
private boolean empty() {
return columns == NO_COLUMNS;
}
private Optional<HydrologyColumnSample> columnAt(int blockX, int blockZ) {
@@ -1479,7 +1704,27 @@ public final class HydrologyTileCache implements AutoCloseable {
}
}
private record LocalChunkColumns(long epoch, ChunkColumns columns) {
/** Direct-mapped over a 4 by 4 chunk neighbourhood, so a placement crossing chunk edges keeps its chunks. */
private static final class LocalChunks {
private final ChunkColumns[] slots = new ChunkColumns[LOCAL_CHUNK_SLOTS];
private final long[] aroundKeys = new long[LOCAL_CHUNK_SLOTS];
private final byte[] around = new byte[LOCAL_CHUNK_SLOTS];
private long epoch = Long.MIN_VALUE;
private TileBounds prefetched;
private ChunkColumns get(int chunkX, int chunkZ) {
ChunkColumns columns = slots[slot(chunkX, chunkZ)];
return columns != null && columns.chunkX() == chunkX && columns.chunkZ() == chunkZ ? columns : null;
}
private ChunkColumns put(ChunkColumns columns) {
slots[slot(columns.chunkX(), columns.chunkZ())] = columns;
return columns;
}
private static int slot(int chunkX, int chunkZ) {
return (chunkX & 3) | (chunkZ & 3) << 2;
}
}
private record ChunkCoordinate(int x, int z) {
@@ -15,7 +15,7 @@ public final class HydrologyCaveStorage {
}
int chunkX = x >> 4;
int chunkZ = z >> 4;
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
TectonicPlate<Matter> plate = mantle.getLoadedRegion(chunkX >> 5, chunkZ >> 5);
if (plate == null || plate.isClosed()) {
return null;
}
@@ -195,6 +195,11 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
cache.prepareChunkColumns(blockX, blockZ);
}
/** Whether the chunk or one of its eight neighbours holds a planned footprint column. */
public boolean hasColumnsAround(int chunkX, int chunkZ) {
return cache.hasColumnsAround(chunkX, chunkZ);
}
public HydrologyRenderSample renderSample(double x, double z) {
int blockX = (int) StrictMath.floor(x);
int blockZ = (int) StrictMath.floor(z);
@@ -0,0 +1,84 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.cache.ConcurrentClockCache;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.Arrays;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
/**
* Shares the adaptive cave planes' 8-block density lattice between neighbouring chunks. Every lattice column on a
* chunk edge is sampled by two chunks and every corner by four, with identical coordinates and carver noise, so a
* value computed once is reused bit for bit. Racing writers store the same value.
*/
final class CaveDensityGrid {
static final int LATTICE_SHIFT = 3;
private static final int SEGMENT_SHIFT = 5;
private static final int SEGMENT_SIZE = 1 << SEGMENT_SHIFT;
private static final long COORDINATE_MASK = 0xFFFFFFL;
private static final VarHandle VALUES = MethodHandles.arrayElementVarHandle(double[].class);
private static final AtomicInteger OWNERS = new AtomicInteger();
private final ConcurrentClockCache<Column> columns;
CaveDensityGrid(int capacity) {
columns = new ConcurrentClockCache<>(capacity);
}
static int nextOwner() {
return OWNERS.getAndIncrement();
}
Column column(Object owner, int ownerId, int height, int latticeX, int latticeZ) {
long key = (long) (ownerId & 0xFFFF) << 48 | (latticeX & COORDINATE_MASK) << 24 | (latticeZ & COORDINATE_MASK);
Column cached = columns.get(key);
if (cached == null) {
cached = columns.putIfAbsent(key, new Column(owner, latticeX, latticeZ, height));
}
return cached.owner == owner && cached.latticeX == latticeX && cached.latticeZ == latticeZ ? cached : null;
}
void clear() {
columns.clear();
}
static final class Column {
private final Object owner;
private final int latticeX;
private final int latticeZ;
private final AtomicReferenceArray<double[]> segments;
private Column(Object owner, int latticeX, int latticeZ, int height) {
this.owner = owner;
this.latticeX = latticeX;
this.latticeZ = latticeZ;
segments = new AtomicReferenceArray<>(Math.ceilDiv(Math.max(1, height), SEGMENT_SIZE));
}
double get(int y) {
int segment = y >> SEGMENT_SHIFT;
if (segment >= segments.length()) {
return Double.NaN;
}
double[] values = segments.getAcquire(segment);
return values == null ? Double.NaN : (double) VALUES.getOpaque(values, y & (SEGMENT_SIZE - 1));
}
void set(int y, double value) {
int segment = y >> SEGMENT_SHIFT;
if (segment >= segments.length()) {
return;
}
double[] values = segments.getAcquire(segment);
if (values == null) {
double[] created = new double[SEGMENT_SIZE];
Arrays.fill(created, Double.NaN);
double[] published = segments.compareAndExchange(segment, null, created);
values = published == null ? created : published;
}
VALUES.setOpaque(values, y & (SEGMENT_SIZE - 1), value);
}
}
}
@@ -0,0 +1,210 @@
package art.arcane.iris.generation.mantle;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.world.storage.matter.PreObjectMatterCell;
import art.arcane.volmlib.util.hunk.bits.DataContainer;
import art.arcane.volmlib.util.hunk.storage.MappedHunk;
import art.arcane.volmlib.util.hunk.storage.PaletteOrHunk;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterSlice;
import java.util.Map;
import java.util.Objects;
import java.util.function.IntFunction;
/**
* The terrain view of one mantle chunk's cavern and hydrology cells (journaled pre-object values win),
* copied once under the chunk monitor. Terrain components finish before any content write reaches a
* chunk and every mantle-writer write journals the value it replaces, so the view holds while the chunk
* generates; the raw writers that run after sCave (dimension stack clearing, continuation restore) drop
* it from the chunk context instead.
*/
public final class CaveTerrainSnapshot {
private static final int SECTION_VOLUME = 4096;
private final int chunkX;
private final int chunkZ;
private final MatterCavern[][] caverns;
private final HydrologyCaveCell[][] hydrology;
private boolean hydrologyPresent;
private CaveTerrainSnapshot(int chunkX, int chunkZ, int sections) {
this.chunkX = chunkX;
this.chunkZ = chunkZ;
caverns = new MatterCavern[sections][];
hydrology = new HydrologyCaveCell[sections][];
}
public static CaveTerrainSnapshot capture(MantleChunk<Matter> chunk, int chunkX, int chunkZ) {
Objects.requireNonNull(chunk, "Terrain mantle chunk");
synchronized (chunk) {
int sections = chunk.sectionCount();
CaveTerrainSnapshot snapshot = new CaveTerrainSnapshot(chunkX, chunkZ, sections);
for (int section = 0; section < sections; section++) {
if (!chunk.exists(section)) {
continue;
}
Matter matter = chunk.get(section);
if (matter != null) {
snapshot.captureSection(section, matter);
}
}
return snapshot;
}
}
public boolean covers(int chunkX, int chunkZ) {
return this.chunkX == chunkX && this.chunkZ == chunkZ;
}
public boolean hasHydrology() {
return hydrologyPresent;
}
public MatterCavern cavern(int x, int y, int z) {
if (y < 0) {
return null;
}
int section = y >> 4;
if (section >= caverns.length) {
return null;
}
MatterCavern[] values = caverns[section];
return values == null ? null : values[index(x, y, z)];
}
public HydrologyCaveCell hydrology(int x, int y, int z) {
if (!hydrologyPresent || y < 0) {
return null;
}
int section = y >> 4;
if (section >= hydrology.length) {
return null;
}
HydrologyCaveCell[] values = hydrology[section];
return values == null ? null : values[index(x, y, z)];
}
public MatterCavern composedCavern(int x, int y, int z) {
HydrologyCaveCell cell = hydrology(x, y, z);
return cell != null ? cell.asCavern() : cavern(x, y, z);
}
/**
* Visits every cell holding a cavern or a hydrology cell, section by section in slice position order.
*/
public void forEachCell(CellConsumer consumer) {
for (int section = 0; section < caverns.length; section++) {
MatterCavern[] sectionCaverns = caverns[section];
HydrologyCaveCell[] sectionHydrology = hydrologyPresent ? hydrology[section] : null;
if (sectionCaverns == null && sectionHydrology == null) {
continue;
}
int baseY = section << 4;
for (int position = 0; position < SECTION_VOLUME; position++) {
MatterCavern cavern = sectionCaverns == null ? null : sectionCaverns[position];
HydrologyCaveCell cell = sectionHydrology == null ? null : sectionHydrology[position];
if (cavern != null || cell != null) {
consumer.accept(position & 15, baseY + ((position >> 4) & 15), position >> 8, cavern, cell);
}
}
}
}
public MatterCavern[] composedFace(TerrainMatterView.Face face, int height) {
Objects.requireNonNull(face, "Terrain face");
if (height < 0) {
throw new IllegalArgumentException("Terrain face height must be nonnegative");
}
MatterCavern[] result = new MatterCavern[Math.multiplyExact(height, 16)];
for (int y = 0; y < height; y++) {
for (int offset = 0; offset < 16; offset++) {
result[y * 16 + offset] = composedCavern(face.x(offset), y, face.z(offset));
}
}
return result;
}
private void captureSection(int section, Matter matter) {
caverns[section] = copy(matter.getSlice(MatterCavern.class), MatterCavern[]::new);
hydrology[section] = copy(matter.getSlice(HydrologyCaveCell.class), HydrologyCaveCell[]::new);
MatterSlice<PreObjectMatterCell> journal = matter.getSlice(PreObjectMatterCell.class);
if (journal instanceof MappedHunk<?> mapped) {
@SuppressWarnings("unchecked")
Map<Integer, PreObjectMatterCell> entries = (Map<Integer, PreObjectMatterCell>) mapped.getData();
for (Map.Entry<Integer, PreObjectMatterCell> entry : entries.entrySet()) {
restoreOriginals(section, entry.getKey(), entry.getValue());
}
} else if (journal != null) {
for (int position = 0; position < SECTION_VOLUME; position++) {
PreObjectMatterCell cell = journal.get(position & 15, (position >> 4) & 15, position >> 8);
if (cell != null) {
restoreOriginals(section, position, cell);
}
}
}
if (hydrology[section] != null && containsAny(hydrology[section])) {
hydrologyPresent = true;
} else {
hydrology[section] = null;
}
}
private void restoreOriginals(int section, int position, PreObjectMatterCell cell) {
if (cell.cavernCaptured() && (caverns[section] != null || cell.cavern() != null)) {
if (caverns[section] == null) {
caverns[section] = new MatterCavern[SECTION_VOLUME];
}
caverns[section][position] = cell.cavern();
}
if (cell.hydrologyCaptured() && (hydrology[section] != null || cell.hydrology() != null)) {
if (hydrology[section] == null) {
hydrology[section] = new HydrologyCaveCell[SECTION_VOLUME];
}
hydrology[section][position] = cell.hydrology();
}
}
@SuppressWarnings("unchecked")
private static <T> T[] copy(MatterSlice<T> slice, IntFunction<T[]> allocator) {
if (slice == null) {
return null;
}
T[] values = allocator.apply(SECTION_VOLUME);
if (slice instanceof PaletteOrHunk<?> storage && storage.isPalette()
&& slice.getWidth() == 16 && slice.getHeight() == 16 && slice.getDepth() == 16) {
return ((DataContainer<T>) storage.palette()).copyPresent(values) == 0 ? null : values;
}
int width = Math.min(16, slice.getWidth());
int height = Math.min(16, slice.getHeight());
int depth = Math.min(16, slice.getDepth());
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
for (int z = 0; z < depth; z++) {
values[index(x, y, z)] = slice.get(x, y, z);
}
}
}
return values;
}
private static boolean containsAny(Object[] values) {
for (Object value : values) {
if (value != null) {
return true;
}
}
return false;
}
private static int index(int x, int y, int z) {
return ((z & 15) << 8) | ((y & 15) << 4) | (x & 15);
}
@FunctionalInterface
public interface CellConsumer {
void accept(int x, int y, int z, MatterCavern cavern, HydrologyCaveCell hydrology);
}
}
@@ -212,7 +212,7 @@ public interface EngineMantle extends MatterGenerator {
boolean protectUpper = upperCtx != null;
DimensionStackContext stackContext = getEngine().getDimensionStackContext();
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
MantleChunk<Matter> chunk = getMantle().useChunk(x, z);
try {
if (stackContext != null) {
DimensionStackLayout[] layouts = new DimensionStackLayout[256];
@@ -317,24 +317,33 @@ public interface EngineMantle extends MatterGenerator {
}
default boolean cleanupCoveredChunk(int x, int z, boolean force) {
return cleanupCoveredChunk(getMantle().getChunk(x, z), force);
MantleChunk<Matter> chunk = getMantle().useChunk(x, z);
try {
return cleanupUsedChunk(chunk, force);
} finally {
chunk.release();
}
}
default boolean cleanupCoveredChunk(MantleChunk<Matter> chunk, boolean force) {
chunk.use();
try {
synchronized (chunk) {
if (chunk.isFlagged(MantleFlag.CLEANED)) {
return false;
}
chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> cleanupSlices(chunk, force));
return true;
}
return cleanupUsedChunk(chunk, force);
} finally {
chunk.release();
}
}
private boolean cleanupUsedChunk(MantleChunk<Matter> chunk, boolean force) {
synchronized (chunk) {
if (chunk.isFlagged(MantleFlag.CLEANED)) {
return false;
}
chunk.raiseFlagUnchecked(MantleFlag.CLEANED, () -> cleanupSlices(chunk, force));
return true;
}
}
private void cleanupSlices(MantleChunk<Matter> chunk, boolean force) {
MantleSliceRetention.deleteUnlessRetained(chunk, NativeBlockState.class);
if (force) {
@@ -61,6 +61,7 @@ public class IrisCaveCarver3D {
private static final int MINIMUM_PARALLEL_DENSITY_CELLS = 16_384;
private static final int MAXIMUM_DENSITY_WORKERS = 4;
private static final int[] LATTICE_TILE_OFFSETS = {0, 1, 16, 17};
private static final int STANDALONE_DENSITY_GRID_COLUMNS = 1024;
private final Engine engine;
private final IrisData data;
@@ -96,9 +97,16 @@ public class IrisCaveCarver3D {
private final int densityMaximumY;
private final ThreadLocal<Boolean> carving = ThreadLocal.withInitial(() -> false);
private final ThreadLocal<CaveCarveScratch> scratchCache = ThreadLocal.withInitial(CaveCarveScratch::new);
private final CaveDensityGrid densityGrid;
private final int densityGridOwner = CaveDensityGrid.nextOwner();
public IrisCaveCarver3D(Engine engine, IrisCaveProfile profile) {
this(engine, profile, new CaveDensityGrid(STANDALONE_DENSITY_GRID_COLUMNS));
}
IrisCaveCarver3D(Engine engine, IrisCaveProfile profile, CaveDensityGrid densityGrid) {
this.engine = engine;
this.densityGrid = densityGrid;
this.data = engine.getData();
this.profile = profile;
this.carveAir = new MatterCavern(true, "", LIQUID_AIR);
@@ -1542,7 +1550,7 @@ public class IrisCaveCarver3D {
int rowOffset = sampleXIndex * axisSamples;
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16);
adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityNoWarpNoModules(x, y, z0 + sampleLocalZ);
adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ);
}
}
@@ -1595,7 +1603,7 @@ public class IrisCaveCarver3D {
int rowOffset = sampleXIndex * axisSamples;
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16);
adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityNoWarpNoModules(x, y, z0 + sampleLocalZ);
adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ);
}
}
@@ -1643,7 +1651,7 @@ public class IrisCaveCarver3D {
int rowOffset = sampleXIndex * axisSamples;
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16);
adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpOnly(scratch, x, y, z0 + sampleLocalZ);
adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ);
}
}
@@ -1696,14 +1704,7 @@ public class IrisCaveCarver3D {
int rowOffset = sampleXIndex * axisSamples;
for (int sampleZIndex = adaptivePlaneSampleBounds[2]; sampleZIndex <= adaptivePlaneSampleBounds[3]; sampleZIndex++) {
int sampleLocalZ = Math.min(sampleZIndex * adaptiveSampleStep, 16);
adaptivePlaneDensity[rowOffset + sampleZIndex] = sampleDensityWarpModules(
scratch,
x,
y,
z0 + sampleLocalZ,
localModules,
activeModuleCount
);
adaptivePlaneDensity[rowOffset + sampleZIndex] = latticeDensity(scratch, x, y, z0 + sampleLocalZ);
}
}
@@ -2463,6 +2464,33 @@ public class IrisCaveCarver3D {
scratch.adaptiveGeometryAxisCells = axisCells;
}
private double latticeDensity(CaveCarveScratch scratch, int x, int y, int z) {
int latticeMask = (1 << CaveDensityGrid.LATTICE_SHIFT) - 1;
CaveDensityGrid.Column column = ((x | z) & latticeMask) != 0 ? null : densityGrid.column(this, densityGridOwner,
densityMaximumY + 1, x >> CaveDensityGrid.LATTICE_SHIFT, z >> CaveDensityGrid.LATTICE_SHIFT);
if (column == null) {
return samplePlaneDensity(scratch, x, y, z);
}
double cached = column.get(y);
if (!Double.isNaN(cached)) {
return cached;
}
double density = samplePlaneDensity(scratch, x, y, z);
column.set(y, density);
return density;
}
private double samplePlaneDensity(CaveCarveScratch scratch, int x, int y, int z) {
if (!hasWarp) {
return sampleDensityNoWarpNoModules(x, y, z);
}
int activeModuleCount = hasModules ? prepareActiveModules(scratch, y) : 0;
if (activeModuleCount == 0) {
return sampleDensityWarpOnly(scratch, x, y, z);
}
return sampleDensityWarpModules(scratch, x, y, z, scratch.activeModules, activeModuleCount);
}
private double sampleDensityNoWarpNoModules(int x, int y, int z) {
double density = baseDensity.noiseFastSigned3D(x, y, z) * baseWeight;
density += detailDensity.noiseFastSigned3D(x, y, z) * detailWeight;
@@ -54,6 +54,8 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private static final double THRESHOLD_PENALTY = 0.24D;
private static final int MAX_BLENDED_PROFILE_PASSES = 2;
private static final int MAX_POOLED_WEIGHT_BUFFERS = 64;
/** Lattice columns kept for neighbouring chunks: about 3 KB each on land, roughly 50 MB when full. */
private static final int DENSITY_GRID_COLUMNS = 16_384;
private static final int KERNEL_WIDTH = (BLEND_RADIUS * 2) + 1;
private static final int KERNEL_SIZE = KERNEL_WIDTH * KERNEL_WIDTH;
private static final int[] KERNEL_DX = new int[KERNEL_SIZE];
@@ -62,6 +64,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
private static final ThreadLocal<BlendScratch> BLEND_SCRATCH = ThreadLocal.withInitial(BlendScratch::new);
private final Object profileCarverLock = new Object();
private final CaveDensityGrid densityGrid = new CaveDensityGrid(DENSITY_GRID_COLUMNS);
private volatile Map<IrisCaveProfile, IrisCaveCarver3D> profileCarvers = new IdentityHashMap<>();
static {
@@ -94,6 +97,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
// Carvers rebuild deterministically from the carve seed, so output is unchanged.
synchronized (profileCarverLock) {
profileCarvers = new IdentityHashMap<>();
densityGrid.clear();
}
}
@@ -524,7 +528,7 @@ public class MantleCarvingComponent extends IrisMantleComponent {
return carver;
}
IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile);
IrisCaveCarver3D createdCarver = new IrisCaveCarver3D(getEngineMantle().getEngine(), profile, densityGrid);
synchronized (profileCarverLock) {
IrisCaveCarver3D published = profileCarvers.get(profile);
if (published != null) {
@@ -55,8 +55,12 @@ public interface MantleComponent extends Comparable<MantleComponent> {
return getInputRadius();
}
default boolean isInputGenerationLazy() {
return false;
/**
* The part of {@code inputRadius} generated before this component runs. Content writers generate the
* terrain of any chunk read beyond it on first access, so this sizes the eager window, not what may be read.
*/
default int getEagerInputRadius(int inputRadius) {
return inputRadius;
}
default IrisData getData() {
@@ -67,15 +67,16 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
}
@Override
public boolean isInputGenerationLazy() {
return true;
public int getEagerInputRadius(int inputRadius) {
return 0;
}
@Override
public void generateLayer(MantleWriter writer, int chunkX, int chunkZ, ChunkContext context) {
IrisComplex complex = context.getComplex();
IrisHydrologyRuntime runtime = complex.getHydrologyRuntime();
if (runtime == null || !complex.allowsMantleChunkWrite(chunkX, chunkZ)) {
if (runtime == null || !complex.allowsMantleChunkWrite(chunkX, chunkZ)
|| !runtime.hasColumnsAround(chunkX, chunkZ)) {
return;
}
int minimumX = chunkX << 4;
@@ -79,9 +79,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
private static final byte LIQUID_FORCED_AIR = 3;
private static final Map<String, CaveRejectLogState> CAVE_REJECT_LOG_STATE = new ConcurrentHashMap<>();
private static final Set<String> MISSING_LOAD_KEY_WARNED = ConcurrentHashMap.newKeySet();
private static final long SOURCE_PLAN_STATS_INTERVAL_MS = 30_000L;
private final Object collisionRuleLock;
private final ObjectSourcePlanCache sourcePlans;
private final AtomicLong lastSourcePlanStatsLog = new AtomicLong();
private volatile int collisionRuleState;
public MantleObjectComponent(EngineMantle engineMantle) {
@@ -126,6 +128,11 @@ public class MantleObjectComponent extends IrisMantleComponent {
return calculateInputRadius(getRadius(), hasCollisionRules());
}
@Override
public int getEagerInputRadius(int inputRadius) {
return Math.min(inputRadius, calculateEagerInputRadius(getRadius(), hasCollisionRules()));
}
@Override
public void hotload() {
super.hotload();
@@ -138,18 +145,41 @@ public class MantleObjectComponent extends IrisMantleComponent {
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
ObjectDestinationTransaction transaction = new ObjectDestinationTransaction(writer, x, z);
int radius = getRadius();
int sourceChunkRadius = sourceChunkRadius(radius);
replaySourceChunks(
x,
z,
getRadius(),
radius,
(sourceX, sourceZ) -> {
ObjectSourcePlan plan = sourcePlans.get(sourceX, sourceZ,
ObjectSourcePlan plan = sourcePlans.acquire(sourceX, sourceZ, x, z, sourceChunkRadius,
() -> buildSourcePlan(writer, sourceX, sourceZ, context));
transaction.apply(plan);
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
if (plan != null) {
transaction.apply(plan);
plan.persistContinuations(writer, sourceX, sourceZ, x, z);
}
}
);
transaction.commit();
logSourcePlanStats();
}
private void logSourcePlanStats() {
long now = System.currentTimeMillis();
long last = lastSourcePlanStatsLog.get();
if (now - last < SOURCE_PLAN_STATS_INTERVAL_MS || !lastSourcePlanStatsLog.compareAndSet(last, now)) {
return;
}
ObjectSourcePlanCache.Stats stats = sourcePlans.stats();
IrisLogging.debug("Object source plans: lookups=" + stats.lookups()
+ " builds=" + stats.builds()
+ " waits=" + stats.waits()
+ " replays=" + stats.replays()
+ " drained=" + stats.drained()
+ " retired=" + stats.retired()
+ " retained=" + stats.retained()
+ " retainedMiB=" + (stats.retainedBytes() >> 20)
+ " budgetMiB=" + (stats.budgetBytes() >> 20));
}
private ObjectSourcePlan buildSourcePlan(
@@ -247,6 +277,17 @@ public class MantleObjectComponent extends IrisMantleComponent {
return radius > 0 ? Math.ceilDiv(radius, 16) : 0;
}
/**
* Every replayed source chunk plus one chunk of placement footprint. Larger footprints read past it and
* get their terrain generated on first access.
*/
static int calculateEagerInputRadius(int radius, boolean sourceAnchoredCollisions) {
int normalizedRadius = Math.max(0, radius);
long sourceLegs = sourceAnchoredCollisions ? 2L : 1L;
long inputRadius = (sourceChunkRadius(normalizedRadius) * 16L * sourceLegs) + Math.min(normalizedRadius, 16) + 1L;
return inputRadius >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) inputRadius;
}
static int calculateInputRadius(int radius, boolean sourceAnchoredCollisions) {
int normalizedRadius = Math.max(0, radius);
long sourceChunkRadius = sourceChunkRadius(normalizedRadius);
@@ -35,10 +35,12 @@ import art.arcane.iris.world.history.TransitionGenerationPlan;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.pack.value.IrisPosition;
import art.arcane.iris.generation.block.TileData;
import art.arcane.iris.generation.context.ChunkContext;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.volmlib.util.collection.KSet;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.function.Function3;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.math.RNG;
@@ -90,6 +92,11 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
private final ThreadLocal<Integer> activeComponentPriority = new ThreadLocal<>();
@Getter(AccessLevel.NONE)
private final ThreadLocal<ObjectPlacementCapture> objectPlacementCapture = new ThreadLocal<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
@EqualsAndHashCode.Exclude
@ToString.Exclude
private TerrainAccess terrainAccess;
public MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int radius, boolean multicore) {
this(engineMantle, mantle, x, z, radius, radius * 2, multicore);
@@ -104,17 +111,7 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
int accessRadius,
boolean multicore
) {
this.engineMantle = engineMantle;
this.mantle = mantle;
this.radius = accessRadius;
this.x = x;
this.z = z;
IrisComplex complex = engineMantle.getComplex();
this.transitionGenerationPlan = complex == null ? null : complex.getTransitionGenerationPlan();
// Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the
// boxed per-block map lookup on the placement and carve hot paths.
this.windowSide = (this.radius * 2) + 1;
this.window = new AtomicReferenceArray<>(windowSide * windowSide);
this(engineMantle, mantle, x, z, accessRadius);
final boolean foliaMaintenance = J.isFolia()
&& WorldMaintenance.isWorldMaintenanceActive(engineMantle.getEngine().getWorld().identity());
@@ -142,6 +139,41 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
private MantleWriter(EngineMantle engineMantle, Mantle<Matter> mantle, int x, int z, int accessRadius) {
this.engineMantle = engineMantle;
this.mantle = mantle;
this.radius = accessRadius;
this.x = x;
this.z = z;
IrisComplex complex = engineMantle.getComplex();
this.transitionGenerationPlan = complex == null ? null : complex.getTransitionGenerationPlan();
// Every coordinate acquireChunk accepts lives in this window, so a flat array replaces the
// boxed per-block map lookup on the placement and carve hot paths.
this.windowSide = (this.radius * 2) + 1;
this.window = new AtomicReferenceArray<>(windowSide * windowSide);
}
/**
* A writer over one already loaded chunk, for generating that chunk's terrain components without a
* prefetch.
*/
static MantleWriter forChunk(EngineMantle engineMantle, Mantle<Matter> mantle, MantleChunk<Matter> chunk,
int chunkX, int chunkZ) {
MantleWriter writer = new MantleWriter(engineMantle, mantle, chunkX, chunkZ, 0);
writer.window.set(0, chunk.use());
return writer;
}
/**
* From now on every chunk this writer hands out has its terrain components generated first. Called once
* the terrain passes of the window are done and before any content pass reads or writes.
*/
void requireTerrainOnAccess(List<MantleComponent> terrainComponents, ChunkContext context, IrisComplex complex) {
terrainAccess = terrainComponents.isEmpty()
? null
: new TerrainAccess(terrainComponents.toArray(new MantleComponent[0]), context, complex);
}
static int resolvePrefetchParallelism(boolean foliaMaintenance, boolean multicore, int availableProcessors) {
if (foliaMaintenance) {
return 1;
@@ -844,20 +876,24 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
return null;
}
while (true) {
MantleChunk<Matter> chunk = window.get(index);
if (chunk != null) {
return chunk;
}
MantleChunk<Matter> chunk = window.get(index);
while (chunk == null) {
// Losing this race must release our own use, never the winner's: the winner is already
// writing through the chunk it published.
MantleChunk<Matter> acquired = mantle.getChunk(cx, cz).use();
MantleChunk<Matter> acquired = mantle.useChunk(cx, cz);
if (window.compareAndSet(index, null, acquired)) {
return acquired;
chunk = acquired;
} else {
acquired.release();
chunk = window.get(index);
}
acquired.release();
}
TerrainAccess access = terrainAccess;
if (access != null && !access.ready[index]) {
access.ensure(chunk, cx, cz);
access.ready[index] = true;
}
return chunk;
}
@ChunkCoordinates
@@ -1502,6 +1538,9 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
@Override
public void close() {
for (int index = 0; index < window.length(); index++) {
if (window.get(index) == null) {
continue;
}
MantleChunk<Matter> chunk = window.getAndSet(index, null);
if (chunk != null) {
chunk.release();
@@ -1509,6 +1548,45 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
private final class TerrainAccess {
private final MantleComponent[] components;
private final ChunkContext context;
private final IrisComplex complex;
private final boolean[] ready = new boolean[window.length()];
private TerrainAccess(MantleComponent[] components, ChunkContext context, IrisComplex complex) {
this.components = components;
this.context = context;
this.complex = complex;
}
private void ensure(MantleChunk<Matter> chunk, int chunkX, int chunkZ) {
if (chunk.isFlagged(MantleFlag.PLANNED) || complex != null && !complex.allowsMantleChunkWrite(chunkX, chunkZ)) {
return;
}
for (MantleComponent component : components) {
if (chunk.isFlagged(component.getFlag()) || !hasPrerequisites(chunk, component)) {
continue;
}
chunk.raiseFlagSuspend(component.getFlag(), () -> {
try (MantleWriter terrainWriter = forChunk(engineMantle, mantle, chunk, chunkX, chunkZ)) {
terrainWriter.withComponentPriority(component.getPriority(),
() -> component.generateLayer(terrainWriter, chunkX, chunkZ, context));
}
});
}
}
private static boolean hasPrerequisites(MantleChunk<Matter> chunk, MantleComponent component) {
for (MantleFlag prerequisite : component.getPrerequisiteFlags()) {
if (!chunk.isFlagged(prerequisite)) {
return false;
}
}
return true;
}
}
public final class ObjectPlacementCapture implements AutoCloseable {
private final ObjectContinuationBundle.PlacementKey key;
private final Long2ObjectOpenHashMap<Matter> fragments = new Long2ObjectOpenHashMap<>();
@@ -90,7 +90,7 @@ public interface MatterGenerator {
int prefetchRadius = passPlans[0].passChunkRadius();
LongOpenHashSet partialChunks = new LongOpenHashSet();
List<MantleComponent> requiredComponents = enabledComponents();
List<MantleFlag> terrainFlags = terrainFlags(requiredComponents);
boolean terrainOnAccess = false;
try (MantleWriter writer = new MantleWriter(
getEngine().getMantle(),
@@ -129,6 +129,10 @@ public interface MatterGenerator {
if (enabledComponentCount == 0) {
continue;
}
if (!terrainOnAccess && includesContent(enabledComponents, enabledComponentCount)) {
writer.requireTerrainOnAccess(terrainComponents(requiredComponents), context, complex);
terrainOnAccess = true;
}
// Every multicore generation claims its components through the in-flight map.
// A caller outside the pool spreads its window across the pool; a pool thread
@@ -185,9 +189,6 @@ public interface MatterGenerator {
continue;
}
if (phase == MatterGenerationPhase.CONTENT) {
requireTerrainPhase(chunk, passX, passZ, terrainFlags);
}
int eligibleComponentCount = 0;
for (int componentIndex = 0; componentIndex < enabledComponentCount; componentIndex++) {
MantleComponent component = enabledComponents[componentIndex];
@@ -303,9 +304,8 @@ public interface MatterGenerator {
: 0;
int componentInputRadius = component.getInputRadius(x, z, invocationChunkRadius, context);
passAccessInputRadius = Math.max(passAccessInputRadius, componentInputRadius);
if (!component.isInputGenerationLazy()) {
passGenerationInputRadius = Math.max(passGenerationInputRadius, componentInputRadius);
}
passGenerationInputRadius = Math.max(passGenerationInputRadius,
component.getEagerInputRadius(componentInputRadius));
}
int accessInvocationRadius = accessDownstreamBlockRadius + passBlockRadius;
int generationInvocationRadius = generationDownstreamBlockRadius + passBlockRadius;
@@ -332,24 +332,23 @@ public interface MatterGenerator {
return List.copyOf(components);
}
private static List<MantleFlag> terrainFlags(List<MantleComponent> components) {
List<MantleFlag> flags = new ArrayList<>();
for (MantleComponent component : components) {
if (component.getGenerationPhase() == MatterGenerationPhase.TERRAIN) {
flags.add(component.getFlag());
private static boolean includesContent(MantleComponent[] components, int count) {
for (int index = 0; index < count; index++) {
if (components[index].getGenerationPhase() == MatterGenerationPhase.CONTENT) {
return true;
}
}
return List.copyOf(flags);
return false;
}
private static void requireTerrainPhase(MantleChunk<Matter> chunk, int chunkX, int chunkZ,
List<MantleFlag> flags) {
for (MantleFlag flag : flags) {
if (!chunk.isFlagged(flag)) {
throw new IllegalStateException("Content generation at " + chunkX + "," + chunkZ
+ " requires completed terrain component " + flag.name());
private static List<MantleComponent> terrainComponents(List<MantleComponent> components) {
List<MantleComponent> terrain = new ArrayList<>();
for (MantleComponent component : components) {
if (component.getGenerationPhase() == MatterGenerationPhase.TERRAIN) {
terrain.add(component);
}
}
return terrain;
}
private static boolean hasCompletedComponents(MantleChunk<Matter> chunk, List<MantleComponent> components) {
@@ -26,7 +26,7 @@ public final class ObjectContinuationPersistence {
List<ObjectContinuationBundle.ChunkPosition> touched = List.copyOf(destinations.keySet());
for (Map.Entry<ObjectContinuationBundle.ChunkPosition, Supplier<Matter>> entry : destinations.entrySet()) {
ObjectContinuationBundle.ChunkPosition destination = entry.getKey();
MantleChunk<Matter> chunk = mantle.getChunk(destination.x(), destination.z()).use();
MantleChunk<Matter> chunk = mantle.useChunk(destination.x(), destination.z());
try {
synchronized (chunk) {
if (chunk.isFlagged(MantleFlag.REAL)) {
@@ -3,6 +3,7 @@ package art.arcane.iris.generation.mantle;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.block.TileData;
@@ -32,6 +33,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
private final List<Mutation> mutations;
private final List<PlacementRange> placements = new ArrayList<>();
private final Long2ObjectOpenHashMap<Int2ObjectOpenHashMap<OverlayCell>> overlay;
private boolean hydrologyGateKnown;
private int hydrologyGateChunkX;
private int hydrologyGateChunkZ;
private boolean hydrologyGateOpen;
ObjectDestinationTransaction(MantleWriter writer, int destinationChunkX, int destinationChunkZ) {
this.writer = writer;
@@ -54,10 +59,10 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
if (checkpoint < 0 || checkpoint > mutations.size()) {
throw new IllegalArgumentException("Mutation checkpoint is outside the transaction");
}
ArrayList<List<Mutation>> accepted = new ArrayList<>();
ArrayList<PlacementRange> accepted = new ArrayList<>();
for (PlacementRange placement : placements) {
if (placement.start() >= checkpoint) {
accepted.add(List.copyOf(mutations.subList(placement.start(), placement.end())));
accepted.add(new PlacementRange(placement.start() - checkpoint, placement.end() - checkpoint));
}
}
return new ObjectSourcePlan(mutations.subList(checkpoint, mutations.size()), accepted);
@@ -350,10 +355,24 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
private boolean hasProtectedHydrology(int x, int y, int z) {
if (!mayHoldHydrologyCells(x >> 4, z >> 4)) {
return false;
}
HydrologyCaveCell hydrology = getDataIfPresent(x, y, z, HydrologyCaveCell.class);
return hydrology != null && hydrology.protectsPlacement();
}
private boolean mayHoldHydrologyCells(int chunkX, int chunkZ) {
if (!hydrologyGateKnown || hydrologyGateChunkX != chunkX || hydrologyGateChunkZ != chunkZ) {
IrisComplex complex = writer.getEngine().getComplex();
hydrologyGateOpen = complex == null || complex.mayHoldHydrologyCells(chunkX, chunkZ);
hydrologyGateChunkX = chunkX;
hydrologyGateChunkZ = chunkZ;
hydrologyGateKnown = true;
}
return hydrologyGateOpen;
}
private void rollback(LinkedHashMap<DataKey, Object> originals, Throwable failure) {
ArrayList<Map.Entry<DataKey, Object>> entries = new ArrayList<>(originals.entrySet());
entries.sort((first, second) -> Boolean.compare(
@@ -432,7 +451,7 @@ final class ObjectDestinationTransaction implements ObjectPassPlacer {
}
}
private record PlacementRange(int start, int end) {
record PlacementRange(int start, int end) {
}
record DataKey(int x, int y, int z, Class<?> type) {
@@ -4,93 +4,242 @@ import art.arcane.iris.generation.decoration.tree.TreeBlockMaterial;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.integration.Identifier;
import art.arcane.iris.world.storage.matter.IrisMatterSupport;
import art.arcane.iris.world.storage.matter.TileWrapper;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.matter.IrisMatter;
import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import art.arcane.volmlib.util.matter.MatterMarker;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
final class ObjectSourcePlan {
private static final int PLAN_BYTES = 256;
private static final int DESTINATION_INDEX_BYTES = 96;
private static final int MUTATION_BYTES = 96;
private static final int PLACEMENT_BYTES = 256;
private static final int PLACEMENT_DESTINATION_BYTES = 128;
private static final int REFERENCE_BYTES = 8;
private final Long2ObjectOpenHashMap<List<ObjectDestinationTransaction.Mutation>> destinations;
static final ObjectSourcePlan EMPTY = new ObjectSourcePlan(List.of());
private static final int PLAN_BYTES = 160;
private static final int DESTINATION_BYTES = 12;
private static final int MUTATION_BYTES = 8;
private static final int VALUE_BYTES = 8;
private static final int PLACEMENT_BYTES = 128;
private static final int PLACEMENT_DESTINATION_BYTES = 40;
private static final int UNKNOWN_PAYLOAD_BYTES = 256;
private static final int CUSTOM_BIT = 1 << 8;
private static final int Y_SHIFT = 9;
private static final int MAXIMUM_Y = (1 << (31 - Y_SHIFT)) - 1;
private final long[] destinations;
private final int[] offsets;
private final int[] cells;
private final int[] valueIndexes;
private final Object[] values;
private final Placement[] placements;
private final int estimatedRetainedBytes;
private final List<Placement> placements;
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations) {
this(mutations, List.of());
}
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<List<ObjectDestinationTransaction.Mutation>> placements) {
ObjectSourcePlan(List<ObjectDestinationTransaction.Mutation> mutations, List<ObjectDestinationTransaction.PlacementRange> accepted) {
int count = mutations.size();
long[] mutationDestinations = new long[count];
Long2IntOpenHashMap destinationSlots = new Long2IntOpenHashMap();
destinationSlots.defaultReturnValue(-1);
LongArrayList firstSeen = new LongArrayList();
for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = Objects.requireNonNull(mutations.get(index), "mutation");
long destination = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
mutationDestinations[index] = destination;
if (destinationSlots.putIfAbsent(destination, firstSeen.size()) == -1) {
firstSeen.add(destination);
}
}
destinations = firstSeen.toLongArray();
Arrays.sort(destinations);
for (int slot = 0; slot < destinations.length; slot++) {
destinationSlots.put(destinations[slot], slot);
}
offsets = new int[destinations.length + 1];
for (int index = 0; index < count; index++) {
offsets[destinationSlots.get(mutationDestinations[index]) + 1]++;
}
for (int slot = 0; slot < destinations.length; slot++) {
offsets[slot + 1] += offsets[slot];
}
int[] cursor = Arrays.copyOf(offsets, destinations.length);
int[] positions = new int[count];
cells = new int[count];
valueIndexes = new int[count];
ValueTable table = new ValueTable();
for (int index = 0; index < count; index++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(index);
int position = cursor[destinationSlots.get(mutationDestinations[index])]++;
positions[index] = position;
ObjectDestinationTransaction.DataKey key = mutation.key();
if (key.y() < 0 || key.y() > MAXIMUM_Y) {
throw new IllegalArgumentException("Object mutation height is outside the plan range: " + key.y());
}
Object value;
int custom;
switch (mutation) {
case ObjectDestinationTransaction.SetMutation set -> {
value = Objects.requireNonNull(set.value(), "mutation value");
if (typeOf(value) != key.type()) {
throw new IllegalArgumentException("Object mutation type " + key.type().getName()
+ " does not match its value " + value.getClass().getName());
}
custom = 0;
}
case ObjectDestinationTransaction.CustomBlockMutation block -> {
value = Objects.requireNonNull(block.state(), "custom block state");
custom = CUSTOM_BIT;
}
}
cells[position] = (key.y() << Y_SHIFT) | custom | ((key.z() & 15) << 4) | (key.x() & 15);
valueIndexes[position] = table.indexOf(value);
}
values = table.values.toArray();
ArrayList<Placement> crossing = new ArrayList<>();
for (int ordinal = 0; ordinal < placements.size(); ordinal++) {
List<ObjectDestinationTransaction.Mutation> placement = placements.get(ordinal);
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments = group(placement);
if (fragments.size() > 1) {
crossing.add(new Placement(ordinal, bounds(placement), List.copyOf(fragments.keySet()), fragments));
for (int ordinal = 0; ordinal < accepted.size(); ordinal++) {
ObjectDestinationTransaction.PlacementRange range = accepted.get(ordinal);
Placement placement = crossingPlacement(ordinal, range.start(), range.end(), mutations, mutationDestinations, destinationSlots, positions);
if (placement != null) {
crossing.add(placement);
}
}
this.placements = List.copyOf(crossing);
Long2ObjectOpenHashMap<ArrayList<ObjectDestinationTransaction.Mutation>> grouped = new Long2ObjectOpenHashMap<>(2);
long weight = PLAN_BYTES;
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
long key = destinationKey(mutation.x() >> 4, mutation.z() >> 4);
ArrayList<ObjectDestinationTransaction.Mutation> local = grouped.get(key);
if (local == null) {
local = new ArrayList<>();
grouped.put(key, local);
weight += DESTINATION_INDEX_BYTES;
}
local.add(mutation);
Object payload = switch (mutation) {
case ObjectDestinationTransaction.SetMutation set -> set.value();
case ObjectDestinationTransaction.CustomBlockMutation custom -> custom.state();
};
weight = Math.min(Integer.MAX_VALUE, weight + MUTATION_BYTES + payloadBytes(payload));
}
destinations = new Long2ObjectOpenHashMap<>(grouped.size());
for (Long2ObjectMap.Entry<ArrayList<ObjectDestinationTransaction.Mutation>> entry : grouped.long2ObjectEntrySet()) {
destinations.put(entry.getLongKey(), List.copyOf(entry.getValue()));
}
for (Placement placement : this.placements) {
placements = crossing.toArray(new Placement[0]);
long weight = PLAN_BYTES
+ (long) destinations.length * DESTINATION_BYTES
+ (long) count * MUTATION_BYTES
+ table.payloadBytes
+ (long) values.length * VALUE_BYTES;
for (Placement placement : placements) {
weight += PLACEMENT_BYTES + (long) placement.touched().size() * PLACEMENT_DESTINATION_BYTES;
for (List<ObjectDestinationTransaction.Mutation> fragment : placement.fragments().values()) {
long retainedCapacity = Math.max(10L, fragment.size() + (fragment.size() + 1L) / 2L);
weight += retainedCapacity * REFERENCE_BYTES;
}
estimatedRetainedBytes = (int) Math.min(Integer.MAX_VALUE, weight);
}
private ObjectSourcePlan(long[] destinations) {
this.destinations = destinations;
this.offsets = new int[destinations.length + 1];
this.cells = new int[0];
this.valueIndexes = new int[0];
this.values = new Object[0];
this.placements = new Placement[0];
this.estimatedRetainedBytes = PLAN_BYTES + destinations.length * DESTINATION_BYTES;
}
ObjectSourcePlan destinationIndex() {
return new ObjectSourcePlan(destinations);
}
private static Placement crossingPlacement(
int ordinal,
int start,
int end,
List<ObjectDestinationTransaction.Mutation> mutations,
long[] mutationDestinations,
Long2IntOpenHashMap destinationSlots,
int[] positions
) {
if (start < 0 || end > mutations.size() || start >= end) {
throw new IllegalArgumentException("Object placement range is outside the plan");
}
long first = mutationDestinations[start];
int index = start + 1;
while (index < end && mutationDestinations[index] == first) {
index++;
}
if (index == end) {
return null;
}
LongArrayList touched = new LongArrayList(4);
Long2IntOpenHashMap touchedIndexes = new Long2IntOpenHashMap(4);
touchedIndexes.defaultReturnValue(-1);
ArrayList<int[]> runs = new ArrayList<>(4);
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (int mutationIndex = start; mutationIndex < end; mutationIndex++) {
ObjectDestinationTransaction.Mutation mutation = mutations.get(mutationIndex);
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
long destination = mutationDestinations[mutationIndex];
int touchedIndex = touchedIndexes.get(destination);
if (touchedIndex < 0) {
touchedIndexes.put(destination, touched.size());
touched.add(destination);
runs.add(new int[]{destinationSlots.get(destination), positions[mutationIndex], 1});
} else {
runs.get(touchedIndex)[2]++;
}
}
this.estimatedRetainedBytes = weight >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) weight;
int size = touched.size();
ArrayList<ObjectContinuationBundle.ChunkPosition> chunks = new ArrayList<>(size);
int[] slots = new int[size];
int[] runStarts = new int[size];
int[] runLengths = new int[size];
for (int touchedIndex = 0; touchedIndex < size; touchedIndex++) {
long destination = touched.getLong(touchedIndex);
chunks.add(new ObjectContinuationBundle.ChunkPosition(destinationX(destination), destinationZ(destination)));
int[] run = runs.get(touchedIndex);
slots[touchedIndex] = run[0];
runStarts[touchedIndex] = run[1];
runLengths[touchedIndex] = run[2];
}
return new Placement(ordinal, new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ),
List.copyOf(chunks), slots, runStarts, runLengths);
}
int destinationSlot(int chunkX, int chunkZ) {
return Arrays.binarySearch(destinations, destinationKey(chunkX, chunkZ));
}
int destinationCount() {
return destinations.length;
}
boolean isEmpty() {
return destinations.length == 0;
}
List<ObjectDestinationTransaction.Mutation> mutationsFor(int chunkX, int chunkZ) {
List<ObjectDestinationTransaction.Mutation> mutations = destinations.get(destinationKey(chunkX, chunkZ));
return mutations == null ? List.of() : mutations;
int slot = destinationSlot(chunkX, chunkZ);
return slot < 0 ? List.of() : decode(chunkX, chunkZ, offsets[slot], offsets[slot + 1] - offsets[slot]);
}
void persistContinuations(MantleWriter writer, int sourceX, int sourceZ, int destinationX, int destinationZ) {
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(destinationX, destinationZ);
if (placements.length == 0) {
return;
}
int destinationSlot = destinationSlot(destinationX, destinationZ);
if (destinationSlot < 0) {
return;
}
for (Placement placement : placements) {
if (!placement.fragments().containsKey(destination)) {
if (!placement.touches(destinationSlot)) {
continue;
}
ObjectContinuationBundle.PlacementKey key = new ObjectContinuationBundle.PlacementKey(ObjectContinuationBundle.Kind.BIOME, sourceX, sourceZ, placement.ordinal());
for (Map.Entry<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> entry : placement.fragments().entrySet()) {
ObjectContinuationBundle.ChunkPosition target = entry.getKey();
if (target.equals(destination)) {
for (int touchedIndex = 0; touchedIndex < placement.slots().length; touchedIndex++) {
if (placement.slots()[touchedIndex] == destinationSlot) {
continue;
}
ObjectContinuationBundle.ChunkPosition target = placement.touched().get(touchedIndex);
int runStart = placement.runStarts()[touchedIndex];
int runLength = placement.runLengths()[touchedIndex];
writer.withChunkFence(target.x(), target.z(), () -> {
MantleChunk<Matter> chunk = writer.acquireChunk(target.x(), target.z());
if (chunk.isFlagged(MantleFlag.REAL)) {
@@ -107,13 +256,32 @@ final class ObjectSourcePlan {
}
}
ObjectContinuationBundle.Fragment fragment = new ObjectContinuationBundle.Fragment(key, placement.bounds(), placement.touched(),
encode(entry.getValue(), writer.getMantle().getWorldHeight()));
encode(decode(target.x(), target.z(), runStart, runLength), writer.getMantle().getWorldHeight()));
writer.setData(x, 0, z, bundle == null ? new ObjectContinuationBundle(List.of(fragment)) : bundle.with(fragment));
});
}
}
}
private List<ObjectDestinationTransaction.Mutation> decode(int chunkX, int chunkZ, int start, int length) {
ObjectDestinationTransaction.Mutation[] decoded = new ObjectDestinationTransaction.Mutation[length];
int baseX = chunkX << 4;
int baseZ = chunkZ << 4;
for (int index = 0; index < length; index++) {
int cell = cells[start + index];
int x = baseX | (cell & 15);
int z = baseZ | ((cell >>> 4) & 15);
int y = cell >>> Y_SHIFT;
Object value = values[valueIndexes[start + index]];
decoded[index] = (cell & CUSTOM_BIT) != 0
? new ObjectDestinationTransaction.CustomBlockMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, NativeBlockState.class), (NativeBlockState) value)
: new ObjectDestinationTransaction.SetMutation(
new ObjectDestinationTransaction.DataKey(x, y, z, typeOf(value)), value);
}
return List.of(decoded);
}
static byte[] encode(List<ObjectDestinationTransaction.Mutation> mutations, int height) {
IrisMatterSupport.ensureRegistered();
Matter payload = new IrisMatter(16, height, 16);
@@ -134,54 +302,41 @@ final class ObjectSourcePlan {
return ObjectContinuationBundle.encode(payload);
}
private static LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> group(
List<ObjectDestinationTransaction.Mutation> mutations
) {
LinkedHashMap<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> grouped = new LinkedHashMap<>();
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
ObjectContinuationBundle.ChunkPosition destination = new ObjectContinuationBundle.ChunkPosition(mutation.x() >> 4, mutation.z() >> 4);
grouped.computeIfAbsent(destination, ignored -> new ArrayList<>()).add(mutation);
}
return grouped;
}
private static ObjectContinuationBundle.Bounds bounds(List<ObjectDestinationTransaction.Mutation> mutations) {
int minimumX = Integer.MAX_VALUE;
int minimumZ = Integer.MAX_VALUE;
int maximumX = Integer.MIN_VALUE;
int maximumZ = Integer.MIN_VALUE;
for (ObjectDestinationTransaction.Mutation mutation : mutations) {
minimumX = Math.min(minimumX, mutation.x());
minimumZ = Math.min(minimumZ, mutation.z());
maximumX = Math.max(maximumX, mutation.x());
maximumZ = Math.max(maximumZ, mutation.z());
}
return new ObjectContinuationBundle.Bounds(minimumX, minimumZ, maximumX, maximumZ);
}
int estimatedRetainedBytes() {
return estimatedRetainedBytes;
}
static Class<?> typeOf(Object value) {
return value instanceof NativeBlockState ? NativeBlockState.class : value.getClass();
}
static long destinationKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
}
private static int destinationX(long destination) {
return (int) (destination >> 32);
}
private static int destinationZ(long destination) {
return (int) destination;
}
private static long payloadBytes(Object value) {
return switch (value) {
case null -> 0L;
case String text -> stringBytes(text);
case TreeBlockMaterial material -> 24L + stringBytes(material.materialKey());
case Identifier identifier -> 32L + stringBytes(identifier.namespace()) + stringBytes(identifier.key());
case NativeBlockState ignored -> 64L;
case MatterCavern cavern -> 32L + stringBytes(cavern.getCustomBiome());
case HydrologyCaveCell cell -> 64L + stringBytes(cell.fluidProfileKey()) + stringBytes(cell.floodedBiomeKey());
case Byte ignored -> 24L;
case Short ignored -> 24L;
case Integer ignored -> 24L;
case Long ignored -> 24L;
case Float ignored -> 24L;
case Double ignored -> 24L;
case Boolean ignored -> 24L;
case Character ignored -> 24L;
case TreeBlockMaterial material -> 16L + stringBytes(material.materialKey());
case Identifier identifier -> 24L + stringBytes(identifier.namespace()) + stringBytes(identifier.key());
case NativeBlockState ignored -> 0L;
case MatterCavern cavern -> 24L + stringBytes(cavern.getCustomBiome());
case HydrologyCaveCell cell -> 48L + stringBytes(cell.fluidProfileKey()) + stringBytes(cell.floodedBiomeKey());
case MatterMarker marker -> 16L + stringBytes(marker.getTag());
case TileWrapper ignored -> 4L * UNKNOWN_PAYLOAD_BYTES;
case Enum<?> ignored -> 0L;
default -> Integer.MAX_VALUE;
case Number ignored -> 16L;
case Boolean ignored -> 16L;
case Character ignored -> 16L;
default -> UNKNOWN_PAYLOAD_BYTES;
};
}
@@ -189,12 +344,45 @@ final class ObjectSourcePlan {
return value == null ? 0L : 48L + 2L * value.length();
}
private record Placement(int ordinal, ObjectContinuationBundle.Bounds bounds,
List<ObjectContinuationBundle.ChunkPosition> touched,
Map<ObjectContinuationBundle.ChunkPosition, List<ObjectDestinationTransaction.Mutation>> fragments) {
private static final class ValueTable {
private final ArrayList<Object> values = new ArrayList<>();
private final Object2IntOpenHashMap<Object> equalValues = new Object2IntOpenHashMap<>();
private final Reference2IntOpenHashMap<Object> identityValues = new Reference2IntOpenHashMap<>();
private long payloadBytes;
private ValueTable() {
equalValues.defaultReturnValue(-1);
identityValues.defaultReturnValue(-1);
}
private int indexOf(Object value) {
boolean byValue = value instanceof String || value instanceof TreeBlockMaterial;
int index = byValue ? equalValues.getInt(value) : identityValues.getInt(value);
if (index >= 0) {
return index;
}
index = values.size();
values.add(value);
payloadBytes += payloadBytes(value);
if (byValue) {
equalValues.put(value, index);
} else {
identityValues.put(value, index);
}
return index;
}
}
private static long destinationKey(int chunkX, int chunkZ) {
return ((long) chunkX << 32) | (chunkZ & 0xffffffffL);
private record Placement(int ordinal, ObjectContinuationBundle.Bounds bounds,
List<ObjectContinuationBundle.ChunkPosition> touched,
int[] slots, int[] runStarts, int[] runLengths) {
private boolean touches(int slot) {
for (int touchedSlot : slots) {
if (touchedSlot == slot) {
return true;
}
}
return false;
}
}
}
@@ -8,14 +8,22 @@ import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
import java.util.function.Supplier;
import static art.arcane.iris.generation.cache.Cache.key;
final class ObjectSourcePlanCache {
private static final long MAXIMUM_RETAINED_BYTES = 256L * 1024L * 1024L;
private static final long MAXIMUM_RETAINED_BYTES = 512L * 1024L * 1024L;
private static final int ENTRY_BYTES = 64;
private final long maximumRetainedBytes;
private final LongAdder lookups = new LongAdder();
private final LongAdder builds = new LongAdder();
private final LongAdder waits = new LongAdder();
private final LongAdder replays = new LongAdder();
private final LongAdder drained = new LongAdder();
private final LongAdder retired = new LongAdder();
private volatile State state;
ObjectSourcePlanCache() {
@@ -31,33 +39,84 @@ final class ObjectSourcePlanCache {
}
static long retainedByteBudget(long maximumHeapBytes) {
return Math.max(1L, Math.min(MAXIMUM_RETAINED_BYTES, maximumHeapBytes / 64L));
return Math.max(1L, Math.min(MAXIMUM_RETAINED_BYTES, maximumHeapBytes / 32L));
}
ObjectSourcePlan get(int sourceChunkX, int sourceChunkZ, Supplier<ObjectSourcePlan> builder) {
/**
* Returns the plan whose mutations the destination still has to replay, or null when the source writes nothing
* there. Each destination consumes a retained plan once; a plan whose destinations were all consumed keeps only
* its destination index, so a repeated consumption rebuilds it instead of trusting released state. The entry is
* dropped once every destination within the source chunk radius has asked for it.
*/
ObjectSourcePlan acquire(int sourceChunkX, int sourceChunkZ, int destinationChunkX, int destinationChunkZ,
int sourceChunkRadius, Supplier<ObjectSourcePlan> builder) {
Objects.requireNonNull(builder, "Source plan builder");
lookups.increment();
State current = state;
long source = CacheKey.mix(key(sourceChunkX, sourceChunkZ));
ObjectSourcePlan cached = current.plans.getIfPresent(source);
if (cached != null) {
return cached;
Entry entry = current.plans.getIfPresent(source);
if (entry == null) {
entry = load(current, source, sourceChunkRadius, builder);
}
boolean lastVisitor = entry.visitors.visit(destinationChunkX - sourceChunkX, destinationChunkZ - sourceChunkZ);
ObjectSourcePlan plan = claim(current, source, entry, destinationChunkX, destinationChunkZ, builder);
if (lastVisitor) {
retired.increment();
Visitors visitors = entry.visitors;
current.plans.asMap().computeIfPresent(source, (Long key, Entry mapped) -> mapped.visitors == visitors ? null : mapped);
}
return plan;
}
private ObjectSourcePlan claim(State current, long source, Entry entry, int destinationChunkX, int destinationChunkZ,
Supplier<ObjectSourcePlan> builder) {
int slot = entry.slot(destinationChunkX, destinationChunkZ);
if (slot < 0) {
return null;
}
Claim claim = entry.claim(slot);
if (claim == null) {
replays.increment();
return replay(current, source, entry, destinationChunkX, destinationChunkZ, builder);
}
if (claim.last()) {
drained.increment();
current.plans.asMap().replace(source, entry, entry.released());
}
return claim.plan();
}
private ObjectSourcePlan replay(State current, long source, Entry consumed, int destinationChunkX, int destinationChunkZ,
Supplier<ObjectSourcePlan> builder) {
Entry rebuilt = new Entry(build(builder), consumed.visitors);
int slot = rebuilt.slot(destinationChunkX, destinationChunkZ);
Claim claim = slot < 0 ? null : rebuilt.claim(slot);
if (claim == null) {
return null;
}
if (!claim.last() && rebuilt.weight() <= maximumRetainedBytes) {
current.plans.asMap().replace(source, consumed, rebuilt);
}
return claim.plan();
}
private Entry load(State current, long source, int sourceChunkRadius, Supplier<ObjectSourcePlan> builder) {
Pending created = new Pending(Thread.currentThread());
Pending pending = current.pending.putIfAbsent(source, created);
if (pending != null) {
waits.increment();
return pending.await();
}
try {
ObjectSourcePlan plan = current.plans.getIfPresent(source);
if (plan == null) {
plan = builder.get();
if (plan != null && plan.estimatedRetainedBytes() < Integer.MAX_VALUE
&& plan.estimatedRetainedBytes() <= maximumRetainedBytes) {
current.plans.put(source, plan);
Entry entry = current.plans.getIfPresent(source);
if (entry == null) {
entry = new Entry(build(builder), new Visitors(sourceChunkRadius));
if (entry.weight() <= maximumRetainedBytes) {
current.plans.put(source, entry);
}
}
created.result.complete(plan);
return plan;
created.result.complete(entry);
return entry;
} catch (RuntimeException | Error failure) {
created.failure = failure;
created.result.completeExceptionally(failure);
@@ -67,6 +126,11 @@ final class ObjectSourcePlanCache {
}
}
private ObjectSourcePlan build(Supplier<ObjectSourcePlan> builder) {
builds.increment();
return Objects.requireNonNull(builder.get(), "Source plan builder returned no plan");
}
void clear() {
state = new State(maximumRetainedBytes);
}
@@ -77,28 +141,134 @@ final class ObjectSourcePlanCache {
return current.plans.estimatedSize();
}
Stats stats() {
State current = state;
current.plans.cleanUp();
long retainedBytes = current.plans.policy().eviction()
.map(eviction -> eviction.weightedSize().orElse(0L))
.orElse(0L);
return new Stats(lookups.sum(), builds.sum(), waits.sum(), replays.sum(), drained.sum(), retired.sum(),
current.plans.estimatedSize(), retainedBytes, maximumRetainedBytes);
}
record Stats(long lookups, long builds, long waits, long replays, long drained, long retired, long retained,
long retainedBytes, long budgetBytes) {
}
private static final class State {
private final Cache<Long, ObjectSourcePlan> plans;
private final Cache<Long, Entry> plans;
private final ConcurrentHashMap<Long, Pending> pending = new ConcurrentHashMap<>();
private State(long maximumRetainedBytes) {
plans = Caffeine.newBuilder()
.maximumWeight(maximumRetainedBytes)
.weigher((Long key, ObjectSourcePlan plan) -> plan.estimatedRetainedBytes())
.weigher((Long key, Entry entry) -> entry.weight())
.build();
}
}
private record Claim(ObjectSourcePlan plan, boolean last) {
}
private static final class Entry {
private final ObjectSourcePlan index;
private final boolean[] claimed;
private final Visitors visitors;
private final int weight;
private ObjectSourcePlan plan;
private int remaining;
private Entry(ObjectSourcePlan plan, Visitors visitors) {
this.index = plan.isEmpty() ? ObjectSourcePlan.EMPTY : plan;
this.plan = plan.isEmpty() ? null : plan;
this.claimed = new boolean[plan.destinationCount()];
this.visitors = visitors;
this.remaining = claimed.length;
this.weight = weigh(index, claimed, visitors);
}
private Entry(ObjectSourcePlan index, boolean[] claimed, Visitors visitors) {
this.index = index;
this.claimed = claimed;
this.visitors = visitors;
this.weight = weigh(index, claimed, visitors);
}
private int slot(int destinationChunkX, int destinationChunkZ) {
return index.destinationSlot(destinationChunkX, destinationChunkZ);
}
private synchronized Claim claim(int slot) {
ObjectSourcePlan retained = plan;
if (retained == null || claimed[slot]) {
return null;
}
claimed[slot] = true;
if (--remaining == 0) {
plan = null;
return new Claim(retained, true);
}
return new Claim(retained, false);
}
private Entry released() {
return new Entry(index.destinationIndex(), claimed, visitors);
}
private int weight() {
return weight;
}
private static int weigh(ObjectSourcePlan index, boolean[] claimed, Visitors visitors) {
return (int) Math.min(Integer.MAX_VALUE,
ENTRY_BYTES + (long) claimed.length + visitors.retainedBytes() + index.estimatedRetainedBytes());
}
}
private static final class Visitors {
private final int radius;
private final int side;
private final long[] seen;
private int remaining;
private Visitors(int radius) {
if (radius < 0) {
throw new IllegalArgumentException("Source chunk radius must not be negative");
}
this.radius = radius;
this.side = 2 * radius + 1;
this.seen = new long[(side * side + 63) >>> 6];
this.remaining = side * side;
}
private synchronized boolean visit(int offsetX, int offsetZ) {
if (Math.abs(offsetX) > radius || Math.abs(offsetZ) > radius) {
return false;
}
int bit = (offsetX + radius) * side + offsetZ + radius;
long mask = 1L << (bit & 63);
if ((seen[bit >>> 6] & mask) != 0L) {
return false;
}
seen[bit >>> 6] |= mask;
return --remaining == 0;
}
private long retainedBytes() {
return 32L + 8L * seen.length;
}
}
private static final class Pending {
private final Thread owner;
private final CompletableFuture<ObjectSourcePlan> result = new CompletableFuture<>();
private final CompletableFuture<Entry> result = new CompletableFuture<>();
private Throwable failure;
private Pending(Thread owner) {
this.owner = owner;
}
private ObjectSourcePlan await() {
private Entry await() {
if (owner == Thread.currentThread()) {
throw new IllegalStateException("Recursive source plan construction");
}
@@ -134,8 +134,12 @@ public final class TerrainMatterView {
SOUTH(false, 15);
private final int[] positions = new int[256];
private final boolean fixedX;
private final int coordinate;
Face(boolean fixedX, int coordinate) {
this.fixedX = fixedX;
this.coordinate = coordinate;
for (int y = 0; y < 16; y++) {
for (int offset = 0; offset < 16; offset++) {
int x = fixedX ? coordinate : offset;
@@ -144,6 +148,14 @@ public final class TerrainMatterView {
}
}
}
int x(int offset) {
return fixedX ? coordinate : offset;
}
int z(int offset) {
return fixedX ? offset : coordinate;
}
}
public static <T> void iterate(MantleChunk<Matter> chunk, Class<T> type,
@@ -109,7 +109,7 @@ public class ModeOverworld extends IrisEngineMode implements EngineMode {
registerTerrainStage(sCave);
registerTerrainStage(sPost);
registerTerrainStage(sFloatingTerrainSolid);
registerStage((x, z, k, p, m, c) -> cave.decorateNaturalCaves(x, z, k));
registerStage((x, z, k, p, m, c) -> cave.decorateNaturalCaves(x, z, k, c));
// Never burst these three: all of them write the same block hunk (and sDecorant reads
// the surface sInsertMatter writes), so parallel order is scheduler-dependent. The
// production path already runs them inline in this order; sequential registration
@@ -0,0 +1,53 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.chunk.ColumnExtentListeningHunk;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
/**
* Forwards a chunk's block writes to {@link BlockUpdater#catchBlockUpdates} at world coordinates.
* The last state seen to need neither a block update nor custom handling is remembered, so the
* long runs of plain terrain skip the per-block checks. Racing writers only ever store such states.
*/
final class ChunkBlockUpdateListener implements ColumnExtentListeningHunk.Listener<NativeBlockState> {
private final BlockUpdater updater;
private final int originX;
private final int originZ;
private NativeBlockState quiet;
ChunkBlockUpdateListener(BlockUpdater updater, int originX, int originZ) {
this.updater = updater;
this.originX = originX;
this.originZ = originZ;
}
@Override
public void onWrite(int x, int y, int z, NativeBlockState state) {
if (state == null || state == quiet) {
return;
}
if (B.isUpdatable(state) || state.isCustom()) {
updater.catchBlockUpdates(originX + x, y, originZ + z, state);
return;
}
quiet = state;
}
}
@@ -59,6 +59,7 @@ import art.arcane.volmlib.util.documentation.BlockCoordinates;
import art.arcane.volmlib.util.documentation.ChunkCoordinates;
import art.arcane.volmlib.util.function.Function2;
import art.arcane.volmlib.util.hunk.Hunk;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.mantle.runtime.MantleChunk;
import art.arcane.volmlib.util.mantle.flag.MantleFlag;
import art.arcane.volmlib.util.math.M;
@@ -250,6 +251,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
default void save() {
NativeStructureOwnershipStore.flush(this);
syncGenerationHistory();
getMantle().save();
getWorldManager().onSave();
saveEngineData();
@@ -259,6 +261,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
default void saveNow() {
NativeStructureOwnershipStore.flush(this);
syncGenerationHistory();
getMantle().saveAllNow();
saveEngineData();
}
@@ -267,6 +270,8 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
void saveEngineData();
void syncGenerationHistory();
default String getName() {
return getDimension().getName();
}
@@ -337,25 +342,101 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
@BlockCoordinates
default IrisBiome getCaveOrMantleBiome(int x, int y, int z) {
IrisBiome mantleBiome = mantleCaveBiome(x, y, z);
return mantleBiome != null ? mantleBiome : getCaveBiome(x, y, z);
}
private IrisBiome mantleCaveBiome(int x, int y, int z) {
IrisBiome flooded = floodedCaveBiome(x, y, z);
return flooded != null ? flooded : customCaveBiome(getMantle().getMantle().get(x, y, z, MatterCavern.class));
}
private IrisBiome floodedCaveBiome(int x, int y, int z) {
HydrologyCaveCell hydrology = HydrologyCaveStorage.getIfPresent(
getMantle().getMantle(), x, y, z);
if (hydrology != null && !hydrology.floodedBiomeKey().isEmpty()) {
IrisBiome biome = getData().getBiomeLoader().load(hydrology.floodedBiomeKey());
if (biome != null) {
return biome;
return getData().getBiomeLoader().load(hydrology.floodedBiomeKey());
}
return null;
}
private IrisBiome customCaveBiome(MatterCavern cavern) {
if (cavern != null && cavern.getCustomBiome() != null && !cavern.getCustomBiome().isEmpty()) {
return getData().getBiomeLoader().load(cavern.getCustomBiome());
}
return null;
}
private MantleChunk<Matter> useCavernChunk(int x, int z) {
Mantle<Matter> mantle = getMantle().getMantle();
int chunkX = x >> 4;
int chunkZ = z >> 4;
return mantle.hasTectonicPlate(chunkX >> 5, chunkZ >> 5) ? mantle.useChunk(chunkX, chunkZ) : null;
}
/**
* Fills {@code biomes[i]} with {@link #getBiomeOrMantle(int, int, int)} and {@code regions[i]} with
* {@link #getRegion(int, int, int)} at y = i * step of one column, resolving what does not vary along it once.
*/
@BlockCoordinates
default void getBiomeOrMantleColumn(int x, int z, int step, IrisBiome[] biomes, IrisRegion[] regions) {
if (getDimensionStackContext() != null) {
for (int index = 0; index < biomes.length; index++) {
biomes[index] = getBiomeOrMantle(x, index * step, z);
regions[index] = getRegion(x, index * step, z);
}
return;
}
IrisComplex complex = getComplex();
int caveTop = getHeight(x, z) - 2;
IrisRegion region = getRegion(x, 0, z);
IrisBiome surface = null;
boolean cavernChunkResolved = false;
MantleChunk<Matter> cavernChunk = null;
boolean caveColumnResolved = false;
IrisBiome caveBase = null;
IrisBiome caveSurface = null;
int caveSurfaceY = 0;
try {
for (int index = 0; index < biomes.length; index++) {
int y = index * step;
regions[index] = region;
if (y > caveTop || complex.isTerrain3DSurface(x, y, z)) {
if (surface == null) {
surface = getSurfaceBiome(x, z);
}
biomes[index] = surface;
continue;
}
IrisBiome biome = floodedCaveBiome(x, y, z);
if (biome == null) {
if (!cavernChunkResolved) {
cavernChunk = useCavernChunk(x, z);
cavernChunkResolved = true;
}
biome = cavernChunk == null || y < 0 || y >= getMantle().getMantle().getWorldHeight()
? null : customCaveBiome(cavernChunk.get(x & 15, y, z & 15, MatterCavern.class));
}
if (biome == null) {
biome = resolveConfiguredCaveBiome(x, y, z, null);
}
if (biome == null) {
if (!caveColumnResolved) {
boolean naturalFallback = answersFromNaturalTerrain(x, z);
caveSurface = caveDepthSurfaceBiome(x, z, naturalFallback);
caveSurfaceY = caveDepthSurfaceY(x, z, naturalFallback);
caveBase = getCaveBiome(x, z);
caveColumnResolved = true;
}
biome = depthCaveBiome(caveBase, y, caveSurface, caveSurfaceY);
}
biomes[index] = biome;
}
} finally {
if (cavernChunk != null) {
cavernChunk.release();
}
}
MatterCavern m = getMantle().getMantle().get(x, y, z, MatterCavern.class);
if (m != null && m.getCustomBiome() != null && !m.getCustomBiome().isEmpty()) {
IrisBiome biome = getData().getBiomeLoader().load(m.getCustomBiome());
if (biome != null) {
return biome;
}
}
return getCaveBiome(x, y, z);
}
@ChunkCoordinates
@@ -382,16 +463,22 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return configuredBiome;
}
boolean naturalFallback = answersFromNaturalTerrain(x, z);
DimensionStackContext dimensionStackContext = getDimensionStackContext();
IrisBiome surfaceBiome = naturalFallback || dimensionStackContext == null
? naturalFallback
? getComplex().naturalSurfaceBiome(x, z)
: getSurfaceBiome(x, z)
: getComplex().getTrueBiomeStream().get(x, z);
int surfaceY = naturalFallback
IrisBiome surfaceBiome = caveDepthSurfaceBiome(x, z, naturalFallback);
int surfaceY = caveDepthSurfaceY(x, z, naturalFallback);
return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY);
}
private IrisBiome caveDepthSurfaceBiome(int x, int z, boolean naturalFallback) {
if (naturalFallback) {
return getComplex().naturalSurfaceBiome(x, z);
}
return getDimensionStackContext() == null ? getSurfaceBiome(x, z) : getComplex().getTrueBiomeStream().get(x, z);
}
private int caveDepthSurfaceY(int x, int z, boolean naturalFallback) {
return naturalFallback
? getComplex().naturalTrueHeight(x, z)
: getComplex().getHeightStream().get(x, z).intValue();
return resolveDepthCaveBiome(x, y, z, surfaceBiome, surfaceY);
}
@BlockCoordinates
@@ -427,7 +514,10 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
IrisBiome surfaceBiome,
int surfaceY
) {
IrisBiome caveBiome = getCaveBiome(x, z);
return depthCaveBiome(getCaveBiome(x, z), y, surfaceBiome, surfaceY);
}
private static IrisBiome depthCaveBiome(IrisBiome caveBiome, int y, IrisBiome surfaceBiome, int surfaceY) {
if (caveBiome == null) {
return surfaceBiome;
}
@@ -673,7 +763,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
return o.getObject().getLoadKey() + "@" + o.getId();
}
MantleChunk<Matter> chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
MantleChunk<Matter> chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4);
try {
String raw = chunk.get(x & 15, y, z & 15, String.class);
return (raw == null || raw.isEmpty()) ? null : raw;
@@ -683,7 +773,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
}
default PlacedObject getObjectPlacement(int x, int y, int z) {
MantleChunk<Matter> chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
MantleChunk<Matter> chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4);
try {
return getObjectPlacement(x, y, z, chunk);
} finally {
@@ -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) {
@@ -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,9 +215,12 @@ 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<Matter> continuations = restoreObjectContinuations(x, z);
if (!continuations.isEmpty()) {
ctx.setCaveTerrain(null);
}
for (EngineStage i : stages) {
i.generate(x, z, blocks, biomes, multicore, ctx);
}
@@ -259,7 +262,7 @@ public interface EngineMode extends Staged {
}
private void restoreObjectMetadata(int x, int z, Matter continuation) {
MantleChunk<Matter> chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
MantleChunk<Matter> chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4);
try {
synchronized (chunk) {
MatterSlice<NativeBlockState> states = continuation.getSlice(NativeBlockState.class);
@@ -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);
}
}
@@ -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;
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Frame> slots = new AtomicReferenceArray<>(SLOTS);
private final ThreadLocal<Frame> 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;
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<GenerationRuntimeBinding> 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);
}
}
@@ -21,6 +21,7 @@ package art.arcane.iris.generation.runtime;
import art.arcane.iris.generation.stream.GenerationStreams;
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.configuration.IrisSettings;
@@ -115,7 +116,7 @@ public class IrisComplex implements DataProvider {
private static final AtomicLong lastBoundsFailureLog = new AtomicLong(0L);
private static final int GRID_BOUNDS_CACHE_SIZE = 8192;
private static final int STUDIO_NOISE_CACHE_SIZE = 32_768;
private static final int TERRAIN_COLUMN_CACHE_SIZE = 65_536;
private static final int TERRAIN_COLUMN_CACHE_SIZE = 524_288;
/** One million corners: about 16 MB, roughly a 4000 by 4000 block area at the 4-block grid. */
private static final int SHARED_CORNER_BOUNDS_CAPACITY = 1 << 20;
/** The slope streams measure the rise across a run of this many blocks, so gradient is slope over run. */
@@ -386,10 +387,9 @@ public class IrisComplex implements DataProvider {
proceduralTerrainHeight = new ProceduralTerrainHeightSampler(
(x, z) -> getHeight(engine, x, z, engine.getSeedManager().getHeight()),
engine.getDimension().getTerrainSamplingStep());
baseTerrainHeightStream = GenerationStreams.cache2DDouble(ProceduralStream.of(
this::sampleUnblendedNaturalTerrainHeight,
Interpolated.DOUBLE
), "baseTerrainHeightStream", engine, cacheSize);
baseTerrainHeightStream = GenerationStreams.cache2DDouble(
ProceduralStream.ofDouble(this::sampleUnblendedNaturalTerrainHeight),
"baseTerrainHeightStream", engine, cacheSize);
boolean terrain3DEnabled = false;
for (IrisBiome biome : generatorBiomes) {
if (biome.getTerrain3D() != null) {
@@ -992,7 +992,11 @@ public class IrisComplex implements DataProvider {
HydrologyColumnLayer layer = sample.primarySurfaceLayerOrNull();
if (hydrologyBanks3D != null && layer != null && layer.terrainOwned() && !layer.channel()) {
Optional<Terrain3DColumn> column = hydrologyBanks3D.columnIfReady(blockCoordinate(x), blockCoordinate(z));
return column.isPresent() ? column.get().topY() : naturalHeightStream.getDouble(x, z);
if (column.isPresent()) {
return column.get().topY();
}
ProvisionalSampling.mark();
return naturalHeightStream.getDouble(x, z);
}
return sample.terrainHeight();
}
@@ -1188,8 +1192,11 @@ public class IrisComplex implements DataProvider {
return false;
}
HydrologyColumnLayer layer = snapshot.column() == null ? null : snapshot.column().primarySurfaceLayerOrNull();
return layer == null || !layer.terrainOwned() || layer.channel()
|| hydrologyBanks3D.columnIfReady(x, z).isPresent();
if (layer == null || !layer.terrainOwned() || layer.channel() || hydrologyBanks3D.columnIfReady(x, z).isPresent()) {
return true;
}
ProvisionalSampling.mark();
return false;
}
public ProceduralStream<Double> getRawHeightStream() {
@@ -1222,6 +1229,17 @@ public class IrisComplex implements DataProvider {
|| !transitionGenerationPlan.boundary().isHistoricalChunk(chunkX, chunkZ);
}
/**
* Whether the mantle chunk can hold hydrology cave cells. The hydrology component publishes them
* only from footprint columns within one block of the chunk, so a chunk with none around it holds
* none. A transition world or a studio keeps cells an earlier plan wrote, so there every chunk may.
*/
public boolean mayHoldHydrologyCells(int chunkX, int chunkZ) {
return hydrologyRuntime == null || transitionGenerationPlan != null
|| terrainEngine == null || terrainEngine.isStudio()
|| hydrologyRuntime.hasColumnsAround(chunkX, chunkZ);
}
public boolean allowsNewDiscreteContentAt(int blockX, int blockZ) {
return transitionGenerationPlan == null
|| transitionGenerationPlan.allowsNewDiscreteContentAt(blockX, blockZ);
@@ -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> runtimeAssembly = new ThreadLocal<>();
@Getter(AccessLevel.NONE)
@Setter(AccessLevel.NONE)
final ThreadLocal<BiomeEnvironmentBinding> 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<GenerationRuntime> 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) {
@@ -524,7 +519,7 @@ public class IrisEngine implements Engine {
return drawBiomeEnvironment(x, z, saved.get());
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "draw a pregeneration preview")) {
openGenerationHistoryReadScope(x, z, "draw a pregeneration preview")) {
return Engine.super.draw(x, z);
}
});
@@ -540,7 +535,7 @@ public class IrisEngine implements Engine {
return saved.get();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a biome environment")) {
openGenerationHistoryReadScope(x, z, "resolve a biome environment")) {
return Engine.super.getBiomeEnvironment(x, y, z);
}
}
@@ -552,7 +547,7 @@ public class IrisEngine implements Engine {
return saved.get();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a mantle biome environment")) {
openGenerationHistoryReadScope(x, z, "resolve a mantle biome environment")) {
return Engine.super.getBiomeOrMantleEnvironment(x, y, z);
}
}
@@ -564,7 +559,7 @@ public class IrisEngine implements Engine {
return saved.get();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome environment")) {
openGenerationHistoryReadScope(x, z, "resolve a surface biome environment")) {
return Engine.super.getSurfaceBiomeEnvironment(x, z);
}
}
@@ -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;
}
@@ -600,7 +595,7 @@ public class IrisEngine implements Engine {
return saved.get().biome();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a biome")) {
openGenerationHistoryReadScope(x, z, "resolve a biome")) {
return Engine.super.getBiome(x, y, z);
}
}
@@ -613,11 +608,27 @@ public class IrisEngine implements Engine {
return saved.get().biome();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a biome or mantle biome")) {
openGenerationHistoryReadScope(x, z, "resolve a biome or mantle biome")) {
return Engine.super.getBiomeOrMantle(x, y, z);
}
}
@BlockCoordinates
@Override
public void getBiomeOrMantleColumn(int x, int z, int step, IrisBiome[] biomes, IrisRegion[] regions) {
if (usesSavedBiomeEnvironment()) {
for (int index = 0; index < biomes.length; index++) {
biomes[index] = getBiomeOrMantle(x, index * step, z);
regions[index] = getRegion(x, index * step, z);
}
return;
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryReadScope(x, z, "resolve a biome or mantle biome column")) {
Engine.super.getBiomeOrMantleColumn(x, z, step, biomes, regions);
}
}
@BlockCoordinates
@Override
public IrisRegion getRegion(int x, int z) {
@@ -626,7 +637,7 @@ public class IrisEngine implements Engine {
return saved.get().region();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a region")) {
openGenerationHistoryReadScope(x, z, "resolve a region")) {
return Engine.super.getRegion(x, z);
}
}
@@ -639,7 +650,7 @@ public class IrisEngine implements Engine {
return saved.get().region();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical region")) {
openGenerationHistoryReadScope(x, z, "resolve a vertical region")) {
return Engine.super.getRegion(x, y, z);
}
}
@@ -652,7 +663,7 @@ public class IrisEngine implements Engine {
return saved.get().biome();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave or mantle biome")) {
openGenerationHistoryReadScope(x, z, "resolve a cave or mantle biome")) {
return Engine.super.getCaveOrMantleBiome(x, y, z);
}
}
@@ -670,7 +681,7 @@ public class IrisEngine implements Engine {
return Engine.super.getCaveBiome(x, z);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a cave biome")) {
openGenerationHistoryReadScope(x, z, "resolve a cave biome")) {
return Engine.super.getCaveBiome(x, z);
}
}
@@ -697,7 +708,7 @@ public class IrisEngine implements Engine {
return Engine.super.getCaveBiome(x, y, z, state);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a vertical cave biome")) {
openGenerationHistoryReadScope(x, z, "resolve a vertical cave biome")) {
return Engine.super.getCaveBiome(x, y, z, state);
}
}
@@ -713,7 +724,7 @@ public class IrisEngine implements Engine {
return Engine.super.getSurfaceBiome(x, z);
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve a surface biome")) {
openGenerationHistoryReadScope(x, z, "resolve a surface biome")) {
return Engine.super.getSurfaceBiome(x, z);
}
}
@@ -722,7 +733,7 @@ public class IrisEngine implements Engine {
@Override
public double getSlope(int x, int z) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve terrain slope")) {
openGenerationHistoryReadScope(x, z, "resolve terrain slope")) {
return Engine.super.getSlope(x, z);
}
}
@@ -737,7 +748,7 @@ public class IrisEngine implements Engine {
@Override
public int getHeight(int x, int z, boolean ignoreFluid) {
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x, z, "resolve terrain height")) {
openGenerationHistoryReadScope(x, z, "resolve terrain height")) {
return Engine.super.getHeight(x, z, ignoreFluid);
}
}
@@ -750,7 +761,7 @@ public class IrisEngine implements Engine {
return recorded.get().objectKeys();
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(x << 4, z << 4, "resolve chunk objects")) {
openGenerationHistoryReadScope(x << 4, z << 4, "resolve chunk objects")) {
return getMantle().getObjectComponent().guess(x, z);
}
}
@@ -763,7 +774,7 @@ public class IrisEngine implements Engine {
return new HashSet<>(recorded.get().pointsOfInterest());
}
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(
openGenerationHistoryReadScope(
chunkX << 4, chunkZ << 4, "resolve chunk points of interest")) {
Set<ChunkGenerationSemantics.PointOfInterest> pois = new HashSet<>();
getMantle().getMantle().iterateChunk(
@@ -859,7 +870,7 @@ public class IrisEngine implements Engine {
int queryMinimumZ = Math.max(minimumZ, chunkMinimumZ);
int queryMaximumZ = Math.min(maximumZ, chunkMinimumZ + 15);
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
openGenerationHistoryCoordinateScopeUnchecked(
openGenerationHistoryReadScope(
chunkMinimumX,
chunkMinimumZ,
"resolve native structure volumes")) {
@@ -979,20 +990,23 @@ public class IrisEngine implements Engine {
}
public boolean hasGenerationRuntimeScope() {
return generationRuntimeScopes.current() != null;
return threadState.binding() != null;
}
private boolean usesScopedNaturalTerrain() {
return hasGenerationRuntimeScope() && getComplex().isNaturalTerrainContext();
}
private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryCoordinateScopeUnchecked(
private GenerationHistoryRuntimeRouter.CoordinateScope openGenerationHistoryReadScope(
int blockX,
int blockZ,
String operation
) {
try {
return openGenerationHistoryCoordinateScope(blockX, blockZ);
GenerationHistoryRuntimeRouter router = generationHistoryRuntimeRouter;
return router == null
? openGenerationHistoryCoordinateScope(blockX, blockZ)
: router.openReadScope(blockX, blockZ);
} catch (IOException failure) {
throw new IllegalStateException("Unable to " + operation + " through Iris generation history at "
+ blockX + "," + blockZ + ".", failure);
@@ -1058,7 +1072,7 @@ public class IrisEngine implements Engine {
}
public GenerationRuntimeBinding captureGenerationRuntimeBinding() {
GenerationRuntimeBinding scoped = generationRuntimeScopes.current();
GenerationRuntimeBinding scoped = threadState.binding();
return scoped == null ? getActiveGenerationRuntimeBinding() : scoped;
}
@@ -1074,7 +1088,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 +1279,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 +1367,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 +1377,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 +1387,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<Long> getHash32() {
RuntimeAssembly assembly = runtimeAssembly.get();
RuntimeAssembly assembly = threadState.assembly();
if (assembly != null && assembly.hash32 != null) {
return assembly.hash32;
}
@@ -1465,7 +1491,7 @@ public class IrisEngine implements Engine {
IrisContext.Scope generationScope = IrisContext.open(this, lease.sessionId(), null)) {
getEngineData().getStatistics().generatedChunk();
PrecisionStopwatch p = PrecisionStopwatch.start();
Hunk<NativeBlockState> blocks = vblocks.listen((xx, y, zz, t) -> catchBlockUpdates(x + xx, y, z + zz, t));
Hunk<NativeBlockState> blocks = new ColumnExtentListeningHunk<>(vblocks, new ChunkBlockUpdateListener(this, x, z));
if (getDimension().isDebugChunkCrossSections() && ((x >> 4) % getDimension().getDebugCrossSectionsMod() == 0 || (z >> 4) % getDimension().getDebugCrossSectionsMod() == 0)) {
NativeBlockState crossSection = B.getState("CRYING_OBSIDIAN");
@@ -1481,7 +1507,7 @@ public class IrisEngine implements Engine {
boolean skipRealFlag = platformHooks.shouldBypassMantleStages(this);
if (!skipRealFlag) {
MantleChunk<Matter> chunk = getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
MantleChunk<Matter> chunk = getMantle().getMantle().useChunk(x >> 4, z >> 4);
try {
synchronized (chunk) {
Matter section = chunk.get(0);
@@ -1529,6 +1555,7 @@ public class IrisEngine implements Engine {
private void scheduleWorldSave() {
backgroundTasks.scheduleAdmittedTask(this, () -> NativeStructureOwnershipStore.flush(this));
backgroundTasks.scheduleAdmittedTask(this, this::syncGenerationHistory);
getMantle().save();
getWorldManager().onSave();
saveEngineData();
@@ -1539,6 +1566,20 @@ public class IrisEngine implements Engine {
engineDataStore.saveEngineData();
}
@Override
public void syncGenerationHistory() {
GenerationHistoryRuntimeRouter router = getGenerationHistoryRuntimeRouter().orElse(null);
if (router == null) {
return;
}
try {
router.history().sync();
} catch (IOException failure) {
throw new IllegalStateException("Unable to force Iris generation history to stable storage for "
+ getWorld().name() + ".", failure);
}
}
@Override
public void blockUpdatedMetric() {
bud.incrementAndGet();
@@ -1578,16 +1619,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;
}
@@ -1596,7 +1642,7 @@ public class IrisEngine implements Engine {
}
public GenerationKernelRegistry.Version getGenerationKernelVersion() {
RuntimeAssembly assembly = runtimeAssembly.get();
RuntimeAssembly assembly = threadState.assembly();
if (assembly != null) {
return assembly.kernelVersion;
}
@@ -1604,7 +1650,7 @@ public class IrisEngine implements Engine {
}
public GenerationKernelRegistry.RuntimeKernel getGenerationRuntimeKernel() {
RuntimeAssembly assembly = runtimeAssembly.get();
RuntimeAssembly assembly = threadState.assembly();
if (assembly != null) {
return assembly.runtimeKernel;
}
@@ -1612,7 +1658,7 @@ public class IrisEngine implements Engine {
}
public TransitionGenerationPlan getTransitionGenerationPlan() {
RuntimeAssembly assembly = runtimeAssembly.get();
RuntimeAssembly assembly = threadState.assembly();
if (assembly != null) {
return assembly.transitionPlan;
}
@@ -1736,14 +1782,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
@@ -1765,7 +1811,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();
@@ -1784,14 +1830,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;
}
}
@@ -335,7 +335,7 @@ public class IrisEngineMantle implements EngineMantle {
@Override
public void writeSection(Matter section, java.io.DataOutputStream dos) throws IOException {
section.writeDos(dos);
section.writeTrimmedDos(dos);
}
@Override
@@ -157,7 +157,7 @@ final class WorldChunkMaintenance {
}
if (!J.isFolia()) {
manager.getMantle().getChunk(chunkX, chunkZ).flag(MantleFlag.DISCOVERED, true);
manager.getMantle().flag(chunkX, chunkZ, MantleFlag.DISCOVERED, true);
return;
}
@@ -473,7 +473,7 @@ final class WorldChunkMaintenance {
preparing = true;
}
opened = router.openSavedChunkMantle(target.x(), target.z());
loaded = opened.mantle().getChunk(target.x(), target.z()).use();
loaded = opened.mantle().useChunk(target.x(), target.z());
opened.detachThread();
synchronized (this) {
if (cancelled) {
@@ -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<NativeBiome> {
private final RNG rng;
private final KMap<String, ResolvedBiome> resolvedBiomes = new KMap<>();
private final KMap<String, ResolvedBiome> resolvedPhysicalBiomes = new KMap<>();
private final KMap<String, NativeBiome> resolvedBiomes = new KMap<>();
private final KMap<String, NativeBiome> resolvedPhysicalBiomes = new KMap<>();
public IrisBiomeActuator(Engine engine) {
super(engine, "Biome");
@@ -64,7 +58,6 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
int depth = h.getDepth();
int height = h.getHeight();
Engine engine = getEngine();
Mantle<Matter> mantle = engine.getMantle().getMantle();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
IrisComplex complex = context.getComplex();
DimensionStackContext dimensionStackContext = engine.getDimensionStackContext();
@@ -74,7 +67,7 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
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<NativeBiome> {
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<NativeBiome> {
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<NativeBiome> biomes,
ChunkContext context) {
if (!context.getComplex().allowsMantleChunkWrite(x >> 4, z >> 4)) {
return;
}
Mantle<Matter> mantle = engine.getMantle().getMantle();
MantleChunk<Matter> 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<NativeBiome> output,
Mantle<Matter> mantle,
int localX,
int localZ,
int worldX,
@@ -167,12 +132,9 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
applyBiomeRange(
localX,
localZ,
worldX,
worldZ,
rangeStart,
internalY - 1,
output,
mantle,
resolvePhysicalKey(activeKey)
);
}
@@ -181,33 +143,14 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
}
}
private void writeColumn(
Hunk<NativeBiome> output,
Mantle<Matter> 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<NativeBiome> output,
Mantle<Matter> mantle,
Engine engine,
ResolvedBiome bottom,
NativeBiome bottom,
DimensionStackLayout layout
) {
List<DimensionStackLayout.Layer> layers = layout.layersBottomToTop();
@@ -222,18 +165,15 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
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<NativeBiome> {
applyBiomeRange(
localX,
localZ,
worldX,
worldZ,
layer.renderMinY(),
layer.renderMaxY(),
output,
mantle,
resolved
);
}
@@ -262,46 +199,15 @@ public class IrisBiomeActuator extends EngineAssignedActuator<NativeBiome> {
private void applyBiomeRange(
int localX,
int localZ,
int worldX,
int worldZ,
int minimumY,
int maximumY,
Hunk<NativeBiome> output,
Mantle<Matter> 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<Matter> 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<NativeBiome> {
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;
}
}
@@ -29,6 +29,7 @@ import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.volmlib.util.stream.ProceduralStream;
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
import art.arcane.iris.generation.mantle.CaveTerrainSnapshot;
import art.arcane.iris.generation.mantle.TerrainMatterView;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.biome.IrisBiome;
@@ -40,6 +41,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;
@@ -122,9 +124,13 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
}
MantleChunk<Matter> mantleChunk = mantle.getChunk(x, z).use();
MantleChunk<Matter> mantleChunk = mantle.useChunk(x, z);
try {
PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start();
CaveTerrainSnapshot terrain = CaveTerrainSnapshot.capture(mantleChunk, x, z);
if (context != null) {
context.setCaveTerrain(terrain);
}
int worldHeightSpan = getEngine().getWorld().maxHeight() - getEngine().getWorld().minHeight();
int caveLavaHeight = getEngine().getDimension().getCaveLavaHeight();
CarveResolutionContext resolutionContext = new CarveResolutionContext(
@@ -141,32 +147,20 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
chunkBlockZ
);
CarveResolver carveResolver = new CarveResolver(resolutionContext);
TerrainMatterView.iterate(mantleChunk, MatterCavern.class, (xx, yy, zz, cavern) -> carveResolver.apply(
xx,
yy,
zz,
cavern,
dataIfPresent(mantleChunk, xx, yy, zz, HydrologyCaveCell.class)
));
TerrainMatterView.iterate(mantleChunk, HydrologyCaveCell.class, (xx, yy, zz, hydrology) -> {
if (dataIfPresent(mantleChunk, xx, yy, zz, MatterCavern.class) == null) {
carveResolver.apply(xx, yy, zz, null, hydrology);
}
});
terrain.forEachCell(carveResolver::apply);
if (scratch.customCaveBiomePresent) {
addInternalWallsFromMantle(mantleChunk, walls, columnMasks);
addInternalWallsFromMantle(terrain, walls, columnMasks);
} else {
addInternalWallsFromMasks(walls, columnMasks);
}
addCrossChunkBoundaryWalls(mantle, mantleChunk, walls, boundaryMasks, x, z, surfaceHeights);
addCrossChunkBoundaryWalls(mantle, terrain, walls, boundaryMasks, x, z, surfaceHeights);
getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds());
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
try {
IrisData wallData = getData();
walls.forEach((rx, yy, rz, cavern) -> {
HydrologyCaveCell hydrology = dataIfPresent(
mantleChunk, rx, yy, rz, HydrologyCaveCell.class);
HydrologyCaveCell hydrology = terrain.hydrology(rx, yy, rz);
if (hydrology != null && hydrology.protectsPlacement()) {
return;
}
@@ -190,8 +184,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
processColumnFromMask(
output,
mantleChunk,
mantle,
terrain,
columnMasks[columnIndex],
columnIndex,
x,
@@ -208,7 +201,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
processBoundaryColumnFromMask(
output,
mantleChunk,
terrain,
boundaryMasks[columnIndex],
walls,
columnIndex,
@@ -471,7 +464,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
}
private void addInternalWallsFromMantle(MantleChunk<Matter> mc, CarveWallBuffer walls, CarveColumnMask[] columnMasks) {
private void addInternalWallsFromMantle(CaveTerrainSnapshot terrain, CarveWallBuffer walls, CarveColumnMask[] columnMasks) {
for (int columnIndex = 0; columnIndex < 256; columnIndex++) {
CarveColumnMask columnMask = columnMasks[columnIndex];
if (columnMask.isEmpty()) {
@@ -482,18 +475,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
int rz = columnIndex & 15;
int yy = columnMask.nextSetBit(0);
while (yy >= 0) {
MatterCavern cavern = composedCavernAt(mc, rx, yy, rz);
MatterCavern cavern = terrain.composedCavern(rx, yy, rz);
if (cavern != null) {
if (rz < 15 && composedCavernAt(mc, rx, yy, rz + 1) == null) {
if (rz < 15 && terrain.composedCavern(rx, yy, rz + 1) == null) {
walls.put(rx, yy, rz + 1, cavern);
}
if (rx < 15 && composedCavernAt(mc, rx + 1, yy, rz) == null) {
if (rx < 15 && terrain.composedCavern(rx + 1, yy, rz) == null) {
walls.put(rx + 1, yy, rz, cavern);
}
if (rz > 0 && composedCavernAt(mc, rx, yy, rz - 1) == null) {
if (rz > 0 && terrain.composedCavern(rx, yy, rz - 1) == null) {
walls.put(rx, yy, rz - 1, cavern);
}
if (rx > 0 && composedCavernAt(mc, rx - 1, yy, rz) == null) {
if (rx > 0 && terrain.composedCavern(rx - 1, yy, rz) == null) {
walls.put(rx - 1, yy, rz, cavern);
}
}
@@ -504,7 +497,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
private void addCrossChunkBoundaryWalls(
Mantle<Matter> mantle,
MantleChunk<Matter> mc,
CaveTerrainSnapshot terrain,
CarveWallBuffer walls,
CarveColumnMask[] boundaryMasks,
int chunkX,
@@ -533,10 +526,10 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
int height = maxY + 1;
MatterCavern[] ownWest = west == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.WEST, height);
MatterCavern[] ownEast = east == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.EAST, height);
MatterCavern[] ownNorth = north == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.NORTH, height);
MatterCavern[] ownSouth = south == null ? null : TerrainMatterView.getComposedFace(mc, TerrainMatterView.Face.SOUTH, height);
MatterCavern[] ownWest = west == null ? null : terrain.composedFace(TerrainMatterView.Face.WEST, height);
MatterCavern[] ownEast = east == null ? null : terrain.composedFace(TerrainMatterView.Face.EAST, height);
MatterCavern[] ownNorth = north == null ? null : terrain.composedFace(TerrainMatterView.Face.NORTH, height);
MatterCavern[] ownSouth = south == null ? null : terrain.composedFace(TerrainMatterView.Face.SOUTH, height);
MatterCavern[] fromWest = west == null ? null : TerrainMatterView.getComposedFace(west, TerrainMatterView.Face.EAST, height);
MatterCavern[] fromEast = east == null ? null : TerrainMatterView.getComposedFace(east, TerrainMatterView.Face.WEST, height);
MatterCavern[] fromNorth = north == null ? null : TerrainMatterView.getComposedFace(north, TerrainMatterView.Face.SOUTH, height);
@@ -578,25 +571,16 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
private MantleChunk<Matter> existingMantleChunk(Mantle<Matter> mantle, int chunkX, int chunkZ) {
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(chunkX >> 5, chunkZ >> 5));
TectonicPlate<Matter> plate = mantle.getLoadedRegion(chunkX >> 5, chunkZ >> 5);
if (plate == null || plate.isClosed()) {
return null;
}
return plate.get(chunkX & 31, chunkZ & 31);
}
private MatterCavern composedCavernAt(MantleChunk<Matter> mantleChunk, int x, int y, int z) {
return TerrainMatterView.getComposedCavern(mantleChunk, x, y, z);
}
private static <T> T dataIfPresent(MantleChunk<Matter> mantleChunk, int x, int y, int z, Class<T> type) {
return TerrainMatterView.get(mantleChunk, x, y, z, type);
}
private void processColumnFromMask(
Hunk<NativeBlockState> output,
MantleChunk<Matter> mc,
Mantle<Matter> mantle,
CaveTerrainSnapshot terrain,
CarveColumnMask columnMask,
int columnIndex,
int chunkX,
@@ -630,7 +614,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
zone.ceiling = buf;
} else {
if (zone.isValid(resolverState.height)) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache);
}
zone = new CaveZone();
@@ -643,14 +627,14 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
if (zone.isValid(resolverState.height)) {
processZone(output, mc, mantle, zone, rx, rz, worldX, worldZ, resolverState,
processZone(output, terrain, zone, rx, rz, worldX, worldZ, resolverState,
caveBiomeCache, customBiomeCache);
}
}
private void processBoundaryColumnFromMask(
Hunk<NativeBlockState> output,
MantleChunk<Matter> mantleChunk,
CaveTerrainSnapshot terrain,
CarveColumnMask boundaryMask,
CarveWallBuffer walls,
int columnIndex,
@@ -677,7 +661,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
if (y == zoneCeiling + 1) {
zoneCeiling = y;
} else {
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
resolverState, caveBiomeCache, customBiomeCache);
zoneFloor = y;
zoneCeiling = y;
@@ -685,13 +669,13 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
y = boundaryMask.nextSetBit(y + 1);
}
paintBoundaryZone(output, mantleChunk, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
paintBoundaryZone(output, terrain, walls, rx, rz, worldX, worldZ, zoneFloor, zoneCeiling,
resolverState, caveBiomeCache, customBiomeCache);
}
private void paintBoundaryZone(
Hunk<NativeBlockState> output,
MantleChunk<Matter> mantleChunk,
CaveTerrainSnapshot terrain,
CarveWallBuffer walls,
int rx,
int rz,
@@ -714,8 +698,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
if (floorBiome != null) {
HydrologyCaveCell floorHydrology = dataIfPresent(
mantleChunk, rx, zoneFloor, rz, HydrologyCaveCell.class);
HydrologyCaveCell floorHydrology = terrain.hydrology(rx, zoneFloor, rz);
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
KList<NativeBlockState> floorLayers = floorBiome.generateLayers(
resolverState.dimension, worldX, worldZ, rng, 3, zoneFloor, resolverState.data, resolverState.complex);
@@ -727,8 +710,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
if (floorY < 0) {
break;
}
HydrologyCaveCell hydrology = dataIfPresent(
mantleChunk, rx, floorY, rz, HydrologyCaveCell.class);
HydrologyCaveCell hydrology = terrain.hydrology(rx, floorY, rz);
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != HydrologyCaveAction.SEAL_GUARD) {
@@ -763,8 +745,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
if (ceilingY >= worldMaxY) {
break;
}
HydrologyCaveCell hydrology = dataIfPresent(
mantleChunk, rx, ceilingY, rz, HydrologyCaveCell.class);
HydrologyCaveCell hydrology = terrain.hydrology(rx, ceilingY, rz);
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != HydrologyCaveAction.SEAL_GUARD) {
@@ -799,7 +780,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
return (h & 15L) == 0L;
}
private void processZone(Hunk<NativeBlockState> output, MantleChunk<Matter> mc, Mantle<Matter> mantle,
private void processZone(Hunk<NativeBlockState> output, CaveTerrainSnapshot terrain,
CaveZone zone, int rx, int rz, int xx, int zz,
CaveInputs resolverState,
Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache,
@@ -814,16 +795,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
output.setRaw(rx, zone.ceiling, rz, AIR);
}
IrisBiome floorBiome = resolveCaveBoundaryBiome(mc, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(mc, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
IrisBiome floorBiome = resolveCaveBoundaryBiome(terrain, rx, zone.floor, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(terrain, rx, zone.ceiling, rz, xx, zz, resolverState, caveBiomeCache, customBiomeCache);
if (floorBiome == null && ceilingBiome == null) {
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
normalizeCaveZoneWaterlogging(output, terrain, zone, rx, rz, xx, zz);
return;
}
if (floorBiome != null) {
HydrologyCaveCell floorHydrology = dataIfPresent(
mc, rx, zone.floor, rz, HydrologyCaveCell.class);
HydrologyCaveCell floorHydrology = terrain.hydrology(rx, zone.floor, rz);
IrisRiverMaterialConfig bedMaterial = undergroundBedMaterial();
KList<NativeBlockState> floorBlocks = floorBiome.generateLayers(resolverState.dimension, xx, zz, rng, 3, zone.floor, resolverState.data, resolverState.complex);
for (int i = 0; i < zone.floor - 1; i++) {
@@ -831,7 +811,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
break;
}
int y = zone.floor - i - 1;
HydrologyCaveCell hydrology = dataIfPresent(mc, rx, y, rz, HydrologyCaveCell.class);
HydrologyCaveCell hydrology = terrain.hydrology(rx, y, rz);
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != HydrologyCaveAction.SEAL_GUARD) {
@@ -864,7 +844,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
if (cy >= maxY) {
break;
}
HydrologyCaveCell hydrology = dataIfPresent(mc, rx, cy, rz, HydrologyCaveCell.class);
HydrologyCaveCell hydrology = terrain.hydrology(rx, cy, rz);
if (hydrology != null
&& hydrology.protectsPlacement()
&& hydrology.action() != HydrologyCaveAction.SEAL_GUARD) {
@@ -883,56 +863,71 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
}
normalizeCaveZoneWaterlogging(output, mc, zone, rx, rz, xx, zz);
normalizeCaveZoneWaterlogging(output, terrain, zone, rx, rz, xx, zz);
}
public void decorateNaturalCaves(int blockX, int blockZ, Hunk<NativeBlockState> output) {
public void decorateNaturalCaves(int blockX, int blockZ, Hunk<NativeBlockState> output, ChunkContext context) {
Mantle<Matter> mantle = getEngine().getMantle().getMantle();
MantleChunk<Matter> chunk = mantle.getChunk(blockX >> 4, blockZ >> 4).use();
CaveInputs resolver = new CaveInputs(getEngine());
CaveTerrainSnapshot terrain = context == null ? null : context.getCaveTerrain();
if (context != null) {
context.setCaveTerrain(null);
}
if (terrain == null || !terrain.covers(blockX >> 4, blockZ >> 4)) {
MantleChunk<Matter> chunk = mantle.useChunk(blockX >> 4, blockZ >> 4);
try {
terrain = CaveTerrainSnapshot.capture(chunk, blockX >> 4, blockZ >> 4);
} finally {
chunk.release();
}
}
CaveInputs resolver = CaveInputs.forContentChunk(getEngine(), blockX >> 4, blockZ >> 4);
Long2ObjectOpenHashMap<IrisBiome> caveBiomes = new Long2ObjectOpenHashMap<>(256);
Map<String, IrisBiome> customBiomes = new HashMap<>();
try {
int width = output.getWidth();
int depth = output.getDepth();
int height = output.getHeight();
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;
for (int y = 1; y < height; y++) {
NativeBlockState state = output.getRaw(localX, y, localZ);
if (B.isSolid(state)) {
if (floor >= 0) {
CaveZone zone = new CaveZone();
zone.setFloor(floor);
zone.setCeiling(y - 1);
if (zone.isValid(resolver.height)
&& !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) {
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
}
if (markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) {
mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR);
}
IrisBiome floorBiome = resolveCaveBoundaryBiome(chunk, localX, floor, localZ,
worldX, worldZ, resolver, caveBiomes, customBiomes);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(chunk, localX, y - 1, localZ,
worldX, worldZ, resolver, caveBiomes, customBiomes);
decorateZone(output, zone, localX, localZ, worldX, worldZ,
floorBiome, ceilingBiome);
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) {
CaveZone zone = new CaveZone();
zone.setFloor(floor);
zone.setCeiling(y - 1);
if (zone.isValid(resolver.height)
&& !getComplex().isTerrain3DOpening(worldX, floor, worldZ)) {
if (markerRoll(worldX, zone.ceiling, worldZ, 0x9E3779B97F4A7C15L)) {
mantle.set(worldX, zone.ceiling, worldZ, MarkerMatter.CAVE_CEILING);
}
if (markerRoll(worldX, zone.floor, worldZ, 0xC2B2AE3D27D4EB4FL)) {
mantle.set(worldX, zone.floor, worldZ, MarkerMatter.CAVE_FLOOR);
}
IrisBiome floorBiome = resolveCaveBoundaryBiome(terrain, localX, floor, localZ,
worldX, worldZ, resolver, caveBiomes, customBiomes);
IrisBiome ceilingBiome = resolveCaveBoundaryBiome(terrain, localX, y - 1, localZ,
worldX, worldZ, resolver, caveBiomes, customBiomes);
decorateZone(output, zone, localX, localZ, worldX, worldZ,
floorBiome, ceilingBiome);
if (extent != null) {
top = extent.highestStoredY(localX, localZ);
}
floor = -1;
}
} else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) {
floor = y;
floor = -1;
}
} else if (floor < 0 && B.isSolid(output.getRaw(localX, y - 1, localZ))) {
floor = y;
}
}
}
} finally {
chunk.release();
}
}
@@ -957,7 +952,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
private void normalizeCaveZoneWaterlogging(
Hunk<NativeBlockState> output,
MantleChunk<Matter> mantleChunk,
CaveTerrainSnapshot terrain,
CaveZone zone,
int localX,
int localZ,
@@ -967,13 +962,11 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
int minimumY = Math.max(0, zone.floor - 1);
int maximumY = Math.min(output.getHeight() - 1, zone.ceiling + 1);
for (int y = minimumY; y <= maximumY; y++) {
HydrologyCaveCell hydrology = dataIfPresent(
mantleChunk, localX, y, localZ, HydrologyCaveCell.class);
HydrologyCaveCell hydrology = terrain.hydrology(localX, y, localZ);
if (hydrology == null) {
continue;
}
MatterCavern baseline = dataIfPresent(
mantleChunk, localX, y, localZ, MatterCavern.class);
MatterCavern baseline = terrain.cavern(localX, y, localZ);
NativeBlockState current = output.getRaw(localX, y, localZ);
NativeBlockState columnFluid = getComplex().resolveHydrologyFluid(
hydrology.fluidProfileKey(),
@@ -992,8 +985,8 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
}
IrisBiome resolveCaveBoundaryBiome(MantleChunk<Matter> mantleChunk, int x, int y, int z, int worldX, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
MatterCavern cavern = composedCavernAt(mantleChunk, x, y, z);
IrisBiome resolveCaveBoundaryBiome(CaveTerrainSnapshot terrain, int x, int y, int z, int worldX, int worldZ, CaveInputs resolverState, Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, Map<String, IrisBiome> customBiomeCache) {
MatterCavern cavern = terrain == null ? null : terrain.composedCavern(x, y, z);
return resolveCaveBoundaryBiome(
cavern, worldX, y, worldZ, resolverState, caveBiomeCache, customBiomeCache);
}
@@ -1170,19 +1163,39 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
private final ProceduralStream<IrisBiome> caveBiomes;
private final ProceduralStream<Double> heights;
private final CaveColumn[] columns;
private final boolean chunkLimited;
private final int chunkX;
private final int chunkZ;
CaveInputs(Engine engine) {
this(engine, false, 0, 0);
}
/**
* Inputs for the content stage of one chunk. A coordinate inside that chunk resolves under the chunk's
* own generation route, exactly the binding the natural terrain stage samples with, so it takes the
* captured column path; any other coordinate keeps the per-coordinate history dispatch.
*/
static CaveInputs forContentChunk(Engine engine, int chunkX, int chunkZ) {
return new CaveInputs(engine, true, chunkX, chunkZ);
}
private CaveInputs(Engine engine, boolean contentChunk, int chunkX, int chunkZ) {
this.engine = engine;
complex = engine.getComplex();
dimension = engine.getDimension();
data = engine.getData();
height = engine.getHeight();
IrisContext context = IrisContext.get();
boolean scoped = engine instanceof IrisEngine irisEngine
boolean generating = engine instanceof IrisEngine irisEngine
&& irisEngine.hasGenerationRuntimeScope() && !engine.getPlatformHooks().isMainThread()
&& context != null && context.getChunkContext() != null
&& context.getChunkContext().getComplex() == complex
&& context.getChunkContext().isNaturalTerrain();
&& context.getChunkContext().getComplex() == complex;
boolean natural = generating && context.getChunkContext().isNaturalTerrain();
boolean scoped = natural || generating && contentChunk;
this.chunkLimited = !natural;
this.chunkX = chunkX;
this.chunkZ = chunkZ;
snapshot = scoped ? IrisDimensionCarvingResolver.snapshot(engine) : null;
minimumY = scoped ? engine.getWorld().minHeight() : 0;
stack = scoped ? engine.getDimensionStackContext() : null;
@@ -1193,7 +1206,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<NativeBlockState>
}
IrisBiome resolve(int x, int y, int z) {
if (snapshot == null) {
if (snapshot == null || chunkLimited && ((x >> 4) != chunkX || (z >> 4) != chunkZ)) {
return engine.getCaveBiome(x, y, z, state);
}
IrisBiome configured = snapshot.resolveBiome(x, y + minimumY, z);
@@ -19,11 +19,11 @@ public class IrisCustomModifier extends EngineAssignedModifier<NativeBlockState>
@Override
public void onModify(int x, int z, Hunk<NativeBlockState> output, boolean multicore, ChunkContext context) {
MantleChunk<Matter> mc = getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4);
MantleChunk<Matter> mc = getEngine().getMantle().getMantle().useChunk(x >> 4, z >> 4);
if (!mc.isFlagged(MantleFlag.CUSTOM_ACTIVE)) {
mc.release();
return;
}
mc.use();
BurstExecutor burst = MultiBurst.burst.burst(output.getHeight());
burst.setMulticore(multicore);
@@ -23,6 +23,7 @@ import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisDepositBiomeScope;
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositHeightDistribution;
import art.arcane.iris.generation.decoration.IrisDepositPlacementScope;
@@ -45,15 +46,20 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch;
import art.arcane.volmlib.nativelib.terrain.NativeBlockState;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.block.VectorMap;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.pack.loading.IrisRegistrant;
import art.arcane.iris.pack.loading.ResourceLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState> {
private static final int CLUMPS_PER_BATCH = 8;
private static final int PREPARATION_BATCH_COUNT = 4;
private final RNG rng;
private volatile DepositVariantScan variantScan;
public IrisDepositModifier(Engine engine) {
super(engine, "Deposit");
@@ -87,11 +93,11 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null;
PreparationContext preparation = new PreparationContext(context, generationEngine,
generationEngine == null ? null : generationEngine.captureGenerationRuntimeBinding(), new Throwable[PREPARATION_BATCH_COUNT]);
MantleChunk chunk = getEngine().getMantle().getMantle().getChunk(x, z).use();
MantleChunk chunk = getEngine().getMantle().getMantle().useChunk(x, z);
Throwable callerFailure = null;
try {
for (int i = 0; i < generators.size(); i++) {
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)));
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)), context);
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
int firstAttempt = first;
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
@@ -163,20 +169,48 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
}
public void generate(IrisDepositGenerator k, MantleChunk chunk, Hunk<NativeBlockState> data, RNG rng, int cx, int cz, boolean safe, HeightMap he, ChunkContext context) {
DepositPlan plan = plan(k, rng);
DepositPlan plan = plan(k, rng, context);
for (int first = 0; first < plan.attempts(); first += CLUMPS_PER_BATCH) {
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
place(prepare(plan, first, limit, cx, cz, he, context), chunk, data, cx, cz, he, context);
}
}
private DepositPlan plan(IrisDepositGenerator generator, RNG rng) {
private DepositPlan plan(IrisDepositGenerator generator, RNG rng, ChunkContext context) {
if (generator.getSpawnChance() < rng.d()) {
return new DepositPlan(generator, rng.getSeed(), false, 0);
}
boolean ore = generator.isOre(getData());
int attempts = rng.i(generator.getMinPerChunk(), generator.getMaxPerChunk() + 1);
return new DepositPlan(generator, rng.getSeed(), ore, attempts);
return new DepositPlan(generator, rng.getSeed(), ore,
attempts == 0 || surfaceBiomeMayMatch(generator, context) ? attempts : 0);
}
/**
* A surface-scoped biome filter reads the surface biome of the clump's own column, so when no column of the
* chunk passes it every clump is rejected at that check. Each clump draws from its own seeded RNG, so skipping
* them all up front changes nothing else.
*/
private static boolean surfaceBiomeMayMatch(IrisDepositGenerator generator, ChunkContext context) {
if (generator.getBiomeScope() != IrisDepositBiomeScope.SURFACE
|| (generator.getIncludedBiomes() == null || generator.getIncludedBiomes().isEmpty())
&& (generator.getExcludedBiomes() == null || generator.getExcludedBiomes().isEmpty())) {
return true;
}
IrisBiome checked = null;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
IrisBiome surface = context.getBiome().get(x, z);
if (surface == checked) {
continue;
}
if (generator.matchesBiome(surface, null)) {
return true;
}
checked = surface;
}
}
return false;
}
private PreparedDeposit prepare(DepositPlan plan, int first, int limit, int cx, int cz, HeightMap he, ChunkContext context) {
@@ -185,15 +219,18 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
boolean needsCaveBiome = oreDeposit || k.usesCaveBiomeFilter();
IrisDimensionCarvingResolver.State carvingState = needsCaveBiome ? new IrisDimensionCarvingResolver.State() : null;
List<PreparedClump> clumps = new ArrayList<>(limit - first);
int terrainLimit = Integer.MIN_VALUE;
for (int l = first; l < limit; l++) {
RNG clumpRng = new RNG(clumpSeed(plan.seed(), l));
long clumpSeed = clumpSeed(plan.seed(), l);
RNG clumpRng = new RNG(clumpSeed);
if (k.getPerClumpSpawnChance() < clumpRng.d()) {
continue;
}
IrisObject clump = k.getClump(getEngine(), clumpRng, getData());
IrisDepositGenerator.ClumpFootprint footprint = k.sampleClumpFootprint(clumpRng, getData());
IrisObject clump = footprint == null ? k.getClump(getEngine(), clumpRng, getData()) : null;
int dim = clump.getW();
int dim = footprint == null ? clump.getW() : footprint.width();
int min = dim / 2;
int max = (int) (16D - dim / 2D);
@@ -257,6 +294,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
}
}
if (clump == null) {
if (k.getPlacementScope() == IrisDepositPlacementScope.TERRAIN && !footprint.empty()) {
if (terrainLimit == Integer.MIN_VALUE) {
terrainLimit = chunkTerrainLimit(cx, cz, he, context, k.getSurfaceClearance());
}
if (y + footprint.minY() > terrainLimit) {
continue;
}
}
RNG replay = new RNG(clumpSeed);
replay.d();
clump = k.getClump(getEngine(), replay, getData());
}
clumps.add(new PreparedClump(clump, x, y, z, clumpRng));
}
return new PreparedDeposit(k, oreDeposit, clumps);
@@ -271,6 +322,7 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
boolean oreDeposit = deposit.ore();
IrisDimensionCarvingResolver.State carvingState = new IrisDimensionCarvingResolver.State();
IrisDimension dimension = getDimension();
boolean variants = depositVariantsPossible();
for (PreparedClump prepared : deposit.clumps()) {
IrisObject clump = prepared.object();
int x = prepared.x();
@@ -325,8 +377,8 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
if (chunk.get(nx, ny, nz, MatterCavern.class) == null) {
NativeBlockState ore = cursor.value();
NativeBlockState remapped = resolveDepositVariant(
cx, cz, nx, ny, nz, ore, dimension, context, carvingState);
NativeBlockState remapped = variants ? resolveDepositVariant(
cx, cz, nx, ny, nz, ore, dimension, context, carvingState) : null;
NativeBlockState finalBlock = remapped != null
? remapped
: B.toDeepSlateOre(current, ore);
@@ -344,6 +396,20 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
: context.getRoundedHeight(localX, localZ);
}
/**
* The highest block a TERRAIN-scoped deposit may place anywhere in the chunk; a clump whose lowest block sits
* above it would have every block rejected by {@link #placementSurfaceAllows}.
*/
private int chunkTerrainLimit(int cx, int cz, HeightMap he, ChunkContext context, int surfaceClearance) {
int highest = Integer.MIN_VALUE;
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
highest = Math.max(highest, getDepositTerrainSurface(cx, cz, x, z, he, context));
}
}
return highest - Math.max(0, surfaceClearance);
}
static int depositSurfaceLimit(int surfaceY) {
return depositSurfaceLimit(surfaceY, 7);
}
@@ -460,6 +526,51 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
return Math.max(minimum, Math.min(center, maximum));
}
/**
* The per-block variant lookup resolves a cave biome for every placed ore, but it can only ever match when
* some biome, region or the dimension of the loaded pack declares variants. Anything the scan cannot
* enumerate (stacked dimensions, unreadable loaders) keeps the lookup.
*/
private boolean depositVariantsPossible() {
IrisData data = getData();
DepositVariantScan scan = variantScan;
if (scan == null || scan.data() != data) {
scan = new DepositVariantScan(data, scanDepositVariants(data));
variantScan = scan;
}
return scan.possible();
}
private boolean scanDepositVariants(IrisData data) {
IrisDimension dimension = getDimension();
if (data == null || dimension == null || getEngine().getDimensionStackContext() != null
|| dimension.getDepositVariants() == null || !dimension.getDepositVariants().isEmpty()) {
return true;
}
Boolean regions = declaresVariants(data.getRegionLoader(), IrisRegion::getDepositVariants);
Boolean biomes = declaresVariants(data.getBiomeLoader(), IrisBiome::getDepositVariants);
return regions == null || regions || biomes == null || biomes;
}
private static <T extends IrisRegistrant> Boolean declaresVariants(ResourceLoader<T> loader,
Function<T, ? extends List<IrisDepositVariant>> variants) {
String[] keys = loader == null ? null : loader.getPossibleKeys();
if (keys == null) {
return null;
}
for (String key : keys) {
T loaded = loader.load(key);
if (loaded == null) {
continue;
}
List<IrisDepositVariant> declared = variants.apply(loaded);
if (declared == null || !declared.isEmpty()) {
return true;
}
}
return false;
}
private NativeBlockState resolveDepositVariant(int cx, int cz, int nx, int localY, int nz, NativeBlockState ore, IrisDimension dimension, ChunkContext context, IrisDimensionCarvingResolver.State carvingState) {
int worldX = (cx << 4) + nx;
int worldZ = (cz << 4) + nz;
@@ -548,6 +659,9 @@ public class IrisDepositModifier extends EngineAssignedModifier<NativeBlockState
private record DepositPlan(IrisDepositGenerator generator, long seed, boolean ore, int attempts) {
}
private record DepositVariantScan(IrisData data, boolean possible) {
}
private record PreparedDeposit(IrisDepositGenerator generator, boolean ore, List<PreparedClump> clumps) {
}
@@ -82,7 +82,7 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
if (getEngine().getDimensionStackContext() == null) {
return;
}
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle().getChunk(x >> 4, z >> 4).use();
MantleChunk<Matter> mantleChunk = getEngine().getMantle().getMantle().useChunk(x >> 4, z >> 4);
try {
MetadataCleaner metadata = new MetadataCleaner(mantleChunk, blocks.getHeight());
for (int localX = 0; localX < blocks.getWidth(); localX++) {
@@ -106,7 +106,22 @@ public final class IrisDimensionStackActuator extends EngineAssignedActuator<Nat
}
}
public static void clearNaturalMetadata(MantleChunk<Matter> 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;
}
context.setCaveTerrain(null);
MantleChunk<Matter> chunk = engine.getMantle().getMantle().useChunk(x >> 4, z >> 4);
try {
synchronized (chunk) {
clearNaturalMetadata(chunk, context, height);
}
} finally {
chunk.release();
}
}
static void clearNaturalMetadata(MantleChunk<Matter> 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++) {
@@ -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<Nativ
}
context.setNaturalBiome(wx & 15, y, wz & 15, selected);
context.floatingBiomes(chunkHeight).record(wx & 15, y, wz & 15, matter.identity());
getEngine().getMantle().getMantle().set(wx, y, wz, BiomeInjectMatter.get(selected.key()));
}
} catch (Throwable e) {
IrisLogging.reportError(e);
@@ -18,6 +18,7 @@
package art.arcane.iris.generation.stage;
import art.arcane.iris.generation.chunk.ColumnExtent;
import art.arcane.iris.generation.decoration.IrisSpeleothems;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.runtime.EngineAssignedModifier;
@@ -203,7 +204,7 @@ public class IrisPerfectionModifier extends EngineAssignedModifier<NativeBlockSt
}
private int getHeight(Hunk<NativeBlockState> 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) {
@@ -30,8 +30,7 @@ import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.decoration.IrisProceduralBlocks;
import art.arcane.iris.generation.hydrology.IrisSurfaceRiverBedConfig;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.decoration.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds;
import art.arcane.iris.generation.decoration.IrisOreBands;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.generation.hydrology.IrisRiverMaterialConfig;
import art.arcane.iris.generation.hydrology.IrisSurfaceRiverBankConfig;
@@ -52,8 +51,6 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
private static final BoundBlockState AIR = BoundBlockState.of("AIR");
@Getter
private final RNG rng;
@Getter
private int lastBedrock = -1;
public IrisTerrainNormalActuator(Engine engine) {
super(engine, "Terrain");
@@ -64,263 +61,25 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
@Override
public void onActuate(int x, int z, Hunk<NativeBlockState> h, boolean multicore, ChunkContext context) {
PrecisionStopwatch p = PrecisionStopwatch.start();
for (int xf = 0; xf < h.getWidth(); xf++) {
terrainSliver(x, z, xf, h, context);
}
paint(x, z, h, context);
getEngine().getMetrics().getTerrain().put(p.getMilliseconds());
}
/**
* This is calling 1/16th of a chunk x/z slice. It is a plane from sky to bedrock 1 thick in the x direction.
* Paints every column of the chunk from its top down to bedrock. Columns are walked z-major so
* consecutive columns share the cache lines of the x-minor block buffer.
*
* @param x the chunk x in blocks
* @param z the chunk z in blocks
* @param xf the current x slice
* @param h the blockdata
* @param x the chunk x in blocks
* @param z the chunk z in blocks
*/
@BlockCoordinates
public void terrainSliver(int x, int z, int xf, Hunk<NativeBlockState> h, ChunkContext context) {
int chunkHeight = h.getHeight();
int chunkDepth = h.getDepth();
IrisDimension dimension = getDimension();
IrisData data = getData();
IrisComplex complex = getComplex();
ProceduralStream<Double> riverWaterSurfaceStream = complex.getRiverWaterSurfaceStream();
RNG localRng = rng;
boolean bedrockEnabled = dimension.isBedrock();
boolean hideOres = dimension.isHideOresForHiddenOre();
ChunkedDataCache<IrisBiome> biomeCache = context.getBiome();
ChunkedDataCache<IrisRegion> regionCache = context.getRegion();
ChunkedDataCache<NativeBlockState> rockCache = context.getRock();
int realX = xf + x;
UpperDimensionContext upperContext = getEngine().getUpperContext();
// Dimension-level ore lookups are chunk-invariant; resolving them per column paid
// four accessor chains times 256 per chunk for constants.
KList<IrisOreGenerator> dimensionSurfaceOres = hideOres ? null : dimension.getSurfaceOreGenerators();
KList<IrisOreGenerator> dimensionUndergroundOres = hideOres ? null : dimension.getUndergroundOreGenerators();
IrisOreGeneratorBounds dimensionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getSurfaceOreGeneratorBounds();
IrisOreGeneratorBounds dimensionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : dimension.getUndergroundOreGeneratorBounds();
IrisSurfaceRiverBankConfig riverBanks = dimension.getHydrology() == null
? null
: dimension.getHydrology().getRivers().getSurface().getBanks();
boolean exposeCutStrata = riverBanks != null && riverBanks.isExposeCutStrata();
IrisSurfaceRiverBedConfig riverBed = dimension.getHydrology() == null
? null
: dimension.getHydrology().getRivers().getSurface().getBed();
boolean padRiverBed = riverBed != null && !riverBed.isAllowGravityBlocks();
IrisRiverMaterialConfig bedMaterial = riverBed == null ? null : riverBed.getMaterial();
IrisRiverMaterialConfig shoreMaterial = riverBanks == null ? null : riverBanks.getShoreMaterial();
IrisRiverMaterialConfig bankMaterial = riverBanks == null ? null : riverBanks.getBankMaterial();
for (int zf = 0; zf < chunkDepth; zf++) {
int realZ = zf + z;
IrisBiome biome = biomeCache.get(xf, zf);
IrisRegion region = regionCache.get(xf, zf);
int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf));
int surfaceFluidHeight = Math.min(
chunkHeight,
(int) Math.round(riverWaterSurfaceStream.getDouble(realX, realZ))
);
int hf = Math.max(surfaceFluidHeight, he);
if (hf < 0) {
continue;
}
int topY = Math.min(hf, chunkHeight - 1);
HydrologyColumnSample hydrology = complex.sampleHydrologyColumn(realX, realZ);
HydrologyColumnLayer hydrologyFluid = hydrology == null
? null
: hydrology.primarySurfaceFluidLayerOrNull();
HydrologyColumnLayer hydrologyTerrain = hydrology == null
? null
: hydrology.primarySurfaceLayerOrNull();
Terrain3DColumn terrainColumn = complex.terrainColumn(realX, realZ, hydrology);
int terrainSpan = terrainColumn == null ? -1 : terrainColumn.spanCount() - 1;
int layerSurfaceY = he;
int layerCeilingY = 0;
boolean exposeRiverStrata = exposeCutStrata
&& hydrologyTerrain != null
&& hydrologyTerrain.terrainOwned()
&& !hydrologyTerrain.channel();
Terrain3DColumn naturalColumn = exposeRiverStrata ? complex.naturalTerrainColumn(realX, realZ) : null;
int cut = 0;
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
NativeBlockState fluid = hydrologyFluid == null || complex.getTransitionDisplacement() != null
? complex.resolveSurfaceFluid(realX, realZ)
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
NativeBlockState rock = rockCache.get(xf, zf);
NativeBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
KList<IrisOreGenerator> biomeSurfaceOres = hideOres ? null : biome.getSurfaceOreGenerators();
KList<IrisOreGenerator> regionSurfaceOres = hideOres ? null : region.getSurfaceOreGenerators();
KList<IrisOreGenerator> biomeUndergroundOres = hideOres ? null : biome.getUndergroundOreGenerators();
KList<IrisOreGenerator> regionUndergroundOres = hideOres ? null : region.getUndergroundOreGenerators();
IrisOreGeneratorBounds biomeSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : biome.getSurfaceOreGeneratorBounds();
IrisOreGeneratorBounds regionSurfaceOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : region.getSurfaceOreGeneratorBounds();
IrisOreGeneratorBounds biomeUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : biome.getUndergroundOreGeneratorBounds();
IrisOreGeneratorBounds regionUndergroundOreBounds = hideOres ? IrisOreGeneratorBounds.EMPTY : region.getUndergroundOreGeneratorBounds();
boolean hasSurfaceOres = biomeSurfaceOreBounds.hasOres() || regionSurfaceOreBounds.hasOres() || dimensionSurfaceOreBounds.hasOres();
boolean hasUndergroundOres = biomeUndergroundOreBounds.hasOres() || regionUndergroundOreBounds.hasOres() || dimensionUndergroundOreBounds.hasOres();
KList<NativeBlockState> blocks = null;
KList<NativeBlockState> ceilingBlocks = null;
KList<NativeBlockState> fblocks = null;
for (int i = topY; i >= 0; i--) {
if (i == 0 && bedrockEnabled) {
h.setRaw(xf, i, zf, BEDROCK.get());
lastBedrock = i;
continue;
}
if (terrainColumn != null && i <= he) {
while (terrainSpan >= 0 && i < terrainColumn.ceiling(terrainSpan)) {
terrainSpan--;
blocks = null;
ceilingBlocks = null;
}
if (terrainSpan < 0 || i > terrainColumn.floor(terrainSpan)) {
h.setRaw(xf, i, zf, AIR.get());
continue;
}
layerSurfaceY = terrainColumn.floor(terrainSpan);
layerCeilingY = terrainColumn.ceiling(terrainSpan);
}
NativeBlockState ore = null;
if (hasSurfaceOres) {
if (biomeSurfaceOreBounds.contains(i)) {
ore = generateOres(biomeSurfaceOres, realX, i, realZ, localRng, data);
}
if (ore == null && regionSurfaceOreBounds.contains(i)) {
ore = generateOres(regionSurfaceOres, realX, i, realZ, localRng, data);
}
if (ore == null && dimensionSurfaceOreBounds.contains(i)) {
ore = generateOres(dimensionSurfaceOres, realX, i, realZ, localRng, data);
}
}
if (ore != null) {
h.setRaw(xf, i, zf, ore);
continue;
}
if (i > he && i <= hf) {
int fdepth = hf - i;
if (hydrologyFluid == null && fblocks == null) {
fblocks = biome.generateSeaLayers(realX, realZ, localRng, hf - he, data);
}
h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select(
fblocks,
fdepth,
fluid,
hydrologyFluid != null
));
continue;
}
if (i <= he) {
int depth = layerSurfaceY - i;
if (depth == 0 && mappedSurfaceBlock != null) {
h.setRaw(xf, i, zf, mappedSurfaceBlock);
continue;
}
if (blocks == null) {
if (exposeRiverStrata) {
int naturalSurfaceY = naturalColumn == null ? hydrology.naturalHeight() : naturalColumn.surfaceY(layerSurfaceY);
cut = Math.max(0, naturalSurfaceY - layerSurfaceY);
}
blocks = biome.generateLayers(dimension, realX, realZ, localRng,
layerSurfaceY + cut, layerSurfaceY + cut, data, complex);
}
if (layerCeilingY > 0 && depth >= 2 && i - layerCeilingY < 2) {
if (ceilingBlocks == null) {
ceilingBlocks = biome.generateCeilingLayers(dimension, realX, realZ, localRng,
2, layerCeilingY, data, complex);
}
int ceilingDepth = i - layerCeilingY;
h.setRaw(xf, i, zf, ceilingBlocks.hasIndex(ceilingDepth)
? ceilingBlocks.get(ceilingDepth) : rock);
continue;
}
int strataIndex = strataIndex(depth, cut, blocks.size());
NativeBlockState layerBlock = paintHydrologyMaterial(
blocks.hasIndex(strataIndex) ? blocks.get(strataIndex) : null,
roleMaterial, depth, localRng, realX, i, realZ, data);
if (layerBlock != null) {
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
layerBlock = riverBed.getPaddingPalette().get(localRng, realX, i, realZ, data);
}
h.setRaw(xf, i, zf, layerBlock);
continue;
}
if (hasUndergroundOres) {
if (biomeUndergroundOreBounds.contains(i)) {
ore = generateOres(biomeUndergroundOres, realX, i, realZ, localRng, data);
}
if (ore == null && regionUndergroundOreBounds.contains(i)) {
ore = generateOres(regionUndergroundOres, realX, i, realZ, localRng, data);
}
if (ore == null && dimensionUndergroundOreBounds.contains(i)) {
ore = generateOres(dimensionUndergroundOres, realX, i, realZ, localRng, data);
}
}
if (ore != null) {
h.setRaw(xf, i, zf, ore);
} else {
h.setRaw(xf, i, zf, rock);
}
}
}
if (upperContext != null) {
UpperDimensionContext.Column upperColumn = upperContext.sampleColumn(realX, realZ);
int upperSurfaceY = upperColumn.surfaceY();
if (upperSurfaceY < chunkHeight - 1) {
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
NativeBlockState upperRock = upperContext.getRockBlock(realX, realZ);
NativeBlockState upperMappedSurface = upperContext.getSurfaceBlock(realX, realZ);
KList<NativeBlockState> upperBlocks = null;
int paletteSourceY = -1;
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
if (y == chunkHeight - 1 && bedrockEnabled) {
h.setRaw(xf, y, zf, BEDROCK.get());
continue;
}
if (!upperColumn.isSolid(y)) {
h.setRaw(xf, y, zf, AIR.get());
continue;
}
int faceY = upperColumn.faceY(y);
int sourceSurfaceY = upperColumn.height() - 1 - faceY;
if (sourceSurfaceY != paletteSourceY) {
paletteSourceY = sourceSurfaceY;
upperBlocks = null;
}
if (y == faceY && upperMappedSurface != null) {
h.setRaw(xf, y, zf, upperMappedSurface);
continue;
}
int depthFromFace = y - faceY;
if (upperBlocks == null && upperBiome != null) {
upperBlocks = upperBiome.generateLayersWithSlope(upperContext.getDimension(),
realX, realZ, localRng, sourceSurfaceY, sourceSurfaceY,
upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY));
}
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
} else {
h.setRaw(xf, y, zf, upperRock);
}
}
}
public void paint(int x, int z, Hunk<NativeBlockState> h, ChunkContext context) {
ColumnPainter painter = new ColumnPainter(x, z, h, context);
int width = h.getWidth();
int depth = h.getDepth();
for (int zf = 0; zf < depth; zf++) {
for (int xf = 0; xf < width; xf++) {
painter.paint(xf, zf);
}
}
}
@@ -385,20 +144,307 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator<NativeBloc
return painted == null ? layerBlock : painted;
}
private NativeBlockState generateOres(KList<IrisOreGenerator> oreGenerators, int x, int y, int z, RNG rng, IrisData data) {
if (oreGenerators == null || oreGenerators.isEmpty()) {
return null;
private final class ColumnPainter {
private final int x;
private final int z;
private final Hunk<NativeBlockState> h;
private final ChunkContext context;
private final int chunkHeight;
private final IrisDimension dimension;
private final IrisData data;
private final IrisComplex complex;
private final ProceduralStream<Double> riverWaterSurfaceStream;
private final boolean bedrockEnabled;
private final boolean hideOres;
private final boolean transitionDisplaced;
private final ChunkedDataCache<IrisBiome> biomeCache;
private final ChunkedDataCache<IrisRegion> regionCache;
private final ChunkedDataCache<NativeBlockState> rockCache;
private final UpperDimensionContext upperContext;
private final IrisOreBands dimensionSurfaceOres;
private final IrisOreBands dimensionUndergroundOres;
private final boolean exposeCutStrata;
private final IrisSurfaceRiverBedConfig riverBed;
private final boolean padRiverBed;
private final IrisRiverMaterialConfig bedMaterial;
private final IrisRiverMaterialConfig shoreMaterial;
private final IrisRiverMaterialConfig bankMaterial;
private final NativeBlockState air;
private final NativeBlockState bedrock;
private IrisOreBands biomeSurfaceOres;
private IrisOreBands regionSurfaceOres;
private IrisOreBands biomeUndergroundOres;
private IrisOreBands regionUndergroundOres;
private ColumnPainter(int x, int z, Hunk<NativeBlockState> h, ChunkContext context) {
this.x = x;
this.z = z;
this.h = h;
this.context = context;
chunkHeight = h.getHeight();
dimension = getDimension();
data = getData();
complex = getComplex();
riverWaterSurfaceStream = complex.getRiverWaterSurfaceStream();
bedrockEnabled = dimension.isBedrock();
hideOres = dimension.isHideOresForHiddenOre();
transitionDisplaced = complex.getTransitionDisplacement() != null;
biomeCache = context.getBiome();
regionCache = context.getRegion();
rockCache = context.getRock();
upperContext = getEngine().getUpperContext();
dimensionSurfaceOres = hideOres ? IrisOreBands.EMPTY : dimension.getSurfaceOreBands();
dimensionUndergroundOres = hideOres ? IrisOreBands.EMPTY : dimension.getUndergroundOreBands();
IrisSurfaceRiverBankConfig riverBanks = dimension.getHydrology() == null
? null
: dimension.getHydrology().getRivers().getSurface().getBanks();
exposeCutStrata = riverBanks != null && riverBanks.isExposeCutStrata();
riverBed = dimension.getHydrology() == null
? null
: dimension.getHydrology().getRivers().getSurface().getBed();
padRiverBed = riverBed != null && !riverBed.isAllowGravityBlocks();
bedMaterial = riverBed == null ? null : riverBed.getMaterial();
shoreMaterial = riverBanks == null ? null : riverBanks.getShoreMaterial();
bankMaterial = riverBanks == null ? null : riverBanks.getBankMaterial();
air = AIR.get();
bedrock = BEDROCK.get();
}
int oreCount = oreGenerators.size();
for (int oreIndex = 0; oreIndex < oreCount; oreIndex++) {
IrisOreGenerator oreGenerator = oreGenerators.get(oreIndex);
NativeBlockState ore = oreGenerator.generate(x, y, z, rng, data);
if (ore != null) {
return ore;
private void paint(int xf, int zf) {
int realX = xf + x;
int realZ = zf + z;
IrisBiome biome = biomeCache.get(xf, zf);
IrisRegion region = regionCache.get(xf, zf);
int he = Math.min(chunkHeight, context.getRoundedHeight(xf, zf));
int surfaceFluidHeight = Math.min(
chunkHeight,
(int) Math.round(riverWaterSurfaceStream.getDouble(realX, realZ))
);
int hf = Math.max(surfaceFluidHeight, he);
if (hf < 0) {
return;
}
int topY = Math.min(hf, chunkHeight - 1);
HydrologyColumnSample hydrology = complex.sampleHydrologyColumn(realX, realZ);
HydrologyColumnLayer hydrologyFluid = hydrology == null
? null
: hydrology.primarySurfaceFluidLayerOrNull();
HydrologyColumnLayer hydrologyTerrain = hydrology == null
? null
: hydrology.primarySurfaceLayerOrNull();
Terrain3DColumn terrainColumn = complex.terrainColumn(realX, realZ, hydrology);
int terrainSpan = terrainColumn == null ? -1 : terrainColumn.spanCount() - 1;
int layerSurfaceY = he;
int layerCeilingY = 0;
boolean exposeRiverStrata = exposeCutStrata
&& hydrologyTerrain != null
&& hydrologyTerrain.terrainOwned()
&& !hydrologyTerrain.channel();
Terrain3DColumn naturalColumn = exposeRiverStrata ? complex.naturalTerrainColumn(realX, realZ) : null;
int cut = 0;
boolean riverOwned = padRiverBed && hydrologyTerrain != null && hydrologyTerrain.terrainOwned();
IrisRiverMaterialConfig roleMaterial = hydrologyRoleMaterial(
hydrologyTerrain, bedMaterial, shoreMaterial, bankMaterial);
int paintDepth = roleMaterial == null ? 0 : roleMaterial.getDepth();
NativeBlockState fluid = hydrologyFluid == null || transitionDisplaced
? complex.resolveSurfaceFluid(realX, realZ)
: complex.resolveHydrologyFluid(hydrologyFluid.profileKey(), realX, realZ);
NativeBlockState rock = rockCache.get(xf, zf);
NativeBlockState mappedSurfaceBlock = complex.getImageMapRuntime().sampleSurfaceBlock(realX, realZ);
biomeSurfaceOres = hideOres ? IrisOreBands.EMPTY : biome.getSurfaceOreBands();
regionSurfaceOres = hideOres ? IrisOreBands.EMPTY : region.getSurfaceOreBands();
biomeUndergroundOres = hideOres ? IrisOreBands.EMPTY : biome.getUndergroundOreBands();
regionUndergroundOres = hideOres ? IrisOreBands.EMPTY : region.getUndergroundOreBands();
boolean hasSurfaceOres = biomeSurfaceOres.hasOres() || regionSurfaceOres.hasOres() || dimensionSurfaceOres.hasOres();
boolean hasUndergroundOres = biomeUndergroundOres.hasOres() || regionUndergroundOres.hasOres() || dimensionUndergroundOres.hasOres();
int bedrockFloor = bedrockEnabled ? 1 : 0;
KList<NativeBlockState> blocks = null;
KList<NativeBlockState> ceilingBlocks = null;
KList<NativeBlockState> fblocks = null;
for (int i = topY; i >= 0; i--) {
if (i == 0 && bedrockEnabled) {
h.setRaw(xf, i, zf, bedrock);
continue;
}
if (terrainColumn != null && i <= he) {
while (terrainSpan >= 0 && i < terrainColumn.ceiling(terrainSpan)) {
terrainSpan--;
blocks = null;
ceilingBlocks = null;
}
if (terrainSpan < 0 || i > terrainColumn.floor(terrainSpan)) {
h.setRaw(xf, i, zf, air);
continue;
}
layerSurfaceY = terrainColumn.floor(terrainSpan);
layerCeilingY = terrainColumn.ceiling(terrainSpan);
}
NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, i, realZ) : null;
if (ore != null) {
h.setRaw(xf, i, zf, ore);
continue;
}
if (i > he && i <= hf) {
int fdepth = hf - i;
if (hydrologyFluid == null && fblocks == null) {
fblocks = biome.generateSeaLayers(realX, realZ, rng, hf - he, data);
}
h.setRaw(xf, i, zf, HydrologyFluidLayerSelector.select(
fblocks,
fdepth,
fluid,
hydrologyFluid != null
));
continue;
}
if (i <= he) {
int depth = layerSurfaceY - i;
if (depth == 0 && mappedSurfaceBlock != null) {
h.setRaw(xf, i, zf, mappedSurfaceBlock);
continue;
}
if (blocks == null) {
if (exposeRiverStrata) {
int naturalSurfaceY = naturalColumn == null ? hydrology.naturalHeight() : naturalColumn.surfaceY(layerSurfaceY);
cut = Math.max(0, naturalSurfaceY - layerSurfaceY);
}
blocks = biome.generateLayers(dimension, realX, realZ, rng,
layerSurfaceY + cut, layerSurfaceY + cut, data, complex);
}
int deepFloor = Math.max(bedrockFloor, layerCeilingY > 0 ? layerCeilingY + 2 : 0);
if (i >= deepFloor && depth >= Math.max(blocks.size(), paintDepth)) {
paintDeep(xf, zf, realX, realZ, i, deepFloor, rock, hasSurfaceOres, hasUndergroundOres);
i = deepFloor;
continue;
}
if (layerCeilingY > 0 && depth >= 2 && i - layerCeilingY < 2) {
if (ceilingBlocks == null) {
ceilingBlocks = biome.generateCeilingLayers(dimension, realX, realZ, rng,
2, layerCeilingY, data, complex);
}
int ceilingDepth = i - layerCeilingY;
h.setRaw(xf, i, zf, ceilingBlocks.hasIndex(ceilingDepth)
? ceilingBlocks.get(ceilingDepth) : rock);
continue;
}
int strataIndex = strataIndex(depth, cut, blocks.size());
NativeBlockState layerBlock = paintHydrologyMaterial(
blocks.hasIndex(strataIndex) ? blocks.get(strataIndex) : null,
roleMaterial, depth, rng, realX, i, realZ, data);
if (layerBlock != null) {
if (riverOwned && depth <= riverBed.getPadding() && IrisProceduralBlocks.isGravityAffected(layerBlock)) {
layerBlock = riverBed.getPaddingPalette().get(rng, realX, i, realZ, data);
}
h.setRaw(xf, i, zf, layerBlock);
continue;
}
ore = hasUndergroundOres ? undergroundOre(realX, i, realZ) : null;
h.setRaw(xf, i, zf, ore != null ? ore : rock);
}
}
if (upperContext != null) {
paintUpper(xf, zf, realX, realZ);
}
}
return null;
/**
* Paints the solid run from {@code top} down to {@code floor}, which lies below every biome
* layer, river material, mapped surface and cave ceiling of its span: only ores or rock land there.
*/
private void paintDeep(int xf, int zf, int realX, int realZ, int top, int floor, NativeBlockState rock,
boolean hasSurfaceOres, boolean hasUndergroundOres) {
for (int y = top; y >= floor; y--) {
NativeBlockState ore = hasSurfaceOres ? surfaceOre(realX, y, realZ) : null;
if (ore == null && hasUndergroundOres) {
ore = undergroundOre(realX, y, realZ);
}
h.setRaw(xf, y, zf, ore != null ? ore : rock);
}
}
private NativeBlockState surfaceOre(int realX, int y, int realZ) {
NativeBlockState ore = null;
if (biomeSurfaceOres.contains(y)) {
ore = biomeSurfaceOres.generate(realX, y, realZ, rng, data);
}
if (ore == null && regionSurfaceOres.contains(y)) {
ore = regionSurfaceOres.generate(realX, y, realZ, rng, data);
}
if (ore == null && dimensionSurfaceOres.contains(y)) {
ore = dimensionSurfaceOres.generate(realX, y, realZ, rng, data);
}
return ore;
}
private NativeBlockState undergroundOre(int realX, int y, int realZ) {
NativeBlockState ore = null;
if (biomeUndergroundOres.contains(y)) {
ore = biomeUndergroundOres.generate(realX, y, realZ, rng, data);
}
if (ore == null && regionUndergroundOres.contains(y)) {
ore = regionUndergroundOres.generate(realX, y, realZ, rng, data);
}
if (ore == null && dimensionUndergroundOres.contains(y)) {
ore = dimensionUndergroundOres.generate(realX, y, realZ, rng, data);
}
return ore;
}
private void paintUpper(int xf, int zf, int realX, int realZ) {
UpperDimensionContext.Column upperColumn = upperContext.sampleColumn(realX, realZ);
int upperSurfaceY = upperColumn.surfaceY();
if (upperSurfaceY >= chunkHeight - 1) {
return;
}
IrisBiome upperBiome = upperContext.getUpperBiome(realX, realZ);
NativeBlockState upperRock = upperContext.getRockBlock(realX, realZ);
NativeBlockState upperMappedSurface = upperContext.getSurfaceBlock(realX, realZ);
KList<NativeBlockState> upperBlocks = null;
int paletteSourceY = -1;
for (int y = chunkHeight - 1; y >= upperSurfaceY; y--) {
if (y == chunkHeight - 1 && bedrockEnabled) {
h.setRaw(xf, y, zf, bedrock);
continue;
}
if (!upperColumn.isSolid(y)) {
h.setRaw(xf, y, zf, air);
continue;
}
int faceY = upperColumn.faceY(y);
int sourceSurfaceY = upperColumn.height() - 1 - faceY;
if (sourceSurfaceY != paletteSourceY) {
paletteSourceY = sourceSurfaceY;
upperBlocks = null;
}
if (y == faceY && upperMappedSurface != null) {
h.setRaw(xf, y, zf, upperMappedSurface);
continue;
}
int depthFromFace = y - faceY;
if (upperBlocks == null && upperBiome != null) {
upperBlocks = upperBiome.generateLayersWithSlope(upperContext.getDimension(),
realX, realZ, rng, sourceSurfaceY, sourceSurfaceY,
upperContext.getData(), upperContext.getSurfaceSlopeStream(sourceSurfaceY));
}
if (upperBlocks != null && upperBlocks.hasIndex(depthFromFace)) {
h.setRaw(xf, y, zf, upperBlocks.get(depthFromFace));
} else {
h.setRaw(xf, y, zf, upperRock);
}
}
}
}
}
@@ -95,6 +95,9 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
private final int minimumY;
private final ChunkContext context;
private final Map<NativeBlockState, BoundaryColumnGeometry.Voxel> voxels = new IdentityHashMap<>();
private NativeBlockState air;
private NativeBlockState lastState;
private BoundaryColumnGeometry.Voxel lastVoxel;
private GeometrySource(Hunk<NativeBlockState> blocks, Hunk<NativeBiome> biomes, int minimumY, ChunkContext context) {
this.blocks = blocks;
@@ -107,9 +110,16 @@ public final class IrisTransitionGeometryActuator extends EngineAssignedComponen
public BoundaryColumnGeometry.Voxel voxel(int localX, int worldY, int localZ) {
NativeBlockState state = blocks.getRaw(localX, worldY - minimumY, localZ);
if (state == null) {
state = IrisPlatforms.get().registries().air();
if (air == null) {
air = IrisPlatforms.get().registries().air();
}
state = air;
}
return voxels.computeIfAbsent(state, GeometrySource::encode);
if (state != lastState) {
lastVoxel = voxels.computeIfAbsent(state, GeometrySource::encode);
lastState = state;
}
return lastVoxel;
}
@Override
@@ -18,7 +18,7 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
super();
this.stream = stream;
this.engine = engine;
this.cache = new WorldCache2DDouble((x, z) -> stream.getDouble(x, z), size);
this.cache = new WorldCache2DDouble((x, z) -> ProvisionalSampling.memoizableDouble(stream, x, z), size);
IrisServices.get(PreservationRegistry.class).registerCache(this);
}
@@ -34,7 +34,7 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
@Override
public Double get(double x, double z) {
return cache.get((int) x, (int) z);
return getDouble(x, z);
}
@Override
@@ -44,7 +44,11 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
@Override
public double getDouble(double x, double z) {
return cache.get((int) x, (int) z);
try {
return cache.get((int) x, (int) z);
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.doubleValue();
}
}
@Override
@@ -75,13 +79,29 @@ public class CachedDoubleStream2D extends BasicStream<Double> implements Procedu
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
cache.fillChunk(chunkX, chunkZ, target);
try {
cache.fillChunk(chunkX, chunkZ, target);
} catch (ProvisionalSampling.Unmemoizable provisional) {
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = getDouble(originX + (index & 15), originZ + (index >> 4));
}
}
}
@Override
public void fillChunkDoubles(int worldX, int worldZ, double[] target) {
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
cache.fillChunk(chunkX, chunkZ, target);
try {
cache.fillChunk(chunkX, chunkZ, target);
} catch (ProvisionalSampling.Unmemoizable provisional) {
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = getDouble(originX + (index & 15), originZ + (index >> 4));
}
}
}
}
@@ -36,7 +36,8 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
super();
this.stream = stream;
this.engine = engine;
cache = WorldCache2D.<T>ofInts(stream::get, size, () -> new ChunkCache2D<>("iris"));
cache = WorldCache2D.<T>ofInts((x, z) -> ProvisionalSampling.memoizable(stream, x, z), size,
() -> new ChunkCache2D<>("iris"));
IrisServices.get(PreservationRegistry.class).registerCache(this);
}
@@ -52,7 +53,11 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
@Override
public T get(double x, double z) {
return cache.get((int) x, (int) z);
try {
return cache.get((int) x, (int) z);
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
}
@Override
@@ -87,6 +92,14 @@ public class CachedStream2D<T> extends BasicStream<T> implements ProceduralStrea
public void fillChunkRaw(int worldX, int worldZ, Object[] target) {
int chunkX = worldX >> 4;
int chunkZ = worldZ >> 4;
cache.fillChunk(chunkX, chunkZ, target);
try {
cache.fillChunk(chunkX, chunkZ, target);
} catch (ProvisionalSampling.Unmemoizable provisional) {
int originX = chunkX << 4;
int originZ = chunkZ << 4;
for (int index = 0; index < 256; index++) {
target[index] = get(originX + (index & 15), originZ + (index >> 4));
}
}
}
}
@@ -0,0 +1,110 @@
package art.arcane.iris.generation.stream;
import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.function.Supplier;
/**
* Tracks answers a thread gave from inputs that are not final yet. The server thread never waits for
* a hydrology plan, so it answers from natural terrain until the plan lands; a value derived from such
* an answer is right for that caller only and must never be memoized, or generation would later read
* it and its output would depend on timing.
*/
public final class ProvisionalSampling {
private static final ThreadLocal<Tracker> TRACKERS = ThreadLocal.withInitial(Tracker::new);
private ProvisionalSampling() {
}
/** Records that the calling thread just answered from inputs that are not final. */
public static void mark() {
TRACKERS.get().provisional = true;
}
/**
* The stream's value for a memoizing cache, or {@link Unmemoizable} carrying it when sampling it
* read a provisional answer. The mark still reaches every enclosing resolver.
*/
public static <T> T memoizable(ProceduralStream<T> stream, int x, int z) {
Tracker tracker = TRACKERS.get();
boolean outer = tracker.begin();
T value;
boolean provisional;
try {
value = stream.get(x, z);
} finally {
provisional = tracker.end(outer);
}
if (provisional) {
throw new Unmemoizable(value);
}
return value;
}
public static double memoizableDouble(ProceduralStream<Double> stream, int x, int z) {
Tracker tracker = TRACKERS.get();
boolean outer = tracker.begin();
double value;
boolean provisional;
try {
value = stream.getDouble(x, z);
} finally {
provisional = tracker.end(outer);
}
if (provisional) {
throw new Unmemoizable(value);
}
return value;
}
public static <T> T memoizable(Supplier<T> resolver) {
Tracker tracker = TRACKERS.get();
boolean outer = tracker.begin();
T value;
boolean provisional;
try {
value = resolver.get();
} finally {
provisional = tracker.end(outer);
}
if (provisional) {
throw new Unmemoizable(value);
}
return value;
}
private static final class Tracker {
private boolean provisional;
private boolean begin() {
boolean outer = provisional;
provisional = false;
return outer;
}
private boolean end(boolean outer) {
boolean inner = provisional;
provisional = outer || inner;
return inner;
}
}
/** Carries a provisional value past a memoizing cache so the cache stores nothing. */
public static final class Unmemoizable extends RuntimeException {
private final transient Object value;
private Unmemoizable(Object value) {
super(null, null, false, false);
this.value = value;
}
@SuppressWarnings("unchecked")
public <T> T value() {
return (T) value;
}
public double doubleValue() {
return (Double) value;
}
}
}
@@ -27,7 +27,7 @@ import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositVariant;
import art.arcane.iris.generation.decoration.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds;
import art.arcane.iris.generation.decoration.IrisOreBands;
import art.arcane.iris.generation.decoration.tree.IrisTreeSettings;
import art.arcane.iris.generation.hydrology.IrisDeepFluidConfig;
import art.arcane.iris.generation.hydrology.IrisHydrology;
@@ -137,8 +137,8 @@ public class IrisDimension extends IrisRegistrant {
private final transient AtomicCache<Map<String, IrisDimensionCarvingEntry>> carvingEntryIndex = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> surfaceOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> undergroundOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> surfaceOreBandsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> undergroundOreBandsCache = new AtomicCache<>();
@MinNumber(2)
@Required
@Description("The human readable name of this dimension")
@@ -484,24 +484,16 @@ public class IrisDimension extends IrisRegistrant {
this.ores = ores == null ? new KList<>() : ores;
surfaceOreCache.reset();
undergroundOreCache.reset();
surfaceOreBoundsCache.reset();
undergroundOreBoundsCache.reset();
surfaceOreBandsCache.reset();
undergroundOreBandsCache.reset();
}
public KList<IrisOreGenerator> getSurfaceOreGenerators() {
return getOres(true);
public IrisOreBands getSurfaceOreBands() {
return surfaceOreBandsCache.aquire(() -> IrisOreBands.of(getSurfaceOres()));
}
public KList<IrisOreGenerator> getUndergroundOreGenerators() {
return getOres(false);
}
public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() {
return surfaceOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres()));
}
public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() {
return undergroundOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres()));
public IrisOreBands getUndergroundOreBands() {
return undergroundOreBandsCache.aquire(() -> IrisOreBands.of(getUndergroundOres()));
}
private KList<IrisOreGenerator> getSurfaceOres() {
@@ -14,7 +14,7 @@ import art.arcane.volmlib.util.noise.CNG;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
@@ -81,8 +81,9 @@ public final class IrisDimensionCarvingResolver {
}
private static IrisDimensionCarvingEntry resolveBoundRootEntry(Engine engine, int worldY, IrisData data, State resolvedState) {
if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) {
return resolvedState.rootEntriesByWorldY.get(worldY);
int slot = resolvedState.rootSlot(worldY);
if (slot >= 0) {
return resolvedState.roots[slot];
}
return selectBoundRootEntry(engine.getDimension(), worldY, data, resolvedState);
@@ -91,7 +92,7 @@ public final class IrisDimensionCarvingResolver {
private static IrisDimensionCarvingEntry selectBoundRootEntry(IrisDimension dimension, int worldY, IrisData data, State resolvedState) {
List<IrisDimensionCarvingEntry> entries = dimension.getCarving();
if (entries == null || entries.isEmpty()) {
resolvedState.rootEntriesByWorldY.put(worldY, null);
resolvedState.rememberRoot(worldY, null);
return null;
}
@@ -104,7 +105,7 @@ public final class IrisDimensionCarvingResolver {
resolved = entry;
}
resolvedState.rootEntriesByWorldY.put(worldY, resolved);
resolvedState.rememberRoot(worldY, resolved);
return resolved;
}
@@ -324,8 +325,9 @@ public final class IrisDimensionCarvingResolver {
}
public IrisBiome resolveBiome(int worldX, int worldY, int worldZ) {
IrisDimensionCarvingEntry root = state.rootEntriesByWorldY.containsKey(worldY)
? state.rootEntriesByWorldY.get(worldY)
int slot = state.rootSlot(worldY);
IrisDimensionCarvingEntry root = slot >= 0
? state.roots[slot]
: selectBoundRootEntry(dimension, worldY, data, state);
if (root == null) {
return null;
@@ -336,7 +338,11 @@ public final class IrisDimensionCarvingResolver {
}
public static final class State {
private final Map<Integer, IrisDimensionCarvingEntry> rootEntriesByWorldY = new HashMap<>();
private static final int ROOT_SLOTS = 512;
private int[] rootWorldYs;
private IrisDimensionCarvingEntry[] roots;
private boolean[] rootsKnown;
private final Map<IrisDimensionCarvingEntry, IrisRaritySelection<CarvingChoice>> selectionPlans = new IdentityHashMap<>();
private final Map<IrisDimensionCarvingEntry, IrisBiome> biomeCache = new IdentityHashMap<>();
private WeakReference<Engine> engineIdentity;
@@ -357,7 +363,10 @@ public final class IrisDimensionCarvingResolver {
engineIdentity = new WeakReference<>(engine);
dimensionIdentity = new WeakReference<>(dimension);
dataIdentity = new WeakReference<>(data);
rootEntriesByWorldY.clear();
if (rootsKnown != null) {
Arrays.fill(rootsKnown, false);
Arrays.fill(roots, null);
}
selectionPlans.clear();
biomeCache.clear();
entryIndex = null;
@@ -366,6 +375,23 @@ public final class IrisDimensionCarvingResolver {
return data;
}
private int rootSlot(int worldY) {
int slot = worldY & (ROOT_SLOTS - 1);
return rootsKnown != null && rootsKnown[slot] && rootWorldYs[slot] == worldY ? slot : -1;
}
private void rememberRoot(int worldY, IrisDimensionCarvingEntry root) {
if (rootsKnown == null) {
rootWorldYs = new int[ROOT_SLOTS];
roots = new IrisDimensionCarvingEntry[ROOT_SLOTS];
rootsKnown = new boolean[ROOT_SLOTS];
}
int slot = worldY & (ROOT_SLOTS - 1);
rootWorldYs[slot] = worldY;
roots[slot] = root;
rootsKnown[slot] = true;
}
private static boolean references(WeakReference<?> identity, Object value) {
return identity != null && identity.get() == value;
}
@@ -23,7 +23,7 @@ import art.arcane.iris.generation.cave.IrisCaveProfile;
import art.arcane.iris.generation.decoration.IrisDepositGenerator;
import art.arcane.iris.generation.decoration.IrisDepositVariant;
import art.arcane.iris.generation.decoration.IrisOreGenerator;
import art.arcane.iris.generation.decoration.IrisOreGeneratorBounds;
import art.arcane.iris.generation.decoration.IrisOreBands;
import art.arcane.iris.generation.decoration.IrisProceduralObjects;
import art.arcane.iris.generation.hydrology.IrisRiverPolicy;
import art.arcane.iris.pack.validation.CompatPools;
@@ -88,8 +88,8 @@ public class IrisRegion extends IrisRegistrant implements Rarity {
private final transient AtomicCache<Color> cacheColor = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> surfaceOreCache = new AtomicCache<>();
private final transient AtomicCache<KList<IrisOreGenerator>> undergroundOreCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> surfaceOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreGeneratorBounds> undergroundOreBoundsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> surfaceOreBandsCache = new AtomicCache<>();
private final transient AtomicCache<IrisOreBands> undergroundOreBandsCache = new AtomicCache<>();
@MinNumber(2)
@Required
@Description("The name of the region")
@@ -217,24 +217,16 @@ public class IrisRegion extends IrisRegistrant implements Rarity {
this.ores = ores == null ? new KList<>() : ores;
surfaceOreCache.reset();
undergroundOreCache.reset();
surfaceOreBoundsCache.reset();
undergroundOreBoundsCache.reset();
surfaceOreBandsCache.reset();
undergroundOreBandsCache.reset();
}
public KList<IrisOreGenerator> getSurfaceOreGenerators() {
return getOres(true);
public IrisOreBands getSurfaceOreBands() {
return surfaceOreBandsCache.aquire(() -> IrisOreBands.of(getSurfaceOres()));
}
public KList<IrisOreGenerator> getUndergroundOreGenerators() {
return getOres(false);
}
public IrisOreGeneratorBounds getSurfaceOreGeneratorBounds() {
return surfaceOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getSurfaceOres()));
}
public IrisOreGeneratorBounds getUndergroundOreGeneratorBounds() {
return undergroundOreBoundsCache.aquire(() -> IrisOreGeneratorBounds.of(getUndergroundOres()));
public IrisOreBands getUndergroundOreBands() {
return undergroundOreBandsCache.aquire(() -> IrisOreBands.of(getUndergroundOres()));
}
private KList<IrisOreGenerator> getSurfaceOres() {
@@ -1,12 +1,11 @@
package art.arcane.iris.generation.terrain;
import art.arcane.iris.generation.cache.ConcurrentClockCache;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.cache.CacheKey;
import art.arcane.volmlib.util.math.RNG;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import java.util.Arrays;
import java.util.IdentityHashMap;
@@ -16,9 +15,10 @@ import java.util.concurrent.atomic.AtomicReferenceArray;
public final class Terrain3DRuntime {
private static final int STEP = 4;
private static final int CACHE_STRIPES = 16;
private static final int MINIMUM_COLUMNS = 16;
private static final int ANCHOR_BLOCK_SIZE = 16;
private static final int MAXIMUM_ANCHORS = 32_768;
private static final long ANCHOR_CACHE_BYTES = 64L * 1024 * 1024;
private static final long ANCHOR_CACHE_BYTES = 128L * 1024 * 1024;
private static final long DENSITY_SALT = 0x536E4A11C924B3D7L;
private static final long CRACK_SALT = 0x7839A16DC4052EFBL;
private static final NodeSample EMPTY_SAMPLE = new NodeSample(0D, 0D, 0D);
@@ -28,10 +28,11 @@ public final class Terrain3DRuntime {
private final Sources sources;
private final Options options;
private final NoiseFactory noiseFactory;
private final BoundedCache<Terrain3DFragmentFilter.DensityColumn> columns;
private final BoundedCache<Anchor> anchors;
private final BlockCache<Terrain3DFragmentFilter.DensityColumn> columns;
private final BlockCache<Anchor> anchors;
private final Terrain3DFragmentFilter fragments;
private final Map<IrisTerrain3D, CompiledProfile> profiles = new IdentityHashMap<>();
private final Object profileLock = new Object();
private volatile Map<IrisTerrain3D, CompiledProfile> profiles = new IdentityHashMap<>();
private final ThreadLocal<ColumnMemo> localColumn = ThreadLocal.withInitial(ColumnMemo::new);
private volatile Object cacheGeneration = new Object();
@@ -46,8 +47,8 @@ public final class Terrain3DRuntime {
this.sources = Objects.requireNonNull(sources, "Terrain sources");
this.options = Objects.requireNonNull(options, "Terrain options");
this.noiseFactory = Objects.requireNonNull(noiseFactory, "Terrain noise factory");
columns = new BoundedCache<>(options.maximumColumns());
anchors = new BoundedCache<>(anchorCacheCapacity(options.height(), options.maximumColumns()));
columns = new BlockCache<>(options.maximumColumns(), 0);
anchors = new BlockCache<>(anchorCacheCapacity(options.height(), options.maximumColumns()), 2);
fragments = new Terrain3DFragmentFilter(this::rawColumn);
}
@@ -59,7 +60,7 @@ public final class Terrain3DRuntime {
long bytesPerAnchor = 160L + 48L * (Math.ceilDiv((long) height, STEP) + 1);
long maximumEntries = Math.min(MAXIMUM_ANCHORS,
Math.min((long) maximumColumns * 8, ANCHOR_CACHE_BYTES / bytesPerAnchor));
return Math.max(CACHE_STRIPES, (int) (maximumEntries / CACHE_STRIPES) * CACHE_STRIPES);
return Math.max(ANCHOR_BLOCK_SIZE, (int) (maximumEntries / ANCHOR_BLOCK_SIZE) * ANCHOR_BLOCK_SIZE);
}
public double height(int x, int z) {
@@ -94,8 +95,8 @@ public final class Terrain3DRuntime {
columns.clear();
anchors.clear();
fragments.clear();
synchronized (profiles) {
profiles.clear();
synchronized (profileLock) {
profiles = new IdentityHashMap<>();
}
cacheGeneration = new Object();
localColumn.remove();
@@ -106,9 +107,8 @@ public final class Terrain3DRuntime {
}
private Terrain3DFragmentFilter.DensityColumn rawColumn(int x, int z) {
long key = pack(x, z);
Terrain3DFragmentFilter.DensityColumn cached = columns.get(key);
return cached == null ? columns.putIfAbsent(key,
Terrain3DFragmentFilter.DensityColumn cached = columns.get(x, z);
return cached == null ? columns.putIfAbsent(x, z,
new Terrain3DFragmentFilter.DensityColumn(createColumn(x, z))) : cached;
}
@@ -196,9 +196,8 @@ public final class Terrain3DRuntime {
}
private Anchor anchor(int x, int z) {
long key = pack(x, z);
Anchor cached = anchors.get(key);
return cached == null ? anchors.putIfAbsent(key, createAnchor(x, z)) : cached;
Anchor cached = anchors.get(x, z);
return cached == null ? anchors.putIfAbsent(x, z, createAnchor(x, z)) : cached;
}
private Anchor createAnchor(int x, int z) {
@@ -227,19 +226,19 @@ public final class Terrain3DRuntime {
if (config == null || !config.isEnabled()) {
return DISABLED;
}
synchronized (profiles) {
CompiledProfile cached = profiles.get(config);
if (cached != null) {
return cached;
}
CompiledProfile cached = profiles.get(config);
if (cached != null) {
return cached;
}
CompiledProfile compiled = compile(config);
synchronized (profiles) {
synchronized (profileLock) {
CompiledProfile existing = profiles.get(config);
if (existing != null) {
return existing;
}
profiles.put(config, compiled);
Map<IrisTerrain3D, CompiledProfile> updated = new IdentityHashMap<>(profiles);
updated.put(config, compiled);
profiles = updated;
return compiled;
}
}
@@ -314,7 +313,7 @@ public final class Terrain3DRuntime {
boolean enabled, int maximumColumns) {
public Options {
if (height < 2 || height > 4096 || !Double.isFinite(fluidHeight)
|| maximumColumns < CACHE_STRIPES) {
|| maximumColumns < MINIMUM_COLUMNS) {
throw new IllegalArgumentException("Invalid volumetric terrain runtime options");
}
}
@@ -408,74 +407,61 @@ public final class Terrain3DRuntime {
double fluidFade, NoiseSource density, NoiseSource cracks) {
}
private static final class BoundedCache<T> {
private final CacheStripe<T>[] stripes;
/**
* Groups the 16x16 block area of one chunk into a single cache entry, so neighbouring lookups share one
* lock-free set probe and a working set of whole chunks is evicted together.
*/
private static final class BlockCache<T> {
private final ConcurrentClockCache<AtomicReferenceArray<T>> blocks;
private final int cellShift;
private final int axisMask;
private final int axisBits;
@SuppressWarnings("unchecked")
private BoundedCache(int maximumSize) {
stripes = (CacheStripe<T>[]) new CacheStripe<?>[CACHE_STRIPES];
for (int index = 0; index < stripes.length; index++) {
stripes[index] = new CacheStripe<>(Math.ceilDiv(maximumSize, CACHE_STRIPES));
private BlockCache(int maximumEntries, int cellShift) {
this.cellShift = cellShift;
axisBits = 4 - cellShift;
axisMask = (1 << axisBits) - 1;
int blockSize = 1 << (axisBits * 2);
blocks = new ConcurrentClockCache<>(Math.ceilDiv(maximumEntries, blockSize));
}
private T get(int x, int z) {
AtomicReferenceArray<T> block = blocks.get(blockKey(x, z));
return block == null ? null : block.getAcquire(slot(x, z));
}
private T putIfAbsent(int x, int z, T value) {
long key = blockKey(x, z);
AtomicReferenceArray<T> block = blocks.get(key);
if (block == null) {
block = blocks.putIfAbsent(key, new AtomicReferenceArray<>(1 << (axisBits * 2)));
}
}
private T get(long key) {
return stripe(key).get(key);
}
private T putIfAbsent(long key, T value) {
return stripe(key).putIfAbsent(key, value);
T existing = block.compareAndExchange(slot(x, z), null, value);
return existing == null ? value : existing;
}
private void clear() {
for (CacheStripe<T> stripe : stripes) {
stripe.clear();
}
blocks.clear();
}
private int size() {
int size = 0;
for (CacheStripe<T> stripe : stripes) {
size += stripe.size();
}
return size;
int[] size = new int[1];
blocks.forEach(block -> {
for (int index = 0; index < block.length(); index++) {
if (block.getAcquire(index) != null) {
size[0]++;
}
}
});
return size[0];
}
private CacheStripe<T> stripe(long key) {
return stripes[(int) CacheKey.mix(key) & (CACHE_STRIPES - 1)];
}
}
private static final class CacheStripe<T> {
private final int maximumSize;
private final Long2ObjectLinkedOpenHashMap<T> entries = new Long2ObjectLinkedOpenHashMap<>(16);
private CacheStripe(int maximumSize) {
this.maximumSize = maximumSize;
private static long blockKey(int x, int z) {
return pack(x >> 4, z >> 4);
}
private synchronized T get(long key) {
return entries.getAndMoveToLast(key);
}
private synchronized T putIfAbsent(long key, T value) {
T existing = entries.getAndMoveToLast(key);
if (existing != null) {
return existing;
}
entries.putAndMoveToLast(key, value);
if (entries.size() > maximumSize) {
entries.removeFirst();
}
return value;
}
private synchronized void clear() {
entries.clear();
}
private synchronized int size() {
return entries.size();
private int slot(int x, int z) {
return ((x >> cellShift) & axisMask) << axisBits | ((z >> cellShift) & axisMask);
}
}
}
@@ -57,29 +57,33 @@ 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<BlockData, BukkitBlockState> 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 volatile ConcurrentHashMap<String, ConcurrentHashMap<String, NativeBlockState>> propertyVariants;
private BukkitBlockState(BlockData data, String key) {
this.data = data;
@@ -87,6 +91,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 +206,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 +256,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 +406,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
@@ -428,7 +436,18 @@ public final class BukkitBlockState implements NativeBlockState {
BlockData resolved = Bukkit.createBlockData(merged);
return of(IrisCustomData.of(resolved, custom.getCustom()));
}
return of(Bukkit.createBlockData(BlockStateKey.withProperty(key, name, value)));
ConcurrentHashMap<String, ConcurrentHashMap<String, NativeBlockState>> variants = propertyVariants;
if (variants == null) {
variants = new ConcurrentHashMap<>(4);
propertyVariants = variants;
}
ConcurrentHashMap<String, NativeBlockState> values = variants.computeIfAbsent(name, (String ignored) -> new ConcurrentHashMap<>(4));
NativeBlockState variant = values.get(value);
if (variant == null) {
variant = of(Bukkit.createBlockData(BlockStateKey.withProperty(key, name, value)));
values.putIfAbsent(value, variant);
}
return variant;
}
@Override
@@ -10,6 +10,7 @@ import art.arcane.iris.generation.runtime.DimensionStackLayout;
import art.arcane.iris.generation.runtime.DimensionTerrainContext;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.stream.ProvisionalSampling;
import art.arcane.iris.generation.runtime.GenerationSessionException;
import art.arcane.iris.generation.runtime.GenerationSessionLease;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
@@ -327,7 +328,12 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
}
}
H resolvedHolder = resolveStructureBiomeHolder(x, y, z);
H resolvedHolder;
try {
resolvedHolder = ProvisionalSampling.memoizable(() -> resolveStructureBiomeHolder(x, y, z));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
if (!cacheable) {
return resolvedHolder;
}
@@ -453,7 +459,12 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
if (surfaceHolder != null) {
return surfaceHolder;
}
H resolvedSurfaceHolder = resolveSurfaceStructureBiomeHolder(x, z);
H resolvedSurfaceHolder;
try {
resolvedSurfaceHolder = ProvisionalSampling.memoizable(() -> resolveSurfaceStructureBiomeHolder(x, z));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
H existingSurfaceHolder = surfaceStructureBiomeCache.putIfAbsent(
columnKey, resolvedSurfaceHolder);
if (existingSurfaceHolder != null) {
@@ -624,7 +635,12 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
}
}
H resolvedHolder = resolveVisibleBiomeHolder(x, y, z, resolverState);
H resolvedHolder;
try {
resolvedHolder = ProvisionalSampling.memoizable(() -> resolveVisibleBiomeHolder(x, y, z, resolverState));
} catch (ProvisionalSampling.Unmemoizable provisional) {
return provisional.value();
}
if (!cacheable) {
return resolvedHolder;
}
@@ -633,7 +649,7 @@ public final class BukkitBiomePolicy<H, V> implements NativeBiomeSourcePolicy<H>
return existingHolder;
}
if (noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
if ((y & 63) == 0 && noiseBiomeCache.size() > NOISE_BIOME_CACHE_MAX) {
noiseBiomeCache.clear();
}
@@ -233,6 +233,18 @@ public interface INMSBinding extends NativeTerrainAccess {
return false;
}
default int fullChunkDependencyRadius() {
return -1;
}
default boolean retainChunk(World world, int x, int z) {
return false;
}
default boolean releaseChunk(World world, int x, int z) {
return false;
}
default boolean pollChunkTask(World world) {
return false;
}
@@ -73,7 +73,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;
@@ -1308,12 +1308,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<NativeBiome> biomes = new TerrainChunkBiomeHunkView(tc);
Hunk<NativeBiome> biomes = new ColumnBiomeHunk(d.getMaxHeight() - d.getMinHeight());
try (GenerationHistoryRuntimeRouter.CoordinateScope historyScope =
openGenerationHistoryCoordinateScope(engine, x << 4, z << 4);
GenerationSessionLease lease = engine.acquireGenerationLease("bukkit_terrain_stage");
@@ -132,7 +132,7 @@ public final class EngineBukkitOps {
return;
}
MantleChunk<Matter> chunk = mantle.getChunk(c).use();
MantleChunk<Matter> chunk = mantle.useChunk(c);
try {
Runnable tileTask = () -> materializeTiles(engine, c, chunk);
Runnable customTask = () -> materializeCustomBlocks(engine, c, chunk);
@@ -461,7 +461,7 @@ public final class EngineBukkitOps {
}
public static KList<IrisLootTable> getLootTables(Engine engine, RNG rng, Block b) {
MantleChunk<Matter> mc = engine.getMantle().getMantle().getChunk(b.getChunk()).use();
MantleChunk<Matter> mc = engine.getMantle().getMantle().useChunk(b.getChunk());
try {
return getLootTables(engine, rng, b, mc, hasNativeLootTable(b));
} finally {
@@ -264,7 +264,7 @@ public final class NativeStructureOwnershipStore {
@Override
public NativeStructureOwnershipBundle read(int chunkX, int chunkZ) {
RuntimeMantleOwner owner = owner();
MantleChunk<Matter> chunk = owner.mantle().getMantle().getChunk(chunkX, chunkZ).use();
MantleChunk<Matter> chunk = owner.mantle().getMantle().useChunk(chunkX, chunkZ);
try {
synchronized (chunk) {
return chunk.get(0, 0, 0, NativeStructureOwnershipBundle.class);
@@ -279,7 +279,7 @@ public final class NativeStructureOwnershipStore {
RuntimeMantleOwner owner = owner();
int targetChunkX = unpackX(target);
int targetChunkZ = unpackZ(target);
MantleChunk<Matter> chunk = owner.mantle().getMantle().getChunk(targetChunkX, targetChunkZ).use();
MantleChunk<Matter> chunk = owner.mantle().getMantle().useChunk(targetChunkX, targetChunkZ);
try {
synchronized (chunk) {
Matter section = chunk.getOrCreate(0);
@@ -302,7 +302,7 @@ public final class NativeStructureOwnershipStore {
RuntimeMantleOwner owner = owner();
int targetChunkX = unpackX(target);
int targetChunkZ = unpackZ(target);
MantleChunk<Matter> chunk = owner.mantle().getMantle().getChunk(targetChunkX, targetChunkZ).use();
MantleChunk<Matter> chunk = owner.mantle().getMantle().useChunk(targetChunkX, targetChunkZ);
try {
synchronized (chunk) {
Matter section = chunk.getOrCreate(0);
@@ -396,7 +396,7 @@ public final class NativeStructureOwnershipStore {
private void restorePendingBundle(Mantle<Matter> mantle, long target,
NativeStructureOwnershipBundle bundle) {
MantleChunk<Matter> chunk = mantle.getChunk(unpackX(target), unpackZ(target)).use();
MantleChunk<Matter> chunk = mantle.useChunk(unpackX(target), unpackZ(target));
try {
synchronized (chunk) {
Matter section = chunk.getOrCreate(0);
@@ -8,6 +8,7 @@ import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume;
import art.arcane.iris.generation.runtime.IrisEngine;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.world.history.GenerationHistoryRuntimeRouter;
import art.arcane.volmlib.util.cache.CacheKey;
import art.arcane.volmlib.util.collection.KList;
import java.io.IOException;
@@ -313,7 +314,15 @@ public final class NativeStructureVolumeIndex {
};
}
static int runtimeChunkHash(int runtimeId, long chunkKey) {
return Long.hashCode(CacheKey.mix(chunkKey)) * 31 + runtimeId;
}
private record RuntimeChunkKey(int runtimeId, long chunkKey) {
@Override
public int hashCode() {
return runtimeChunkHash(runtimeId, chunkKey);
}
}
}
@@ -86,6 +86,7 @@ public class IrisObject extends IrisRegistrant {
NativeBlockState stone,
NativeBlockState vair,
NativeBlockState vairDebug,
NativeBlockState dirt,
NativeBlockState[] snowLayers) {
}
@@ -102,6 +103,7 @@ public class IrisObject extends IrisRegistrant {
B.getState("STONE"),
B.getState("VOID_AIR"),
B.getState("COBWEB"),
B.getState("minecraft:dirt"),
new NativeBlockState[]{B.getState("minecraft:snow[layers=1]"), B.getState("minecraft:snow[layers=2]"), B.getState("minecraft:snow[layers=3]"), B.getState("minecraft:snow[layers=4]"), B.getState("minecraft:snow[layers=5]"), B.getState("minecraft:snow[layers=6]"), B.getState("minecraft:snow[layers=7]"), B.getState("minecraft:snow[layers=8]")});
bound = resolved;
return resolved;
@@ -123,6 +125,10 @@ public class IrisObject extends IrisRegistrant {
return bound().vairDebug();
}
static NativeBlockState dirt() {
return bound().dirt();
}
static NativeBlockState snowLayer(int layerIndex) {
return bound().snowLayers()[layerIndex];
}
@@ -733,7 +733,7 @@ final class IrisObjectPlacementRunner {
} else {
String mat = IrisObjectShaping.materialKey(d);
if (mat.equals("minecraft:grass_block") || mat.equals("minecraft:mycelium") || mat.equals("minecraft:podzol") || mat.equals("minecraft:dirt_path")) {
d = B.getState("minecraft:dirt");
d = IrisObject.States.dirt();
}
}
@@ -745,11 +745,10 @@ final class IrisObjectPlacementRunner {
if (translating) {
i.add(translateOffset);
}
d = config.getRotation().rotate(d, spinx, spiny, spinz);
int targetLayer = ceilingHang ? topLayer : lowest;
if (i.getBlockY() != targetLayer)
continue;
d = config.getRotation().rotate(d, spinx, spiny, spinz);
if (hasEdits) {
for (IrisObjectReplace j : config.getEdit()) {
@@ -31,9 +31,12 @@ import art.arcane.iris.generation.block.IrisCustomData;
import art.arcane.iris.generation.geometry.IrisBlockVector;
import art.arcane.volmlib.util.collection.KList;
import art.arcane.volmlib.util.collection.KMap;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.bukkit.Axis;
import org.bukkit.block.BlockFace;
@@ -50,6 +53,7 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Snippet("object-rotator")
@Accessors(chain = true)
@@ -59,6 +63,8 @@ import java.util.Set;
@Data
public class IrisObjectRotation {
private static volatile StateRotator PLATFORM_ROTATOR = null;
private static final Object ROTATED_AWAY = new Object();
private static final int SPIN_STEPS = 5;
public interface StateRotator {
NativeBlockState rotate(IrisObjectRotation rotation, NativeBlockState state, int spinx, int spiny, int spinz);
@@ -91,6 +97,10 @@ public class IrisObjectRotation {
@Description("The z axis rotation")
private IrisAxisRotationClamp zAxis = new IrisAxisRotationClamp();
@Getter(AccessLevel.NONE)
@ToString.Exclude
private final transient ConcurrentHashMap<NativeBlockState, Object[]> stateRotations = new ConcurrentHashMap<>();
public static IrisObjectRotation xFlip180() {
IrisObjectRotation rt = new IrisObjectRotation();
IrisAxisRotationClamp rtx = new IrisAxisRotationClamp();
@@ -316,9 +326,19 @@ public class IrisObjectRotation {
return state;
}
BlockData raw = original.clone();
BlockData rotated = rotate(raw, spinx, spiny, spinz);
return rotated == null ? null : BukkitBlockState.of(rotated);
Object[] rotations = stateRotations.computeIfAbsent(state, (NativeBlockState ignored) -> new Object[SPIN_STEPS * SPIN_STEPS * SPIN_STEPS]);
int index = (spinStep(spinx) * SPIN_STEPS + spinStep(spiny)) * SPIN_STEPS + spinStep(spinz);
Object memo = rotations[index];
if (memo == null) {
BlockData rotated = rotate(original.clone(), spinx, spiny, spinz);
memo = rotated == null ? ROTATED_AWAY : BukkitBlockState.of(rotated);
rotations[index] = memo;
}
return memo == ROTATED_AWAY ? null : (NativeBlockState) memo;
}
private static int spinStep(int spin) {
return (int) Math.ceil(Math.abs((spin % 360D) / 90D));
}
private static boolean canRotateBlockData(BlockData data) {
@@ -154,8 +154,7 @@ public final class IrisStaticObjectLayer {
return;
}
persistContinuations(engine, x >> 4, z >> 4, output.getHeight());
MantleChunk<Matter> chunk = engine.getMantle().getMantle().getChunk(x >> 4, z >> 4);
chunk.use();
MantleChunk<Matter> chunk = engine.getMantle().getMantle().useChunk(x >> 4, z >> 4);
try {
for (Block block : blocks) {
Matter section = chunk.getOrCreate(block.y() >> 4);
@@ -9,14 +9,10 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeSet;
public final class BoundaryColumnGeometry implements NativeBlockColumn {
public static final int MAXIMUM_HEIGHT = 65_536;
private static final Comparator<Voxel> VOXEL_ORDER = Comparator.comparing(Voxel::stateKey)
.thenComparing(Voxel::phase)
.thenComparing(Voxel::fluidStateKey)
.thenComparing(Voxel::protectedContent);
private static final Comparator<Voxel> VOXEL_ORDER = BoundaryColumnGeometry::compareVoxels;
private static final Voxel AIR = new Voxel("minecraft:air", Phase.AIR, "", false);
private static final BoundaryColumnGeometry EMPTY = new BoundaryColumnGeometry(0, List.of(), new int[0], new short[0]);
@@ -42,36 +38,24 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
if (voxels.size() > MAXIMUM_HEIGHT) {
throw new IllegalArgumentException("Geometry column exceeds maximum height");
}
TreeSet<Voxel> used = new TreeSet<>(VOXEL_ORDER);
Voxel previous = null;
Runs runs = new Runs(voxels.size());
for (Voxel voxel : voxels) {
if (previous == null || !previous.equals(voxel)) {
used.add(voxel);
previous = voxel;
}
runs.add(voxel);
}
List<Voxel> palette = List.copyOf(used);
Map<Voxel, Short> indices = new HashMap<>(palette.size());
for (int index = 0; index < palette.size(); index++) {
if (index > Short.MAX_VALUE) {
throw new IllegalArgumentException("Geometry palette exceeds compact index capacity");
}
indices.put(palette.get(index), (short) index);
return runs.build(minimumY);
}
private static int compareVoxels(Voxel first, Voxel second) {
int order = first.stateKey().compareTo(second.stateKey());
if (order != 0) {
return order;
}
int[] ends = new int[voxels.size()];
short[] values = new short[voxels.size()];
int runCount = 0;
previous = null;
for (int offset = 0; offset < voxels.size(); offset++) {
Voxel voxel = voxels.get(offset);
if (previous == null || !previous.equals(voxel)) {
values[runCount++] = indices.get(voxel);
previous = voxel;
}
ends[runCount - 1] = offset + 1;
order = first.phase().compareTo(second.phase());
if (order != 0) {
return order;
}
return new BoundaryColumnGeometry(minimumY, palette,
Arrays.copyOf(ends, runCount), Arrays.copyOf(values, runCount));
order = first.fluidStateKey().compareTo(second.fluidStateKey());
return order != 0 ? order : Boolean.compare(first.protectedContent(), second.protectedContent());
}
public int minimumY() {
@@ -256,6 +240,90 @@ public final class BoundaryColumnGeometry implements NativeBlockColumn {
}
}
static final class Runs {
private static final int LINEAR_PALETTE_LIMIT = 32;
private final Voxel[] voxels;
private final int[] ends;
private int count;
private int length;
Runs(int capacity) {
voxels = new Voxel[capacity];
ends = new int[capacity];
}
void clear() {
count = 0;
length = 0;
}
void add(Voxel voxel) {
Objects.requireNonNull(voxel, "Geometry voxel");
if (count == 0 || !sameVoxel(voxels[count - 1], voxel)) {
voxels[count++] = voxel;
}
ends[count - 1] = ++length;
}
BoundaryColumnGeometry build(int minimumY) {
Voxel[] distinct = new Voxel[count];
int[] slots = new int[count];
Map<Voxel, Integer> lookup = null;
int distinctCount = 0;
for (int run = 0; run < count; run++) {
Voxel voxel = voxels[run];
int slot = lookup == null ? linearIndex(distinct, distinctCount, voxel) : lookup.getOrDefault(voxel, -1);
if (slot < 0) {
slot = distinctCount;
distinct[distinctCount++] = voxel;
if (lookup != null) {
lookup.put(voxel, slot);
} else if (distinctCount > LINEAR_PALETTE_LIMIT) {
lookup = new HashMap<>();
for (int index = 0; index < distinctCount; index++) {
lookup.put(distinct[index], index);
}
}
}
slots[run] = slot;
}
if (distinctCount > Short.MAX_VALUE + 1) {
throw new IllegalArgumentException("Geometry palette exceeds compact index capacity");
}
Voxel[] palette = Arrays.copyOf(distinct, distinctCount);
Arrays.sort(palette, VOXEL_ORDER);
short[] slotIndices = new short[distinctCount];
if (lookup == null) {
for (int slot = 0; slot < distinctCount; slot++) {
slotIndices[slot] = (short) linearIndex(palette, distinctCount, distinct[slot]);
}
} else {
for (int index = 0; index < distinctCount; index++) {
slotIndices[lookup.get(palette[index])] = (short) index;
}
}
short[] values = new short[count];
for (int run = 0; run < count; run++) {
values[run] = slotIndices[slots[run]];
}
return new BoundaryColumnGeometry(minimumY, Arrays.asList(palette), Arrays.copyOf(ends, count), values);
}
private static int linearIndex(Voxel[] values, int size, Voxel voxel) {
for (int index = 0; index < size; index++) {
if (sameVoxel(values[index], voxel)) {
return index;
}
}
return -1;
}
private static boolean sameVoxel(Voxel first, Voxel second) {
return first == second || first.equals(second);
}
}
public enum Phase {
AIR,
SOLID,
@@ -82,6 +82,27 @@ public final class GenerationHistory {
return savedBiomes;
}
public void sync() throws IOException {
IOException failure = null;
try {
semantics.sync();
} catch (IOException semanticFailure) {
failure = semanticFailure;
}
try {
savedBiomes.sync();
} catch (IOException biomeFailure) {
if (failure == null) {
failure = biomeFailure;
} else {
failure.addSuppressed(biomeFailure);
}
}
if (failure != null) {
throw failure;
}
}
public static GenerationHistory create(
Path dimensionRoot,
Path packSource,
@@ -478,6 +499,7 @@ public final class GenerationHistory {
private GenerationActivation promotePendingLocked(
BoundarySignatureCapture signatureCapture, boolean startupRecovery, boolean validateSemantics
) throws IOException {
sync();
WorldChunkInventory inventory = recoverUnstoredClaims(startupRecovery);
validateReferencedState(Optional.empty(), validateSemantics);
Optional<GenerationActivation> pending = store.pendingActivation();
@@ -26,6 +26,7 @@ public final class GenerationHistoryPaths {
private final Path legacyPackRoot;
private final Path legacyMantleRoot;
private final Path regionRoot;
private volatile PackRoot lastPackRoot;
private GenerationHistoryPaths(Path dimensionRoot) {
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot")
@@ -71,7 +72,13 @@ public final class GenerationHistoryPaths {
}
public Path packRoot(String epochId) {
return epochRoot(epochId).resolve(LEGACY_PACK_DIRECTORY_NAME);
PackRoot cached = lastPackRoot;
if (cached != null && cached.epochId().equals(epochId)) {
return cached.path();
}
Path resolved = epochRoot(epochId).resolve(LEGACY_PACK_DIRECTORY_NAME);
lastPackRoot = new PackRoot(epochId, resolved);
return resolved;
}
public Path activationsRoot() {
@@ -108,4 +115,7 @@ public final class GenerationHistoryPaths {
public Path regionRoot() {
return regionRoot;
}
private record PackRoot(String epochId, Path path) {
}
}
@@ -391,6 +391,25 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
return new CoordinateScope(blockX, blockZ, stage.route, stage);
}
/**
* A coordinate scope for engine reads. Inside another chunk's route whose runtime the thread is bound to, it borrows
* that runtime instead of opening a stage: every generation stage binds the active activation, which cannot change
* while the route holds its transition participation.
*/
public CoordinateScope openReadScope(int blockX, int blockZ) throws IOException {
RuntimeRoute current = scopedRoute.get();
if (current != null) {
if (current.chunkX() == Math.floorDiv(blockX, GenerationBoundary.CHUNK_SIZE)
&& current.chunkZ() == Math.floorDiv(blockZ, GenerationBoundary.CHUNK_SIZE)) {
return new CoordinateScope(blockX, blockZ, current, null);
}
if (current.binding == engine.captureGenerationRuntimeBinding()) {
return new CoordinateScope(blockX, blockZ, null, null);
}
}
return openCoordinateScope(blockX, blockZ);
}
public void recordNaturalTerrain(SavedTerrainChunk terrain) {
SavedTerrainChunk captured = Objects.requireNonNull(terrain, "natural terrain");
RuntimeRoute route = scopedRoute.get();
@@ -500,6 +519,11 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
} catch (Throwable detachFailure) {
failure = detachFailure;
}
try {
history.sync();
} catch (Throwable syncFailure) {
failure = appendFailure(failure, syncFailure);
}
try {
biomes.close();
} catch (Throwable biomeFailure) {
@@ -550,9 +574,9 @@ public final class GenerationHistoryRuntimeRouter implements AutoCloseable {
SavedMantleAccess access = acquireSavedMantle(activation, requireEpoch(activation));
try {
Mantle<Matter> mantle = access.mantle();
MantleChunk<Matter> anchor = mantle.getChunk(regionX << 5, regionZ << 5).use();
MantleChunk<Matter> anchor = mantle.useChunk(regionX << 5, regionZ << 5);
try {
TectonicPlate<Matter> plate = mantle.getLoadedRegions().get(Mantle.key(regionX, regionZ));
TectonicPlate<Matter> plate = mantle.getLoadedRegion(regionX, regionZ);
HashMap<Long, ObjectContinuationBundle> bundles = new HashMap<>();
for (int x = 0; x < 32; x++) {
for (int z = 0; z < 32; z++) {
@@ -227,12 +227,12 @@ public final class GenerationSemanticCapture {
}
resolvedCaves.set(position);
if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) {
semantics.addCaveBiome(explicitBiomeKey);
caveBiomes.add(semantics, explicitBiomeKey);
return;
}
IrisBiome caveBiome = caveBiomes.resolve(blockX, y, blockZ);
if (caveBiome != null && caveBiome.getLoadKey() != null) {
semantics.addCaveBiome(caveBiome.getLoadKey());
caveBiomes.add(semantics, caveBiome.getLoadKey());
}
}
@@ -280,6 +280,7 @@ public final class GenerationSemanticCapture {
private final IrisDimensionCarvingResolver.State fallbackState = new IrisDimensionCarvingResolver.State();
private IrisDimensionCarvingResolver.Snapshot snapshot;
private int minimumY;
private String lastKey;
private CaveBiomes(Engine engine, boolean scoped) {
this.engine = engine;
@@ -290,6 +291,13 @@ public final class GenerationSemanticCapture {
: null;
}
private void add(ChunkGenerationSemantics.Builder semantics, String key) {
if (key != lastKey) {
semantics.addCaveBiome(key);
lastKey = key;
}
}
private IrisBiome resolve(int x, int y, int z) {
if (!scoped) {
return engine.getCaveBiome(x, y, z);
@@ -2,6 +2,7 @@ package art.arcane.iris.world.history;
import art.arcane.iris.generation.hydrology.HydrologyFeatureType;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.iris.world.storage.DeferredDurability;
import art.arcane.iris.world.storage.Durability;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
@@ -93,6 +94,7 @@ public final class GenerationSemanticIndex {
private final PointerPublisher pointerPublisher;
private final CatalogPublisher catalogPublisher;
private final boolean unpublished;
private final DeferredDurability durability;
private long regionDecodeCount;
private volatile IOException journalFailure;
private long appendingRegionKey;
@@ -118,6 +120,8 @@ public final class GenerationSemanticIndex {
this.pointerPublisher = pointerPublisher;
this.catalogPublisher = catalogPublisher;
this.unpublished = unpublished;
this.durability = new DeferredDurability(
DeferredDurability.DEFAULT_FLUSH_DELAY_MILLIS, failure -> journalFailure = failure);
}
public static GenerationSemanticIndex load(Path dimensionRoot) throws IOException {
@@ -265,7 +269,7 @@ public final class GenerationSemanticIndex {
appendingBase = baseRegion;
publicationLock.unlock();
try {
SemanticJournal.append(directory, regionX, regionZ, updates, unpublished);
SemanticJournal.append(directory, regionX, regionZ, updates, unpublished, durability);
} catch (JournalAppendFailure failure) {
journalFailure = failure;
throw failure;
@@ -289,6 +293,15 @@ public final class GenerationSemanticIndex {
}
}
public void sync() throws IOException {
try {
durability.sync();
} catch (IOException failure) {
journalFailure = failure;
throw failure;
}
}
public int discardUnstoredClaims(WorldChunkInventory stored, Set<Long> activationIds) throws IOException {
Objects.requireNonNull(stored, "stored chunks");
Set<Long> selected = Set.copyOf(activationIds);
@@ -1062,7 +1075,11 @@ public final class GenerationSemanticIndex {
private void cacheRegion(long regionKey, RegionShard region) {
regions.put(regionKey, region);
cacheSummary(regionKey, region.summary());
if (region.summary == null) {
summaries.remove(regionKey);
} else {
cacheSummary(regionKey, region.summary);
}
while (regions.size() > MAXIMUM_CACHED_REGIONS) {
Iterator<Map.Entry<Long, RegionShard>> entries = regions.entrySet().iterator();
entries.next();
@@ -1940,7 +1957,8 @@ public final class GenerationSemanticIndex {
int regionX,
int regionZ,
List<ChunkGenerationSemantics> claims,
boolean unpublished
boolean unpublished,
DeferredDurability durability
) throws IOException {
Path file = directory.resolve(journalFileName(regionX, regionZ));
boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
@@ -1974,10 +1992,10 @@ public final class GenerationSemanticIndex {
}
}
if (!unpublished) {
Durability.force(channel);
durability.written(file);
}
if (created) {
RegionShard.forceDirectory(directory);
durability.linked(directory);
}
} catch (IOException failure) {
try {
@@ -6,6 +6,7 @@ import art.arcane.iris.generation.terrain.IrisRegion;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -30,6 +31,8 @@ public final class SavedBiomeCapture {
TransitionGenerationPlan transition = engine.getComplex().getTransitionGenerationPlan();
Map<Long, Optional<SavedBiomeChunk>> historical = new HashMap<>();
Map<Integer, List<SavedBiomeChunk.Span>> vertical = new HashMap<>(16);
Cells cells = new Cells(activationId);
Column column = new Column(engine, minimumY, height);
for (int localX = 0; localX < 16; localX++) {
for (int localZ = 0; localZ < 16; localZ++) {
int blockX = startX + localX;
@@ -37,7 +40,7 @@ public final class SavedBiomeCapture {
IrisRegion region = engine.getRegion(blockX, blockZ);
IrisBiome surface = engine.getSurfaceBiome(blockX, blockZ);
int surfaceY = minimumY + engine.getHeight(blockX, blockZ);
SavedBiomeChunk.Cell surfaceCell = cell(activationId, surface, region);
SavedBiomeChunk.Cell surfaceCell = cells.cell(surface, region);
if (floating != null) {
FloatingBiomeOverlay.Identity floatingSurface = floating.surfaceAt(localX, localZ);
if (floatingSurface != null) {
@@ -53,33 +56,33 @@ public final class SavedBiomeCapture {
int quartKey = quartX * 16 + quartZ;
List<SavedBiomeChunk.Span> spans = vertical.get(quartKey);
if (spans == null) {
spans = captureVertical(new Column(engine, activationId, startX + quartX, startZ + quartZ), transition, saved, historical, floating);
spans = captureVertical(column, startX + quartX, startZ + quartZ, cells, transition, saved, historical, floating);
vertical.put(quartKey, spans);
}
SavedBiomeChunk.Cell caveBase = cell(activationId, engine.getCaveBiome(blockX, blockZ), region);
SavedBiomeChunk.Cell caveBase = cells.cell(engine.getCaveBiome(blockX, blockZ), region);
result.column(localX, localZ, new SavedBiomeChunk.Column(surfaceCell, caveBase, spans));
}
}
return result.build();
}
private static List<SavedBiomeChunk.Span> captureVertical(Column column, TransitionGenerationPlan transition,
SavedBiomeRuntime saved, Map<Long, Optional<SavedBiomeChunk>> historical, FloatingBiomeOverlay floating) throws IOException {
Engine engine = column.engine();
int minimumY = engine.getMinHeight();
int maximumY = minimumY + engine.getHeight();
private static List<SavedBiomeChunk.Span> captureVertical(Column column, int blockX, int blockZ, Cells cells,
TransitionGenerationPlan transition, SavedBiomeRuntime saved,
Map<Long, Optional<SavedBiomeChunk>> historical, FloatingBiomeOverlay floating) throws IOException {
column.engine().getBiomeOrMantleColumn(blockX, blockZ, 4, column.biomes(), column.regions());
int minimumY = column.minimumY();
int maximumY = minimumY + column.height();
List<SavedBiomeChunk.Span> spans = new ArrayList<>();
SavedBiomeChunk.Cell previous = null;
int startY = minimumY;
for (int worldY = minimumY; worldY < maximumY; worldY += 4) {
IrisBiome biome = engine.getBiomeOrMantle(column.blockX(), worldY - minimumY, column.blockZ());
IrisRegion region = engine.getRegion(column.blockX(), worldY - minimumY, column.blockZ());
SavedBiomeChunk.Cell current = cell(column.activationId(), biome, region);
for (int index = 0; index < column.biomes().length; index++) {
int worldY = minimumY + index * 4;
SavedBiomeChunk.Cell current = cells.cell(column.biomes()[index], column.regions()[index]);
if (floating != null) {
current = overlay(current, floating.volumeAt(column.blockX() & 15, worldY - minimumY, column.blockZ() & 15));
current = overlay(current, floating.volumeAt(blockX & 15, worldY - minimumY, blockZ & 15));
}
if (transition != null) {
current = historicalCell(new Sample(column.blockX(), worldY, column.blockZ(), false), current, transition, saved, historical);
current = historicalCell(new Sample(blockX, worldY, blockZ, false), current, transition, saved, historical);
}
if (previous != null && !previous.equals(current)) {
spans.add(new SavedBiomeChunk.Span(startY, worldY, previous));
@@ -132,7 +135,40 @@ public final class SavedBiomeCapture {
Objects.requireNonNull(region, "region").getLoadKey());
}
private record Column(Engine engine, long activationId, int blockX, int blockZ) {
private record Column(Engine engine, int minimumY, int height, IrisBiome[] biomes, IrisRegion[] regions) {
private Column(Engine engine, int minimumY, int height) {
this(engine, minimumY, height, new IrisBiome[(height + 3) / 4], new IrisRegion[(height + 3) / 4]);
}
}
private static final class Cells {
private final long activationId;
private IrisBiome[] biomes = new IrisBiome[8];
private IrisRegion[] regions = new IrisRegion[8];
private SavedBiomeChunk.Cell[] cells = new SavedBiomeChunk.Cell[8];
private int size;
private Cells(long activationId) {
this.activationId = activationId;
}
private SavedBiomeChunk.Cell cell(IrisBiome biome, IrisRegion region) {
for (int index = 0; index < size; index++) {
if (biomes[index] == biome && regions[index] == region) {
return cells[index];
}
}
SavedBiomeChunk.Cell created = SavedBiomeCapture.cell(activationId, biome, region);
if (size == cells.length) {
biomes = Arrays.copyOf(biomes, size * 2);
regions = Arrays.copyOf(regions, size * 2);
cells = Arrays.copyOf(cells, size * 2);
}
biomes[size] = biome;
regions[size] = region;
cells[size++] = created;
return created;
}
}
private record Sample(int x, int y, int z, boolean surface) {
@@ -1,5 +1,6 @@
package art.arcane.iris.world.history;
import art.arcane.iris.world.storage.DeferredDurability;
import art.arcane.iris.world.storage.Durability;
import java.io.BufferedInputStream;
@@ -52,6 +53,7 @@ public final class SavedBiomeStore {
private final Path dimensionRoot;
private final Path directory;
private final boolean unpublished;
private final DeferredDurability durability;
private final WriteStripe[] regionLocks = new WriteStripe[64];
private final LinkedHashMap<Long, RegionIndex> regions = new LinkedHashMap<>(64, 0.75F, true);
private final LinkedHashMap<Long, Optional<SavedBiomeChunk>> chunks = new LinkedHashMap<>(128, 0.75F, true);
@@ -61,6 +63,8 @@ public final class SavedBiomeStore {
private SavedBiomeStore(Path dimensionRoot, boolean unpublished) throws IOException {
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot").toAbsolutePath().normalize();
this.unpublished = unpublished;
this.durability = new DeferredDurability(
DeferredDurability.DEFAULT_FLUSH_DELAY_MILLIS, failure -> writeFailure = failure);
directory = this.dimensionRoot.resolve("iris/generation/biomes");
requireSafeAncestors();
for (int index = 0; index < regionLocks.length; index++) {
@@ -227,7 +231,7 @@ public final class SavedBiomeStore {
}
}
if (!unpublished) {
Durability.force(output.getChannel());
durability.written(region.path);
}
appendCompleted = true;
} catch (IOException failure) {
@@ -269,6 +273,15 @@ public final class SavedBiomeStore {
}
}
public void sync() throws IOException {
try {
durability.sync();
} catch (IOException failure) {
writeFailure = failure;
throw failure;
}
}
public int discardUnstoredClaims(WorldChunkInventory inventory, Set<Long> selectedActivations) throws IOException {
Objects.requireNonNull(inventory, "inventory");
Set<Long> selected = Set.copyOf(selectedActivations);
@@ -4,9 +4,7 @@ import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.OptionalInt;
@@ -65,14 +63,15 @@ public final class SavedTerrainChunk {
}
Math.addExact(minimumY, height);
List<TerrainBoundarySignature> columns = new ArrayList<>(256);
BoundaryColumnGeometry.Runs runs = new BoundaryColumnGeometry.Runs(height);
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
if (boundaryOnly && x != 0 && x != 15 && z != 0 && z != 15) {
columns.add(null);
continue;
}
List<BoundaryColumnGeometry.Voxel> voxels = new ArrayList<>(height);
Map<String, Short> biomePalette = new LinkedHashMap<>();
runs.clear();
List<String> biomePalette = new ArrayList<>(4);
short[] biomeIndices = new short[height / 4];
int surface = 0;
int floor = 0;
@@ -80,7 +79,7 @@ public final class SavedTerrainChunk {
for (int offset = 0; offset < height; offset++) {
int y = minimumY + offset;
BoundaryColumnGeometry.Voxel voxel = Objects.requireNonNull(source.voxel(x, y, z), "voxel");
voxels.add(voxel);
runs.add(voxel);
if (voxel.phase() != BoundaryColumnGeometry.Phase.AIR) {
surface = offset;
}
@@ -92,15 +91,10 @@ public final class SavedTerrainChunk {
}
if (offset % 4 == 0) {
String biome = Objects.requireNonNull(source.biome(x, y, z), "physical biome");
Short index = biomePalette.get(biome);
if (index == null) {
index = (short) biomePalette.size();
biomePalette.put(biome, index);
}
biomeIndices[offset / 4] = index;
biomeIndices[offset / 4] = paletteIndex(biomePalette, biome);
}
}
BoundaryColumnGeometry geometry = BoundaryColumnGeometry.fromVoxels(minimumY, voxels);
BoundaryColumnGeometry geometry = runs.build(minimumY);
OptionalInt groundSurface = source.groundSurface(x, z);
if (groundSurface.isPresent()) {
floor = geometry.surfaceOffsetNear(groundSurface.getAsInt());
@@ -115,13 +109,24 @@ public final class SavedTerrainChunk {
fluid > floor ? OptionalInt.of(fluid) : OptionalInt.empty(), OptionalInt.empty()),
new TerrainBoundarySignature.Samples(
new TerrainBoundarySignature.VerticalLayout(minimumY, 4, biomeIndices.length),
new TerrainBoundarySignature.BiomeEncoding(List.copyOf(biomePalette.keySet()), biomeIndices)),
new TerrainBoundarySignature.BiomeEncoding(biomePalette, biomeIndices)),
geometry));
}
}
return new SavedTerrainChunk(chunkX, chunkZ, nativeStatus, columns);
}
private static short paletteIndex(List<String> palette, String biome) {
for (int index = 0; index < palette.size(); index++) {
String candidate = palette.get(index);
if (candidate == biome || candidate.equals(biome)) {
return (short) index;
}
}
palette.add(biome);
return (short) (palette.size() - 1);
}
public SavedTerrainChunk boundaryOnly() {
if (!hasColumn(1, 1)) {
return this;
@@ -72,7 +72,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private static final long MANTLE_CLEANUP_DRAIN_SECONDS = 60L;
private static final long CHUNK_FLUSH_DRAIN_SECONDS = 30L;
private static final long ADMISSION_WAIT_BOUND_MS = 500L;
private static final int HYDROLOGY_PREFETCH_HALO_BLOCKS = 512;
private static final long RETAINED_CHUNK_BYTES = 1024L * 1024L;
private final World world;
private final IrisRuntimeSchedulerMode runtimeSchedulerMode;
private final IrisPaperLikeBackendMode paperLikeBackendMode;
@@ -122,6 +122,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private volatile Engine metricsEngine;
private volatile Mantle cachedMantle;
private volatile HydrologyTileCache.PregenerationScope hydrologyPrefetchScope;
private volatile PregenChunkRetention retention;
private final PregenMantleBackpressure backpressure;
public AsyncPregenMethod(World world, int unusedThreads) {
@@ -247,6 +248,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
if (pending != null && pending.decrementAndGet() == 0) {
onRegionDrained(rk);
} else if (MantleHeapPressure.overHighWater()) {
releaseRetention();
evictRegion(rk);
}
} catch (Throwable e) {
@@ -278,6 +280,66 @@ public class AsyncPregenMethod implements PregeneratorMethod {
boundsMinRegionZ = minRegionZ;
boundsMaxRegionX = maxRegionX;
boundsMaxRegionZ = maxRegionZ;
retention = createRetention(minRegionX, minRegionZ, maxRegionX, maxRegionZ);
}
private PregenChunkRetention createRetention(int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
if (foliaRuntime) {
return null;
}
try {
INMSBinding binding = INMS.get();
int radius = binding.fullChunkDependencyRadius();
if (radius < 0 || radius > 31) {
return null;
}
PregenChunkRetention.Tickets tickets = new PregenChunkRetention.Tickets() {
@Override
public void retain(int chunkX, int chunkZ) {
binding.retainChunk(world, chunkX, chunkZ);
}
@Override
public void release(int chunkX, int chunkZ) {
binding.releaseChunk(world, chunkX, chunkZ);
}
};
return new PregenChunkRetention(tickets, radius, retentionCapacity(Runtime.getRuntime().maxMemory()),
minRegionX, minRegionZ, maxRegionX, maxRegionZ);
} catch (Throwable e) {
PregenDiagnostics.probeFailed("pregen chunk retention for world " + world.getName(), e);
return null;
}
}
static int retentionCapacity(long maximumHeapBytes) {
return (int) Math.min(Integer.MAX_VALUE, Math.max(0L, maximumHeapBytes) / RETAINED_CHUNK_BYTES);
}
private void retainAround(int x, int z) {
PregenChunkRetention active = retention;
if (active == null) {
return;
}
try {
active.retainAround(x, z);
} catch (Throwable e) {
retention = null;
IrisLogging.reportError("Async pregen chunk retention failed at " + x + "," + z + " in world " + world.getName()
+ "; neighbouring regions reload their borders from disk from now on.", e);
try {
active.releaseAll();
} catch (Throwable releaseFailure) {
IrisLogging.reportError(releaseFailure);
}
}
}
private void releaseRetention() {
PregenChunkRetention active = retention;
if (active != null) {
active.releaseAll();
}
}
@Override
@@ -288,10 +350,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
HydrologyTileCache.PregenerationArea area = new HydrologyTileCache.PregenerationArea(
centerBlockX, centerBlockZ,
(long) boundsMinRegionX * 512L - HYDROLOGY_PREFETCH_HALO_BLOCKS,
(long) boundsMinRegionZ * 512L - HYDROLOGY_PREFETCH_HALO_BLOCKS,
((long) boundsMaxRegionX + 1L) * 512L - 1L + HYDROLOGY_PREFETCH_HALO_BLOCKS,
((long) boundsMaxRegionZ + 1L) * 512L - 1L + HYDROLOGY_PREFETCH_HALO_BLOCKS);
(long) boundsMinRegionX * 512L,
(long) boundsMinRegionZ * 512L,
((long) boundsMaxRegionX + 1L) * 512L - 1L,
((long) boundsMaxRegionZ + 1L) * 512L - 1L);
HydrologyTileCache.PregenerationScope scope = engine.getComplex().getHydrologyRuntime().preparePregeneration(area);
hydrologyPrefetchScope = scope;
if (closing.get()) {
@@ -315,6 +377,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
private void onRegionDrained(long rk) {
regionPending.remove(rk);
PregenChunkRetention active = retention;
if (active != null) {
active.drained((int) (rk >> 32), (int) rk);
}
evictRegion(rk);
}
@@ -846,8 +912,10 @@ public class AsyncPregenMethod implements PregeneratorMethod {
if (!drain.drained()) {
abandonOutstandingRequests(drain);
}
syncGenerationHistory();
mantleCleanup.close(MANTLE_CLEANUP_DRAIN_SECONDS, TimeUnit.SECONDS);
releaseRetention();
flushAllRemainingChunks();
chunkFlush.close(CHUNK_FLUSH_DRAIN_SECONDS, TimeUnit.SECONDS);
executor.shutdown();
@@ -874,6 +942,19 @@ public class AsyncPregenMethod implements PregeneratorMethod {
}
}
private void syncGenerationHistory() {
Engine engine = resolveMetricsEngine();
if (engine == null) {
return;
}
try {
engine.syncGenerationHistory();
} catch (Throwable e) {
IrisLogging.reportError("Async pregen could not force generation history to stable storage for world "
+ world.getName() + ".", e);
}
}
private boolean isCancelled() {
return closing.get() || generationFailure.get() != null || Thread.currentThread().isInterrupted();
}
@@ -884,6 +965,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
@Override
public void reclaimMemory() {
releaseRetention();
if (regionChunks.isEmpty() && pendingEvictions.isEmpty()) {
return;
}
@@ -935,6 +1017,7 @@ public class AsyncPregenMethod implements PregeneratorMethod {
throwIfGenerationFailed();
regionPending.computeIfAbsent(rkey(x >> 5, z >> 5), k -> new AtomicInteger(1)).incrementAndGet();
retainAround(x, z);
markSubmitted(x, z, listener);
executor.generate(x, z, listener);
GenerationFailure failure = generationFailure.get();
@@ -112,6 +112,19 @@ public class MedievalPregenMethod implements PregeneratorMethod {
return cached;
}
private void syncGenerationHistory() {
Engine engine = resolveEngine();
if (engine == null) {
return;
}
try {
engine.syncGenerationHistory();
} catch (Throwable e) {
IrisLogging.reportError("Medieval pregen could not force generation history to stable storage for world "
+ world.getName() + ".", e);
}
}
private void waitForChunks() {
for (CompletableFuture<?> i : futures) {
try {
@@ -179,6 +192,7 @@ public class MedievalPregenMethod implements PregeneratorMethod {
if (prefetchPool != null) {
prefetchPool.shutdownNow();
}
syncGenerationHistory();
unloadAndSaveAllChunks(true);
chunkFlush.close(UNLOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
} finally {
@@ -0,0 +1,193 @@
/*
* Iris is a World Generator for Minecraft Bukkit Servers
* Copyright (c) 2022 Arcane Arts (Volmit Software)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package art.arcane.iris.world.pregen;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Keeps the chunks a pregen FULL request loads around itself resident until no later request of the
* pregen area can need them. Without it the server unloads a chunk as soon as the in-flight requests
* around it move on, and the next request that reaches it (the next lattice pass over the region, or the
* neighbouring region's border) deserializes it from disk again.
*
* <p>A request needs every chunk within the dependency radius, so a retained chunk is needed by exactly
* the in-area regions its own radius square overlaps. It is released once all of them have drained: a
* drained region keeps only the border strips its undrained neighbours still reach. Retained positions
* are capped; over the cap the strips of the drained regions farthest from the active region are released
* first (they are needed last), and a released strip is simply reloaded by the request that reaches it, as
* the pregen did before retention.
*/
final class PregenChunkRetention {
private static final int REGION_WORDS = 16;
private final Tickets tickets;
private final int radius;
private final int capacity;
private final Map<Long, long[]> held = new HashMap<>();
private final Set<Long> drained = new HashSet<>();
private final int minRegionX;
private final int minRegionZ;
private final int maxRegionX;
private final int maxRegionZ;
private int size;
PregenChunkRetention(Tickets tickets, int radius, int capacity,
int minRegionX, int minRegionZ, int maxRegionX, int maxRegionZ) {
if (radius < 0 || radius > 31) {
throw new IllegalArgumentException("Retention radius must fit inside one region: " + radius);
}
this.tickets = tickets;
this.radius = radius;
this.capacity = Math.max(1, capacity);
this.minRegionX = minRegionX;
this.minRegionZ = minRegionZ;
this.maxRegionX = maxRegionX;
this.maxRegionZ = maxRegionZ;
}
static long regionKey(int regionX, int regionZ) {
return (((long) regionX) << 32) | (regionZ & 0xFFFFFFFFL);
}
/**
* Retains the dependency square of a FULL request at the chunk. The request loads every position in it
* anyway, so retaining loads nothing.
*/
synchronized void retainAround(int chunkX, int chunkZ) {
for (int x = chunkX - radius; x <= chunkX + radius; x++) {
long[] bits = null;
long bitsKey = 0L;
for (int z = chunkZ - radius; z <= chunkZ + radius; z++) {
long key = regionKey(x >> 5, z >> 5);
if (bits == null || key != bitsKey) {
bitsKey = key;
bits = held.computeIfAbsent(key, ignored -> new long[REGION_WORDS]);
}
int index = ((z & 31) << 5) | (x & 31);
long mask = 1L << (index & 63);
if ((bits[index >> 6] & mask) != 0L) {
continue;
}
bits[index >> 6] |= mask;
size++;
tickets.retain(x, z);
}
}
if (size > capacity) {
releaseFarthestDrained(chunkX >> 5, chunkZ >> 5);
}
}
/**
* Records a drained region and releases every retained position no undrained in-area region reaches.
*/
synchronized void drained(int regionX, int regionZ) {
drained.add(regionKey(regionX, regionZ));
for (int x = regionX - 1; x <= regionX + 1; x++) {
for (int z = regionZ - 1; z <= regionZ + 1; z++) {
long key = regionKey(x, z);
long[] bits = held.get(key);
if (bits != null && release(key, bits, true)) {
held.remove(key);
}
}
}
}
synchronized void releaseAll() {
for (Map.Entry<Long, long[]> entry : held.entrySet()) {
release(entry.getKey(), entry.getValue(), false);
}
held.clear();
}
synchronized int size() {
return size;
}
private void releaseFarthestDrained(int regionX, int regionZ) {
List<Long> candidates = new ArrayList<>();
for (Long key : held.keySet()) {
if (drained.contains(key)) {
candidates.add(key);
}
}
candidates.sort(Comparator.comparingLong((Long key) -> {
long dx = (int) (key >> 32) - regionX;
long dz = key.intValue() - regionZ;
return dx * dx + dz * dz;
}).reversed());
int target = capacity - capacity / 8;
for (Long key : candidates) {
if (size <= target) {
return;
}
release(key, held.remove(key), false);
}
}
private boolean release(long key, long[] bits, boolean onlyUnreachable) {
int baseX = ((int) (key >> 32)) << 5;
int baseZ = ((int) key) << 5;
boolean empty = true;
for (int word = 0; word < REGION_WORDS; word++) {
long remaining = bits[word];
while (remaining != 0L) {
int bit = Long.numberOfTrailingZeros(remaining);
remaining &= remaining - 1L;
int index = (word << 6) | bit;
int x = baseX + (index & 31);
int z = baseZ + (index >> 5);
if (onlyUnreachable && reachedByUndrainedRegion(x, z)) {
empty = false;
continue;
}
bits[word] &= ~(1L << bit);
size--;
tickets.release(x, z);
}
}
return empty;
}
private boolean reachedByUndrainedRegion(int chunkX, int chunkZ) {
for (int regionX = (chunkX - radius) >> 5; regionX <= (chunkX + radius) >> 5; regionX++) {
for (int regionZ = (chunkZ - radius) >> 5; regionZ <= (chunkZ + radius) >> 5; regionZ++) {
if (regionX >= minRegionX && regionX <= maxRegionX && regionZ >= minRegionZ && regionZ <= maxRegionZ
&& !drained.contains(regionKey(regionX, regionZ))) {
return true;
}
}
}
return false;
}
interface Tickets {
void retain(int chunkX, int chunkZ);
void release(int chunkX, int chunkZ);
}
}
@@ -18,16 +18,24 @@
package art.arcane.iris.world.pregen;
import art.arcane.iris.generation.concurrent.MultiBurst;
import art.arcane.iris.spi.IrisLogging;
import art.arcane.volmlib.util.mantle.runtime.Mantle;
import art.arcane.volmlib.util.math.M;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
/**
* Keeps the resident tectonic plate count near its cap during pregeneration. Over the cap, the least recently
* used idle plates are saved and unloaded one at a time on the eviction executor while submissions continue;
* a submission only waits once residency passes the cap by the eviction overshoot.
*/
public final class PregenMantleBackpressure {
private static final long LOG_INTERVAL_MS = 5_000L;
@@ -38,6 +46,9 @@ public final class PregenMantleBackpressure {
private final Runnable onBudgetTimeout;
private final Supplier<String> diagnostics;
private final BooleanSupplier cancelled;
private final Executor evictionExecutor;
private final AtomicBoolean evicting = new AtomicBoolean();
private final AtomicBoolean evictionFailed = new AtomicBoolean();
private final ReentrantLock lock = new ReentrantLock();
private final Condition progressed = lock.newCondition();
@@ -46,6 +57,12 @@ public final class PregenMantleBackpressure {
}
public PregenMantleBackpressure(Supplier<Mantle> mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier<String> diagnostics, BooleanSupplier cancelled) {
this(mantleSupplier, maxResidentTectonicPlates, waitMs, timeoutMs, onBudgetTimeout, diagnostics, cancelled,
command -> MultiBurst.ioBurst.execute(command));
}
public PregenMantleBackpressure(Supplier<Mantle> mantleSupplier, int maxResidentTectonicPlates, int waitMs, long timeoutMs, Runnable onBudgetTimeout, Supplier<String> diagnostics, BooleanSupplier cancelled, Executor evictionExecutor) {
this.evictionExecutor = evictionExecutor;
this.mantleSupplier = mantleSupplier;
this.maxResidentTectonicPlates = maxResidentTectonicPlates;
this.waitMs = Math.max(1, waitMs);
@@ -75,33 +92,38 @@ public final class PregenMantleBackpressure {
return;
}
evictionFailed.set(false);
requestEviction(mantle, cap);
int ceiling = cap + evictionOvershoot(cap);
if (mantle.getLoadedRegionCount() <= ceiling || evictionFailed.getAndSet(false)) {
return;
}
long waitStart = M.ms();
long lastLog = 0L;
while (mantle.getLoadedRegionCount() > cap) {
int resident = mantle.getLoadedRegionCount();
while (resident > ceiling) {
if (isCancelled()) {
return;
}
int freed;
int resident;
int before = resident;
if (!awaitProgress() || evictionFailed.getAndSet(false)) {
return;
}
try {
mantle.trim(0L);
freed = mantle.unloadTectonicPlate(0);
requestEviction(mantle, cap);
resident = mantle.getLoadedRegionCount();
if (resident > cap && mantle.saveOldestIdleTectonicPlate()) {
freed++;
resident = mantle.getLoadedRegionCount();
}
} catch (Throwable e) {
IrisLogging.reportError(e);
break;
return;
}
if (resident <= cap) {
break;
if (resident <= ceiling) {
return;
}
long elapsed = M.ms() - waitStart;
if (elapsed >= timeoutMs && freed == 0) {
if (elapsed >= timeoutMs && resident >= before) {
IrisLogging.warn("Pregen mantle backpressure exceeded " + timeoutMs + "ms with " + resident
+ " tectonic plates resident (residency target " + cap + "); no idle plate could be evicted. "
+ "Allowing the active dependency working set to exceed the residency target; "
@@ -116,12 +138,42 @@ public final class PregenMantleBackpressure {
// Pausing to stay under the plate cap is the design working, and it is reported every five
// seconds for the whole duration of a large pregen. Only exceeding the budget is a warning.
IrisLogging.info("Pregen mantle backpressure: " + resident + " tectonic plates resident (residency target " + cap
+ "), freed " + freed + " last pass, waited " + elapsed + "ms.");
+ "), waited " + elapsed + "ms.");
}
}
}
if (!awaitProgress()) {
return;
static int evictionOvershoot(int cap) {
return Math.max(2, cap / 8);
}
private void requestEviction(Mantle mantle, int cap) {
if (!evicting.compareAndSet(false, true)) {
return;
}
try {
evictionExecutor.execute(() -> evictToCap(mantle, cap));
} catch (Throwable e) {
evicting.set(false);
IrisLogging.reportError(e);
}
}
private void evictToCap(Mantle mantle, int cap) {
try {
while (!mantle.isClosed() && !isCancelled() && mantle.getLoadedRegionCount() > cap
&& mantle.saveOldestIdleTectonicPlate()) {
signalProgress();
}
} catch (Throwable e) {
if (!mantle.isClosed()) {
evictionFailed.set(true);
IrisLogging.reportError(e);
}
} finally {
evicting.set(false);
signalProgress();
}
}
@@ -0,0 +1,138 @@
package art.arcane.iris.world.storage;
import java.io.File;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
/**
* Group commit for append-only stores: writers record which files and directory entries they
* changed, and one background pass forces them shortly afterwards, outside every store lock.
* {@link #sync()} is the durability barrier for saves, shutdown and pregeneration completion.
*/
public final class DeferredDurability {
public static final long DEFAULT_FLUSH_DELAY_MILLIS = 2_000L;
private static final ScheduledThreadPoolExecutor FLUSHER = createFlusher();
private final Set<Path> files = ConcurrentHashMap.newKeySet();
private final Set<Path> directories = ConcurrentHashMap.newKeySet();
private final ReentrantLock flushLock = new ReentrantLock();
private final AtomicBoolean scheduled = new AtomicBoolean();
private final long flushDelayMillis;
private final Consumer<IOException> backgroundFailure;
public DeferredDurability(long flushDelayMillis, Consumer<IOException> backgroundFailure) {
if (flushDelayMillis < 0L) {
throw new IllegalArgumentException("Flush delay must not be negative.");
}
this.flushDelayMillis = flushDelayMillis;
this.backgroundFailure = Objects.requireNonNull(backgroundFailure, "backgroundFailure");
}
public void written(Path file) {
if (!Durability.enabled()) {
return;
}
files.add(file);
schedule();
}
public void linked(Path directory) {
if (!Durability.enabled() || File.separatorChar == '\\') {
return;
}
directories.add(directory);
schedule();
}
public void sync() throws IOException {
flushLock.lock();
try {
IOException failure = null;
for (Path file : files) {
files.remove(file);
try {
forceFile(file);
} catch (IOException fileFailure) {
failure = append(failure, fileFailure);
}
}
for (Path directory : directories) {
directories.remove(directory);
try {
forceDirectory(directory);
} catch (IOException directoryFailure) {
failure = append(failure, directoryFailure);
}
}
if (failure != null) {
throw failure;
}
} finally {
flushLock.unlock();
}
}
private void schedule() {
if (scheduled.compareAndSet(false, true)) {
FLUSHER.schedule(this::flushInBackground, flushDelayMillis, TimeUnit.MILLISECONDS);
}
}
private void flushInBackground() {
scheduled.set(false);
try {
sync();
} catch (IOException failure) {
backgroundFailure.accept(failure);
} catch (RuntimeException failure) {
backgroundFailure.accept(new IOException("Deferred durability sync failed unexpectedly.", failure));
}
}
private static void forceFile(Path file) throws IOException {
try (FileChannel channel = FileChannel.open(file, StandardOpenOption.WRITE)) {
Durability.force(channel);
} catch (NoSuchFileException replaced) {
// Compaction or rewrite already published this file's contents durably under another name.
}
}
private static void forceDirectory(Path directory) throws IOException {
try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
Durability.force(channel);
} catch (UnsupportedOperationException failure) {
throw new IOException("Directory cannot be durability-synced: " + directory, failure);
}
}
private static IOException append(IOException failure, IOException additional) {
if (failure == null) {
return additional;
}
failure.addSuppressed(additional);
return failure;
}
private static ScheduledThreadPoolExecutor createFlusher() {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1, runnable -> {
Thread thread = new Thread(runnable, "Iris Durability Flush");
thread.setDaemon(true);
return thread;
});
executor.setKeepAliveTime(30L, TimeUnit.SECONDS);
executor.allowCoreThreadTimeOut(true);
executor.setRemoveOnCancelPolicy(true);
return executor;
}
}
@@ -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
@@ -0,0 +1,84 @@
package art.arcane.iris.generation.cache;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ConcurrentClockCacheTest {
@Test
public void firstPublishedValueWins() {
ConcurrentClockCache<Object> cache = new ConcurrentClockCache<>(16);
Object first = new Object();
assertNull(cache.get(7L));
assertSame(first, cache.putIfAbsent(7L, first));
assertSame(first, cache.putIfAbsent(7L, new Object()));
assertSame(first, cache.get(7L));
cache.clear();
assertNull(cache.get(7L));
}
@Test
public void capacityBoundsRetainedEntries() {
ConcurrentClockCache<Long> cache = new ConcurrentClockCache<>(64);
for (long key = 0; key < 10_000; key++) {
assertEquals(Long.valueOf(key), cache.putIfAbsent(key, key));
}
AtomicInteger retained = new AtomicInteger();
cache.forEach(value -> {
retained.incrementAndGet();
assertEquals(value, cache.get(value));
});
assertEquals(cache.capacity(), retained.get());
assertTrue(cache.capacity() >= 64);
}
@Test
public void referencedEntriesSurviveAStreamOfOneTimeKeys() {
ConcurrentClockCache<Long> cache = new ConcurrentClockCache<>(4);
cache.putIfAbsent(-1L, -1L);
for (long key = 0; key < 1_000; key++) {
assertEquals(Long.valueOf(-1L), cache.get(-1L));
cache.putIfAbsent(key, key);
}
assertEquals(Long.valueOf(-1L), cache.get(-1L));
}
@Test
public void concurrentWritersAgreeOnOneValuePerKey() throws Exception {
ConcurrentClockCache<Object> cache = new ConcurrentClockCache<>(1 << 12);
List<Future<Object[]>> results = new ArrayList<>();
try (ExecutorService executor = Executors.newFixedThreadPool(8)) {
for (int thread = 0; thread < 8; thread++) {
results.add(executor.submit(() -> {
Object[] seen = new Object[256];
for (int key = 0; key < seen.length; key++) {
seen[key] = cache.putIfAbsent(key, new Object());
}
return seen;
}));
}
Object[] reference = results.getFirst().get(10, TimeUnit.SECONDS);
for (Future<Object[]> result : results) {
Object[] seen = result.get(10, TimeUnit.SECONDS);
for (int key = 0; key < seen.length; key++) {
Object cached = cache.get(key);
if (cached != null) {
assertSame(cached, seen[key]);
assertSame(reference[key], seen[key]);
}
}
}
}
}
}
@@ -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<NativeBlockState> 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<Thread> 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);
}
}
@@ -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);
Loaded 100 of 180 files, more files were not shown because too many files have changed in this diff. Show more