mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 23:10:32 +00:00
Stabilized shutdown and Studio
- Shutdown protection initializes before enable and drains inline when retention fails - Cleanup preserves class loader retention and closes native providers in order - Error reports retain context and handle empty stack traces - Studio maps retain partial coverage and suspend hidden views - Hydrology previews refresh when plans and rasters complete - Terrain scratch hashing spreads lattice samples across cache slots - Plugin and probe artifacts include LWJGL libraries and native resources - Regression tests cover shutdown, error reporting, map progress, and terrain scratch
This commit is contained in:
1 parent
f64a7c6753
commit
3dc958edcf
31 files changed
+1973
-217
No files matched your search
+92
@@ -70,6 +70,7 @@ import java.util.stream.Stream;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -143,6 +144,95 @@ public class HydrologyRuntimeTest {
|
||||
assertTrue(planners.plannerThreads().stream().noneMatch(name -> name.equals(Thread.currentThread().getName())));
|
||||
}
|
||||
|
||||
@Test(timeout = 180_000L)
|
||||
public void previewRevisionAdvancesOnlyWhenPlansOrRastersBecomeAvailable() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(settings(0, false));
|
||||
ExecutorService pool = pool(planners, 1);
|
||||
HydrologyRuntime runtime = runtime(planners, pool, () -> false);
|
||||
CountDownLatch blocker = block(pool);
|
||||
CompletableFuture<Void> plans = runtime.prepareWindowsAround(0, 0, 0, 0, 1);
|
||||
|
||||
assertEquals(0L, runtime.previewRevision());
|
||||
assertFalse(plans.isDone());
|
||||
blocker.countDown();
|
||||
plans.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
long planned = runtime.previewRevision();
|
||||
assertTrue(planned > 0L);
|
||||
assertTrue(runtime.planPreview(0, 0, 15, 15).complete());
|
||||
assertEquals(planned, runtime.previewRevision());
|
||||
|
||||
CountDownLatch rasterBlocker = block(pool);
|
||||
for (int repeat = 0; repeat < 100; repeat++) {
|
||||
assertTrue(runtime.rasterPreview(0, 0).provisional());
|
||||
assertEquals(planned, runtime.previewRevision());
|
||||
}
|
||||
rasterBlocker.countDown();
|
||||
await(() -> runtime.previewRevision() == planned + 1L, "raster publication advances preview readiness");
|
||||
ChunkWaterRaster ready = runtime.rasterPreview(0, 0);
|
||||
assertFalse(ready.provisional());
|
||||
for (int repeat = 0; repeat < 100; repeat++) {
|
||||
assertSame(ready, runtime.rasterPreview(0, 0));
|
||||
assertSame(ready, runtime.raster(0, 0));
|
||||
assertEquals(planned + 1L, runtime.previewRevision());
|
||||
}
|
||||
assertFalse(runtime.raster(1, 0).provisional());
|
||||
assertEquals(planned + 2L, runtime.previewRevision());
|
||||
}
|
||||
|
||||
@Test(timeout = 180_000L)
|
||||
public void concurrentRasterPublicationAdvancesPreviewRevisionOnce() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(settings(0, false));
|
||||
ExecutorService pool = pool(planners, 1);
|
||||
HydrologyRuntime runtime = runtime(planners, pool, () -> false);
|
||||
runtime.prepareWindowsAround(0, 0, 0, 0, 1).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
long planned = runtime.previewRevision();
|
||||
CountDownLatch gate = new CountDownLatch(1);
|
||||
planners.naturalGate = gate;
|
||||
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||
Thread demand = new Thread(() -> {
|
||||
try {
|
||||
runtime.raster(0, 0);
|
||||
} catch (Throwable error) {
|
||||
failure.set(error);
|
||||
}
|
||||
});
|
||||
demand.start();
|
||||
try {
|
||||
await(() -> planners.naturalSamples.get() > 0, "demanded raster starts");
|
||||
assertTrue(runtime.rasterPreview(0, 0).provisional());
|
||||
await(() -> planners.naturalSamples.get() >= 2, "preview raster starts beside demand");
|
||||
assertEquals(planned, runtime.previewRevision());
|
||||
} finally {
|
||||
gate.countDown();
|
||||
demand.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS));
|
||||
}
|
||||
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
assertFalse(demand.isAlive());
|
||||
assertNull(failure.get());
|
||||
assertFalse(runtime.rasterPreview(0, 0).provisional());
|
||||
assertEquals(planned + 1L, runtime.previewRevision());
|
||||
}
|
||||
|
||||
@Test(timeout = 180_000L)
|
||||
public void failedRasterDoesNotAdvancePreviewRevision() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(settings(0, false));
|
||||
ExecutorService pool = pool(planners, 1);
|
||||
HydrologyPlanCache cache = new HydrologyPlanCache(planners.settings, planners::region, planners::window, pool, () -> false);
|
||||
HydrologyRuntime runtime = new HydrologyRuntime(planners.settings, cache, (x, z) -> {
|
||||
throw new IllegalStateException("terrain sampling failed");
|
||||
}, () -> false, planners.ticker::get);
|
||||
runtimes.add(runtime);
|
||||
runtime.prepareWindowsAround(0, 0, 0, 0, 1).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
long planned = runtime.previewRevision();
|
||||
|
||||
assertThrows(IllegalStateException.class, () -> runtime.raster(0, 0));
|
||||
|
||||
assertEquals(planned, runtime.previewRevision());
|
||||
}
|
||||
|
||||
@Test(timeout = 180_000L)
|
||||
public void rasterCachedPerChunk() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(settings());
|
||||
@@ -868,6 +958,7 @@ public class HydrologyRuntimeTest {
|
||||
|
||||
private static void assertDisabled(HydrologyRuntime runtime) {
|
||||
assertFalse(runtime.enabled());
|
||||
assertEquals(0L, runtime.previewRevision());
|
||||
assertSame(ChunkWaterRaster.EMPTY, runtime.raster(0, 0));
|
||||
assertSame(ChunkWaterRaster.EMPTY, runtime.rasterSnapshot(0, 0));
|
||||
assertSame(ChunkWaterRaster.EMPTY, runtime.rasterAt(0, 0));
|
||||
@@ -879,6 +970,7 @@ public class HydrologyRuntimeTest {
|
||||
assertSame(ChunkWaterRaster.EMPTY, runtime.rasterPreview(0, 0));
|
||||
assertEquals(HydrologyPlanPreview.EMPTY, runtime.planPreview(0, 0, 511, 511));
|
||||
runtime.close();
|
||||
assertEquals(0L, runtime.previewRevision());
|
||||
}
|
||||
|
||||
private static void settle(FakePlanners planners, ExecutorService pool, int windows) throws Exception {
|
||||
|
||||
+67
@@ -139,6 +139,73 @@ public class HydrologyPlanCacheTest {
|
||||
assertEquals(KEY, results[0].plan().key());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readyRevisionCountsSuccessfulPlanPublicationsWithoutCountingReads() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(unpersisted());
|
||||
ExecutorService pool = pool(1);
|
||||
HydrologyPlanCache cache = cache(planners, pool, () -> false);
|
||||
RegionKey region = WindowPlanner.lakeRegions(KEY).getFirst();
|
||||
Gate gate = new Gate();
|
||||
planners.hook = key -> gate.pass();
|
||||
CompletableFuture<RegionPlan> pending = cache.previewRegion(region);
|
||||
await(() -> gate.arrivals() == 1, "region planning starts before publication");
|
||||
assertEquals(0L, cache.readyRevision());
|
||||
assertFalse(pending.isDone());
|
||||
gate.release();
|
||||
RegionPlan ready = pending.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
assertEquals(1L, cache.readyRevision());
|
||||
assertSame(ready, cache.regionIfReady(region).orElseThrow());
|
||||
assertSame(ready, cache.previewRegion(region).get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
|
||||
assertSame(ready, cache.region(region));
|
||||
assertEquals(1L, cache.readyRevision());
|
||||
|
||||
cache.previewWindow(KEY).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
long published = WindowPlanner.lakeRegions(KEY).size() + 1L;
|
||||
assertEquals(published, cache.readyRevision());
|
||||
assertTrue(cache.windowIfReady(KEY).isPresent());
|
||||
cache.window(KEY);
|
||||
assertEquals(published, cache.readyRevision());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void failedPlansDoNotAdvanceReadyRevision() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(unpersisted());
|
||||
ExecutorService pool = pool(1);
|
||||
HydrologyPlanCache cache = cache(planners, pool, () -> false);
|
||||
RegionKey region = new RegionKey(0, 0);
|
||||
planners.hook = key -> {
|
||||
throw new IllegalStateException("region planning failed");
|
||||
};
|
||||
|
||||
CompletableFuture<RegionPlan> failed = cache.previewRegion(region);
|
||||
assertThrows(ExecutionException.class, () -> failed.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
|
||||
assertEquals(0L, cache.readyRevision());
|
||||
planners.hook = key -> { };
|
||||
cache.previewRegion(region).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
assertEquals(1L, cache.readyRevision());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelledPlanCompletionDoesNotAdvanceReadyRevision() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(unpersisted());
|
||||
ExecutorService pool = pool(1);
|
||||
HydrologyPlanCache cache = cache(planners, pool, () -> false);
|
||||
Gate gate = new Gate();
|
||||
planners.hook = key -> gate.pass();
|
||||
CompletableFuture<RegionPlan> pending = cache.previewRegion(new RegionKey(0, 0));
|
||||
await(() -> gate.arrivals() == 1, "region planning starts before cancellation");
|
||||
|
||||
cache.close();
|
||||
gate.release();
|
||||
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
|
||||
assertTrue(pending.isCompletedExceptionally());
|
||||
assertEquals(0L, cache.readyRevision());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void interruptedPlanIsNotCached() throws Exception {
|
||||
FakePlanners planners = new FakePlanners(unpersisted());
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package art.arcane.iris.generation.noise;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class TerrainBatchScratchTest {
|
||||
@Test
|
||||
public void integerLatticesDistributeWithoutLongLinearProbeClusters() throws Exception {
|
||||
TerrainBatchScratch scratch = new TerrainBatchScratch();
|
||||
for (int origin : new int[]{0, -1536, 1536, 16_777_212}) {
|
||||
scratch.clearLattice(4096);
|
||||
for (int index = 0; index < 1024; index++) {
|
||||
double x = origin + (index & 31) * 6D;
|
||||
double z = origin + (index >> 5) * 6D;
|
||||
assertEquals(index, scratch.lattice(x, z));
|
||||
}
|
||||
assertEquals(1024, scratch.latticeCount());
|
||||
for (int index = 1023; index >= 0; index--) {
|
||||
double x = origin + (index & 31) * 6D;
|
||||
double z = origin + (index >> 5) * 6D;
|
||||
assertEquals(index, scratch.lattice(x, z));
|
||||
assertEquals(x, scratch.latticeX(index), 0D);
|
||||
assertEquals(z, scratch.latticeZ(index), 0D);
|
||||
}
|
||||
assertTrue("clustered lattice at " + origin, longestCluster(scratch) < 32);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rawCoordinateIdentityAndInsertionOrderSurviveRehashingAndReset() {
|
||||
TerrainBatchScratch scratch = new TerrainBatchScratch();
|
||||
long[] bits = {0L, Long.MIN_VALUE, Double.doubleToRawLongBits(1D), Double.doubleToRawLongBits(-1D),
|
||||
Double.doubleToRawLongBits(0.125D), Double.doubleToRawLongBits(-0.125D),
|
||||
Double.doubleToRawLongBits(Double.POSITIVE_INFINITY), Double.doubleToRawLongBits(Double.NEGATIVE_INFINITY),
|
||||
0x7FF8000000000001L, 0x7FF8000000000002L};
|
||||
for (int pass = 0; pass < 2; pass++) {
|
||||
scratch.clearLattice(bits.length * bits.length);
|
||||
for (int x = 0; x < bits.length; x++) {
|
||||
for (int z = 0; z < bits.length; z++) {
|
||||
int expected = x * bits.length + z;
|
||||
double worldX = Double.longBitsToDouble(bits[x]);
|
||||
double worldZ = Double.longBitsToDouble(bits[z]);
|
||||
assertEquals(expected, scratch.lattice(worldX, worldZ));
|
||||
assertEquals(expected, scratch.lattice(worldX, worldZ));
|
||||
assertEquals(bits[x], Double.doubleToRawLongBits(scratch.latticeX(expected)));
|
||||
assertEquals(bits[z], Double.doubleToRawLongBits(scratch.latticeZ(expected)));
|
||||
}
|
||||
}
|
||||
assertEquals(bits.length * bits.length, scratch.latticeCount());
|
||||
}
|
||||
}
|
||||
|
||||
private static int longestCluster(TerrainBatchScratch scratch) throws Exception {
|
||||
Field field = TerrainBatchScratch.class.getDeclaredField("latticeSlots");
|
||||
field.setAccessible(true);
|
||||
int[] slots = (int[]) field.get(scratch);
|
||||
int longest = 0;
|
||||
int length = 0;
|
||||
for (int index = 0; index < slots.length * 2; index++) {
|
||||
length = slots[index % slots.length] == 0 ? 0 : length + 1;
|
||||
longest = Math.max(longest, length);
|
||||
}
|
||||
return longest;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package art.arcane.iris.generation.runtime;
|
||||
|
||||
import art.arcane.iris.spi.IrisLogging;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.doCallRealMethod;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class IrisEngineFailureReportingTest {
|
||||
@Test
|
||||
public void failedGenerationReportsOnceAndRetainsItsCause() throws Exception {
|
||||
IrisEngine engine = mock(IrisEngine.class);
|
||||
IllegalStateException failure = new IllegalStateException("Hydrology raster failed");
|
||||
Field platformHooks = IrisEngine.class.getDeclaredField("platformHooks");
|
||||
platformHooks.setAccessible(true);
|
||||
platformHooks.set(engine, mock(EnginePlatformHooks.class));
|
||||
doThrow(failure).when(engine).openGenerationHistoryCoordinateScope(-352, -144);
|
||||
doCallRealMethod().when(engine).generate(-352, -144, null, null, false);
|
||||
doCallRealMethod().when(engine).fail("Failed to generate -352, -144", failure);
|
||||
doCallRealMethod().when(engine).hasFailed();
|
||||
|
||||
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||
WrongEngineBroException reported = assertThrows(WrongEngineBroException.class,
|
||||
() -> engine.generate(-352, -144, null, null, false));
|
||||
|
||||
assertSame(failure, reported.getCause());
|
||||
assertTrue(engine.hasFailed());
|
||||
logging.verify(() -> IrisLogging.error("Failed to generate -352, -144"));
|
||||
logging.verify(() -> IrisLogging.reportError(failure));
|
||||
logging.verifyNoMoreInteractions();
|
||||
}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package art.arcane.iris.platform.bukkit;
|
||||
|
||||
import art.arcane.iris.spi.LogLevel;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class BukkitPlatformErrorReportingTest {
|
||||
@After
|
||||
public void releaseHost() {
|
||||
BukkitPlatform.releaseHost();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contextualReportEmitsOneCompleteTraceThroughTheHost() {
|
||||
ArrayList<LogEntry> messages = new ArrayList<>();
|
||||
ArrayList<Throwable> reported = new ArrayList<>();
|
||||
BukkitPlatform.hostBridge(new BukkitPlatform.HostBridge(
|
||||
(level, message) -> messages.add(new LogEntry(level, message)), null,
|
||||
failure -> {
|
||||
reported.add(failure);
|
||||
failure.printStackTrace(System.err);
|
||||
}, null, null, null, null, null, null));
|
||||
IllegalStateException failure = new IllegalStateException("raster failure",
|
||||
new ArrayIndexOutOfBoundsException("invalid sample"));
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
PrintStream previousError = System.err;
|
||||
try (PrintStream errors = new PrintStream(output, true, StandardCharsets.UTF_8)) {
|
||||
System.setErr(errors);
|
||||
|
||||
new BukkitPlatform().reportError("Generation failed", failure);
|
||||
} finally {
|
||||
System.setErr(previousError);
|
||||
}
|
||||
|
||||
assertEquals(List.of(new LogEntry(LogLevel.ERROR, "Generation failed")), messages);
|
||||
assertEquals(List.of(failure), reported);
|
||||
String trace = output.toString(StandardCharsets.UTF_8);
|
||||
assertEquals(1L, trace.lines().filter(line -> line.contains("raster failure")).count());
|
||||
assertEquals(1L, trace.lines().filter(line -> line.contains("Caused by:")).count());
|
||||
assertTrue(trace.contains("BukkitPlatformErrorReportingTest.contextualReportEmitsOneCompleteTraceThroughTheHost"));
|
||||
}
|
||||
|
||||
private record LogEntry(LogLevel level, String message) {
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,10 @@ import org.junit.Test;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
@@ -38,13 +41,23 @@ public class BukkitBindingTest {
|
||||
Fixture fixture = new Fixture();
|
||||
when(fixture.lifecycle.isServerStopping()).thenReturn(true);
|
||||
assertTrue(fixture.binding.isServerStopping());
|
||||
when(fixture.lifecycle.deferPluginClassLoaderClose()).thenReturn(true);
|
||||
assertTrue(fixture.binding.deferPluginClassLoaderClose());
|
||||
fixture.binding.initializeLifecycle(getClass().getClassLoader());
|
||||
verify(fixture.lifecycle).initializeLifecycle(getClass().getClassLoader());
|
||||
fixture.binding.deferPluginClassLoaderClose();
|
||||
fixture.binding.releasePluginClassLoaderClose();
|
||||
verify(fixture.lifecycle).deferPluginClassLoaderClose();
|
||||
verify(fixture.lifecycle).releasePluginClassLoaderClose();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void limitedBindingRejectsClassLoaderProtectionAndUsesSynchronousShutdown() {
|
||||
INMSBinding binding = mock(INMSBinding.class, CALLS_REAL_METHODS);
|
||||
assertFalse(binding.isServerStopping());
|
||||
assertThrows(IllegalStateException.class,
|
||||
() -> binding.initializeLifecycle(getClass().getClassLoader()));
|
||||
assertThrows(IllegalStateException.class, binding::deferPluginClassLoaderClose);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void injectAttachesVanillaSpawnersOnlyToOverworldEnvironments() throws NoSuchFieldException, IllegalAccessException {
|
||||
Fixture fixture = new Fixture();
|
||||
@@ -71,8 +84,8 @@ public class BukkitBindingTest {
|
||||
}
|
||||
|
||||
private static final class Fixture {
|
||||
private final NativeTerrainAccess terrain = mock(NativeTerrainAccess.class);
|
||||
private final NativeWorldGeneration generation = mock(NativeWorldGeneration.class);
|
||||
private final NativeTerrainAccess terrain = mock(NativeTerrainAccess.class);
|
||||
private final NativeWorldLifecycleFactory.Controller lifecycle = mock(NativeWorldLifecycleFactory.Controller.class);
|
||||
private final BukkitBinding binding;
|
||||
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package art.arcane.iris.platform.bukkit.nms;
|
||||
|
||||
import art.arcane.iris.platform.agent.Agent;
|
||||
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||
import art.arcane.iris.testsupport.PlatformBinding;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
import org.mockito.MockedStatic;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
public class BukkitWorldLifecyclePolicyTest {
|
||||
@ClassRule
|
||||
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
|
||||
|
||||
@Test
|
||||
public void nativeProvidersCloseBeforeThePluginClassLoaderIsReleased() throws Exception {
|
||||
ClassLoader loader = mock(ClassLoader.class);
|
||||
try (MockedStatic<SlimJar> providers = mockStatic(SlimJar.class);
|
||||
MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||
agent.when(() -> Agent.releaseClassLoader(loader)).thenAnswer(invocation -> {
|
||||
providers.verify(SlimJar::closeNativeProviders);
|
||||
return null;
|
||||
});
|
||||
BukkitWorldLifecyclePolicy.INSTANCE.releaseClassLoader(loader);
|
||||
agent.verify(() -> Agent.releaseClassLoader(loader));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nativeProviderCloseFailureStillReleasesThePluginClassLoader() throws Exception {
|
||||
ClassLoader loader = mock(ClassLoader.class);
|
||||
IOException failure = new IOException("provider close failed");
|
||||
try (MockedStatic<SlimJar> providers = mockStatic(SlimJar.class);
|
||||
MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||
providers.when(SlimJar::closeNativeProviders).thenThrow(failure);
|
||||
IllegalStateException reported = assertThrows(IllegalStateException.class,
|
||||
() -> BukkitWorldLifecyclePolicy.INSTANCE.releaseClassLoader(loader));
|
||||
assertSame(failure, reported.getCause());
|
||||
agent.verify(() -> Agent.releaseClassLoader(loader));
|
||||
}
|
||||
}
|
||||
@Test
|
||||
public void providerAndLoaderCloseFailuresAreBothPreserved() throws Exception {
|
||||
ClassLoader loader = mock(ClassLoader.class);
|
||||
IOException providerFailure = new IOException("provider close failed");
|
||||
IllegalStateException loaderFailure = new IllegalStateException("loader close failed");
|
||||
try (MockedStatic<SlimJar> providers = mockStatic(SlimJar.class);
|
||||
MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||
providers.when(SlimJar::closeNativeProviders).thenThrow(providerFailure);
|
||||
agent.when(() -> Agent.releaseClassLoader(loader)).thenThrow(loaderFailure);
|
||||
IllegalStateException reported = assertThrows(IllegalStateException.class,
|
||||
() -> BukkitWorldLifecyclePolicy.INSTANCE.releaseClassLoader(loader));
|
||||
assertSame(loaderFailure, reported);
|
||||
assertEquals(1, reported.getSuppressed().length);
|
||||
assertSame(providerFailure, reported.getSuppressed()[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanPreview;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.studio.map.components.MapLayer;
|
||||
import art.arcane.iris.studio.map.components.MapStyle;
|
||||
import art.arcane.iris.studio.map.components.MapTile;
|
||||
import art.arcane.iris.studio.map.components.MapTileRenderer;
|
||||
import art.arcane.iris.studio.map.components.PickDisplay;
|
||||
import art.arcane.iris.studio.map.components.TileType;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MapTileProgressTest {
|
||||
@Test
|
||||
public void retriesOnlySampleUnresolvedHeightPointsAndReuseBiomeData() {
|
||||
AtomicBoolean pending = new AtomicBoolean(true);
|
||||
AtomicInteger heightSamples = new AtomicInteger();
|
||||
AtomicInteger biomeSamples = new AtomicInteger();
|
||||
Engine engine = engine(biomeSamples);
|
||||
when(engine.getComplex().getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> {
|
||||
heightSamples.incrementAndGet();
|
||||
if (x == 0D && z == 0D && pending.get()) {
|
||||
ProvisionalSampling.mark();
|
||||
return 60D;
|
||||
}
|
||||
return 80D + x * 0.02D + z * 0.01D;
|
||||
}));
|
||||
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||
MapLayer layer = MapLayer.biomes(PickDisplay.PICKS, false);
|
||||
MapTileRenderer.TileRender progress = renderer.prepareTile(-8D, -8D, 0.5D, 16, layer, true);
|
||||
|
||||
progress.render(() -> false);
|
||||
int initialHeights = heightSamples.get();
|
||||
int initialBiomes = biomeSamples.get();
|
||||
assertFalse(progress.complete());
|
||||
assertTrue(progress.bytes() > 0L);
|
||||
|
||||
progress.render(() -> false);
|
||||
assertEquals(initialHeights + 1, heightSamples.get());
|
||||
assertEquals(initialBiomes, biomeSamples.get());
|
||||
pending.set(false);
|
||||
MapTile settled = progress.render(() -> false);
|
||||
assertTrue(progress.complete());
|
||||
assertEquals(initialHeights + 2, heightSamples.get());
|
||||
assertEquals(initialBiomes, biomeSamples.get());
|
||||
assertEquals(0L, progress.bytes());
|
||||
progress.render(() -> false);
|
||||
assertEquals(initialHeights + 2, heightSamples.get());
|
||||
assertEquals(initialBiomes, biomeSamples.get());
|
||||
|
||||
MapTile expected = renderer.renderTile(-8D, -8D, 0.5D, 16, layer, true, () -> false);
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
assertEquals(expected.heightAt(x, z), settled.heightAt(x, z), 0D);
|
||||
}
|
||||
}
|
||||
assertArrayEquals(pixels(expected.image(MapStyle.PLAIN.withShading(true))),
|
||||
pixels(settled.image(MapStyle.PLAIN.withShading(true))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedWaterIsNotBlendedAgainWhileHeightsWait() {
|
||||
AtomicBoolean pending = new AtomicBoolean(true);
|
||||
Engine engine = engine(new AtomicInteger());
|
||||
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||
ChunkWaterRaster raster = mock(ChunkWaterRaster.class);
|
||||
when(runtime.enabled()).thenReturn(true);
|
||||
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(HydrologyPlanPreview.EMPTY);
|
||||
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(raster);
|
||||
when(raster.role(anyInt(), anyInt())).thenReturn(WaterRole.POND);
|
||||
when(engine.getComplex().getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(engine.getComplex().getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> {
|
||||
if (pending.get()) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
return 60D;
|
||||
}));
|
||||
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||
MapTileRenderer.TileRender progress = renderer.prepareTile(0D, 0D, 1D, 16, MapLayer.of(TileType.BIOME), true);
|
||||
|
||||
MapTile first = progress.render(() -> false);
|
||||
assertFalse(progress.complete());
|
||||
assertNotEquals(0xFF738249, first.image(MapStyle.PLAIN).getRGB(0, 0));
|
||||
MapTile second = progress.render(() -> false);
|
||||
assertArrayEquals(pixels(first.image(MapStyle.PLAIN)), pixels(second.image(MapStyle.PLAIN)));
|
||||
|
||||
pending.set(false);
|
||||
MapTile complete = progress.render(() -> false);
|
||||
assertTrue(progress.complete());
|
||||
assertEquals(0L, progress.bytes());
|
||||
assertArrayEquals(pixels(first.image(MapStyle.PLAIN)), pixels(complete.image(MapStyle.PLAIN)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void twoBlockPixelsRefineMixedWaterAndHeightsToTheSameFinalTile() {
|
||||
AtomicBoolean pending = new AtomicBoolean(true);
|
||||
Engine engine = engine(new AtomicInteger());
|
||||
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||
ChunkWaterRaster raster = mock(ChunkWaterRaster.class);
|
||||
when(runtime.enabled()).thenReturn(true);
|
||||
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(HydrologyPlanPreview.EMPTY);
|
||||
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(raster);
|
||||
when(runtime.rasterPreview(0, 0)).thenAnswer(invocation -> pending.get() ? ChunkWaterRaster.PROVISIONAL : raster);
|
||||
when(raster.role(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||
int x = invocation.getArgument(0);
|
||||
int z = invocation.getArgument(1);
|
||||
if (x == 1 && z < 4) {
|
||||
return WaterRole.POND;
|
||||
}
|
||||
if (x == 7 && z < 8) {
|
||||
return WaterRole.WATERFALL;
|
||||
}
|
||||
return x < 4 && z == 9 ? WaterRole.INLET : WaterRole.NONE;
|
||||
});
|
||||
when(engine.getComplex().getHydrologyRuntime()).thenReturn(runtime);
|
||||
when(engine.getComplex().getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> {
|
||||
if (x == 0D && z == 0D && pending.get()) {
|
||||
ProvisionalSampling.mark();
|
||||
return 60D;
|
||||
}
|
||||
return 80D + x * 0.02D + z * 0.01D;
|
||||
}));
|
||||
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||
MapLayer layer = MapLayer.of(TileType.BIOME);
|
||||
MapTileRenderer.TileRender progress = renderer.prepareTile(-16D, -16D, 2D, 16, layer, true);
|
||||
|
||||
MapTile first = progress.render(() -> false);
|
||||
int[] firstPixels = pixels(first.image(MapStyle.PLAIN.withShading(true)));
|
||||
assertFalse(progress.complete());
|
||||
assertEquals(0xFF738249, first.image(MapStyle.PLAIN).getRGB(8, 8));
|
||||
MapTile waiting = progress.render(() -> false);
|
||||
assertFalse(progress.complete());
|
||||
assertArrayEquals(firstPixels, pixels(waiting.image(MapStyle.PLAIN.withShading(true))));
|
||||
|
||||
pending.set(false);
|
||||
MapTile actual = progress.render(() -> false);
|
||||
assertTrue(progress.complete());
|
||||
assertEquals(0L, progress.bytes());
|
||||
BufferedImage colors = actual.image(MapStyle.PLAIN);
|
||||
assertEquals(0xFF738249, colors.getRGB(14, 14));
|
||||
assertNotEquals(0xFF738249, colors.getRGB(8, 8));
|
||||
assertNotEquals(colors.getRGB(8, 8), colors.getRGB(11, 8));
|
||||
assertNotEquals(colors.getRGB(8, 8), colors.getRGB(8, 12));
|
||||
assertNotEquals(colors.getRGB(11, 8), colors.getRGB(8, 12));
|
||||
assertArrayEquals(firstPixels, pixels(first.image(MapStyle.PLAIN.withShading(true))));
|
||||
|
||||
MapTile expected = renderer.renderTile(-16D, -16D, 2D, 16, layer, true, () -> false);
|
||||
assertArrayEquals(pixels(expected.image(MapStyle.PLAIN.withShading(true))),
|
||||
pixels(actual.image(MapStyle.PLAIN.withShading(true))));
|
||||
for (int z = 0; z < 16; z++) {
|
||||
for (int x = 0; x < 16; x++) {
|
||||
assertEquals(expected.heightAt(x, z), actual.heightAt(x, z), 0D);
|
||||
assertEquals(expected.subjectAt(x, z), actual.subjectAt(x, z));
|
||||
assertEquals(expected.regionAt(x, z), actual.regionAt(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelledPreparationCanResumeWithoutReusingIncompleteHeightSamples() {
|
||||
Engine engine = engine(new AtomicInteger());
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
when(engine.getComplex().getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> {
|
||||
samples.incrementAndGet();
|
||||
return x + z + 80D;
|
||||
}));
|
||||
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||
MapLayer layer = MapLayer.biomes(PickDisplay.PICKS, false);
|
||||
MapTileRenderer.TileRender progress = renderer.prepareTile(-16D, -16D, 1D, 16, layer, true);
|
||||
assertThrows(CancellationException.class, () -> progress.render(() -> samples.get() >= 9));
|
||||
assertFalse(progress.complete());
|
||||
MapTile actual = progress.render(() -> false);
|
||||
assertTrue(progress.complete());
|
||||
assertEquals(81, samples.get());
|
||||
MapTile expected = renderer.renderTile(-16D, -16D, 1D, 16, layer, true, () -> false);
|
||||
assertArrayEquals(pixels(expected.image(MapStyle.PLAIN.withShading(true))),
|
||||
pixels(actual.image(MapStyle.PLAIN.withShading(true))));
|
||||
}
|
||||
|
||||
private static Engine engine(AtomicInteger biomeSamples) {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setColor("#738249");
|
||||
IrisRegion region = new IrisRegion();
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(complex.getBaseBiomeStream()).thenReturn(ProceduralStream.of((x, z) -> {
|
||||
biomeSamples.incrementAndGet();
|
||||
return biome;
|
||||
}, Interpolated.of(value -> 0D, value -> biome)));
|
||||
when(complex.getRegionStream()).thenReturn(ProceduralStream.of((x, z) -> region,
|
||||
Interpolated.of(value -> 0D, value -> region)));
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static int[] pixels(BufferedImage image) {
|
||||
return image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth());
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,23 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import art.arcane.iris.studio.map.components.MapLayer;
|
||||
import art.arcane.iris.studio.map.components.MapTile;
|
||||
import art.arcane.iris.studio.map.components.MapTileRenderer;
|
||||
import art.arcane.iris.studio.map.components.TileType;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.EventQueue;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -22,21 +25,23 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class VisionRenderControllerTest {
|
||||
@Test
|
||||
public void provisionalTilesRetryWithBackoffAndConvergeWithoutMoving() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||
MapTileRenderer renderer = renderer(work);
|
||||
MapTile tile = mock(MapTile.class);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
when(work.render(any()))
|
||||
.thenAnswer(invocation -> {
|
||||
if (attempts.incrementAndGet() < 3) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
attempts.incrementAndGet();
|
||||
return tile;
|
||||
});
|
||||
when(work.complete()).thenAnswer(invocation -> attempts.get() >= 3);
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame frame = controller.request(spec(renderer, 1L));
|
||||
await(() -> frame.tiles().getFirst().tile() != null);
|
||||
@@ -55,30 +60,47 @@ public class VisionRenderControllerTest {
|
||||
});
|
||||
assertEquals(3, attempts.get());
|
||||
assertSame(tile, controller.request(spec(renderer, 1L)).tiles().getFirst().tile());
|
||||
verify(renderer).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void provisionalTilesAreShownButNeverReusedAsFinal() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
public void resizeZoomAndSuspendRetainProvisionalImageWorkAndBackoff() throws Exception {
|
||||
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||
MapTileRenderer renderer = renderer(work);
|
||||
MapTile incomplete = mock(MapTile.class);
|
||||
MapTile complete = mock(MapTile.class);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
when(work.render(any()))
|
||||
.thenAnswer(invocation -> {
|
||||
if (attempts.incrementAndGet() == 1) {
|
||||
ProvisionalSampling.mark();
|
||||
return incomplete;
|
||||
}
|
||||
return complete;
|
||||
});
|
||||
when(work.complete()).thenAnswer(invocation -> attempts.get() >= 2);
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||
await(() -> first.tiles().getFirst().tile() != null);
|
||||
await(() -> first.tiles().getFirst().tile() != null && controller.progress(first).activeWorkers() == 0);
|
||||
assertSame(incomplete, first.tiles().getFirst().tile());
|
||||
VisionRenderController.Frame resized = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
MapLayer.of(TileType.RIVER), 1L, 32D, 32D, 1D, 48, 48, false));
|
||||
assertSame(incomplete, resized.tiles().getFirst().tile());
|
||||
assertTrue(resized.tiles().getFirst().provisional());
|
||||
VisionRenderController.Frame zoomed = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
MapLayer.of(TileType.RIVER), 1L, 32D, 32D, 1.15D, 48, 48, false));
|
||||
assertSame(incomplete, zoomed.tiles().getFirst().tile());
|
||||
controller.suspend();
|
||||
VisionRenderController.Frame second = controller.request(spec(renderer, 1L));
|
||||
await(() -> second.tiles().getFirst().tile() == complete);
|
||||
assertSame(incomplete, second.tiles().getFirst().tile());
|
||||
assertEquals(0, controller.progress(second).ready());
|
||||
assertEquals(1, attempts.get());
|
||||
await(() -> {
|
||||
controller.retryProvisional(second);
|
||||
return controller.progress(second).ready() == 1;
|
||||
});
|
||||
assertEquals(2, attempts.get());
|
||||
verify(renderer).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||
VisionRenderController.Frame third = controller.request(spec(renderer, 1L));
|
||||
assertSame(complete, third.tiles().getFirst().tile());
|
||||
assertEquals(2, attempts.get());
|
||||
@@ -87,10 +109,11 @@ public class VisionRenderControllerTest {
|
||||
|
||||
@Test
|
||||
public void contentRevisionInvalidatesFinalTiles() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTile tile = mock(MapTile.class);
|
||||
MapTileRenderer.TileRender work = finished(tile);
|
||||
MapTileRenderer renderer = renderer(work);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
when(work.render(any()))
|
||||
.thenAnswer(invocation -> {
|
||||
attempts.incrementAndGet();
|
||||
return tile;
|
||||
@@ -109,14 +132,16 @@ public class VisionRenderControllerTest {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTile overview = mock(MapTile.class);
|
||||
MapTile incomplete = mock(MapTile.class);
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
MapTileRenderer.TileRender overviewWork = finished(overview);
|
||||
MapTileRenderer.TileRender detailWork = mock(MapTileRenderer.TileRender.class);
|
||||
when(detailWork.render(any())).thenReturn(incomplete);
|
||||
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||
.thenAnswer(invocation -> {
|
||||
double scale = invocation.getArgument(2);
|
||||
if (scale < 4D) {
|
||||
ProvisionalSampling.mark();
|
||||
return incomplete;
|
||||
return detailWork;
|
||||
}
|
||||
return overview;
|
||||
return overviewWork;
|
||||
});
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame coarse = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
@@ -142,10 +167,11 @@ public class VisionRenderControllerTest {
|
||||
|
||||
@Test
|
||||
public void closeRejectsLatePublication() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||
MapTileRenderer renderer = renderer(work);
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
when(work.render(any()))
|
||||
.thenAnswer(invocation -> {
|
||||
entered.countDown();
|
||||
while (release.getCount() != 0L) {
|
||||
@@ -167,7 +193,261 @@ public class VisionRenderControllerTest {
|
||||
controller.close();
|
||||
}
|
||||
await(() -> controller.progress(frame).activeWorkers() == 0);
|
||||
assertEquals(null, frame.tiles().getFirst().tile());
|
||||
assertNull(frame.tiles().getFirst().tile());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resizeAndSameLevelZoomReuseFinalTiles() throws Exception {
|
||||
MapTile tile = mock(MapTile.class);
|
||||
MapTileRenderer.TileRender work = finished(tile);
|
||||
MapTileRenderer renderer = renderer(work);
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame first = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
MapLayer.of(TileType.RIVER), 1L, 32D, 32D, 1D, 48, 48, false));
|
||||
await(() -> controller.progress(first).ready() == 1);
|
||||
VisionRenderController.Frame resized = controller.request(spec(renderer, 1L));
|
||||
assertSame(tile, resized.tiles().getFirst().tile());
|
||||
VisionRenderController.Frame zoomed = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
MapLayer.of(TileType.RIVER), 1L, 32D, 32D, 0.9D, 64, 64, false));
|
||||
assertSame(tile, zoomed.tiles().getFirst().tile());
|
||||
assertEquals(1, controller.progress(zoomed).ready());
|
||||
verify(work).render(any());
|
||||
verify(renderer).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentRevisionInvalidatesProvisionalWork() throws Exception {
|
||||
MapTile tile = mock(MapTile.class);
|
||||
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||
MapTileRenderer renderer = renderer(work);
|
||||
when(work.render(any())).thenReturn(tile);
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||
await(() -> first.tiles().getFirst().tile() != null);
|
||||
VisionRenderController.Frame second = controller.request(spec(renderer, 2L));
|
||||
await(() -> second.tiles().getFirst().tile() != null);
|
||||
verify(renderer, times(2)).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void missingDetailUsesProvisionalOverviewWithoutMarkingItFinal() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTile overview = mock(MapTile.class);
|
||||
MapTileRenderer.TileRender overviewWork = mock(MapTileRenderer.TileRender.class);
|
||||
when(overviewWork.render(any())).thenReturn(overview);
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
MapTileRenderer.TileRender detailWork = cancellable(entered);
|
||||
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||
.thenAnswer(invocation -> (double) invocation.getArgument(2) < 4D ? detailWork : overviewWork);
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame coarse = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
MapLayer.of(TileType.RIVER), 1L, 128D, 128D, 4D, 64, 64, false));
|
||||
await(() -> coarse.tiles().getFirst().tile() != null);
|
||||
VisionRenderController.Frame fine = controller.request(spec(renderer, 1L));
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
assertNull(fine.tiles().getFirst().tile());
|
||||
VisionRenderController.Fallback fallback = controller.fallback(fine, fine.tiles().getFirst());
|
||||
assertNotNull(fallback);
|
||||
assertSame(overview, fallback.pieces().getFirst().tile());
|
||||
assertEquals(0, controller.progress(fine).ready());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void zoomingOutKeepsCompleteFinerTilesOverProvisionalExactTile() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTile fineTile = mock(MapTile.class);
|
||||
MapTile coarseTile = mock(MapTile.class);
|
||||
MapTileRenderer.TileRender fineWork = finished(fineTile);
|
||||
MapTileRenderer.TileRender coarseWork = mock(MapTileRenderer.TileRender.class);
|
||||
when(coarseWork.render(any())).thenReturn(coarseTile);
|
||||
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||
.thenAnswer(invocation -> (double) invocation.getArgument(2) < 2D ? fineWork : coarseWork);
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame fine = controller.request(spec(renderer, 1L));
|
||||
await(() -> controller.progress(fine).ready() == 1);
|
||||
VisionRenderController.Frame coarse = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
MapLayer.of(TileType.RIVER), 1L, 64D, 64D, 2D, 64, 64, false));
|
||||
await(() -> coarse.tiles().getFirst().tile() != null);
|
||||
VisionRenderController.Fallback fallback = controller.fallback(coarse, coarse.tiles().getFirst());
|
||||
assertNotNull(fallback);
|
||||
assertSame(fineTile, fallback.pieces().getFirst().tile());
|
||||
assertEquals(0.5D, fallback.pieces().getFirst().size(), 0D);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void suspendedViewCancelsSamplingAndProbeButKeepsCompletedTiles() throws Exception {
|
||||
MapTile tile = mock(MapTile.class);
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
CountDownLatch sampling = new CountDownLatch(1);
|
||||
MapTileRenderer.TileRender pending = cancellable(sampling);
|
||||
MapTileRenderer.TileRender complete = finished(tile);
|
||||
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||
.thenReturn(complete, pending);
|
||||
CountDownLatch probing = new CountDownLatch(1);
|
||||
CountDownLatch interrupted = new CountDownLatch(1);
|
||||
AtomicInteger published = new AtomicInteger();
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||
await(() -> controller.progress(first).ready() == 1);
|
||||
VisionRenderController.Frame next = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||
MapLayer.of(TileType.RIVER), 1L, 96D, 32D, 1D, 64, 64, false));
|
||||
assertTrue(sampling.await(5L, TimeUnit.SECONDS));
|
||||
controller.submitProbe(next, () -> {
|
||||
probing.countDown();
|
||||
try {
|
||||
new CountDownLatch(1).await();
|
||||
return "late";
|
||||
} catch (InterruptedException error) {
|
||||
interrupted.countDown();
|
||||
throw error;
|
||||
}
|
||||
}, value -> published.incrementAndGet());
|
||||
assertTrue(probing.await(5L, TimeUnit.SECONDS));
|
||||
controller.suspend();
|
||||
assertTrue(interrupted.await(5L, TimeUnit.SECONDS));
|
||||
await(() -> controller.progress(next).activeWorkers() == 0);
|
||||
EventQueue.invokeAndWait(() -> { });
|
||||
assertEquals(0, published.get());
|
||||
assertNull(next.tiles().getFirst().tile());
|
||||
VisionRenderController.Frame resumed = controller.request(spec(renderer, 1L));
|
||||
assertSame(tile, resumed.tiles().getFirst().tile());
|
||||
verify(renderer, times(2)).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void retainedWorkCountsAgainstCacheBudgetAndIsReleasedWhenComplete() throws Exception {
|
||||
MapTile tile = mock(MapTile.class);
|
||||
when(tile.bytes()).thenReturn(1L);
|
||||
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||
MapTileRenderer renderer = renderer(work);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(work.render(any())).thenAnswer(invocation -> {
|
||||
attempts.incrementAndGet();
|
||||
return tile;
|
||||
});
|
||||
when(work.complete()).thenAnswer(invocation -> attempts.get() >= 2);
|
||||
when(work.bytes()).thenReturn(256L * 1024L * 1024L);
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||
await(() -> first.tiles().getFirst().tile() != null);
|
||||
VisionRenderController.Frame second = controller.request(spec(renderer, 1L));
|
||||
await(() -> controller.progress(second).ready() == 1);
|
||||
VisionRenderController.Frame third = controller.request(spec(renderer, 1L));
|
||||
assertSame(tile, third.tiles().getFirst().tile());
|
||||
verify(renderer, times(2)).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||
assertEquals(2, attempts.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelledRetryReleasesWorkAndKeepsPublishedImage() throws Exception {
|
||||
MapTile incomplete = mock(MapTile.class);
|
||||
MapTile complete = mock(MapTile.class);
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
MapTileRenderer.TileRender partial = mock(MapTileRenderer.TileRender.class);
|
||||
MapTileRenderer.TileRender replacement = finished(complete);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(partial.render(any())).thenAnswer(invocation -> {
|
||||
if (attempts.incrementAndGet() == 1) {
|
||||
return incomplete;
|
||||
}
|
||||
BooleanSupplier cancelled = invocation.getArgument(0);
|
||||
entered.countDown();
|
||||
await(cancelled);
|
||||
throw new CancellationException();
|
||||
});
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||
.thenReturn(partial, replacement);
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||
await(() -> first.tiles().getFirst().tile() != null);
|
||||
await(() -> {
|
||||
controller.retryProvisional(first);
|
||||
return entered.getCount() == 0L;
|
||||
});
|
||||
controller.suspend();
|
||||
await(() -> controller.progress(first).activeWorkers() == 0);
|
||||
assertSame(incomplete, first.tiles().getFirst().tile());
|
||||
VisionRenderController.Frame resumed = controller.request(spec(renderer, 1L));
|
||||
await(() -> controller.progress(resumed).ready() == 1);
|
||||
assertSame(complete, resumed.tiles().getFirst().tile());
|
||||
verify(renderer, times(2)).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void previewReadinessWakesBackoffAtBoundedCadenceIncludingCompletionDuringRender() throws Exception {
|
||||
MapTile tile = mock(MapTile.class);
|
||||
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||
MapTileRenderer renderer = renderer(work);
|
||||
AtomicLong time = new AtomicLong();
|
||||
AtomicLong readiness = new AtomicLong();
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(renderer.previewRevision()).thenAnswer(invocation -> readiness.get());
|
||||
when(work.render(any())).thenAnswer(invocation -> {
|
||||
if (attempts.incrementAndGet() == 3) {
|
||||
readiness.incrementAndGet();
|
||||
}
|
||||
return tile;
|
||||
});
|
||||
when(work.complete()).thenAnswer(invocation -> attempts.get() >= 4);
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { }, time::get)) {
|
||||
VisionRenderController.Frame frame = controller.request(spec(renderer, 1L));
|
||||
await(() -> attempts.get() == 1 && controller.progress(frame).activeWorkers() == 0);
|
||||
time.set(VisionRenderController.FIRST_RETRY_NANOS);
|
||||
controller.retryProvisional(frame);
|
||||
await(() -> attempts.get() == 2 && controller.progress(frame).activeWorkers() == 0);
|
||||
time.addAndGet(VisionRenderController.FIRST_RETRY_NANOS);
|
||||
controller.retryProvisional(frame);
|
||||
assertEquals(2, attempts.get());
|
||||
readiness.incrementAndGet();
|
||||
controller.retryProvisional(frame);
|
||||
await(() -> attempts.get() == 3 && controller.progress(frame).activeWorkers() == 0);
|
||||
time.addAndGet(VisionRenderController.FIRST_RETRY_NANOS - 1L);
|
||||
controller.retryProvisional(frame);
|
||||
assertEquals(3, attempts.get());
|
||||
time.incrementAndGet();
|
||||
controller.retryProvisional(frame);
|
||||
await(() -> controller.progress(frame).ready() == 1);
|
||||
assertEquals(4, attempts.get());
|
||||
verify(renderer).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||
}
|
||||
}
|
||||
|
||||
private static MapTileRenderer.TileRender cancellable(CountDownLatch entered) {
|
||||
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||
when(work.render(any())).thenAnswer(invocation -> {
|
||||
BooleanSupplier cancelled = invocation.getArgument(0);
|
||||
entered.countDown();
|
||||
while (!cancelled.getAsBoolean() && !Thread.currentThread().isInterrupted()) {
|
||||
try {
|
||||
Thread.sleep(1L);
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
throw new CancellationException();
|
||||
});
|
||||
return work;
|
||||
}
|
||||
|
||||
private static MapTileRenderer renderer(MapTileRenderer.TileRender work) {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean())).thenReturn(work);
|
||||
return renderer;
|
||||
}
|
||||
|
||||
private static MapTileRenderer.TileRender finished(MapTile tile) {
|
||||
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||
when(work.render(any())).thenReturn(tile);
|
||||
when(work.complete()).thenReturn(true);
|
||||
return work;
|
||||
}
|
||||
|
||||
private static VisionRenderController.RenderSpec spec(MapTileRenderer renderer, long revision) {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package art.arcane.iris.studio.render;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanPreview;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class WaterMapProgressTest {
|
||||
@Test
|
||||
public void completeDetailedRastersDoNotDrawPlanFallbacks() {
|
||||
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||
Reach reach = mock(Reach.class);
|
||||
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt()))
|
||||
.thenReturn(new HydrologyPlanPreview(List.of(reach), List.of(), List.of(), List.of(), true));
|
||||
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(ChunkWaterRaster.EMPTY);
|
||||
|
||||
WaterMap map = WaterMap.draw(runtime, 32, 0D, 0D, 1D, () -> false);
|
||||
|
||||
assertFalse(map.provisional());
|
||||
verifyNoInteractions(reach);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completedPixelsSurviveRuntimeRasterEviction() {
|
||||
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||
ChunkWaterRaster pond = pond();
|
||||
AtomicBoolean pending = new AtomicBoolean(true);
|
||||
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(HydrologyPlanPreview.EMPTY);
|
||||
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(ChunkWaterRaster.EMPTY);
|
||||
when(runtime.rasterPreview(0, 0)).thenReturn(pond, ChunkWaterRaster.PROVISIONAL);
|
||||
when(runtime.rasterPreview(1, 0)).thenAnswer(invocation -> pending.get() ? ChunkWaterRaster.PROVISIONAL : pond);
|
||||
WaterMap map = new WaterMap(runtime, 32, 0D, 0D, 1D);
|
||||
|
||||
map.advance(() -> false);
|
||||
assertTrue(map.provisional());
|
||||
assertEquals(HydrologyFeatureType.POND, map.at(1, 1));
|
||||
assertNull(map.at(17, 1));
|
||||
pending.set(false);
|
||||
map.advance(() -> false);
|
||||
|
||||
assertFalse(map.provisional());
|
||||
assertEquals(HydrologyFeatureType.POND, map.at(1, 1));
|
||||
assertEquals(HydrologyFeatureType.POND, map.at(17, 1));
|
||||
assertNull(map.at(1, 17));
|
||||
assertTrue(map.bytes() < 16_384L);
|
||||
verify(runtime, times(1)).rasterPreview(0, 0);
|
||||
verify(runtime, times(2)).rasterPreview(1, 0);
|
||||
verify(runtime, times(1)).planPreview(anyInt(), anyInt(), anyInt(), anyInt());
|
||||
|
||||
map.advance(() -> false);
|
||||
verify(runtime, times(2)).rasterPreview(1, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelledWaterSamplingResumesToTheSameFinalPixels() {
|
||||
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||
ChunkWaterRaster raster = pond();
|
||||
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(HydrologyPlanPreview.EMPTY);
|
||||
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(raster);
|
||||
AtomicInteger rows = new AtomicInteger();
|
||||
WaterMap map = new WaterMap(runtime, 32, -16.5D, -16.5D, 0.5D);
|
||||
|
||||
assertThrows(CancellationException.class, () -> map.advance(() -> rows.incrementAndGet() > 2));
|
||||
assertTrue(map.provisional());
|
||||
map.advance(() -> false);
|
||||
assertFalse(map.provisional());
|
||||
WaterMap expected = WaterMap.draw(runtime, 32, -16.5D, -16.5D, 0.5D, () -> false);
|
||||
for (int z = 0; z < 32; z++) {
|
||||
for (int x = 0; x < 32; x++) {
|
||||
assertEquals(expected.at(x, z), map.at(x, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ChunkWaterRaster pond() {
|
||||
ChunkWaterRaster raster = mock(ChunkWaterRaster.class);
|
||||
when(raster.role(anyInt(), anyInt())).thenReturn(WaterRole.POND);
|
||||
return raster;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user