Improved world generation

- Terrain and cave scans skip repeated work
- River and structure lookups reuse cached results
- Chunk history saves share disk writes
- History lookups continue during disk writes
- Failed saves retain their original errors
- Deposit failures stop incomplete chunks
- Tests check output and save safety
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-07 03:17:09 -04:00
1 parent 151677f92f
commit 0dcd1900be
41 files changed
+4398 -191

No files matched your search

@@ -131,6 +131,26 @@ public class IrisEngineMantle implements EngineMantle {
return getComponents().getLast().passChunkRadius();
}
@Override
public void cleanupChunksCoveredBy(int newRealX, int newRealZ, boolean force, ChunkCleanupCallback callback) {
Objects.requireNonNull(callback, "callback");
int radius = Math.max(getRadius(), getRealRadius());
MissingRealChunk missingChunk = radius > 0 ? new MissingRealChunk() : null;
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
int candidateX = newRealX + offsetX;
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
int candidateZ = newRealZ + offsetZ;
if (getMantle().hasFlag(candidateX, candidateZ, MantleFlag.CLEANED)
|| !isCovered(candidateX, candidateZ, missingChunk)) {
continue;
}
if (cleanupCoveredChunk(candidateX, candidateZ, force)) {
callback.onChunkCleaned(candidateX, candidateZ);
}
}
}
}
@Override
public List<MantlePass> getComponents() {
return componentsCache.aquire(() -> {
@@ -228,6 +248,33 @@ public class IrisEngineMantle implements EngineMantle {
return Objects.requireNonNull(storageDirectory, "mantle storage directory").toAbsolutePath().normalize();
}
private boolean isCovered(int x, int z, MissingRealChunk missingChunk) {
int radius = Math.max(getRadius(), getRealRadius());
if (missingChunk != null && missingChunk.known
&& Math.abs((long) missingChunk.x - x) <= radius
&& Math.abs((long) missingChunk.z - z) <= radius) {
if (!getMantle().hasFlag(missingChunk.x, missingChunk.z, MantleFlag.REAL)) {
return false;
}
missingChunk.known = false;
}
for (int offsetX = -radius; offsetX <= radius; offsetX++) {
for (int offsetZ = -radius; offsetZ <= radius; offsetZ++) {
int chunkX = x + offsetX;
int chunkZ = z + offsetZ;
if (!getMantle().hasFlag(chunkX, chunkZ, MantleFlag.REAL)) {
if (missingChunk != null) {
missingChunk.x = chunkX;
missingChunk.z = chunkZ;
missingChunk.known = true;
}
return false;
}
}
}
return true;
}
private static Mantle<Matter> createMantle(
Engine engine,
Path storageDirectory,
@@ -467,4 +514,10 @@ public class IrisEngineMantle implements EngineMantle {
}
};
}
private static final class MissingRealChunk {
private int x;
private int z;
private boolean known;
}
}
@@ -5,6 +5,7 @@ import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Comparator;
import java.util.Objects;
import java.util.Set;
@@ -361,11 +362,11 @@ public final class ChunkGenerationSemantics {
this.chunkX = chunkX;
this.chunkZ = chunkZ;
this.activationId = activationId;
surfaceBiomeKeys = new TreeSet<>();
caveBiomeKeys = new TreeSet<>();
regionKeys = new TreeSet<>();
riverProfileKeys = new TreeSet<>();
objectKeys = new TreeSet<>();
surfaceBiomeKeys = new HashSet<>();
caveBiomeKeys = new HashSet<>();
regionKeys = new HashSet<>();
riverProfileKeys = new HashSet<>();
objectKeys = new HashSet<>();
riverFeatures = new TreeSet<>(RIVER_FEATURE_COMPARATOR);
structures = new TreeSet<>(STRUCTURE_COMPARATOR);
pointsOfInterest = new TreeSet<>(POI_COMPARATOR);
@@ -519,8 +520,11 @@ public final class ChunkGenerationSemantics {
}
private static void addKey(Set<String> target, String key, String kind) {
if (target.contains(Objects.requireNonNull(key, "key"))) {
return;
}
String validated = requireResourceKey(key);
if (target.size() >= MAX_KEYS_PER_KIND && !target.contains(validated)) {
if (target.size() >= MAX_KEYS_PER_KIND) {
throw new IllegalArgumentException(
"A chunk cannot contain more than " + MAX_KEYS_PER_KIND + " " + kind + " keys"
);
@@ -15,6 +15,7 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
@@ -23,11 +24,14 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.stream.Stream;
public final class GenerationHistory {
private static final int MAXIMUM_CACHED_BOUNDARIES = 4;
private static final int MAXIMUM_CACHED_TERRAIN_SIGNATURES = 4;
private static final int MAXIMUM_SEMANTIC_BATCH = 32;
private static final int MAXIMUM_PENDING_SEMANTIC_CLAIMS = 128;
private final GenerationHistoryPaths paths;
private final GenerationPackRepository packs;
@@ -41,6 +45,8 @@ public final class GenerationHistory {
private final GenerationAdmission admission;
private final Map<Long, GenerationBoundary> boundaryCache;
private final Map<Long, TerrainBoundarySignatureStore.Snapshot> terrainSignatureCache;
private final ArrayBlockingQueue<PendingSemanticClaim> pendingSemanticClaims =
new ArrayBlockingQueue<>(MAXIMUM_PENDING_SEMANTIC_CLAIMS);
private GenerationHistory(
GenerationHistoryPaths paths,
@@ -594,12 +600,64 @@ public final class GenerationHistory {
}
}
public synchronized boolean claimGeneratedSemantics(
public boolean claimGeneratedSemantics(
GenerationStage stage,
ChunkGenerationSemantics update
) throws IOException {
GenerationStage requiredStage = Objects.requireNonNull(stage, "generation stage");
ChunkGenerationSemantics requiredUpdate = Objects.requireNonNull(update, "update");
PendingSemanticClaim pending = new PendingSemanticClaim(
Objects.requireNonNull(stage, "generation stage"),
Objects.requireNonNull(update, "update"));
while (!pendingSemanticClaims.offer(pending)) {
synchronized (this) {
persistPendingSemanticClaims();
}
}
synchronized (this) {
while (!pending.claim.completed) {
persistPendingSemanticClaims();
}
return pending.claim.result();
}
}
private void persistPendingSemanticClaims() {
List<PendingSemanticClaim> batch = new ArrayList<>(MAXIMUM_SEMANTIC_BATCH);
List<GenerationSemanticIndex.Claim> claims = new ArrayList<>(MAXIMUM_SEMANTIC_BATCH);
PendingSemanticClaim current = null;
try {
for (int count = 0; count < MAXIMUM_SEMANTIC_BATCH; count++) {
current = pendingSemanticClaims.poll();
if (current == null) {
break;
}
batch.add(current);
try {
validateSemanticClaim(current.stage, current.claim.update);
claims.add(current.claim);
} catch (RuntimeException failure) {
current.claim.fail(failure);
}
current = null;
}
semantics.claimAndPersistBatch(claims);
} catch (RuntimeException | Error failure) {
boolean reported = false;
if (current != null) {
reported = current.claim.fail(failure);
}
for (PendingSemanticClaim pending : batch) {
reported |= pending.claim.fail(failure);
}
while ((current = pendingSemanticClaims.poll()) != null) {
reported |= current.claim.fail(failure);
}
if (!reported) {
throw failure;
}
}
}
private void validateSemanticClaim(GenerationStage requiredStage, ChunkGenerationSemantics requiredUpdate) {
requiredStage.requireOpen(this);
if (!requiredUpdate.sealed()) {
throw new IllegalArgumentException("Generated semantics must be sealed.");
@@ -611,14 +669,13 @@ public final class GenerationHistory {
if (requiredUpdate.activationId() != requiredStage.activation().activationId()) {
throw new IllegalArgumentException("Generated semantics activation does not match the open stage.");
}
return semantics.claimAndPersist(requiredUpdate);
}
public synchronized void forEachRecordedSemantic(GenerationSemanticIndex.RecordConsumer consumer) throws IOException {
semantics.forEachRecord(consumer);
}
public synchronized Optional<ChunkGenerationSemantics> semantics(int chunkX, int chunkZ) {
public Optional<ChunkGenerationSemantics> semantics(int chunkX, int chunkZ) {
return semantics.get(chunkX, chunkZ);
}
@@ -663,7 +720,7 @@ public final class GenerationHistory {
}
}
public synchronized GenerationActivation resolveActivation(int chunkX, int chunkZ) {
public GenerationActivation resolveActivation(int chunkX, int chunkZ) {
long activationId = ownership.resolve(
chunkX,
chunkZ,
@@ -1108,6 +1165,16 @@ public final class GenerationHistory {
};
}
private static final class PendingSemanticClaim {
private final GenerationStage stage;
private final GenerationSemanticIndex.Claim claim;
private PendingSemanticClaim(GenerationStage stage, ChunkGenerationSemantics update) {
this.stage = stage;
this.claim = new GenerationSemanticIndex.Claim(update);
}
}
public static final class GenerationStage implements AutoCloseable {
private final GenerationHistory owner;
private final int chunkX;
@@ -17,9 +17,8 @@ import art.arcane.volmlib.util.matter.Matter;
import art.arcane.volmlib.util.matter.MatterCavern;
import art.arcane.volmlib.util.matter.MatterStructurePOI;
import java.util.HashSet;
import java.util.BitSet;
import java.util.Objects;
import java.util.Set;
public final class GenerationSemanticCapture {
private static final int CHUNK_SIZE = 16;
@@ -137,7 +136,7 @@ public final class GenerationSemanticCapture {
Mantle<Matter> mantle = engine.getMantle().getMantle();
int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE);
int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE);
Set<CavePosition> resolvedCaves = new HashSet<>();
BitSet resolvedCaves = new BitSet();
mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> {
if (!caveSpace.isOpen(localX, y, localZ)) {
return;
@@ -197,16 +196,17 @@ public final class GenerationSemanticCapture {
private static void captureCave(
Engine engine,
ChunkGenerationSemantics.Builder semantics,
Set<CavePosition> resolvedCaves,
BitSet resolvedCaves,
int blockX,
int y,
int blockZ,
String explicitBiomeKey
) {
CavePosition position = new CavePosition(blockX, y, blockZ);
if (!resolvedCaves.add(position)) {
int position = (y << 8) | ((blockX & 15) << 4) | (blockZ & 15);
if (resolvedCaves.get(position)) {
return;
}
resolvedCaves.set(position);
if (explicitBiomeKey != null && !explicitBiomeKey.isBlank()) {
semantics.addCaveBiome(explicitBiomeKey);
return;
@@ -259,6 +259,4 @@ public final class GenerationSemanticCapture {
boolean isOpen(int localX, int internalY, int localZ);
}
private record CavePosition(int x, int y, int z) {
}
}
@@ -42,6 +42,7 @@ import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Predicate;
import java.util.regex.Matcher;
@@ -85,10 +86,14 @@ public final class GenerationSemanticIndex {
private final Long2IntOpenHashMap journalEntries;
private final Long2IntOpenHashMap regionRecordCounts;
private final ReentrantReadWriteLock lock;
private final ReentrantLock publicationLock;
private final ShardPublisher publisher;
private final PointerPublisher pointerPublisher;
private final CatalogPublisher catalogPublisher;
private long regionDecodeCount;
private volatile IOException journalFailure;
private long appendingRegionKey;
private RegionShard appendingBase;
private GenerationSemanticIndex(
Path dimensionRoot,
@@ -104,6 +109,7 @@ public final class GenerationSemanticIndex {
journalEntries = new Long2IntOpenHashMap();
regionRecordCounts = new Long2IntOpenHashMap();
lock = new ReentrantReadWriteLock();
publicationLock = new ReentrantLock();
this.publisher = publisher;
this.pointerPublisher = pointerPublisher;
this.catalogPublisher = catalogPublisher;
@@ -174,36 +180,102 @@ public final class GenerationSemanticIndex {
if (!requiredClaim.sealed()) {
throw new IllegalArgumentException("Generation semantic claims must be sealed.");
}
Claim pending = new Claim(requiredClaim);
claimAndPersistBatch(List.of(pending));
return pending.result();
}
void claimAndPersistBatch(List<Claim> claims) {
lock.writeLock().lock();
publicationLock.lock();
try {
ChunkGenerationSemantics existing = getRecordLocked(
requiredClaim.chunkX(),
requiredClaim.chunkZ()
);
ChunkGenerationSemantics merged = existing == null
? requiredClaim
: existing.merge(requiredClaim);
if (merged == existing || merged.equals(existing)) {
return false;
requireJournalUsable();
Map<Long, List<Claim>> batches = new LinkedHashMap<>();
for (Claim pending : claims) {
if (!pending.update.sealed()) {
pending.fail(new IllegalArgumentException("Generation semantic claims must be sealed."));
continue;
}
long regionKey = packRegion(pending.update.chunkX() >> 5, pending.update.chunkZ() >> 5);
batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(pending);
}
int regionX = requiredClaim.chunkX() >> 5;
int regionZ = requiredClaim.chunkZ() >> 5;
long regionKey = packRegion(regionX, regionZ);
RegionShard baseRegion = loadRegionLocked(regionKey, regionX, regionZ);
RegionShard nextRegion = baseRegion.withRecord(merged);
ensureStorageDirectory();
SemanticJournal.append(directory, regionX, regionZ, merged);
cacheRegion(regionKey, nextRegion);
if (existing == null) {
regionRecordCounts.addTo(regionKey, 1);
for (Map.Entry<Long, List<Claim>> batch : batches.entrySet()) {
persistClaimBatch(batch.getKey(), batch.getValue());
}
journalEntries.addTo(regionKey, 1);
return true;
} catch (IOException failure) {
for (Claim pending : claims) {
pending.fail(failure);
}
} catch (RuntimeException | Error failure) {
journalFailure = new IOException("Generation semantic batch failed unexpectedly.", failure);
for (Claim pending : claims) {
pending.fail(failure);
}
throw failure;
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
private void persistClaimBatch(long regionKey, List<Claim> claims) {
try {
requireJournalUsable();
int regionX = regionX(regionKey);
int regionZ = regionZ(regionKey);
RegionShard baseRegion = loadRegionLocked(regionKey, regionX, regionZ);
RegionShard nextRegion = baseRegion;
List<ChunkGenerationSemantics> updates = new ArrayList<>(claims.size());
for (Claim pending : claims) {
ChunkGenerationSemantics update = pending.update;
ChunkGenerationSemantics existing = nextRegion.get(update.chunkX(), update.chunkZ());
ChunkGenerationSemantics merged;
try {
merged = existing == null ? update : existing.merge(update);
} catch (IllegalArgumentException | IllegalStateException failure) {
pending.fail(failure);
continue;
}
if (merged == existing || merged.equals(existing)) {
pending.completed = existing == baseRegion.get(update.chunkX(), update.chunkZ());
continue;
}
nextRegion = nextRegion.withRecord(merged);
updates.add(merged);
pending.persisted = true;
}
if (updates.isEmpty()) {
return;
}
ensureStorageDirectory();
appendingRegionKey = regionKey;
appendingBase = baseRegion;
publicationLock.unlock();
try {
SemanticJournal.append(directory, regionX, regionZ, updates);
} catch (JournalAppendFailure failure) {
journalFailure = failure;
throw failure;
} catch (RuntimeException | Error failure) {
journalFailure = new IOException("Generation semantic batch failed unexpectedly.", failure);
throw failure;
} finally {
publicationLock.lock();
appendingBase = null;
}
cacheRegion(regionKey, nextRegion);
regionRecordCounts.put(regionKey, nextRegion.recordCount());
journalEntries.addTo(regionKey, updates.size());
for (Claim pending : claims) {
pending.completed = true;
}
} catch (IOException failure) {
for (Claim pending : claims) {
pending.fail(failure);
}
}
}
public int discardUnstoredClaims(WorldChunkInventory stored, Set<Long> activationIds) throws IOException {
Objects.requireNonNull(stored, "stored chunks");
Set<Long> selected = Set.copyOf(activationIds);
@@ -213,6 +285,7 @@ public final class GenerationSemanticIndex {
}
}
lock.writeLock().lock();
publicationLock.lock();
try {
compactJournals();
int removed = 0;
@@ -248,19 +321,23 @@ public final class GenerationSemanticIndex {
}
return removed;
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
public void compactJournals() throws IOException {
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
ArrayList<Long> regionKeys = new ArrayList<>(journalEntries.keySet());
regionKeys.sort(GenerationSemanticIndex::compareRegionKeys);
for (long regionKey : regionKeys) {
compactJournal(regionKey);
}
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
@@ -271,7 +348,9 @@ public final class GenerationSemanticIndex {
}
SealedClaimConsumer requiredConsumer = Objects.requireNonNull(consumer, "sealed claim consumer");
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
for (long regionKey : allRegionKeys()) {
if (!loadSummaryLocked(regionKey).hasSealedActivation(activationId)) {
continue;
@@ -288,12 +367,13 @@ public final class GenerationSemanticIndex {
}
}
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
public boolean hasSealedClaim(int chunkX, int chunkZ, long activationId) {
lock.writeLock().lock();
publicationLock.lock();
try {
ChunkGenerationSemantics semantics = getRecordLocked(chunkX, chunkZ);
return semantics != null
@@ -302,14 +382,16 @@ public final class GenerationSemanticIndex {
} catch (IOException error) {
throw new IllegalStateException("Unable to resolve a sealed generation semantic claim.", error);
} finally {
lock.writeLock().unlock();
publicationLock.unlock();
}
}
public boolean recordAndPersist(ChunkGenerationSemantics update) throws IOException {
ChunkGenerationSemantics requiredUpdate = Objects.requireNonNull(update, "update");
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
ChunkGenerationSemantics existing = getRecordLocked(
requiredUpdate.chunkX(),
requiredUpdate.chunkZ()
@@ -345,34 +427,39 @@ public final class GenerationSemanticIndex {
deleteUnreferencedShard(regionX, regionZ, previousHash, shardHash);
return true;
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
public Optional<ChunkGenerationSemantics> get(int chunkX, int chunkZ) {
lock.writeLock().lock();
publicationLock.lock();
try {
return Optional.ofNullable(getRecordLocked(chunkX, chunkZ));
} catch (IOException error) {
throw new IllegalStateException("Unable to load generation semantics for chunk "
+ chunkX + "," + chunkZ + ".", error);
} finally {
lock.writeLock().unlock();
publicationLock.unlock();
}
}
public int recordCount() {
lock.readLock().lock();
publicationLock.lock();
try {
return totalRecordCountLocked();
} finally {
publicationLock.unlock();
lock.readLock().unlock();
}
}
public List<ChunkGenerationSemantics> recordsSnapshot() {
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
List<ChunkGenerationSemantics> snapshot = new ArrayList<>(totalRecordCountLocked());
for (long regionKey : allRegionKeys()) {
RegionShard region = loadRegionLocked(
@@ -388,6 +475,7 @@ public final class GenerationSemanticIndex {
} catch (IOException error) {
throw new IllegalStateException("Unable to load generation semantic snapshot.", error);
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
@@ -395,7 +483,9 @@ public final class GenerationSemanticIndex {
public void forEachRecord(RecordConsumer consumer) throws IOException {
RecordConsumer requiredConsumer = Objects.requireNonNull(consumer, "consumer");
lock.writeLock().lock();
publicationLock.lock();
try {
requireJournalUsable();
for (long regionKey : allRegionKeys()) {
RegionShard region = loadRegionLocked(
regionKey,
@@ -407,33 +497,40 @@ public final class GenerationSemanticIndex {
}
}
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
int cachedRegionCount() {
lock.readLock().lock();
publicationLock.lock();
try {
return regions.size();
} finally {
publicationLock.unlock();
lock.readLock().unlock();
}
}
long regionDecodeCount() {
lock.readLock().lock();
publicationLock.lock();
try {
return regionDecodeCount;
} finally {
publicationLock.unlock();
lock.readLock().unlock();
}
}
int cachedSummaryCount() {
lock.readLock().lock();
publicationLock.lock();
try {
return summaries.size();
} finally {
publicationLock.unlock();
lock.readLock().unlock();
}
}
@@ -452,12 +549,14 @@ public final class GenerationSemanticIndex {
"eligibility"
);
lock.writeLock().lock();
publicationLock.lock();
try {
if (requiredQuery.kind() == SemanticKind.STRUCTURE) {
return findNearestStructure(requiredQuery, requiredEligibility);
}
return findNearestChunk(requiredQuery, requiredEligibility);
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
@@ -472,6 +571,7 @@ public final class GenerationSemanticIndex {
"eligibility"
);
lock.writeLock().lock();
publicationLock.lock();
try {
RiverMatch best = null;
BigInteger bestDistance = null;
@@ -540,6 +640,7 @@ public final class GenerationSemanticIndex {
} catch (IOException error) {
throw new IllegalStateException("Unable to search recorded river semantics.", error);
} finally {
publicationLock.unlock();
lock.writeLock().unlock();
}
}
@@ -874,6 +975,7 @@ public final class GenerationSemanticIndex {
}
private ChunkGenerationSemantics getRecordLocked(int chunkX, int chunkZ) throws IOException {
requireJournalUsable();
long regionKey = packRegion(chunkX >> 5, chunkZ >> 5);
if (!regionRecordCounts.containsKey(regionKey)) {
return null;
@@ -882,6 +984,10 @@ public final class GenerationSemanticIndex {
}
private RegionShard loadRegionLocked(long regionKey, int regionX, int regionZ) throws IOException {
requireJournalUsable();
if (appendingBase != null && appendingRegionKey == regionKey) {
return appendingBase;
}
RegionShard cached = regions.get(regionKey);
if (cached != null) {
return cached;
@@ -898,6 +1004,7 @@ public final class GenerationSemanticIndex {
}
private RegionSummary loadSummaryLocked(long regionKey) throws IOException {
requireJournalUsable();
RegionSummary cached = summaries.get(regionKey);
if (cached != null) {
return cached;
@@ -959,6 +1066,12 @@ public final class GenerationSemanticIndex {
}
}
private void requireJournalUsable() throws IOException {
if (journalFailure != null) {
throw new IOException("Generation semantic journal requires recovery before further access.", journalFailure);
}
}
private List<Long> allRegionKeys() {
ArrayList<Long> ordered = new ArrayList<>(regionRecordCounts.keySet());
ordered.sort(GenerationSemanticIndex::compareRegionKeys);
@@ -1165,7 +1278,8 @@ public final class GenerationSemanticIndex {
ChunkGenerationSemantics.BlockPosition origin,
int maximumChunkRadius,
boolean orderByBlockDistance
) {
) throws IOException {
requireJournalUsable();
int originChunkX = Math.floorDiv(origin.x(), 16);
int originChunkZ = Math.floorDiv(origin.z(), 16);
long minimumChunkX = (long) originChunkX - maximumChunkRadius;
@@ -1760,6 +1874,40 @@ public final class GenerationSemanticIndex {
void publish(Path directory, Set<Long> regionKeys) throws IOException;
}
static final class Claim {
final ChunkGenerationSemantics update;
boolean persisted;
boolean completed;
private Throwable failure;
Claim(ChunkGenerationSemantics update) {
this.update = update;
}
boolean fail(Throwable failure) {
if (!completed) {
this.failure = failure;
persisted = false;
completed = true;
return true;
}
return this.failure == failure;
}
boolean result() throws IOException {
if (failure instanceof IOException ioFailure) {
throw ioFailure;
}
if (failure instanceof RuntimeException runtimeFailure) {
throw runtimeFailure;
}
if (failure instanceof Error error) {
throw error;
}
return persisted;
}
}
private static final class SemanticJournal {
private static final int ENTRY_MAGIC = 0x4953574C;
private static final int HEADER_BYTES = Integer.BYTES * 2;
@@ -1768,8 +1916,65 @@ public final class GenerationSemanticIndex {
Path directory,
int regionX,
int regionZ,
ChunkGenerationSemantics claim
List<ChunkGenerationSemantics> claims
) throws IOException {
Path file = directory.resolve(journalFileName(regionX, regionZ));
boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
if (!created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
throw invalid(file, "path is not a regular file");
}
FileChannel opened = FileChannel.open(
file,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE
);
boolean uncertain = created;
boolean restored = false;
try (FileChannel channel = opened) {
long size = channel.size();
if (size > MAX_JOURNAL_BYTES) {
throw invalid(file, "journal exceeds its size limit");
}
uncertain = true;
try {
channel.position(size);
long written = size;
for (ChunkGenerationSemantics claim : claims) {
ByteBuffer entry = encodeEntry(regionX, regionZ, claim);
if (written > MAX_JOURNAL_BYTES - entry.remaining()) {
throw invalid(file, "journal exceeds its size limit");
}
written += entry.remaining();
while (entry.hasRemaining()) {
channel.write(entry);
}
}
channel.force(true);
if (created) {
RegionShard.forceDirectory(directory);
}
} catch (IOException failure) {
try {
channel.truncate(size);
channel.force(true);
if (created) {
RegionShard.forceDirectory(directory);
}
restored = true;
} catch (IOException rollback) {
failure.addSuppressed(rollback);
}
throw failure;
}
} catch (IOException failure) {
if (uncertain && !restored) {
throw new JournalAppendFailure(file, failure);
}
throw failure;
}
}
private static ByteBuffer encodeEntry(int regionX, int regionZ, ChunkGenerationSemantics claim) throws IOException {
RegionShard single = new RegionShard(regionX, regionZ).withRecord(claim);
byte[] payload = single.encode();
CRC32 checksum = new CRC32();
@@ -1780,30 +1985,7 @@ public final class GenerationSemanticIndex {
entry.put(payload);
entry.putInt((int) checksum.getValue());
entry.flip();
Path file = directory.resolve(journalFileName(regionX, regionZ));
boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS);
if (!created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) {
throw invalid(file, "path is not a regular file");
}
try (FileChannel channel = FileChannel.open(
file,
StandardOpenOption.CREATE,
StandardOpenOption.WRITE
)) {
long size = channel.size();
if (size > MAX_JOURNAL_BYTES - entry.remaining()) {
throw invalid(file, "journal exceeds its size limit");
}
channel.position(size);
while (entry.hasRemaining()) {
channel.write(entry);
}
channel.force(true);
}
if (created) {
RegionShard.forceDirectory(directory);
}
return entry;
}
private static Replay replay(Path file, int expectedRegionX, int expectedRegionZ) throws IOException {
@@ -1916,6 +2098,12 @@ public final class GenerationSemanticIndex {
}
}
private static final class JournalAppendFailure extends IOException {
private JournalAppendFailure(Path file, IOException cause) {
super("Generation semantic journal could not confirm a durable append or rollback: " + file, cause);
}
}
private static final class ShardPointer {
private static final int MAGIC = 0x49535054;
private static final int FORMAT_VERSION = 1;
@@ -7,7 +7,9 @@ import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.OptionalInt;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@@ -24,6 +26,7 @@ public final class NativeTerrainReceipt {
public static byte[] encode(SavedTerrainChunk chunk, long activationId, String epochId) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
UtfCache strings = new UtfCache();
try (DataOutputStream output = new DataOutputStream(new BufferedOutputStream(new GZIPOutputStream(bytes)))) {
output.writeInt(MAGIC);
output.writeInt(VERSION);
@@ -37,7 +40,7 @@ public final class NativeTerrainReceipt {
if (!chunk.hasColumn(x, z)) {
continue;
}
writeColumn(output, chunk.column((chunk.chunkX() << 4) + x, (chunk.chunkZ() << 4) + z));
writeColumn(output, chunk.column((chunk.chunkX() << 4) + x, (chunk.chunkZ() << 4) + z), strings);
}
}
}
@@ -109,7 +112,7 @@ public final class NativeTerrainReceipt {
}
}
private static void writeColumn(DataOutputStream output, TerrainBoundarySignature signature) throws IOException {
private static void writeColumn(DataOutputStream output, TerrainBoundarySignature signature, UtfCache strings) throws IOException {
output.writeInt(signature.surfaceHeight());
output.writeInt(signature.oceanFloorHeight());
output.writeInt(signature.fluidHeight().orElse(-1));
@@ -119,15 +122,15 @@ public final class NativeTerrainReceipt {
output.writeInt(layout.sampleStep());
output.writeInt(layout.sampleCount());
for (int index = 0; index < layout.sampleCount(); index++) {
output.writeUTF(signature.biomeAtSample(index));
strings.write(output, signature.biomeAtSample(index));
}
BoundaryColumnGeometry geometry = signature.geometry();
output.writeInt(geometry.minimumY());
output.writeInt(geometry.palette().size());
for (BoundaryColumnGeometry.Voxel voxel : geometry.palette()) {
output.writeUTF(voxel.stateKey());
strings.write(output, voxel.stateKey());
output.writeByte(voxel.phase().ordinal());
output.writeUTF(voxel.fluidStateKey());
strings.write(output, voxel.fluidStateKey());
output.writeBoolean(voxel.protectedContent());
}
int[] ends = geometry.runEnds();
@@ -194,4 +197,45 @@ public final class NativeTerrainReceipt {
public record Decoded(SavedTerrainChunk terrain, long activationId, String epochId) {
}
private static final class UtfCache {
private static final int MAXIMUM_ENTRIES = 256;
private static final int MAXIMUM_ENCODED_BYTES = 64 * 1024;
private final Map<String, byte[]> encoded = new HashMap<>();
private final ByteArrayOutputStream scratch = new ByteArrayOutputStream();
private final DataOutputStream encoder = new DataOutputStream(scratch);
private int encodedBytes;
private boolean entryLimitReached;
private void write(DataOutputStream output, String value) throws IOException {
if (entryLimitReached) {
output.writeUTF(value);
return;
}
byte[] cached = encoded.get(value);
if (cached != null) {
output.write(cached);
return;
}
if (encoded.size() >= MAXIMUM_ENTRIES) {
entryLimitReached = true;
encoded.clear();
output.writeUTF(value);
return;
}
if (value.length() > MAXIMUM_ENCODED_BYTES - encodedBytes - Short.BYTES) {
output.writeUTF(value);
return;
}
scratch.reset();
encoder.writeUTF(value);
if (scratch.size() <= MAXIMUM_ENCODED_BYTES - encodedBytes) {
byte[] bytes = scratch.toByteArray();
encoded.put(value, bytes);
encodedBytes += bytes.length;
}
scratch.writeTo(output);
}
}
}
@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
@@ -42,20 +43,22 @@ public final class SavedBiomeStore {
private static final int MAXIMUM_CACHED_REGIONS = 64;
private static final int MAXIMUM_CACHED_CHUNKS = 128;
private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L;
private static final int MAXIMUM_WRITE_BATCH = 64;
private final Path dimensionRoot;
private final Path directory;
private final Object[] regionLocks = new Object[64];
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);
private long cachedBytes;
private volatile IOException writeFailure;
private SavedBiomeStore(Path dimensionRoot) throws IOException {
this.dimensionRoot = Objects.requireNonNull(dimensionRoot, "dimensionRoot").toAbsolutePath().normalize();
directory = this.dimensionRoot.resolve("iris/generation/biomes");
requireSafeAncestors();
for (int index = 0; index < regionLocks.length; index++) {
regionLocks[index] = new Object();
regionLocks[index] = new WriteStripe();
}
}
@@ -76,6 +79,7 @@ public final class SavedBiomeStore {
}
}
synchronized (regionLock(chunkX, chunkZ)) {
requireWritable();
synchronized (chunks) {
Optional<SavedBiomeChunk> cached = chunks.get(chunkKey);
if (cached != null) {
@@ -99,46 +103,153 @@ public final class SavedBiomeStore {
public boolean claimAndPersist(SavedBiomeChunk chunk) throws IOException {
SavedBiomeChunk required = Objects.requireNonNull(chunk, "chunk");
Object lock = regionLock(required.chunkX(), required.chunkZ());
synchronized (lock) {
if (alreadyClaimed(required)) {
return false;
WriteStripe lock = regionLock(required.chunkX(), required.chunkZ());
if (!lock.writing) {
synchronized (lock) {
if (alreadyClaimed(required)) {
return false;
}
}
}
byte[] body = encode(required);
byte[] record = ByteBuffer.allocate(body.length + 8)
.putInt(body.length).put(body).putInt(checksum(body)).array();
PendingWrite pending = new PendingWrite(required, record);
lock.pending.add(pending);
synchronized (lock) {
if (alreadyClaimed(required)) {
return false;
if (!pending.completed) {
lock.writing = true;
try {
while (!pending.completed) {
persistPending(lock);
}
} finally {
lock.writing = false;
}
}
RegionIndex region = region(required.chunkX(), required.chunkZ());
if (pending.failure != null) {
throw pending.failure;
}
return pending.persisted;
}
}
private void persistPending(WriteStripe stripe) {
Map<Long, List<PendingWrite>> batches = new LinkedHashMap<>();
PendingWrite grouping = null;
try {
for (int count = 0; count < MAXIMUM_WRITE_BATCH; count++) {
grouping = stripe.pending.poll();
if (grouping == null) {
break;
}
long regionKey = key(grouping.chunk.chunkX() >> 5, grouping.chunk.chunkZ() >> 5);
batches.computeIfAbsent(regionKey, ignored -> new ArrayList<>()).add(grouping);
grouping = null;
}
for (List<PendingWrite> batch : batches.values()) {
persistBatch(batch);
}
} catch (RuntimeException | Error failure) {
IOException unavailable = new IOException("Saved biome append failed unexpectedly", failure);
writeFailure = unavailable;
if (grouping != null) {
grouping.failure = unavailable;
grouping.completed = true;
}
for (List<PendingWrite> batch : batches.values()) {
for (PendingWrite pending : batch) {
if (!pending.completed) {
pending.failure = unavailable;
pending.completed = true;
}
}
}
throw failure;
}
}
private void persistBatch(List<PendingWrite> batch) {
Map<Integer, PendingWrite> claimed = new LinkedHashMap<>();
try {
for (PendingWrite pending : batch) {
try {
if (alreadyClaimed(pending.chunk)) {
pending.completed = true;
continue;
}
int slot = slot(pending.chunk.chunkX(), pending.chunk.chunkZ());
PendingWrite previous = claimed.get(slot);
if (previous == null) {
claimed.put(slot, pending);
} else if (!previous.chunk.equals(pending.chunk)) {
throw new IOException("Conflicting saved biome claim at chunk "
+ pending.chunk.chunkX() + ", " + pending.chunk.chunkZ());
}
} catch (IOException failure) {
pending.failure = failure;
pending.completed = true;
}
}
if (claimed.isEmpty()) {
return;
}
requireWritable();
SavedBiomeChunk first = batch.getFirst().chunk;
RegionIndex region = region(first.chunkX(), first.chunkZ());
ensureRegionFile(region);
long offset;
long offset = 0L;
boolean durable = false;
try (RandomAccessFile output = new RandomAccessFile(region.path.toFile(), "rw")) {
offset = output.length();
if (offset + record.length > MAXIMUM_REGION_BYTES) {
long length = 0L;
for (PendingWrite pending : claimed.values()) {
length += pending.record.length;
}
if (offset + length > MAXIMUM_REGION_BYTES) {
throw new IOException("Saved biome region exceeds its storage limit: " + region.path);
}
try {
output.seek(offset);
output.write(record);
for (PendingWrite pending : claimed.values()) {
output.write(pending.record);
}
output.getChannel().force(true);
durable = true;
} catch (IOException failure) {
try {
output.setLength(offset);
output.getChannel().force(true);
} catch (IOException rollback) {
failure.addSuppressed(rollback);
writeFailure = failure;
}
throw failure;
}
} catch (IOException failure) {
if (durable) {
writeFailure = failure;
}
throw failure;
}
int slot = slot(required.chunkX(), required.chunkZ());
region.offsets[slot] = offset;
region.lengths[slot] = body.length;
cacheChunk(key(required.chunkX(), required.chunkZ()), Optional.of(required));
return true;
for (PendingWrite pending : claimed.values()) {
SavedBiomeChunk chunk = pending.chunk;
int slot = slot(chunk.chunkX(), chunk.chunkZ());
region.offsets[slot] = offset;
region.lengths[slot] = pending.record.length - 8;
cacheChunk(key(chunk.chunkX(), chunk.chunkZ()), Optional.of(chunk));
pending.persisted = true;
offset += pending.record.length;
}
} catch (IOException failure) {
for (PendingWrite pending : batch) {
if (!pending.completed) {
pending.failure = failure;
}
}
}
for (PendingWrite pending : batch) {
pending.completed = true;
}
}
@@ -164,6 +275,7 @@ public final class SavedBiomeStore {
int regionX = parseRegionCoordinate(matcher.group(1));
int regionZ = parseRegionCoordinate(matcher.group(2));
synchronized (regionLock(regionX << 5, regionZ << 5)) {
requireWritable();
RegionIndex region = region(regionX << 5, regionZ << 5);
boolean[] discarded = new boolean[1024];
int count = 0;
@@ -205,6 +317,7 @@ public final class SavedBiomeStore {
}
private boolean alreadyClaimed(SavedBiomeChunk chunk) throws IOException {
requireWritable();
Optional<SavedBiomeChunk> existing = get(chunk.chunkX(), chunk.chunkZ());
if (existing.isEmpty()) {
return false;
@@ -440,7 +553,7 @@ public final class SavedBiomeStore {
}
}
private Object regionLock(int chunkX, int chunkZ) {
private WriteStripe regionLock(int chunkX, int chunkZ) {
long key = key(chunkX >> 5, chunkZ >> 5);
return regionLocks[(int) (key ^ (key >>> 32)) & (regionLocks.length - 1)];
}
@@ -656,6 +769,30 @@ public final class SavedBiomeStore {
throw new IOException("Invalid saved biome variable integer");
}
private void requireWritable() throws IOException {
if (writeFailure != null) {
throw new IOException("Saved biome writes are unavailable after an uncertain append", writeFailure);
}
}
private static final class WriteStripe {
private final ConcurrentLinkedQueue<PendingWrite> pending = new ConcurrentLinkedQueue<>();
private volatile boolean writing;
}
private static final class PendingWrite {
private final SavedBiomeChunk chunk;
private final byte[] record;
private boolean completed;
private boolean persisted;
private IOException failure;
private PendingWrite(SavedBiomeChunk chunk, byte[] record) {
this.chunk = chunk;
this.record = record;
}
}
private static final class DecodeBudget {
private long remaining = SavedBiomeChunk.MAXIMUM_ESTIMATED_BYTES - 8192L;
@@ -16,12 +16,16 @@ public final class RiverPolicyResolver {
}
public static EffectiveRiverPolicy resolve(IrisDimension dimension, IrisRegion region, IrisBiome biome) {
return resolve(
dimension == null ? null : dimension.getRiverPolicy(),
return resolve(dimension == null ? null : dimension.getRiverPolicy(),
region == null ? null : region.getRiverPolicy(),
biome == null ? null : biome.getRiverPolicy(),
loaderOf(biome, region, dimension)
);
biome == null ? null : biome.getRiverPolicy(), loaderOf(biome, region, dimension));
}
public static Resolution resolveWithStatus(IrisDimension dimension, IrisRegion region, IrisBiome biome) {
State state = resolveState(dimension == null ? null : dimension.getRiverPolicy(),
region == null ? null : region.getRiverPolicy(),
biome == null ? null : biome.getRiverPolicy(), loaderOf(biome, region, dimension), true);
return new Resolution(state.build(), state.complete);
}
public static EffectiveRiverPolicy resolve(
@@ -42,11 +46,16 @@ public final class RiverPolicyResolver {
IrisRiverPolicy biomePolicy,
IrisData data
) {
State state = new State();
return resolveState(dimensionPolicy, regionPolicy, biomePolicy, data, false).build();
}
private static State resolveState(IrisRiverPolicy dimensionPolicy, IrisRiverPolicy regionPolicy,
IrisRiverPolicy biomePolicy, IrisData data, boolean trackCompleteness) {
State state = new State(trackCompleteness);
state.apply(dimensionPolicy, RiverConfinement.REGION, data);
state.apply(regionPolicy, RiverConfinement.REGION, data);
state.apply(biomePolicy, RiverConfinement.BIOME, data);
return state.build();
return state;
}
private static IrisData loaderOf(IrisRegistrant... registrants) {
@@ -59,7 +68,12 @@ public final class RiverPolicyResolver {
return null;
}
public record Resolution(EffectiveRiverPolicy policy, boolean complete) {
}
private static final class State {
private final Runnable unresolvedReference;
private boolean complete = true;
private IrisRiverPlacementMode placement = IrisRiverPlacementMode.NATURAL;
private IrisRiverRoutingMode routing = IrisRiverRoutingMode.ALLOW;
private boolean outletAdmission = true;
@@ -80,6 +94,10 @@ public final class RiverPolicyResolver {
private Double shoreWidth = null;
private Boolean erosion = null;
private State(boolean trackCompleteness) {
unresolvedReference = trackCompleteness ? this::unresolvedReference : null;
}
/**
* {@code scope} is the confinement a {@code confined: true} at this level binds courses to; {@code data} is the
* pack whose version-content gate filters the biome selections (null skips filtering).
@@ -113,19 +131,19 @@ public final class RiverPolicyResolver {
profiles = List.copyOf(policy.getProfiles());
}
if (policy.getSurfaceBiomes() != null) {
surfaceBiomes = List.copyOf(policy.compatBiomes(policy.getSurfaceBiomes(), data, "surfaceBiomes"));
surfaceBiomes = List.copyOf(policy.compatBiomes(policy.getSurfaceBiomes(), data, "surfaceBiomes", unresolvedReference));
}
if (policy.getMouthBiomes() != null) {
mouthBiomes = List.copyOf(policy.compatBiomes(policy.getMouthBiomes(), data, "mouthBiomes"));
mouthBiomes = List.copyOf(policy.compatBiomes(policy.getMouthBiomes(), data, "mouthBiomes", unresolvedReference));
}
if (policy.getShoreBiomes() != null) {
shoreBiomes = List.copyOf(policy.compatBiomes(policy.getShoreBiomes(), data, "shoreBiomes"));
shoreBiomes = List.copyOf(policy.compatBiomes(policy.getShoreBiomes(), data, "shoreBiomes", unresolvedReference));
}
if (policy.getBankBiomes() != null) {
bankBiomes = List.copyOf(policy.compatBiomes(policy.getBankBiomes(), data, "bankBiomes"));
bankBiomes = List.copyOf(policy.compatBiomes(policy.getBankBiomes(), data, "bankBiomes", unresolvedReference));
}
if (policy.getFloodedCaveBiomes() != null) {
floodedCaveBiomes = List.copyOf(policy.compatBiomes(policy.getFloodedCaveBiomes(), data, "floodedCaveBiomes"));
floodedCaveBiomes = List.copyOf(policy.compatBiomes(policy.getFloodedCaveBiomes(), data, "floodedCaveBiomes", unresolvedReference));
}
if (policy.getSurfacePools() != null) {
surfacePools = List.copyOf(policy.getSurfacePools());
@@ -147,6 +165,10 @@ public final class RiverPolicyResolver {
}
}
private void unresolvedReference() {
complete = false;
}
private EffectiveRiverPolicy build() {
return new EffectiveRiverPolicy(
placement,
@@ -1,5 +1,7 @@
package art.arcane.iris.engine.hydrology.runtime;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.core.loader.ResourceLoader;
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
import art.arcane.iris.engine.hydrology.HydrologyColumnSample;
import art.arcane.iris.engine.hydrology.HydrologyDiagnosticCandidate;
@@ -78,6 +80,7 @@ import java.util.function.Predicate;
public final class IrisHydrologyRuntime implements AutoCloseable {
private static final int MAXIMUM_CACHE_TILES = 64;
private static final int MAXIMUM_TERRAIN_SAMPLES = 65_536;
private static final int MAXIMUM_RESOLVED_POLICIES = 1024;
private static final int MAXIMUM_FEATURE_SEARCH_TILES = 1_089;
private static final int BIOME_PATCH_SCALE = 32;
private static final long SURFACE_WIDTH_SALT = 0x5355524657494454L;
@@ -100,6 +103,9 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
private final Cache<HydrologyTileKey, Boolean> unplannedQueries = Caffeine.newBuilder()
.maximumSize(MAXIMUM_CACHE_TILES)
.build();
private final Cache<PolicyKey, ResolvedPolicy> resolvedPolicies = Caffeine.newBuilder()
.maximumSize(MAXIMUM_RESOLVED_POLICIES)
.build();
private final LinkedHashMap<Long, HydrologyTerrainSample> terrainSamples;
public IrisHydrologyRuntime(IrisHydrologyRuntimeContext context) {
@@ -483,6 +489,7 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
public void close() {
cache.close();
unplannedQueries.invalidateAll();
resolvedPolicies.invalidateAll();
routingTerrainSampler.close();
synchronized (terrainSampleLock) {
terrainSamples.clear();
@@ -571,9 +578,10 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
boolean ocean = naturalSample.ocean();
IrisBiome biome = naturalSample.biome();
IrisRegion region = naturalSample.region();
EffectiveRiverPolicy policy = RiverPolicyResolver.resolve(context.dimension(), region, biome);
ResolvedPolicy resolvedPolicy = resolvePolicy(region, biome);
EffectiveRiverPolicy policy = resolvedPolicy.policy();
String parentBiomeKey = requireBiomeKey(biome);
List<String> profiles = validProfiles(policy.profiles());
List<String> profiles = resolvedPolicy.profiles();
int configuredFluidY = settings.underground().minimumFluidY()
+ (settings.underground().maximumFluidY() - settings.underground().minimumFluidY()) / 2;
if (!rivers.getUnderground().getFluidLevel().isFlat()) {
@@ -651,6 +659,35 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
};
}
private ResolvedPolicy resolvePolicy(IrisRegion region, IrisBiome biome) {
IrisData data = policyData(region, biome);
ResourceLoader<IrisBiome> loader = data == null ? null : data.getBiomeLoader();
PolicyKey key = new PolicyKey(region, biome, data, loader);
ResolvedPolicy cached = resolvedPolicies.getIfPresent(key);
if (cached != null) {
return cached;
}
RiverPolicyResolver.Resolution resolution = RiverPolicyResolver.resolveWithStatus(context.dimension(), region, biome);
EffectiveRiverPolicy policy = resolution.policy();
ResolvedPolicy resolved = new ResolvedPolicy(policy, validProfiles(policy.profiles()));
if (resolution.complete() && data == policyData(region, biome)
&& loader == (data == null ? null : data.getBiomeLoader())) {
resolvedPolicies.put(key, resolved);
}
return resolved;
}
private IrisData policyData(IrisRegion region, IrisBiome biome) {
IrisData data = biome == null ? null : biome.getLoader();
if (data == null && region != null) {
data = region.getLoader();
}
if (data == null) {
data = context.dimension().getLoader();
}
return data;
}
private List<String> validProfiles(List<String> requested) {
if (requested == null || requested.isEmpty()) {
return List.of(defaultProfileKey);
@@ -1230,4 +1267,22 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
}
return biome.getLoadKey();
}
private record ResolvedPolicy(EffectiveRiverPolicy policy, List<String> profiles) {
}
private record PolicyKey(IrisRegion region, IrisBiome biome, IrisData data, ResourceLoader<IrisBiome> loader) {
@Override
public boolean equals(Object compared) {
return compared instanceof PolicyKey other
&& region == other.region && biome == other.biome && data == other.data && loader == other.loader;
}
@Override
public int hashCode() {
return ((31 * System.identityHashCode(region) + System.identityHashCode(biome)) * 31
+ System.identityHashCode(data)) * 31 + System.identityHashCode(loader);
}
}
}
@@ -352,6 +352,10 @@ public interface EngineMantle extends MatterGenerator {
if (getMantle().hasFlag(x, z, MantleFlag.CLEANED) || !isCovered(x, z)) {
return false;
}
return cleanupCoveredChunk(x, z, force);
}
default boolean cleanupCoveredChunk(int x, int z, boolean force) {
MantleChunk<Matter> chunk = getMantle().getChunk(x, z).use();
try {
synchronized (chunk) {
@@ -42,6 +42,30 @@ public final class TerrainMatterView {
}
}
public static MatterCavern getComposedCavern(MantleChunk<Matter> chunk, int x, int y, int z) {
if (chunk == null || y < 0) {
return null;
}
synchronized (chunk) {
int section = y >> 4;
if (!chunk.exists(section)) {
return null;
}
Matter matter = chunk.get(section);
if (matter == null) {
return null;
}
PreObjectMatterCell cell = raw(matter, x, y, z, PreObjectMatterCell.class);
HydrologyCaveCell hydrology = cell != null && cell.hydrologyCaptured()
? cell.hydrology() : raw(matter, x, y, z, HydrologyCaveCell.class);
if (hydrology != null) {
return hydrology.asCavern();
}
return cell != null && cell.cavernCaptured()
? cell.cavern() : raw(matter, x, y, z, MatterCavern.class);
}
}
public static <T> void iterate(MantleChunk<Matter> chunk, Class<T> type,
Consumer4<Integer, Integer, Integer, T> consumer) {
Objects.requireNonNull(chunk, "Terrain mantle chunk");
@@ -54,7 +78,8 @@ public final class TerrainMatterView {
List<TerrainEntry<T>> entries = new ArrayList<>();
synchronized (chunk) {
chunk.iterate(type, (x, y, z, value) -> {
T original = get(chunk, x, y, z, type);
PreObjectMatterCell cell = raw(chunk, x, y, z, PreObjectMatterCell.class);
T original = cell != null && cell.captures(type) ? cell.original(type) : value;
if (original != null) {
entries.add(new TerrainEntry<>(x, y, z, original));
}
@@ -88,7 +113,10 @@ public final class TerrainMatterView {
return null;
}
Matter matter = chunk.get(section);
return matter == null || !matter.hasSlice(type) ? null
: matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
return matter == null ? null : raw(matter, x, y, z, type);
}
private static <T> T raw(Matter matter, int x, int y, int z, Class<T> type) {
return !matter.hasSlice(type) ? null : matter.<T>getSlice(type).get(x & 15, y & 15, z & 15);
}
}
@@ -153,6 +153,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
PrecisionStopwatch applyStopwatch = PrecisionStopwatch.start();
try {
IrisData wallData = getData();
walls.forEach((rx, yy, rz, cavern) -> {
HydrologyCaveCell hydrology = dataIfPresent(
mantleChunk, rx, yy, rz, HydrologyCaveCell.class);
@@ -167,7 +168,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
: resolveCustomBiome(customBiomeCache, customBiome);
if (biome != null) {
PlatformBlockState data = biome.getWall().get(rng, worldX, yy, worldZ, getData());
PlatformBlockState data = biome.getWall().get(rng, worldX, yy, worldZ, wallData);
int columnIndex = PowerOfTwoCoordinates.packLocal16(rx, rz);
if (data != null && B.isSolid(output.getRaw(rx, yy, rz)) && yy < surfaceHeights[columnIndex]) {
@@ -566,9 +567,7 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
}
private MatterCavern composedCavernAt(MantleChunk<Matter> mantleChunk, int x, int y, int z) {
MatterCavern baseline = dataIfPresent(mantleChunk, x, y, z, MatterCavern.class);
HydrologyCaveCell hydrology = dataIfPresent(mantleChunk, x, y, z, HydrologyCaveCell.class);
return composeCavern(baseline, hydrology);
return TerrainMatterView.getComposedCavern(mantleChunk, x, y, z);
}
private static <T> T dataIfPresent(MantleChunk<Matter> mantleChunk, int x, int y, int z, Class<T> type) {
@@ -875,12 +874,15 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
Long2ObjectOpenHashMap<IrisBiome> caveBiomes = new Long2ObjectOpenHashMap<>(256);
Map<String, IrisBiome> customBiomes = new HashMap<>();
try {
for (int localX = 0; localX < output.getWidth(); localX++) {
for (int localZ = 0; localZ < output.getDepth(); localZ++) {
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 < output.getHeight(); y++) {
for (int y = 1; y < height; y++) {
PlatformBlockState state = output.getRaw(localX, y, localZ);
if (B.isSolid(state)) {
if (floor >= 0) {
@@ -1093,30 +1095,18 @@ public class IrisCarveModifier extends EngineAssignedModifier<PlatformBlockState
return null;
}
IrisBiome xPos = sampleCaveBiome(caveBiomeCache, x + CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
IrisBiome xNeg = sampleCaveBiome(caveBiomeCache, x - CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
IrisBiome zPos = sampleCaveBiome(caveBiomeCache, x, y, z + CAVE_BIOME_BLEND_RADIUS, resolverState);
IrisBiome zNeg = sampleCaveBiome(caveBiomeCache, x, y, z - CAVE_BIOME_BLEND_RADIUS, resolverState);
if (xPos == center && xNeg == center && zPos == center && zNeg == center) {
return center;
}
int roll = Math.floorMod(rng.nextParallelRNG(BlockPosition.toLong(x, y, z)).nextInt(), CAVE_BIOME_BLEND_TOTAL_WEIGHT);
if (roll < CAVE_BIOME_BLEND_CENTER_WEIGHT) {
return center;
}
roll -= CAVE_BIOME_BLEND_CENTER_WEIGHT;
if (roll == 0) {
return xPos != null ? xPos : center;
}
if (roll == 1) {
return xNeg != null ? xNeg : center;
}
if (roll == 2) {
return zPos != null ? zPos : center;
}
return zNeg != null ? zNeg : center;
IrisBiome neighbor = switch (roll) {
case 0 -> sampleCaveBiome(caveBiomeCache, x + CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
case 1 -> sampleCaveBiome(caveBiomeCache, x - CAVE_BIOME_BLEND_RADIUS, y, z, resolverState);
case 2 -> sampleCaveBiome(caveBiomeCache, x, y, z + CAVE_BIOME_BLEND_RADIUS, resolverState);
default -> sampleCaveBiome(caveBiomeCache, x, y, z - CAVE_BIOME_BLEND_RADIUS, resolverState);
};
return neighbor != null ? neighbor : center;
}
private IrisBiome sampleCaveBiome(Long2ObjectOpenHashMap<IrisBiome> caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) {
@@ -83,7 +83,12 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
long seed = x * 341873128712L + z * 132897987541L;
PreparedDeposit[] prepared = new PreparedDeposit[PREPARATION_BATCH_COUNT];
int pending = 0;
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
&& 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();
Throwable callerFailure = null;
try {
for (int i = 0; i < generators.size(); i++) {
DepositPlan plan = plan(generators.get(i), rng.nextParallelRNG(seed * (i + 1L)));
@@ -92,19 +97,25 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
int limit = Math.min(plan.attempts(), first + CLUMPS_PER_BATCH);
int index = pending++;
burst.queue(scopedDepositTask(
() -> prepared[index] = prepare(plan, firstAttempt, limit, x, z, null, context), context));
() -> prepared[index] = prepare(plan, firstAttempt, limit, x, z, null, context), preparation, index));
if (pending == prepared.length) {
completeBatches(burst, prepared, chunk, terrain, x, z, context);
completeBatches(burst, prepared, chunk, terrain, x, z, preparation);
pending = 0;
}
}
}
completeBatches(burst, prepared, chunk, terrain, x, z, context);
completeBatches(burst, prepared, chunk, terrain, x, z, preparation);
} catch (Throwable failure) {
callerFailure = failure;
throw failure;
} finally {
// complete() must run before release() even when queueing throws — already
// submitted burst tasks must never write into a released chunk.
try {
burst.complete();
if (callerFailure != null) {
preparation.takeFailure(callerFailure);
}
} finally {
chunk.release();
}
@@ -112,29 +123,37 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
}
private void completeBatches(BurstExecutor burst, PreparedDeposit[] prepared, MantleChunk chunk,
Hunk<PlatformBlockState> terrain, int x, int z, ChunkContext context) {
Hunk<PlatformBlockState> terrain, int x, int z, PreparationContext preparation) {
burst.complete();
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) {
Throwable failure = preparation.takeFailure(null);
if (failure instanceof Error error) {
throw error;
}
if (failure instanceof RuntimeException exception) {
throw exception;
}
if (failure != null) {
throw new IllegalStateException("Deposit preparation failed", failure);
}
try (IrisContext.Scope chunkScope = IrisContext.open(getEngine(), preparation.chunk().getGenerationSessionId(), preparation.chunk())) {
for (int i = 0; i < prepared.length; i++) {
PreparedDeposit deposit = prepared[i];
prepared[i] = null;
if (deposit != null) {
place(deposit, chunk, terrain, x, z, null, context);
place(deposit, chunk, terrain, x, z, null, preparation.chunk());
}
}
}
}
private Runnable scopedDepositTask(Runnable task, ChunkContext context) {
IrisEngine generationEngine = getEngine() instanceof IrisEngine irisEngine
&& irisEngine.hasGenerationRuntimeScope() ? irisEngine : null;
IrisEngine.GenerationRuntimeBinding binding = generationEngine == null
? null : generationEngine.captureGenerationRuntimeBinding();
private Runnable scopedDepositTask(Runnable task, PreparationContext preparation, int index) {
return () -> {
try (IrisEngine.GenerationRuntimeScope runtimeScope = generationEngine == null
? null : generationEngine.openGenerationRuntimeScope(binding);
IrisContext.Scope chunkScope = IrisContext.open(getEngine(), context.getGenerationSessionId(), context)) {
try (IrisEngine.GenerationRuntimeScope runtimeScope = preparation.engine() == null
? null : preparation.engine().openGenerationRuntimeScope(preparation.binding());
IrisContext.Scope chunkScope = IrisContext.open(getEngine(), preparation.chunk().getGenerationSessionId(), preparation.chunk())) {
task.run();
} catch (Throwable failure) {
preparation.failures()[index] = failure;
}
};
}
@@ -498,6 +517,34 @@ public class IrisDepositModifier extends EngineAssignedModifier<PlatformBlockSta
return mixed ^ (mixed >>> 31);
}
private record PreparationContext(ChunkContext chunk, IrisEngine engine, IrisEngine.GenerationRuntimeBinding binding,
Throwable[] failures) {
private Throwable takeFailure(Throwable primary) {
for (int i = 0; i < failures.length; i++) {
Throwable failure = failures[i];
failures[i] = null;
if (failure == null || failure == primary) {
continue;
}
if (primary == null) {
primary = failure;
} else if (!isSuppressed(primary, failure)) {
primary.addSuppressed(failure);
}
}
return primary;
}
private boolean isSuppressed(Throwable primary, Throwable failure) {
for (Throwable suppressed : primary.getSuppressed()) {
if (suppressed == failure) {
return true;
}
}
return false;
}
}
private record DepositPlan(IrisDepositGenerator generator, long seed, boolean ore, int attempts) {
}
@@ -90,6 +90,16 @@ final class CompatPools {
String subjectType,
String subjectKey,
String field) {
return surviving(loader, keys, data, subjectType, subjectKey, field, null);
}
static <T extends IrisRegistrant> KList<String> surviving(ResourceLoader<T> loader,
List<String> keys,
IrisData data,
String subjectType,
String subjectKey,
String field,
Runnable unresolvedReference) {
KList<String> kept = new KList<>();
if (keys == null) {
@@ -104,6 +114,9 @@ final class CompatPools {
for (int index = 0; index < keys.size(); index++) {
String key = keys.get(index);
T loaded = loader.load(key);
if (loaded == null && unresolvedReference != null) {
unresolvedReference.run();
}
if (loaded != null && loaded.isCompatExcluded()) {
drop(data, loaded, subjectType, subjectKey, field + "[" + index + "] " + key, null);
@@ -28,7 +28,7 @@ public final class IrisDimensionCarvingResolver {
public static IrisDimensionCarvingEntry resolveRootEntry(Engine engine, int worldY, State state) {
State resolvedState = state == null ? threadState() : state;
resolvedState.bind(engine);
IrisData data = resolvedState.bind(engine);
if (resolvedState.rootEntriesByWorldY.containsKey(worldY)) {
return resolvedState.rootEntriesByWorldY.get(worldY);
}
@@ -42,7 +42,7 @@ public final class IrisDimensionCarvingResolver {
IrisDimensionCarvingEntry resolved = null;
for (IrisDimensionCarvingEntry entry : entries) {
if (!isRootCandidate(engine, entry, worldY, resolvedState)) {
if (!isRootCandidate(data, entry, worldY, resolvedState)) {
continue;
}
@@ -59,12 +59,12 @@ public final class IrisDimensionCarvingResolver {
public static IrisDimensionCarvingEntry resolveFromRoot(Engine engine, IrisDimensionCarvingEntry rootEntry, int worldX, int worldZ, State state) {
State resolvedState = state == null ? threadState() : state;
resolvedState.bind(engine);
IrisData data = resolvedState.bind(engine);
if (rootEntry == null) {
return null;
}
IrisBiome rootBiome = resolveEntryBiome(engine, rootEntry, resolvedState);
IrisBiome rootBiome = resolveBoundEntryBiome(data, rootEntry, resolvedState);
if (rootBiome == null) {
return null;
}
@@ -78,7 +78,7 @@ public final class IrisDimensionCarvingResolver {
IrisDimensionCarvingEntry current = rootEntry;
int depth = remainingDepth;
while (depth > 0) {
IrisDimensionCarvingEntry selected = selectChild(engine, current, worldX, worldZ, entryIndex, resolvedState);
IrisDimensionCarvingEntry selected = selectChild(engine, data, current, worldX, worldZ, entryIndex, resolvedState);
if (selected == null || selected == current) {
break;
}
@@ -107,17 +107,20 @@ public final class IrisDimensionCarvingResolver {
return entry.getRealBiome(engine.getData());
}
state.bind(engine);
return resolveBoundEntryBiome(state.bind(engine), entry, state);
}
private static IrisBiome resolveBoundEntryBiome(IrisData data, IrisDimensionCarvingEntry entry, State state) {
if (state.biomeCache.containsKey(entry)) {
return state.biomeCache.get(entry);
}
IrisBiome biome = entry.getRealBiome(engine.getData());
IrisBiome biome = entry.getRealBiome(data);
state.biomeCache.put(entry, biome);
return biome;
}
private static boolean isRootCandidate(Engine engine, IrisDimensionCarvingEntry entry, int worldY, State state) {
private static boolean isRootCandidate(IrisData data, IrisDimensionCarvingEntry entry, int worldY, State state) {
if (entry == null || !entry.isEnabled()) {
return false;
}
@@ -127,11 +130,12 @@ public final class IrisDimensionCarvingResolver {
return false;
}
return resolveEntryBiome(engine, entry, state) != null;
return resolveBoundEntryBiome(data, entry, state) != null;
}
private static IrisDimensionCarvingEntry selectChild(
Engine engine,
IrisData data,
IrisDimensionCarvingEntry parent,
int worldX,
int worldZ,
@@ -143,14 +147,14 @@ public final class IrisDimensionCarvingResolver {
return parent;
}
IrisBiome parentBiome = resolveEntryBiome(engine, parent, state);
IrisBiome parentBiome = resolveBoundEntryBiome(data, parent, state);
if (parentBiome == null) {
return parent;
}
IrisRaritySelection<CarvingChoice> selectionPlan = state.selectionPlans.get(parent);
if (selectionPlan == null) {
selectionPlan = buildSelectionPlan(engine, parent, parentBiome, entryIndex, state);
selectionPlan = buildSelectionPlan(data, parent, parentBiome, entryIndex, state);
state.selectionPlans.put(parent, selectionPlan);
}
@@ -159,7 +163,7 @@ public final class IrisDimensionCarvingResolver {
}
long seed = resolveChildSeed(engine, state);
CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData());
CNG childGenerator = parent.getChildrenGenerator(seed, data);
double sample = childGenerator.noiseFast2D(worldX, worldZ);
CarvingChoice selected = selectionPlan.select(sample);
if (selected == null || selected.entry == null) {
@@ -170,7 +174,7 @@ public final class IrisDimensionCarvingResolver {
}
private static IrisRaritySelection<CarvingChoice> buildSelectionPlan(
Engine engine,
IrisData data,
IrisDimensionCarvingEntry parent,
IrisBiome parentBiome,
Map<String, IrisDimensionCarvingEntry> entryIndex,
@@ -189,7 +193,7 @@ public final class IrisDimensionCarvingResolver {
continue;
}
IrisBiome childBiome = resolveEntryBiome(engine, child, state);
IrisBiome childBiome = resolveBoundEntryBiome(data, child, state);
if (childBiome == null) {
continue;
}
@@ -256,13 +260,13 @@ public final class IrisDimensionCarvingResolver {
private Map<String, IrisDimensionCarvingEntry> entryIndex;
private Long childSeed;
private void bind(Engine engine) {
private IrisData bind(Engine engine) {
IrisDimension dimension = engine.getDimension();
IrisData data = engine.getData();
if (references(engineIdentity, engine)
&& references(dimensionIdentity, dimension)
&& references(dataIdentity, data)) {
return;
return data;
}
engineIdentity = new WeakReference<>(engine);
dimensionIdentity = new WeakReference<>(dimension);
@@ -272,6 +276,7 @@ public final class IrisDimensionCarvingResolver {
biomeCache.clear();
entryIndex = null;
childSeed = null;
return data;
}
private static boolean references(WeakReference<?> identity, Object value) {
@@ -105,12 +105,16 @@ public class IrisRiverPolicy {
* selection keeps its "explicitly clears the inherited selection" meaning; null (undeclared) stays null.
*/
public KList<String> compatBiomes(KList<String> declared, IrisData data, String field) {
return compatBiomes(declared, data, field, null);
}
public KList<String> compatBiomes(KList<String> declared, IrisData data, String field, Runnable unresolvedReference) {
if (declared == null) {
return null;
}
return CompatPools.surviving(data == null ? null : data.getBiomeLoader(), declared, data,
"river policy", "", field);
"river policy", "", field, unresolvedReference);
}
public KSet<String> getAllBiomeIds() {
@@ -1088,7 +1088,6 @@ public class CNG {
}
private double getSignedNoise(double x, double z) {
ensureFastPathState();
double scl = effectiveScale;
NoiseGenerator localGenerator = generator;
CNG localFracture = fracture;
@@ -1103,7 +1102,6 @@ public class CNG {
}
private double getSignedNoise(double x, double y, double z) {
ensureFastPathState();
double scl = effectiveScale;
NoiseGenerator localGenerator = generator;
CNG localFracture = fracture;