mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
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:
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -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)
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
-96
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
-3
@@ -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();
|
||||
|
||||
-398
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in new issue
Block a user