mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-23 08:30:34 +00:00
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:
commit
85849693b6
180 files changed
+9102
-1964
No files matched your search
+84
@@ -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());
|
||||
}
|
||||
}
|
||||
+84
@@ -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);
|
||||
|
||||
+5
@@ -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;
|
||||
}
|
||||
|
||||
+1
-1
@@ -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));
|
||||
|
||||
+1
-2
@@ -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))
|
||||
|
||||
+1
-5
@@ -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;
|
||||
}
|
||||
|
||||
+128
@@ -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;
|
||||
|
||||
+117
-11
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+5
@@ -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) {
|
||||
|
||||
+1
-1
@@ -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)) {
|
||||
|
||||
+22
-3
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-55
@@ -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) {
|
||||
}
|
||||
|
||||
|
||||
+17
-2
@@ -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++) {
|
||||
|
||||
-2
@@ -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) {
|
||||
|
||||
+310
-264
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-2
@@ -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() {
|
||||
|
||||
+35
-9
@@ -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 {
|
||||
|
||||
+4
-4
@@ -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) {
|
||||
}
|
||||
}
|
||||
+26
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
+5
-7
@@ -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
|
||||
|
||||
+84
@@ -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
Reference in new issue
Block a user