mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-12 03:24:14 +00:00
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:
@@ -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) {
|
||||
|
||||
+18
@@ -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()
|
||||
|
||||
+2
-2
@@ -309,8 +309,8 @@ public class MatterGeneratorCarvePassRadiusTest {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInputGenerationLazy() {
|
||||
return lazyInputGeneration;
|
||||
public int getEagerInputRadius(int inputRadius) {
|
||||
return lazyInputGeneration ? 0 : inputRadius;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+71
-4
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user