Heap-sized terrain bake

- Bake capacity derived from the heap (13 KB per entry against 1/32 of maxMemory, 2048 to 32768 entries); detached runtimes use the floor
- Pregeneration profile no longer raises the global noise cache size or the bake; it only raises burst parallelism
- Removed the hydrology bake reservation, noise cache budget, expansion machinery and the tile cache terrain preparation hook
- Bake thread recents compare a generation counter; clear empties retired entries
- Removed unused column bake, bake registry and bake accessors
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-22 04:08:07 -04:00
1 parent bf9e65252c
commit 6582d9d14d
22 files changed
+188 -1037

No files matched your search

@@ -0,0 +1,105 @@
package art.arcane.iris.generation.bake;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
public class ChunkBakeCacheTest {
private static final long MEGABYTE = 1L << 20;
private static final long GIGABYTE = 1L << 30;
@Test
public void capacityChargesTheGeneratedChunkFootprintAgainstAHeapShare() {
assertEquals(5_041, ChunkBakeCache.capacityFor(2L * GIGABYTE));
assertEquals(10_082, ChunkBakeCache.capacityFor(4L * GIGABYTE));
assertEquals(15_123, ChunkBakeCache.capacityFor(6L * GIGABYTE));
for (long heap = 512L * MEGABYTE; heap <= 64L * GIGABYTE; heap += 256L * MEGABYTE) {
int capacity = ChunkBakeCache.capacityFor(heap);
if (capacity > ChunkBakeCache.MINIMUM_CAPACITY) {
assertTrue("heap " + heap, capacity * ChunkBakeCache.BYTES_PER_ENTRY <= heap / ChunkBakeCache.HEAP_SHARE);
}
}
}
@Test
public void capacityStaysBetweenTheFloorAndTheCeilingAndGrowsWithTheHeap() {
assertEquals(ChunkBakeCache.MINIMUM_CAPACITY, ChunkBakeCache.capacityFor(0L));
assertEquals(ChunkBakeCache.MINIMUM_CAPACITY, ChunkBakeCache.capacityFor(512L * MEGABYTE));
assertEquals(ChunkBakeCache.MAXIMUM_CAPACITY, ChunkBakeCache.capacityFor(64L * GIGABYTE));
assertEquals(ChunkBakeCache.MAXIMUM_CAPACITY, ChunkBakeCache.capacityFor(Long.MAX_VALUE));
int previous = 0;
for (long heap = 0L; heap <= 32L * GIGABYTE; heap += 128L * MEGABYTE) {
int capacity = ChunkBakeCache.capacityFor(heap);
assertTrue(capacity >= previous);
previous = capacity;
}
}
@Test
public void clearRetiresPlanesThatThreadsStillRemember() {
ChunkBakeCache cache = new ChunkBakeCache(new ConstantSource(), 64);
double[] before = (double[]) cache.complete(BakePlane.BASE_HEIGHT, 3, -5);
ChunkPlanes.Plane remembered = cache.planeIfPresent(BakePlane.BASE_HEIGHT, 3, -5);
assertNotNull(remembered);
assertSame(before, cache.complete(BakePlane.BASE_HEIGHT, 3, -5));
cache.clear();
assertNull(cache.planeIfPresent(BakePlane.BASE_HEIGHT, 3, -5));
double[] after = (double[]) cache.complete(BakePlane.BASE_HEIGHT, 3, -5);
assertNotSame(before, after);
assertEquals(before[17], after[17], 0D);
assertNotSame(remembered, cache.planeIfPresent(BakePlane.BASE_HEIGHT, 3, -5));
}
@Test
public void releasedEntriesHoldNoPlanes() {
ChunkPlanes planes = new ChunkPlanes(1, 1);
ConstantSource source = new ConstantSource();
for (BakePlane kind : BakePlane.PLANES) {
planes.complete(kind, source);
assertNotNull(planes.planeIfPresent(kind));
}
planes.release();
for (BakePlane kind : BakePlane.PLANES) {
assertNull(planes.planeIfPresent(kind));
}
}
@Test
public void clearLeavesAnEmptyCacheOfTheSameCapacity() {
ChunkBakeCache cache = new ChunkBakeCache(new ConstantSource(), 64);
for (int chunk = 0; chunk < 8; chunk++) {
cache.complete(BakePlane.HEIGHT, chunk, -chunk);
}
assertEquals(8L, cache.census()[BakePlane.PLANES.length]);
cache.clear();
assertEquals(0L, cache.census()[BakePlane.PLANES.length]);
assertEquals(64, cache.capacity());
}
private static final class ConstantSource implements PlaneSource {
@Override
public void fill(BakePlane plane, int chunkX, int chunkZ, long[] missing, Object values) {
if (values instanceof double[] doubles) {
for (int index = 0; index < 256; index++) {
doubles[index] = chunkX * 1_000D + chunkZ + index;
}
}
}
@Override
public boolean mainThread() {
return false;
}
}
}
@@ -44,8 +44,6 @@ public class HydrologyDiagnosticCacheTest {
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) {
assertSame(tile, cache.get(key));
verify(planner, never()).diagnosticCandidates(tile);
AtomicInteger preparations = new AtomicInteger();
cache.setTerrainPreparation(preparations::incrementAndGet);
List<HydrologyDiagnosticCandidate> result = cache.diagnosticCandidates(key);
supplied.clear();
assertEquals(List.of(candidate), result);
@@ -53,7 +51,6 @@ public class HydrologyDiagnosticCacheTest {
assertSame(result, cache.diagnosticCandidates(key));
verify(planner, times(1)).plan(key);
verify(planner, times(1)).diagnosticCandidates(tile);
assertEquals(1, preparations.get());
}
}
@@ -99,7 +99,6 @@ public class HydrologyTileCacheTest {
new PreparedHydrologyTileStore(root, scope, 64).save(tile);
try (HydrologyTileCache cache = new HydrologyTileCache(planner, 4)) {
cache.enableSharedCache(scope, root);
cache.setTerrainPreparation(() -> { throw new AssertionError("Restoration must not sample terrain"); });
HydrologyTile restored = cache.get(key);
assertEquals(tile.key(), restored.key());
assertEquals(tile.footprint(), restored.footprint());
@@ -877,13 +876,6 @@ public class HydrologyTileCacheTest {
when(tile.tileSize()).thenReturn(64);
HydrologyTileCache first = new HydrologyTileCache(firstPlanner, 4, null, null, scope);
HydrologyTileCache second = new HydrologyTileCache(secondPlanner, 4, null, null, scope);
AtomicInteger terrainPreparations = new AtomicInteger();
first.setTerrainPreparation(terrainPreparations::incrementAndGet);
second.setTerrainPreparation(() -> {
throw new AssertionError("Shared tile reuse must not prepare terrain caches");
});
assertEquals(0, terrainPreparations.get());
assertSame(tile, first.get(key));
assertSame(tile, second.get(key));
@@ -891,7 +883,6 @@ public class HydrologyTileCacheTest {
verify(firstPlanner, times(1)).plan(key);
verify(secondPlanner, never()).plan(key);
verify(secondPlanner, times(1)).reuseResolvedTile(tile);
assertEquals(1, terrainPreparations.get());
}
@Test(timeout = 10000L)
@@ -159,7 +159,7 @@ public class IrisHydrologyPhysicalOceanTest {
shore.setLoadKey("shore");
return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data,
(x, z, height) -> new IrisHydrologyNaturalSample(height, oceanIntent.isOcean(x, z), shore, region),
heights, (x, z) -> "coast", oceanIntent, footprint -> null, () -> false, () -> {}));
heights, (x, z) -> "coast", oceanIntent, footprint -> null, () -> false));
}
private static IrisHydrologyRoutingTerrainSampler sampler(IrisHydrologyRuntime runtime) throws Exception {
@@ -229,7 +229,7 @@ public class IrisHydrologyPolicyCacheTest {
private IrisHydrologyRuntime runtime() {
return new IrisHydrologyRuntime(new IrisHydrologyRuntimeContext(17L, 768, dimension, data,
(x, z, height) -> new IrisHydrologyNaturalSample(height, false, biome.get(), region.get()),
(x, z) -> 180D, (x, z) -> "constant", (x, z) -> false, footprint -> null, () -> false, () -> {}));
(x, z) -> 180D, (x, z) -> "constant", (x, z) -> false, footprint -> null, () -> false));
}
}
@@ -1,96 +0,0 @@
package art.arcane.iris.generation.runtime;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
public class HydrologyNoiseCacheBudgetTest {
@Test
public void reservationsClampToTheRemainingSharedCapacity() {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(4_096L * 15L + 4_095L);
HydrologyNoiseCacheBudget.Reservation first = budget.reserve(4, 10);
HydrologyNoiseCacheBudget.Reservation second = budget.reserve(2, 10);
assertEquals(3, first.additionalChunks());
assertEquals(1, second.additionalChunks());
assertEquals(14L * 4_096L, budget.reservedBytes());
first.close();
first.close();
assertEquals(2L * 4_096L, budget.reservedBytes());
second.close();
assertEquals(0L, budget.reservedBytes());
}
@Test
public void emptyBudgetAndZeroDemandReserveNothing() {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(0L);
try (HydrologyNoiseCacheBudget.Reservation reservation = budget.reserve(5, 32_768)) {
assertEquals(0, reservation.additionalChunks());
}
HydrologyNoiseCacheBudget available = new HydrologyNoiseCacheBudget(1_000_000L);
try (HydrologyNoiseCacheBudget.Reservation reservation = available.reserve(5, 0)) {
assertEquals(0, reservation.additionalChunks());
}
assertEquals(0L, budget.reservedBytes());
assertEquals(0L, available.reservedBytes());
}
@Test
public void requestsCannotExceedTheCapacityLimit() {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
try (HydrologyNoiseCacheBudget.Reservation reservation = budget.reserve(5, 32_768)) {
assertEquals(32_768, reservation.additionalChunks());
assertEquals(5L * 32_768L * 4_096L, budget.reservedBytes());
}
assertThrows(IllegalArgumentException.class, () -> budget.reserve(0, 10));
assertThrows(IllegalArgumentException.class, () -> budget.reserve(5, -1));
assertThrows(IllegalArgumentException.class, () -> budget.reserve(5, 32_769));
assertThrows(IllegalArgumentException.class, () -> new HydrologyNoiseCacheBudget(-1L));
}
@Test(timeout = 10_000L)
public void concurrentOwnersNeverOversubscribeAndReleaseIndependently() throws Exception {
long maximumBytes = 4_096L * 503L;
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(maximumBytes);
CountDownLatch start = new CountDownLatch(1);
List<Future<HydrologyNoiseCacheBudget.Reservation>> pending = new ArrayList<>();
try (ExecutorService workers = Executors.newFixedThreadPool(16)) {
for (int index = 0; index < 32; index++) {
pending.add(workers.submit(() -> {
assertTrue(start.await(5L, TimeUnit.SECONDS));
return budget.reserve(5, 50);
}));
}
start.countDown();
List<HydrologyNoiseCacheBudget.Reservation> reservations = new ArrayList<>();
long expectedBytes = 0L;
for (Future<HydrologyNoiseCacheBudget.Reservation> future : pending) {
HydrologyNoiseCacheBudget.Reservation reservation = future.get(5L, TimeUnit.SECONDS);
reservations.add(reservation);
expectedBytes += reservation.additionalChunks() * 5L * 4_096L;
}
assertEquals(expectedBytes, budget.reservedBytes());
assertTrue(expectedBytes <= maximumBytes);
List<Future<?>> releases = new ArrayList<>();
for (HydrologyNoiseCacheBudget.Reservation reservation : reservations) {
releases.add(workers.submit(() -> {
reservation.close();
reservation.close();
}));
}
for (Future<?> release : releases) {
release.get(5L, TimeUnit.SECONDS);
}
assertEquals(0L, budget.reservedBytes());
}
}
}
@@ -1,11 +1,11 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.generation.bake.BakePlane;
import art.arcane.iris.generation.bake.GeneratorEvaluations;
import art.arcane.iris.generation.block.B;
import art.arcane.iris.generation.image.IrisImageMapApplication;
import art.arcane.iris.generation.image.IrisImageMapRuntime;
import art.arcane.iris.generation.stream.ChunkFillableDoubleStream2D;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.pack.loading.IrisData;
import art.arcane.iris.spi.IrisPlatform;
@@ -67,7 +67,8 @@ public class IrisComplexBakeParityTest {
for (int chunk = 0; chunk < 48; chunk++) {
int chunkX = random.nextInt(-400, 400);
int chunkZ = random.nextInt(-400, 400);
double[] baked = (double[]) batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
double[] baked = new double[256];
batched.baseHeights().fillChunkDoubles(chunkX << 4, chunkZ << 4, baked);
for (int index = 0; index < 256; index++) {
int x = (chunkX << 4) + (index & 15);
int z = (chunkZ << 4) + (index >> 4);
@@ -88,7 +89,7 @@ public class IrisComplexBakeParityTest {
int chunkX = random.nextInt(-400, 400);
int chunkZ = random.nextInt(-400, 400);
GeneratorEvaluations.Snapshot before = GeneratorEvaluations.snapshot();
batched.complex.bakePlane(BakePlane.BASE_HEIGHT, chunkX, chunkZ);
batched.baseHeights().fillChunkDoubles(chunkX << 4, chunkZ << 4, new double[256]);
GeneratorEvaluations.Snapshot afterBatch = GeneratorEvaluations.snapshot();
for (int index = 0; index < 256; index++) {
scalar.complex.getBaseTerrainHeightStream().getDouble((chunkX << 4) + (index & 15), (chunkZ << 4) + (index >> 4));
@@ -195,6 +196,10 @@ public class IrisComplexBakeParityTest {
doReturn(complex).when(engine).getComplex();
}
private ChunkFillableDoubleStream2D baseHeights() {
return (ChunkFillableDoubleStream2D) complex.getBaseTerrainHeightStream();
}
@Override
public void close() {
complex.close();
@@ -1,398 +0,0 @@
package art.arcane.iris.generation.runtime;
import art.arcane.iris.configuration.IrisSettings;
import art.arcane.iris.world.pregen.PregenPerformanceProfile;
import art.arcane.iris.generation.bake.ChunkBakeCache;
import art.arcane.iris.generation.bake.PlaneSource;
import art.arcane.iris.generation.biome.IrisBiome;
import art.arcane.iris.generation.hydrology.HydrologyPlanner;
import art.arcane.iris.generation.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.generation.hydrology.HydrologyTile;
import art.arcane.iris.generation.hydrology.HydrologyTileCache;
import art.arcane.iris.generation.hydrology.HydrologyTileKey;
import art.arcane.iris.generation.hydrology.runtime.IrisHydrologyRuntime;
import art.arcane.iris.generation.stream.CachedDoubleStream2D;
import art.arcane.iris.generation.stream.CachedStream2D;
import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.spi.IrisServices;
import art.arcane.volmlib.util.stream.ProceduralStream;
import org.junit.Test;
import org.mockito.MockedStatic;
import java.lang.reflect.Field;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;
public class IrisComplexHydrologyNoiseCacheTest {
@Test
public void pregenGrowsTheBackingHeightCacheWithoutReplacingItsThreadAwareStream() throws Exception {
IrisSettings previousSettings = IrisSettings.settings;
IrisSettings.settings = new IrisSettings();
IrisSettings.get().getPerformance().setNoiseCacheSize(1_024);
try (Fixture fixture = new Fixture(1_024)) {
ProceduralStream<Double> wrapper = fixture.complex.getRawHeightStream();
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
PregenPerformanceProfile.apply(fixture.engine);
assertTrue(fixture.bakes.capacity() >= 4_096);
assertEquals(1_024L * 256L, fixture.biomes.getMaxSize());
assertSame(wrapper, fixture.complex.getRawHeightStream());
assertEquals(47D, wrapper.getDouble(18, 5), 0D);
assertEquals(13D, fixture.height.getDouble(18, 5), 0D);
} finally {
PregenPerformanceProfile.restore();
IrisSettings.settings = previousSettings;
}
}
@Test
public void pregenPreservesActiveHydrologyGrants() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.expandHydrologyNoiseCaches(budget);
long reservedBytes = budget.reservedBytes();
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
fixture.assertCapacities(32_768);
assertTrue(fixture.bakes.capacity() >= 4_096);
assertEquals(reservedBytes, budget.reservedBytes());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void pregenPreservesLargerRuntimeCacheCapacities() throws Exception {
try (Fixture fixture = new Fixture(65_536)) {
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
fixture.assertCapacities(65_536);
assertTrue(fixture.bakes.capacity() >= 65_536);
}
}
@Test
public void idleRuntimeReservesOnlyOnDemandAndRestoresAfterHydrologyDrains() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
HydrologyTileCache cache = fixture.planningCache(budget);
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
cache.get(new HydrologyTileKey(0, 0));
cache.get(new HydrologyTileKey(0, 0));
fixture.assertCapacities(32_768);
assertEquals(4L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
doAnswer(invocation -> {
fixture.assertCapacities(32_768);
assertEquals(4L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
cache.close();
return null;
}).when(fixture.hydrology).close();
fixture.complex.close();
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
doNothing().when(fixture.hydrology).close();
fixture.complex.close();
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
}
}
@Test(timeout = 10_000L)
public void concurrentCacheExpansionsShareOneReservation() throws Exception {
try (Fixture fixture = new Fixture(1_024);
ExecutorService callers = Executors.newFixedThreadPool(2)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
CountDownLatch entered = new CountDownLatch(2);
CountDownLatch release = new CountDownLatch(1);
Runnable prepare = () -> {
entered.countDown();
await(release);
fixture.complex.expandHydrologyNoiseCaches(budget);
};
Future<?> first = callers.submit(prepare);
Future<?> second = callers.submit(prepare);
assertTrue(entered.await(5L, TimeUnit.SECONDS));
assertEquals(0L, budget.reservedBytes());
release.countDown();
first.get(5L, TimeUnit.SECONDS);
second.get(5L, TimeUnit.SECONDS);
fixture.assertCapacities(32_768);
assertEquals(4L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
}
}
@Test(timeout = 10_000L)
public void closeDrainsAnAdmittedFirstDemandBeforeReleasingItsGrant() throws Exception {
try (Fixture fixture = new Fixture(1_024);
ExecutorService callers = Executors.newFixedThreadPool(2)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
HydrologyTileCache cache = fixture.planningCache(budget);
CountDownLatch preparing = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch closing = new CountDownLatch(1);
cache.setTerrainPreparation(() -> {
preparing.countDown();
await(release);
fixture.complex.expandHydrologyNoiseCaches(budget);
});
doAnswer(invocation -> {
closing.countDown();
cache.close();
return null;
}).when(fixture.hydrology).close();
Future<?> demand = callers.submit(() -> cache.get(new HydrologyTileKey(0, 0)));
assertTrue(preparing.await(5L, TimeUnit.SECONDS));
Future<?> close = callers.submit(fixture.complex::close);
assertTrue(closing.await(5L, TimeUnit.SECONDS));
try {
assertThrows(TimeoutException.class, () -> close.get(100L, TimeUnit.MILLISECONDS));
assertEquals(0L, budget.reservedBytes());
} finally {
release.countDown();
}
demand.get(5L, TimeUnit.SECONDS);
close.get(5L, TimeUnit.SECONDS);
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void volumetricNaturalHeightReservesItsDistinctFifthCache() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
CachedDoubleStream2D natural = fixture.heightCache(1_024);
fixture.complex.setUnblendedNaturalHeightStream(natural);
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.assertCapacities(32_768);
assertEquals(32_768L * 256L, natural.getMaxSize());
assertEquals(5L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
fixture.complex.close();
assertEquals(1_024L * 256L, natural.getMaxSize());
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void partialGrantPreservesTheConfiguredFloorForEveryCache() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(4L * 80L * 4_096L + 4_095L);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.assertCapacities(1_104);
assertEquals(4L * 80L * 4_096L, budget.reservedBytes());
fixture.complex.close();
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void configuredCapacityAboveTargetRemainsUnchanged() throws Exception {
try (Fixture fixture = new Fixture(65_536)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.assertCapacities(65_536);
assertEquals(0L, budget.reservedBytes());
fixture.complex.close();
fixture.assertCapacities(65_536);
}
}
@Test
public void detachedAndNonHydrologyRuntimesDoNotReserve() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
setField(fixture.complex, "detached", true);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.complex.reserveHydrologyBake(budget);
fixture.assertCapacities(1_024);
assertEquals(1_024, fixture.bakes.capacity());
setField(fixture.complex, "detached", false);
fixture.complex.setHydrologyRuntime(null);
fixture.complex.expandHydrologyNoiseCaches(budget);
fixture.complex.reserveHydrologyBake(budget);
fixture.assertCapacities(1_024);
assertEquals(1_024, fixture.bakes.capacity());
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void plannerReservationGrowsTheBakeToTheHydrologyTarget() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.reserveHydrologyBake(budget);
fixture.complex.reserveHydrologyBake(budget);
assertEquals(32_768, fixture.bakes.capacity());
assertEquals(2L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
fixture.complex.ensureTerrainNoiseCacheSize(4_096);
assertEquals(32_768, fixture.bakes.capacity());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
fixture.complex.reserveHydrologyBake(budget);
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void plannerReservationTakesOnlyTheGrantedShareOfTheBudget() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(2L * 80L * 4_096L + 4_095L);
fixture.complex.reserveHydrologyBake(budget);
assertEquals(1_104, fixture.bakes.capacity());
assertEquals(2L * 80L * 4_096L, budget.reservedBytes());
fixture.complex.close();
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void plannerReservationLeavesLargerBakesUnchanged() throws Exception {
try (Fixture fixture = new Fixture(65_536)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.reserveHydrologyBake(budget);
assertEquals(65_536, fixture.bakes.capacity());
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void failedHydrologyDrainRetainsTheReservationUntilRetry() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
HydrologyTileCache cache = fixture.planningCache(budget);
cache.get(new HydrologyTileKey(0, 0));
doThrow(new IllegalStateException("Hydrology is still draining")).when(fixture.hydrology).close();
assertThrows(IllegalStateException.class, fixture.complex::close);
fixture.assertCapacities(32_768);
assertEquals(4L * (32_768L - 1_024L) * 4_096L, budget.reservedBytes());
doAnswer(invocation -> {
cache.close();
return null;
}).when(fixture.hydrology).close();
fixture.complex.close();
fixture.assertCapacities(1_024);
assertEquals(0L, budget.reservedBytes());
}
}
@Test
public void mappedStreamRetainsOnlyTheFinalCacheAtTheExpandedCapacity() throws Exception {
try (Fixture fixture = new Fixture(1_024)) {
CachedStream2D<IrisBiome> inner = fixture.biomes;
CachedStream2D<IrisBiome> mapped = new CachedStream2D<>("mapped", fixture.engine, inner, 1_024);
fixture.complex.setBaseBiomeStream(mapped);
HydrologyNoiseCacheBudget budget = new HydrologyNoiseCacheBudget(Long.MAX_VALUE);
fixture.complex.expandHydrologyNoiseCaches(budget);
assertEquals(1_024L * 256L, inner.getMaxSize());
assertEquals(32_768L * 256L, mapped.getMaxSize());
fixture.complex.close();
assertEquals(1_024L * 256L, mapped.getMaxSize());
}
}
private static void setField(IrisComplex complex, String name, Object value) throws Exception {
Field field = IrisComplex.class.getDeclaredField(name);
field.setAccessible(true);
field.set(complex, value);
}
private static void await(CountDownLatch latch) {
try {
assertTrue(latch.await(5L, TimeUnit.SECONDS));
} catch (InterruptedException failure) {
Thread.currentThread().interrupt();
throw new IllegalStateException(failure);
}
}
private static final class Fixture implements AutoCloseable {
private final MockedStatic<IrisServices> services = mockStatic(IrisServices.class);
private final Engine engine = mock(Engine.class);
private final IrisComplex complex = mock(IrisComplex.class, CALLS_REAL_METHODS);
private final IrisHydrologyRuntime hydrology = mock(IrisHydrologyRuntime.class);
private final CachedStream2D<IrisBiome> biomes;
private final CachedStream2D<IrisRegion> regions;
private final CachedStream2D<InferredType> bridge;
private final CachedDoubleStream2D height;
private final ChunkBakeCache bakes;
private Fixture(int capacity) throws Exception {
services.when(() -> IrisServices.get(PreservationRegistry.class)).thenReturn(mock(PreservationRegistry.class));
biomes = objectCache(capacity);
regions = objectCache(capacity);
bridge = objectCache(capacity);
height = heightCache(capacity);
bakes = new ChunkBakeCache(mock(PlaneSource.class), capacity);
complex.setNaturalHeightStream(height);
complex.setHeightStream(ProceduralStream.ofDouble((x, z) -> 47D));
setField(complex, "bakeCache", bakes);
when(engine.getComplex()).thenReturn(complex);
complex.setBaseBiomeStream(biomes);
complex.setRegionStream(regions);
complex.setBridgeStream(bridge);
complex.setBaseTerrainHeightStream(height);
complex.setUnblendedNaturalHeightStream(height);
complex.setHydrologyRuntime(hydrology);
}
@Override
public void close() {
services.close();
}
private <T> CachedStream2D<T> objectCache(int capacity) {
@SuppressWarnings("unchecked")
ProceduralStream<T> source = mock(ProceduralStream.class);
return new CachedStream2D<>("cache", engine, source, capacity);
}
private CachedDoubleStream2D heightCache(int capacity) {
return new CachedDoubleStream2D("height", engine, ProceduralStream.ofDouble((x, z) -> x - z), capacity);
}
private HydrologyTileCache planningCache(HydrologyNoiseCacheBudget budget) {
HydrologyPlanner planner = mock(HydrologyPlanner.class);
when(planner.settings()).thenReturn(HydrologyPlannerSettings.defaults());
when(planner.plan(any(HydrologyTileKey.class))).thenReturn(mock(HydrologyTile.class));
HydrologyTileCache cache = new HydrologyTileCache(planner, 4);
cache.setTerrainPreparation(() -> complex.expandHydrologyNoiseCaches(budget));
doAnswer(invocation -> {
cache.close();
return null;
}).when(hydrology).close();
return cache;
}
private void assertCapacities(int chunks) {
assertEquals(chunks * 256L, biomes.getMaxSize());
assertEquals(chunks * 256L, regions.getMaxSize());
assertEquals(chunks * 256L, bridge.getMaxSize());
assertEquals(chunks * 256L, height.getMaxSize());
}
}
}
@@ -86,7 +86,7 @@ public class IrisEngineGenerationRuntimeScopeTest {
when(historical.complex.getHydrologyRuntime()).thenReturn(hydrology);
IrisHydrologyRuntimeContext context = new IrisHydrologyRuntimeContext(17L, 384, historical.dimension,
historical.data, (x, z, height) -> null, (x, z) -> 80D, (x, z) -> "constant",
(x, z) -> false, footprint -> null, () -> false, () -> {});
(x, z) -> false, footprint -> null, () -> false);
Field runtimeContext = IrisHydrologyRuntime.class.getDeclaredField("context");
runtimeContext.setAccessible(true);
runtimeContext.set(hydrology, context);
@@ -15,7 +15,6 @@ import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -46,13 +45,17 @@ public class PregenPerformanceProfileTest {
}
@Test
public void directEngineProfileResizesActualTerrainCachesWithoutRebuildingHistory() {
PregenPerformanceProfile.apply(engine);
public void directEngineProfileLeavesTerrainCachesAndHistoryUntouched() {
try {
PregenPerformanceProfile.apply(engine);
assertEquals(4_096, IrisSettings.get().getPerformance().getNoiseCacheSize());
verify(complex).ensureTerrainNoiseCacheSize(4_096);
verify(complex, never()).getHeightStream();
verify(engine, never()).hotloadComplex();
assertEquals(1_024, IrisSettings.get().getPerformance().getNoiseCacheSize());
verify(engine, never()).getComplex();
verify(complex, never()).getHeightStream();
verify(engine, never()).hotloadComplex();
} finally {
PregenPerformanceProfile.restore();
}
}
@Test
@@ -60,42 +63,26 @@ public class PregenPerformanceProfileTest {
PlatformChunkGenerator generator = mock(PlatformChunkGenerator.class);
when(generator.getEngine()).thenReturn(engine);
PregenPerformanceProfile.applyToGenerator(generator);
try {
PregenPerformanceProfile.applyToGenerator(generator);
verify(complex).ensureTerrainNoiseCacheSize(4_096);
verify(generator, never()).hotloadComplexAsync(30L, TimeUnit.SECONDS);
verify(engine, never()).hotloadComplex();
verify(generator, never()).hotloadComplexAsync(30L, TimeUnit.SECONDS);
verify(engine, never()).hotloadComplex();
} finally {
PregenPerformanceProfile.restore();
}
}
@Test
public void largerConfiguredCachesRemainLarger() {
IrisSettings.get().getPerformance().setNoiseCacheSize(8_192);
PregenPerformanceProfile.apply(engine);
assertEquals(8_192, IrisSettings.get().getPerformance().getNoiseCacheSize());
verify(complex).ensureTerrainNoiseCacheSize(8_192);
}
@Test
public void failedCachePreparationDoesNotChangeGlobalTuningOrAcquireAParallelismHold() {
int holders = PregenPerformanceProfile.parallelismHolders();
int parallelism = MultiBurst.burst.parallelism();
doThrow(new IllegalStateException("Runtime closed"))
.when(complex).ensureTerrainNoiseCacheSize(4_096);
assertThrows(IllegalStateException.class, () -> PregenPerformanceProfile.apply(engine));
public void enginesCreatedDuringOrAfterPregenerationKeepTheConfiguredNoiseCacheSize() {
try {
PregenPerformanceProfile.applyToGenerator(null);
assertEquals(1_024, IrisSettings.get().getPerformance().getNoiseCacheSize());
} finally {
PregenPerformanceProfile.restore();
}
assertEquals(1_024, IrisSettings.get().getPerformance().getNoiseCacheSize());
assertEquals(parallelism, MultiBurst.burst.parallelism());
assertEquals(holders, PregenPerformanceProfile.parallelismHolders());
}
@Test
public void profileBeforeWorldCreationStillAppliesGlobalTuning() {
PregenPerformanceProfile.applyToGenerator(null);
assertEquals(4_096, IrisSettings.get().getPerformance().getNoiseCacheSize());
}
@Test