Carve terrain on demand

- eager terrain window covers the replayed object sources plus one chunk of footprint (radius 13 instead of 19 for overworld)
- content writers generate a chunk's missing terrain components before handing it out, so reads past the window stay identical
- getEagerInputRadius replaces isInputGenerationLazy
This commit is contained in:
Brian Neumann-Fopiano
2026-09-28 13:32:53 -04:00
parent 8d93431319
commit 808e133cac
8 changed files with 231 additions and 48 deletions
@@ -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,8 +67,8 @@ public final class MantleHydrologyComponent extends IrisMantleComponent {
}
@Override
public boolean isInputGenerationLazy() {
return true;
public int getEagerInputRadius(int inputRadius) {
return 0;
}
@Override
@@ -128,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();
@@ -272,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,15 @@ 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)
@EqualsAndHashCode.Exclude
@ToString.Exclude
private final boolean[] terrainReady;
@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 +115,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 +143,42 @@ 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);
this.terrainReady = new boolean[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 +881,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();
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 && !terrainReady[index]) {
access.ensure(chunk, cx, cz);
terrainReady[index] = true;
}
return chunk;
}
@ChunkCoordinates
@@ -1509,6 +1550,44 @@ public class MantleWriter implements ObjectPassPlacer, AutoCloseable {
}
}
private final class TerrainAccess {
private final MantleComponent[] components;
private final ChunkContext context;
private final IrisComplex complex;
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) {
@@ -45,6 +45,7 @@ import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
@@ -188,6 +189,23 @@ public class MantleObjectComponentBoundaryRadiusTest {
MantleObjectComponent.calculateInputRadius(Integer.MAX_VALUE, true));
}
@Test
public void eagerInputRadiusCoversEverySourceChunkPlusOneChunkOfFootprint() {
assertEquals(129, MantleObjectComponent.calculateEagerInputRadius(103, false));
assertEquals(65, MantleObjectComponent.calculateEagerInputRadius(33, false));
assertEquals(113, MantleObjectComponent.calculateEagerInputRadius(33, true));
assertEquals(27, MantleObjectComponent.calculateEagerInputRadius(10, false));
assertEquals(1, MantleObjectComponent.calculateEagerInputRadius(0, false));
for (int radius = 0; radius < 300; radius++) {
for (boolean collisions : new boolean[]{false, true}) {
assertTrue(MantleObjectComponent.calculateEagerInputRadius(radius, collisions)
<= MantleObjectComponent.calculateInputRadius(radius, collisions));
}
}
assertEquals(Integer.MAX_VALUE,
MantleObjectComponent.calculateEagerInputRadius(Integer.MAX_VALUE, true));
}
@Test
public void proceduralTreeTransformsContributeToRadius() {
IrisObjectPlacement placement = new IrisObjectPlacement()
@@ -309,8 +309,8 @@ public class MatterGeneratorCarvePassRadiusTest {
}
@Override
public boolean isInputGenerationLazy() {
return lazyInputGeneration;
public int getEagerInputRadius(int inputRadius) {
return lazyInputGeneration ? 0 : inputRadius;
}
@Override
@@ -387,27 +387,94 @@ public class MatterGeneratorConcurrencyTest {
}
@Test
public void contentPhaseRequiresTerrainEvenWhenItsExplicitPrerequisiteIsMissing() {
public void contentPhaseGeneratesTerrainOfEveryChunkBeforeItIsReadOrWritten() {
GeneratorFixture fixture = new GeneratorFixture(true);
List<String> events = new ArrayList<>();
RecordingComponent terrain = new RecordingComponent(ReservedFlag.CARVED, 0, 0) {
@Override
public MatterGenerationPhase getGenerationPhase() {
return MatterGenerationPhase.TERRAIN;
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
assertSame(fixture.mantle.getChunk(x, z), writer.acquireChunk(x, z));
events.add("terrain " + x + "," + z);
}
};
RecordingComponent content = new RecordingComponent(ReservedFlag.OBJECT, 1, 0) {
@Override
public MantleFlag[] getPrerequisiteFlags() {
return new MantleFlag[]{ReservedFlag.CARVED};
}
@Override
public int getInputRadius() {
return 48;
}
@Override
public int getEagerInputRadius(int inputRadius) {
return 0;
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
events.add("content " + x + "," + z);
assertSame(fixture.mantle.getChunk(x + 3, z), writer.acquireChunk(x + 3, z));
writer.acquireChunk(x + 3, z);
events.add("read " + (x + 3) + "," + z);
}
};
TestMatterGenerator generator = fixture.generator(List.of(
new MantlePass(List.of(terrain), 0, 0),
new MantlePass(List.of(content), 0, 0)));
assertThrows(IllegalStateException.class,
() -> generator.generateContentMatter(0, 0, false, fixture.context));
assertFalse(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED));
generator.generateTerrainMatter(0, 0, false, fixture.context);
assertEquals(List.of("terrain 0,0"), events);
generator.generateContentMatter(0, 0, false, fixture.context);
assertEquals(List.of("terrain 0,0", "content 0,0", "terrain 3,0", "read 3,0"), events);
assertTrue(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED));
assertTrue(fixture.mantle.getChunk(3, 0).isFlagged(ReservedFlag.CARVED));
assertFalse(fixture.mantle.getChunk(3, 0).isFlagged(ReservedFlag.OBJECT));
}
@Test
public void contentPhaseGeneratesMissingTerrainOfItsOwnChunk() {
GeneratorFixture fixture = new GeneratorFixture(true);
AtomicInteger terrainRuns = new AtomicInteger();
RecordingComponent terrain = new RecordingComponent(ReservedFlag.CARVED, 0, 0) {
@Override
public MatterGenerationPhase getGenerationPhase() {
return MatterGenerationPhase.TERRAIN;
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
terrainRuns.incrementAndGet();
}
};
RecordingComponent content = new RecordingComponent(ReservedFlag.OBJECT, 1, 0) {
@Override
public MantleFlag[] getPrerequisiteFlags() {
return new MantleFlag[]{ReservedFlag.CARVED};
}
@Override
public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) {
assertEquals(1, terrainRuns.get());
}
};
TestMatterGenerator generator = fixture.generator(List.of(
new MantlePass(List.of(terrain), 0, 0),
new MantlePass(List.of(content), 0, 0)));
generator.generateContentMatter(0, 0, false, fixture.context);
assertEquals(1, terrainRuns.get());
assertTrue(fixture.mantle.getChunk(0, 0).isFlagged(ReservedFlag.OBJECT));
assertTrue(fixture.mantle.getChunk(0, 0).isFlagged(MantleFlag.PLANNED));
}
@Test