mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 23:10:32 +00:00
Integrate Iris Studio
- Merge the unified Studio interface and command entry points. - Adapt water views and provisional sampling to basin hydrology. - Restore diagnostic estimates and the biome water overlay. - Fix render cancellation, preview closure, and platform position callbacks. - Fall back to software rendering when native bindings are unavailable. - Cover renderer, lifecycle, and scheduler regressions.
This commit is contained in:
commit
9f49d29e2b
225 files changed
+29666
-4395
No files matched your search
@@ -0,0 +1,48 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class GpuLibraryTest {
|
||||
@Test
|
||||
public void missingNativeLibrariesUseSoftwareEvenWhenBindingClassesExist() throws Exception {
|
||||
List<URL> classpath = new ArrayList<>();
|
||||
for (String entry : System.getProperty("java.class.path").split(Pattern.quote(File.pathSeparator))) {
|
||||
if (!entry.contains("natives-")) {
|
||||
classpath.add(new File(entry).toURI().toURL());
|
||||
}
|
||||
}
|
||||
ClassLoader loader = GpuLibrary.class.getClassLoader();
|
||||
while (loader != null) {
|
||||
if (loader instanceof URLClassLoader urls) {
|
||||
for (URL url : urls.getURLs()) {
|
||||
if (!url.getPath().contains("natives-")) {
|
||||
classpath.add(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
loader = loader.getParent();
|
||||
}
|
||||
assertFalse(classpath.isEmpty());
|
||||
try (URLClassLoader isolated = new URLClassLoader(classpath.toArray(URL[]::new), ClassLoader.getPlatformClassLoader())) {
|
||||
assertEquals("org.lwjgl.system.MemoryStack", Class.forName("org.lwjgl.system.MemoryStack", false, isolated).getName());
|
||||
assertTrue(Class.forName("org.lwjgl.system.jawt.JAWT", false, isolated).getClassLoader() == isolated);
|
||||
Class<?> library = Class.forName(GpuLibrary.class.getName(), true, isolated);
|
||||
Method available = library.getDeclaredMethod("isLoaded");
|
||||
available.setAccessible(true);
|
||||
|
||||
assertEquals(Boolean.FALSE, available.invoke(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import org.junit.Test;
|
||||
@@ -7,12 +7,12 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
public class ImageMapStudioGUITest {
|
||||
public class ImageMapExplorerTest {
|
||||
@Test
|
||||
public void exportRequestsHotloadWithoutMutatingTheActiveGenerationData() {
|
||||
Engine engine = mock(Engine.class);
|
||||
|
||||
ImageMapStudioGUI.reloadActiveEngine(engine);
|
||||
ImageMapExplorer.reloadActiveEngine(engine);
|
||||
|
||||
verify(engine).hotloadSilently();
|
||||
verifyNoMoreInteractions(engine);
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.image.IrisImageMap;
|
||||
import art.arcane.iris.generation.image.IrisImageMapOrigin;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.image.CompiledIrisImageMap;
|
||||
import art.arcane.iris.generation.image.IrisImageMapRuntime;
|
||||
@@ -0,0 +1,125 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.biome.IrisBiome;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||
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.iris.studio.render.IrisRenderer;
|
||||
import art.arcane.iris.studio.render.RenderType;
|
||||
import art.arcane.iris.structure.object.IrisObjectPlacement;
|
||||
import art.arcane.iris.testsupport.FakeHydrology;
|
||||
import art.arcane.volmlib.util.collection.KList;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class MapHydrologyTest {
|
||||
@Test
|
||||
public void biomeViewIncludesCurrentWaterOverlayByDefault() throws Exception {
|
||||
try (FakeHydrology water = new FakeHydrology(() -> false, (x, z) -> 60, 40, 63)) {
|
||||
Engine engine = engine(water.runtime, biome());
|
||||
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||
MapTile tile = settled(renderer, MapLayer.of(TileType.BIOME));
|
||||
BufferedImage expected = new IrisRenderer(engine).renderStudio(0, 0, 64, 64, RenderType.BIOME, () -> false);
|
||||
assertNotEquals(new Color(0x738249).getRGB(), expected.getRGB(16, 16));
|
||||
assertEquals(expected.getRGB(16, 16), tile.image(MapStyle.PLAIN).getRGB(16, 16));
|
||||
MapTile dry = settled(renderer, MapLayer.biomes(PickDisplay.PICKS, false));
|
||||
assertEquals(new Color(0x738249).getRGB(), dry.image(MapStyle.PLAIN).getRGB(16, 16));
|
||||
assertNotEquals(MapLayer.biomes(PickDisplay.PICKS, true), MapLayer.biomes(PickDisplay.PICKS, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pickingNoiseNeverReceivesTheWaterOverlay() {
|
||||
for (PickDisplay display : new PickDisplay[]{PickDisplay.NOISE, PickDisplay.BOTH}) {
|
||||
assertEquals(MapLayer.biomes(display, false), MapLayer.biomes(display, true));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void diagnosticsUseExistingConfigurationCalculations() {
|
||||
IrisBiome biome = biome();
|
||||
biome.setObjects(new KList<>(new IrisObjectPlacement().setDensity(4).setChance(0.5)));
|
||||
biome.setDecorators(new KList<>(new IrisDecorator().setChance(0.25).setStackMax(2)));
|
||||
Engine engine = engine(FakeHydrology.disabled(), biome);
|
||||
when(engine.getMaxBiomeObjectDensity()).thenReturn(8D);
|
||||
when(engine.getMaxBiomeDecoratorDensity()).thenReturn(256D);
|
||||
when(engine.getMaxBiomeLayerDensity()).thenReturn(4D);
|
||||
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||
for (String name : new String[]{"OBJECT_LOAD", "DECORATOR_LOAD", "LAYER_LOAD"}) {
|
||||
TileType type = TileType.valueOf(name);
|
||||
MapView view = MapView.valueOf(name);
|
||||
assertEquals("Diagnostics", view.group().label());
|
||||
assertTrue(view.description().contains("Configuration estimate"));
|
||||
MapTile tile = renderer.renderTile(0, 0, 1, 8, MapLayer.of(type), false, () -> false);
|
||||
assertEquals(biome.getColor(engine, RenderType.valueOf(name)).getRGB(), tile.image(MapStyle.PLAIN).getRGB(0, 0));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void disabledWaterPreservesBiomeColors() {
|
||||
Engine engine = engine(FakeHydrology.disabled(), biome());
|
||||
MapTile tile = new MapTileRenderer(engine).renderTile(0, 0, 1, 8, MapLayer.of(TileType.BIOME), false, () -> false);
|
||||
assertEquals(new Color(0x738249).getRGB(), tile.image(MapStyle.PLAIN).getRGB(0, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverLegendMatchesCurrentRendererIncludingRecordedCavePools() {
|
||||
assertEquals(IrisRenderer.mapWaterTypes().size(), RiverKey.rows().size());
|
||||
for (int index = 0; index < RiverKey.rows().size(); index++) {
|
||||
assertEquals(IrisRenderer.hydrologyFeatureColor(IrisRenderer.mapWaterTypes().get(index)) & 0xFFFFFF,
|
||||
RiverKey.rows().get(index).color());
|
||||
}
|
||||
assertEquals("cave pool", RiverKey.rows().getLast().label());
|
||||
}
|
||||
|
||||
private static MapTile settled(MapTileRenderer renderer, MapLayer layer) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(10L);
|
||||
while (true) {
|
||||
long revision = ProvisionalSampling.revision();
|
||||
MapTile tile = renderer.renderTile(0, 0, 1, 64, layer, false, () -> false);
|
||||
if (revision == ProvisionalSampling.revision()) {
|
||||
return tile;
|
||||
}
|
||||
assertTrue("water never settled", System.nanoTime() < deadline);
|
||||
Thread.sleep(10L);
|
||||
}
|
||||
}
|
||||
|
||||
private static IrisBiome biome() {
|
||||
IrisBiome biome = new IrisBiome();
|
||||
biome.setColor("#738249");
|
||||
return biome;
|
||||
}
|
||||
|
||||
private static Engine engine(HydrologyRuntime water, IrisBiome biome) {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(complex.getHydrologyRuntime()).thenReturn(water);
|
||||
IrisRegion region = new IrisRegion();
|
||||
when(complex.getBaseBiomeStream()).thenReturn(ProceduralStream.of((x, z) -> biome, Interpolated.of(b -> 0D, d -> biome)));
|
||||
when(complex.getRegionStream()).thenReturn(ProceduralStream.of((x, z) -> region, Interpolated.of(r -> 0D, d -> region)));
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
+33
-46
@@ -1,83 +1,73 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.generation.runtime.MeteredCache;
|
||||
import art.arcane.iris.generation.runtime.PreservationRegistry;
|
||||
import art.arcane.iris.spi.IrisServices;
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.studio.view.GuiHost;
|
||||
import art.arcane.iris.testsupport.PlatformLeakGuard;
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import org.junit.ClassRule;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.UUID;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class NoiseExplorerGUITest {
|
||||
@ClassRule
|
||||
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
|
||||
|
||||
@Test
|
||||
public void openingInitialSourceBuildsSamplerExactlyOnce() throws Exception {
|
||||
AtomicInteger factoryCalls = new AtomicInteger();
|
||||
CountDownLatch firstFactoryCall = new CountDownLatch(1);
|
||||
CountDownLatch releaseFactory = new CountDownLatch(1);
|
||||
CountDownLatch duplicateFactoryCall = new CountDownLatch(1);
|
||||
AtomicReference<NoiseExplorerGUI> explorerReference = new AtomicReference<>();
|
||||
public void hotloadKeepsTheWorldThatOpenedTheNoiseView() throws Exception {
|
||||
UUID worldId = UUID.randomUUID();
|
||||
Engine refreshed = mock(Engine.class);
|
||||
Engine otherWorld = mock(Engine.class);
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
GuiHost.Provider previous = GuiHost.get();
|
||||
GuiHost.Provider provider = mock(GuiHost.Provider.class);
|
||||
when(provider.findEngine(worldId)).thenReturn(refreshed);
|
||||
when(provider.findActiveEngine()).thenReturn(otherWorld);
|
||||
AtomicReference<NoiseExplorerGUI> opened = new AtomicReference<>();
|
||||
GuiHost.set(provider);
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
NoiseExplorerGUI explorer = new NoiseExplorerGUI(sourceSeed -> {
|
||||
int invocation = factoryCalls.incrementAndGet();
|
||||
if (invocation == 1) {
|
||||
firstFactoryCall.countDown();
|
||||
try {
|
||||
releaseFactory.await(3L, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
} else {
|
||||
duplicateFactoryCall.countDown();
|
||||
}
|
||||
return constantSampler();
|
||||
}, "Test source", 12345L);
|
||||
explorer.buildSidebar();
|
||||
explorer.openViewer();
|
||||
explorerReference.set(explorer);
|
||||
});
|
||||
|
||||
assertTrue(firstFactoryCall.await(3L, TimeUnit.SECONDS));
|
||||
releaseFactory.countDown();
|
||||
assertFalse(duplicateFactoryCall.await(300L, TimeUnit.MILLISECONDS));
|
||||
assertEquals(1, factoryCalls.get());
|
||||
SwingUtilities.invokeAndWait(() -> opened.set(NoiseExplorerGUI.createNoiseExplorer(new PaletteExplorerStart(null, worldId, null, null, 0, 0))));
|
||||
Method lookup = NoiseExplorerGUI.class.getDeclaredMethod("engineAfterHotload");
|
||||
lookup.setAccessible(true);
|
||||
assertSame(refreshed, lookup.invoke(opened.get()));
|
||||
} finally {
|
||||
releaseFactory.countDown();
|
||||
NoiseExplorerGUI explorer = explorerReference.get();
|
||||
NoiseExplorerGUI explorer = opened.get();
|
||||
if (explorer != null) {
|
||||
SwingUtilities.invokeAndWait(explorer::close);
|
||||
}
|
||||
GuiHost.set(previous);
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void allExecutorsRegisterForPreservationAndCloseWithWindow() throws Exception {
|
||||
PreservationRegistry previous = IrisServices.getOrNull(PreservationRegistry.class);
|
||||
IrisSettings previousSettings = IrisSettings.settings;
|
||||
RecordingPreservationRegistry preservation = new RecordingPreservationRegistry();
|
||||
AtomicReference<NoiseExplorerGUI> explorerReference = new AtomicReference<>();
|
||||
IrisServices.register(PreservationRegistry.class, preservation);
|
||||
IrisSettings.settings = new IrisSettings();
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> explorerReference.set(
|
||||
new NoiseExplorerGUI(sourceSeed -> constantSampler(), "Test source", 12345L)
|
||||
));
|
||||
SwingUtilities.invokeAndWait(() -> explorerReference.set(NoiseExplorerGUI.createNoiseExplorer(null)));
|
||||
|
||||
assertEquals(2, preservation.executors.size());
|
||||
assertFalse(preservation.executors.isEmpty());
|
||||
SwingUtilities.invokeAndWait(explorerReference.get()::close);
|
||||
for (ExecutorService executor : preservation.executors) {
|
||||
assertTrue(executor.isShutdown());
|
||||
@@ -92,13 +82,10 @@ public final class NoiseExplorerGUITest {
|
||||
} else {
|
||||
IrisServices.register(PreservationRegistry.class, previous);
|
||||
}
|
||||
IrisSettings.settings = previousSettings;
|
||||
}
|
||||
}
|
||||
|
||||
private static NoiseProvider constantSampler() {
|
||||
return (x, z) -> 0.5D;
|
||||
}
|
||||
|
||||
private static final class RecordingPreservationRegistry implements PreservationRegistry {
|
||||
private final List<ExecutorService> executors = new ArrayList<>();
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.volmlib.util.function.NoiseProvider;
|
||||
import org.junit.Test;
|
||||
@@ -0,0 +1,89 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class NoiseTerrainViewTest {
|
||||
@Test
|
||||
public void stationaryViewRetriesProvisionalTerrainWithoutCameraChanges() throws Exception {
|
||||
AtomicReference<TerrainGrid> firstGrid = new AtomicReference<>();
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
CountDownLatch retried = new CountDownLatch(1);
|
||||
TerrainSampling sampling = new TerrainSampling((grid, row, heights, colors) -> {
|
||||
firstGrid.compareAndSet(null, grid);
|
||||
if (grid.equals(firstGrid.get())) {
|
||||
if (row == 0 && attempts.incrementAndGet() > 1) {
|
||||
retried.countDown();
|
||||
}
|
||||
if (attempts.get() == 1) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
}
|
||||
Arrays.fill(heights, row * grid.columns(), (row + 1) * grid.columns(), 20F);
|
||||
}, 0, 64, Double.NaN);
|
||||
ExecutorService workers = Executors.newSingleThreadExecutor();
|
||||
ExecutorService renderers = Executors.newSingleThreadExecutor();
|
||||
AtomicReference<OrbitViewport> viewport = new AtomicReference<>();
|
||||
AtomicReference<NoiseTerrainView> view = new AtomicReference<>();
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
OrbitViewport opened = new OrbitViewport("Terrain refresh test", null);
|
||||
opened.setSize(640, 480);
|
||||
viewport.set(opened);
|
||||
NoiseTerrainView shown = new NoiseTerrainView(opened, renderers, workers, 1);
|
||||
view.set(shown);
|
||||
shown.show(sampling);
|
||||
});
|
||||
assertTrue(retried.await(5, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
view.get().close();
|
||||
viewport.get().close();
|
||||
});
|
||||
workers.shutdownNow();
|
||||
renderers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(3, TimeUnit.SECONDS));
|
||||
assertTrue(renderers.awaitTermination(3, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closedViewIgnoresSubsequentRefreshes() throws Exception {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
TerrainSampling sampling = new TerrainSampling((grid, row, heights, colors) -> samples.incrementAndGet(),
|
||||
0, 64, Double.NaN);
|
||||
ExecutorService workers = Executors.newSingleThreadExecutor();
|
||||
ExecutorService renderers = Executors.newSingleThreadExecutor();
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
OrbitViewport viewport = new OrbitViewport("Closed terrain test", null);
|
||||
viewport.setSize(32, 32);
|
||||
NoiseTerrainView view = new NoiseTerrainView(viewport, renderers, workers, 1);
|
||||
view.close();
|
||||
view.close();
|
||||
view.show(sampling);
|
||||
view.resume();
|
||||
viewport.close();
|
||||
});
|
||||
workers.shutdown();
|
||||
assertTrue(workers.awaitTermination(3, TimeUnit.SECONDS));
|
||||
assertEquals(0, samples.get());
|
||||
} finally {
|
||||
workers.shutdownNow();
|
||||
renderers.shutdownNow();
|
||||
assertTrue(renderers.awaitTermination(3, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.pack.loading.IrisData;
|
||||
import art.arcane.iris.structure.object.IrisObject;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.swing.JLabel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class ObjectExplorerLifecycleTest {
|
||||
private final List<ObjectExplorer> explorers = new ArrayList<>();
|
||||
private final TextureFuture textureFuture = new TextureFuture();
|
||||
private Object previousTextures;
|
||||
|
||||
@Before
|
||||
public void installTextures() throws Exception {
|
||||
Field shared = field(BlockTextures.class, "shared");
|
||||
previousTextures = shared.get(null);
|
||||
shared.set(null, textureFuture);
|
||||
}
|
||||
|
||||
@After
|
||||
public void closeExplorers() throws Exception {
|
||||
SwingUtilities.invokeAndWait(() -> explorers.forEach(ObjectExplorer::close));
|
||||
textureFuture.completeExceptionally(new IllegalStateException("Test texture download ended"));
|
||||
for (ObjectExplorer explorer : explorers) {
|
||||
executor(explorer, "loader").awaitTermination(3, TimeUnit.SECONDS);
|
||||
executor(explorer, "thumbnailPainter").awaitTermination(3, TimeUnit.SECONDS);
|
||||
}
|
||||
SwingUtilities.invokeAndWait(() -> { });
|
||||
field(BlockTextures.class, "shared").set(null, previousTextures);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closingWhileTexturesArePendingReleasesTheLoaderWithoutCancellingSharedTextures() throws Exception {
|
||||
Catalog catalog = new Catalog();
|
||||
ObjectExplorer explorer = open(catalog);
|
||||
assertTrue(textureFuture.started.await(3, TimeUnit.SECONDS));
|
||||
|
||||
SwingUtilities.invokeAndWait(explorer::close);
|
||||
|
||||
assertTrue("Texture waiting retained the closed preview's loader", executor(explorer, "loader").awaitTermination(3, TimeUnit.SECONDS));
|
||||
assertFalse(textureFuture.isDone());
|
||||
assertEquals(0, catalog.reads.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void textureSuccessAfterClosureDoesNotPublishOrRestartLoading() throws Exception {
|
||||
Catalog catalog = new Catalog();
|
||||
ObjectExplorer explorer = open(catalog);
|
||||
assertTrue(textureFuture.started.await(3, TimeUnit.SECONDS));
|
||||
SwingUtilities.invokeAndWait(explorer::close);
|
||||
|
||||
textureFuture.complete(textures());
|
||||
assertTrue(executor(explorer, "loader").awaitTermination(3, TimeUnit.SECONDS));
|
||||
SwingUtilities.invokeAndWait(() -> { });
|
||||
|
||||
assertEquals(0, catalog.reads.get());
|
||||
assertNull(field(ObjectExplorer.class, "textures").get(explorer));
|
||||
assertStopped(explorer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void textureFailureAfterClosureDoesNotPublishAnError() throws Exception {
|
||||
ObjectExplorer explorer = open(new Catalog());
|
||||
assertTrue(textureFuture.started.await(3, TimeUnit.SECONDS));
|
||||
SwingUtilities.invokeAndWait(explorer::close);
|
||||
|
||||
textureFuture.completeExceptionally(new IllegalStateException("Texture download failed"));
|
||||
assertTrue(executor(explorer, "loader").awaitTermination(3, TimeUnit.SECONDS));
|
||||
SwingUtilities.invokeAndWait(() -> { });
|
||||
|
||||
JLabel summary = (JLabel) field(StudioStatusBar.class, "summary").get(explorer.statusBar());
|
||||
assertEquals("", summary.getText());
|
||||
assertStopped(explorer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void repeatedClosureAndLateOpenDoNotSubmitWorkOrRestartTimers() throws Exception {
|
||||
ObjectExplorer explorer = open(new Catalog());
|
||||
assertTrue(textureFuture.started.await(3, TimeUnit.SECONDS));
|
||||
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
explorer.close();
|
||||
explorer.close();
|
||||
explorer.open();
|
||||
});
|
||||
|
||||
assertTrue(executor(explorer, "loader").awaitTermination(3, TimeUnit.SECONDS));
|
||||
assertStopped(explorer);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void anotherPreviewStillReceivesTheSharedTexturesAfterOneCloses() throws Exception {
|
||||
Catalog firstCatalog = new Catalog();
|
||||
Catalog secondCatalog = new Catalog();
|
||||
ObjectExplorer first = open(firstCatalog);
|
||||
ObjectExplorer second = open(secondCatalog);
|
||||
assertTrue(textureFuture.bothStarted.await(3, TimeUnit.SECONDS));
|
||||
SwingUtilities.invokeAndWait(first::close);
|
||||
assertFalse(textureFuture.isCancelled());
|
||||
|
||||
textureFuture.complete(textures());
|
||||
assertTrue(secondCatalog.read.await(3, TimeUnit.SECONDS));
|
||||
SwingUtilities.invokeAndWait(() -> { });
|
||||
|
||||
assertEquals(0, firstCatalog.reads.get());
|
||||
assertEquals(1, secondCatalog.reads.get());
|
||||
assertTrue(executor(first, "loader").awaitTermination(3, TimeUnit.SECONDS));
|
||||
assertFalse(executor(second, "loader").isShutdown());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closingStopsVariantGalleryAnimation() throws Exception {
|
||||
ObjectExplorer explorer = open(new Catalog());
|
||||
ObjectVariantGallery gallery = (ObjectVariantGallery) field(ObjectExplorer.class, "gallery").get(explorer);
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
gallery.showVariants("Variants", 2, 0);
|
||||
explorer.close();
|
||||
});
|
||||
|
||||
assertFalse(((Timer) field(ObjectVariantGallery.class, "loadingTicker").get(gallery)).isRunning());
|
||||
}
|
||||
|
||||
private ObjectExplorer open(Catalog catalog) throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
ObjectExplorer explorer = new ObjectExplorer(engine, null, catalog);
|
||||
explorers.add(explorer);
|
||||
explorer.open();
|
||||
});
|
||||
return explorers.getLast();
|
||||
}
|
||||
|
||||
private static BlockTextures textures() {
|
||||
BlockTextures textures = mock(BlockTextures.class);
|
||||
MinecraftBiomeColors colors = mock(MinecraftBiomeColors.class);
|
||||
when(textures.biomeColors()).thenReturn(colors);
|
||||
return textures;
|
||||
}
|
||||
|
||||
private static void assertStopped(ObjectExplorer explorer) throws Exception {
|
||||
OrbitViewport viewport = (OrbitViewport) field(ObjectExplorer.class, "viewport").get(explorer);
|
||||
assertFalse(((Timer) field(OrbitViewport.class, "loadingTicker").get(viewport)).isRunning());
|
||||
assertFalse(((Timer) field(OrbitViewport.class, "loadingCloser").get(viewport)).isRunning());
|
||||
assertTrue(executor(viewport, "renderThread").isShutdown());
|
||||
}
|
||||
|
||||
private static ExecutorService executor(Object owner, String name) throws Exception {
|
||||
return (ExecutorService) field(owner.getClass(), name).get(owner);
|
||||
}
|
||||
|
||||
private static Field field(Class<?> owner, String name) throws Exception {
|
||||
Field field = owner.getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field;
|
||||
}
|
||||
|
||||
private static final class TextureFuture extends CompletableFuture<BlockTextures> {
|
||||
private final CountDownLatch started = new CountDownLatch(1);
|
||||
private final CountDownLatch bothStarted = new CountDownLatch(2);
|
||||
|
||||
@Override
|
||||
public BlockTextures get() throws InterruptedException, ExecutionException {
|
||||
started.countDown();
|
||||
bothStarted.countDown();
|
||||
return super.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockTextures join() {
|
||||
started.countDown();
|
||||
bothStarted.countDown();
|
||||
return super.join();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Catalog implements ObjectSource {
|
||||
private final AtomicInteger reads = new AtomicInteger();
|
||||
private final CountDownLatch read = new CountDownLatch(1);
|
||||
|
||||
@Override
|
||||
public String noun() {
|
||||
return "object";
|
||||
}
|
||||
|
||||
@Override
|
||||
public EmptyState emptyState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> keys(IrisData data) {
|
||||
reads.incrementAndGet();
|
||||
read.countDown();
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IrisObject> variants(IrisData data, String key) {
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class OrbitViewportLifecycleTest {
|
||||
@Test
|
||||
public void closedViewportIgnoresLateLoadingAndScenes() throws Exception {
|
||||
AtomicReference<OrbitViewport> holder = new AtomicReference<>();
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
OrbitViewport viewport = new OrbitViewport("Closed viewport test", "Empty");
|
||||
holder.set(viewport);
|
||||
viewport.setSize(32, 32);
|
||||
viewport.close();
|
||||
viewport.close();
|
||||
viewport.showLoading("Late textures");
|
||||
viewport.show(new DelayedScene());
|
||||
});
|
||||
OrbitViewport viewport = holder.get();
|
||||
assertFalse(((Timer) field(viewport, "loadingTicker")).isRunning());
|
||||
assertNull(field(viewport, "scene"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderCompletingAfterClosureDoesNotPublishOrRequestAnotherFrame() throws Exception {
|
||||
DelayedScene scene = new DelayedScene();
|
||||
AtomicReference<OrbitViewport> holder = new AtomicReference<>();
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
OrbitViewport viewport = new OrbitViewport("Closing viewport test", "Empty");
|
||||
holder.set(viewport);
|
||||
viewport.setSize(32, 32);
|
||||
viewport.showLoading("Blocks");
|
||||
viewport.show(scene);
|
||||
});
|
||||
OrbitViewport viewport = holder.get();
|
||||
try {
|
||||
assertTrue(scene.started.await(3, TimeUnit.SECONDS));
|
||||
SwingUtilities.invokeAndWait(viewport::close);
|
||||
scene.release.countDown();
|
||||
assertTrue(((ExecutorService) field(viewport, "renderThread")).awaitTermination(3, TimeUnit.SECONDS));
|
||||
SwingUtilities.invokeAndWait(() -> { });
|
||||
|
||||
assertNull(field(viewport, "picture"));
|
||||
assertFalse(((Timer) field(viewport, "loadingTicker")).isRunning());
|
||||
assertFalse(((Timer) field(viewport, "loadingCloser")).isRunning());
|
||||
} finally {
|
||||
scene.release.countDown();
|
||||
SwingUtilities.invokeAndWait(viewport::close);
|
||||
}
|
||||
}
|
||||
|
||||
private static Object field(Object owner, String name) throws Exception {
|
||||
Field field = owner.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field.get(owner);
|
||||
}
|
||||
|
||||
private static final class DelayedScene implements OrbitScene {
|
||||
private final CountDownLatch started = new CountDownLatch(1);
|
||||
private final CountDownLatch release = new CountDownLatch(1);
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrbitPicture render(OrbitCamera camera, int width, int height, int coveredLeftWidth) {
|
||||
started.countDown();
|
||||
while (release.getCount() != 0) {
|
||||
try {
|
||||
assertTrue(release.await(3, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
return OrbitPicture.whole(new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB), OptionalDouble.empty()).stillCatchingUp();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OrbitProjection projection(OrbitCamera camera, int width, int height, int coveredLeftWidth) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.AbstractExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class TerrainCoverTest {
|
||||
private static final int WIDTH = 1200;
|
||||
private static final int HEIGHT = 800;
|
||||
private static final TerrainSampling FLAT = new TerrainSampling((grid, row, heights, colors) -> {
|
||||
}, 0, 64, Double.NaN);
|
||||
private static final OrbitProjection PROJECTION = NoiseTerrainScene.projectionFrom(Vec3.ZERO, NoiseTerrainScene.START,
|
||||
WIDTH, HEIGHT, 0);
|
||||
|
||||
@Test
|
||||
public void backdropNotSampledYetBorrowsTheNearestSampledCoarserTile() {
|
||||
TerrainTile coarser = TerrainTile.containing(0D, 0D, backdropLevel() + 1);
|
||||
|
||||
List<TerrainTile> drawn = drawnTiles(Map.of(coarser, flatLayer(coarser)));
|
||||
|
||||
assertTrue(drawn.contains(coarser));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tilesTooCoarseToCutHolesInDoNotStandIn() {
|
||||
TerrainTile tooCoarse = TerrainTile.containing(0D, 0D, backdropLevel() + TerrainHole.MOST_LEVELS_FINER + 1);
|
||||
|
||||
List<TerrainTile> drawn = drawnTiles(Map.of(tooCoarse, flatLayer(tooCoarse)));
|
||||
|
||||
assertFalse(drawn.contains(tooCoarse));
|
||||
}
|
||||
|
||||
private static int backdropLevel() {
|
||||
TerrainCover.Result nothingSampled = TerrainCover.of(tilesWith(Map.of()), PROJECTION, WIDTH, HEIGHT, 0);
|
||||
int detailLevel = Integer.numberOfTrailingZeros(nothingSampled.detailStep());
|
||||
return detailLevel + 3;
|
||||
}
|
||||
|
||||
private static List<TerrainTile> drawnTiles(Map<TerrainTile, TerrainLayer> sampled) {
|
||||
return TerrainCover.of(tilesWith(sampled), PROJECTION, WIDTH, HEIGHT, 0).layers().stream()
|
||||
.map(HoledLayer::tile)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private static TerrainTiles tilesWith(Map<TerrainTile, TerrainLayer> sampled) {
|
||||
return new TerrainTiles(FLAT, new NeverRuns(), 1, () -> {
|
||||
}, sampled);
|
||||
}
|
||||
|
||||
private static TerrainLayer flatLayer(TerrainTile tile) {
|
||||
return TerrainLayer.of(NoiseTerrain.empty(tile.grid()), FLAT.floorY());
|
||||
}
|
||||
|
||||
// Keeps the wanted tiles unsampled, so each test decides exactly what is ready.
|
||||
private static final class NeverRuns extends AbstractExecutorService {
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
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.assertNotNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public final class TerrainTilesTest {
|
||||
@Test
|
||||
public void provisionalTerrainStandsInUntilACompleteRetryReplacesIt() throws Exception {
|
||||
AtomicBoolean provisional = new AtomicBoolean(true);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
CountDownLatch first = new CountDownLatch(1);
|
||||
CountDownLatch second = new CountDownLatch(1);
|
||||
TerrainSampling sampling = new TerrainSampling((grid, row, heights, colors) -> {
|
||||
if (row == 0) {
|
||||
attempts.incrementAndGet();
|
||||
}
|
||||
if (provisional.get()) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
Arrays.fill(heights, row * grid.columns(), (row + 1) * grid.columns(), provisional.get() ? 10F : 20F);
|
||||
}, 0, 64, Double.NaN);
|
||||
ExecutorService workers = Executors.newSingleThreadExecutor();
|
||||
TerrainTile tile = new TerrainTile(0, 0, 0);
|
||||
TerrainTiles tiles = new TerrainTiles(sampling, workers, 1, () -> {
|
||||
if (attempts.get() == 1) {
|
||||
first.countDown();
|
||||
} else {
|
||||
second.countDown();
|
||||
}
|
||||
}, Map.of());
|
||||
try {
|
||||
tiles.sampleOnly(List.of(tile), Set.of(tile));
|
||||
assertTrue(first.await(3, TimeUnit.SECONDS));
|
||||
assertNotNull(tiles.layerOf(tile));
|
||||
assertTrue(tiles.isStandingIn(tile));
|
||||
assertEquals(10F, tiles.layerOf(tile).terrain().height(0, 0), 0F);
|
||||
|
||||
provisional.set(false);
|
||||
tiles.sampleOnly(List.of(tile), Set.of(tile));
|
||||
assertFalse(second.await(100, TimeUnit.MILLISECONDS));
|
||||
assertEquals(1, attempts.get());
|
||||
Thread.sleep(500L);
|
||||
tiles.sampleOnly(List.of(tile), Set.of(tile));
|
||||
|
||||
assertTrue(second.await(3, TimeUnit.SECONDS));
|
||||
assertFalse(tiles.isStandingIn(tile));
|
||||
assertEquals(20F, tiles.layerOf(tile).terrain().height(0, 0), 0F);
|
||||
tiles.sampleOnly(List.of(tile), Set.of(tile));
|
||||
assertEquals(2, attempts.get());
|
||||
} finally {
|
||||
tiles.close();
|
||||
workers.shutdownNow();
|
||||
assertTrue(workers.awaitTermination(3, TimeUnit.SECONDS));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
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.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
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.when;
|
||||
|
||||
public class VisionRenderControllerTest {
|
||||
@Test
|
||||
public void provisionalTilesRetryWithBackoffAndConvergeWithoutMoving() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTile tile = mock(MapTile.class);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
.thenAnswer(invocation -> {
|
||||
if (attempts.incrementAndGet() < 3) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
return tile;
|
||||
});
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame frame = controller.request(spec(renderer, 1L));
|
||||
await(() -> frame.tiles().getFirst().tile() != null);
|
||||
controller.retryProvisional(frame);
|
||||
assertEquals(1, attempts.get());
|
||||
await(() -> {
|
||||
controller.retryProvisional(frame);
|
||||
return attempts.get() == 2 && controller.progress(frame).activeWorkers() == 0;
|
||||
});
|
||||
controller.retryProvisional(frame);
|
||||
assertEquals(2, attempts.get());
|
||||
assertEquals(0, controller.progress(frame).ready());
|
||||
await(() -> {
|
||||
controller.retryProvisional(frame);
|
||||
return controller.progress(frame).ready() == 1;
|
||||
});
|
||||
assertEquals(3, attempts.get());
|
||||
assertSame(tile, controller.request(spec(renderer, 1L)).tiles().getFirst().tile());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void provisionalTilesAreShownButNeverReusedAsFinal() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTile incomplete = mock(MapTile.class);
|
||||
MapTile complete = mock(MapTile.class);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
.thenAnswer(invocation -> {
|
||||
if (attempts.incrementAndGet() == 1) {
|
||||
ProvisionalSampling.mark();
|
||||
return incomplete;
|
||||
}
|
||||
return complete;
|
||||
});
|
||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||
await(() -> first.tiles().getFirst().tile() != null);
|
||||
assertSame(incomplete, first.tiles().getFirst().tile());
|
||||
VisionRenderController.Frame second = controller.request(spec(renderer, 1L));
|
||||
await(() -> second.tiles().getFirst().tile() == complete);
|
||||
assertEquals(2, attempts.get());
|
||||
VisionRenderController.Frame third = controller.request(spec(renderer, 1L));
|
||||
assertSame(complete, third.tiles().getFirst().tile());
|
||||
assertEquals(2, attempts.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentRevisionInvalidatesFinalTiles() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
MapTile tile = mock(MapTile.class);
|
||||
AtomicInteger attempts = new AtomicInteger();
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
.thenAnswer(invocation -> {
|
||||
attempts.incrementAndGet();
|
||||
return 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);
|
||||
assertEquals(2, attempts.get());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void closeRejectsLatePublication() throws Exception {
|
||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||
CountDownLatch entered = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
||||
.thenAnswer(invocation -> {
|
||||
entered.countDown();
|
||||
while (release.getCount() != 0L) {
|
||||
try {
|
||||
release.await();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
return mock(MapTile.class);
|
||||
});
|
||||
VisionRenderController controller = new VisionRenderController(() -> { });
|
||||
VisionRenderController.Frame frame = controller.request(spec(renderer, 1L));
|
||||
try {
|
||||
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||
controller.close();
|
||||
controller.close();
|
||||
} finally {
|
||||
release.countDown();
|
||||
controller.close();
|
||||
}
|
||||
await(() -> controller.progress(frame).activeWorkers() == 0);
|
||||
assertEquals(null, frame.tiles().getFirst().tile());
|
||||
}
|
||||
|
||||
private static VisionRenderController.RenderSpec spec(MapTileRenderer renderer, long revision) {
|
||||
return new VisionRenderController.RenderSpec(renderer, MapLayer.of(TileType.RIVER), revision,
|
||||
32D, 32D, 1D, 64, 64, false);
|
||||
}
|
||||
|
||||
private static void await(BooleanSupplier condition) throws Exception {
|
||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||
while (!condition.getAsBoolean() && System.nanoTime() < deadline) {
|
||||
Thread.sleep(5L);
|
||||
}
|
||||
assertTrue("render did not converge", condition.getAsBoolean());
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package art.arcane.iris.studio.map;
|
||||
|
||||
import art.arcane.iris.generation.runtime.Engine;
|
||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
import org.junit.Test;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public final class WorldExplorerTest {
|
||||
@Test
|
||||
public void closedExplorerIgnoresShowAndRefresh() throws Exception {
|
||||
Engine engine = mock(Engine.class);
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
WorldExplorer explorer = new WorldExplorer(() -> engine);
|
||||
explorer.close();
|
||||
explorer.close();
|
||||
explorer.show(0D, 0D, true);
|
||||
explorer.refresh(0D, 0D);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hiddenHotloadDiscardsPendingWorldMeasurement() throws Exception {
|
||||
CountDownLatch started = new CountDownLatch(1);
|
||||
CountDownLatch release = new CountDownLatch(1);
|
||||
Engine engine = engineWithHeights(ProceduralStream.ofDouble((x, z) -> {
|
||||
started.countDown();
|
||||
try {
|
||||
assertTrue(release.await(3, TimeUnit.SECONDS));
|
||||
} catch (InterruptedException interrupted) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return 64D;
|
||||
}));
|
||||
AtomicReference<WorldExplorer> opened = new AtomicReference<>();
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
WorldExplorer explorer = new WorldExplorer(() -> engine);
|
||||
opened.set(explorer);
|
||||
explorer.show(0D, 0D, true);
|
||||
});
|
||||
assertTrue(started.await(3, TimeUnit.SECONDS));
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
opened.get().hide();
|
||||
opened.get().refresh(0D, 0D);
|
||||
});
|
||||
release.countDown();
|
||||
((ExecutorService) field(opened.get(), "lookups")).submit(() -> { }).get(3, TimeUnit.SECONDS);
|
||||
SwingUtilities.invokeAndWait(() -> { });
|
||||
assertNull(field(opened.get(), "sampledEngine"));
|
||||
} finally {
|
||||
release.countDown();
|
||||
SwingUtilities.invokeAndWait(() -> opened.get().close());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void provisionalWorldBoundsAreMeasuredAgainWithoutInteraction() throws Exception {
|
||||
AtomicInteger samples = new AtomicInteger();
|
||||
CountDownLatch retried = new CountDownLatch(1);
|
||||
Engine engine = engineWithHeights(ProceduralStream.ofDouble((x, z) -> {
|
||||
int sample = samples.incrementAndGet();
|
||||
if (sample <= 81) {
|
||||
ProvisionalSampling.mark();
|
||||
} else {
|
||||
retried.countDown();
|
||||
}
|
||||
return 64D;
|
||||
}));
|
||||
AtomicReference<WorldExplorer> opened = new AtomicReference<>();
|
||||
try {
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
WorldExplorer explorer = new WorldExplorer(() -> engine);
|
||||
opened.set(explorer);
|
||||
explorer.show(0D, 0D, true);
|
||||
});
|
||||
assertTrue(retried.await(3, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
SwingUtilities.invokeAndWait(() -> opened.get().close());
|
||||
}
|
||||
}
|
||||
|
||||
private static Engine engineWithHeights(ProceduralStream<Double> heights) {
|
||||
Engine engine = mock(Engine.class);
|
||||
IrisComplex complex = mock(IrisComplex.class);
|
||||
when(engine.getComplex()).thenReturn(complex);
|
||||
when(engine.getHeight()).thenReturn(256);
|
||||
when(complex.getHeightStream()).thenReturn(heights);
|
||||
when(complex.getFluidHeight()).thenReturn(64D);
|
||||
return engine;
|
||||
}
|
||||
|
||||
private static Object field(Object owner, String name) throws Exception {
|
||||
Field field = owner.getClass().getDeclaredField(name);
|
||||
field.setAccessible(true);
|
||||
return field.get(owner);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.configuration.IrisSettings;
|
||||
import art.arcane.iris.studio.map.MapWindow;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -38,28 +39,28 @@ public class DesktopGuiLaunchTest {
|
||||
|
||||
@Test
|
||||
public void mapLaunchDoesNotCreateAWindowWithoutDesktop() throws Exception {
|
||||
VisionGUI.launch(null, null);
|
||||
MapWindow.openMap(null);
|
||||
|
||||
assertNoDesktopFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noiseLaunchDoesNotCreateAWindowWithoutDesktop() throws Exception {
|
||||
NoiseExplorerGUI.launch();
|
||||
MapWindow.openNoise(null);
|
||||
|
||||
assertNoDesktopFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void generatorLaunchDoesNotCreateAWindowWithoutDesktop() throws Exception {
|
||||
NoiseExplorerGUI.launchGeneratorKey("test", null, 12345L);
|
||||
MapWindow.openNoise(null, new MapWindow.NoiseSelection("test", null, 12345L));
|
||||
|
||||
assertNoDesktopFailure();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void imageMapLaunchDoesNotReadEngineWithoutDesktop() throws Exception {
|
||||
ImageMapStudioGUI.launch(null);
|
||||
MapWindow.openImageMaps(null);
|
||||
|
||||
assertNoDesktopFailure();
|
||||
}
|
||||
|
||||
@@ -1,486 +0,0 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||
import art.arcane.iris.studio.render.IrisRenderer;
|
||||
import art.arcane.iris.studio.render.RenderType;
|
||||
import art.arcane.iris.testsupport.Await;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.time.Duration;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.function.LongSupplier;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
public class VisionRenderControllerTest {
|
||||
@Test
|
||||
public void visibleTilesAreUniqueAndOrderedCenterFirst() {
|
||||
VisionRenderController.RenderSpec spec = spec(RenderType.BIOME, 7L, 0D, 0D, 4D, 1440, 792);
|
||||
|
||||
List<VisionRenderController.VisibleTile> tiles = VisionRenderController.visibleTiles(spec);
|
||||
|
||||
assertEquals(336, tiles.size());
|
||||
Set<String> coordinates = new HashSet<>();
|
||||
double previousDistance = -1D;
|
||||
for (VisionRenderController.VisibleTile tile : tiles) {
|
||||
assertTrue(coordinates.add(tile.tileX() + ":" + tile.tileZ()));
|
||||
assertTrue(tile.distanceSquared() >= previousDistance);
|
||||
previousDistance = tile.distanceSquared();
|
||||
}
|
||||
assertEquals(1_376_256L, VisionRenderController.sampleCount(tiles.size()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void negativeWorldCoordinatesUseFloorBasedTileOwnership() {
|
||||
VisionRenderController.RenderSpec spec = spec(RenderType.BIOME, 1L, -32D, -32D, 1D, 1, 1);
|
||||
|
||||
List<VisionRenderController.VisibleTile> tiles = VisionRenderController.visibleTiles(spec);
|
||||
|
||||
assertEquals(1, tiles.size());
|
||||
assertEquals(-1L, tiles.get(0).tileX());
|
||||
assertEquals(-1L, tiles.get(0).tileZ());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cacheIdentityContainsOnlyAtlasGenerationModeZoomAndWorldPage() {
|
||||
VisionRenderController.TileKey baseline = new VisionRenderController.TileKey(4L, RenderType.BIOME, 4D, 8L, 9L);
|
||||
|
||||
assertNotEquals(baseline, new VisionRenderController.TileKey(5L, RenderType.BIOME, 4D, 8L, 9L));
|
||||
assertNotEquals(baseline, new VisionRenderController.TileKey(4L, RenderType.RIVER, 4D, 8L, 9L));
|
||||
assertNotEquals(baseline, new VisionRenderController.TileKey(4L, RenderType.BIOME, 4.5D, 8L, 9L));
|
||||
assertNotEquals(baseline, new VisionRenderController.TileKey(4L, RenderType.BIOME, 4D, 7L, 9L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyPublishedPageIsFinalResolutionAndIdenticalRequestsAreCacheHits() throws Exception {
|
||||
IrisRenderer renderer = mock(IrisRenderer.class);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
when(renderer.renderStudioBase(
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
anyInt(),
|
||||
any(RenderType.class),
|
||||
any(BooleanSupplier.class)
|
||||
)).thenAnswer(invocation -> {
|
||||
assertEquals(VisionRenderController.TILE_PIXELS, (int) invocation.getArgument(3));
|
||||
calls.incrementAndGet();
|
||||
return new BufferedImage(
|
||||
VisionRenderController.TILE_PIXELS,
|
||||
VisionRenderController.TILE_PIXELS,
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
);
|
||||
});
|
||||
passThroughRefinement(renderer);
|
||||
VisionRenderController controller = controller(2);
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128);
|
||||
try {
|
||||
VisionRenderController.Frame first = controller.request(spec);
|
||||
Await.until("the first render to report every tile ready", Duration.ofSeconds(2L),
|
||||
() -> controller.progress(first).ready() == controller.progress(first).total());
|
||||
int rendered = calls.get();
|
||||
|
||||
VisionRenderController.Frame cached = controller.request(spec);
|
||||
|
||||
assertEquals(rendered, calls.get());
|
||||
assertEquals(controller.progress(cached).total(), controller.progress(cached).ready());
|
||||
for (VisionRenderController.VisibleTile tile : cached.tiles()) {
|
||||
assertEquals(VisionRenderController.TILE_PIXELS, controller.image(cached, tile).getWidth());
|
||||
assertEquals(VisionRenderController.TILE_PIXELS, controller.image(cached, tile).getHeight());
|
||||
}
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void smallPanReusesEveryOverlappingWorldPage() throws Exception {
|
||||
IrisRenderer renderer = exactRenderer();
|
||||
VisionRenderController controller = controller(2);
|
||||
try {
|
||||
VisionRenderController.Frame first = controller.request(spec(renderer, RenderType.BIOME, 1L, 16D, 16D, 4D, 128, 128));
|
||||
Await.until("the first render to report every tile ready", Duration.ofSeconds(2L),
|
||||
() -> controller.progress(first).ready() == controller.progress(first).total());
|
||||
Set<VisionRenderController.TileKey> firstKeys = keys(first);
|
||||
|
||||
VisionRenderController.Frame panned = controller.request(spec(renderer, RenderType.BIOME, 1L, 20D, 20D, 4D, 128, 128));
|
||||
|
||||
assertEquals(firstKeys, keys(panned));
|
||||
assertEquals(panned.tiles().size(), controller.progress(panned).ready());
|
||||
for (VisionRenderController.VisibleTile pannedTile : panned.tiles()) {
|
||||
VisionRenderController.VisibleTile original = tile(first, panned.key(pannedTile));
|
||||
assertSame(controller.image(first, original), controller.image(panned, pannedTile));
|
||||
}
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void newerViewPublishesWithoutWaitingForCancelledQueuedWork() throws Exception {
|
||||
IrisRenderer renderer = mock(IrisRenderer.class);
|
||||
CountDownLatch staleStarted = new CountDownLatch(1);
|
||||
CountDownLatch latestStarted = new CountDownLatch(1);
|
||||
when(renderer.renderStudio(
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
anyInt(),
|
||||
any(RenderType.class),
|
||||
any(BooleanSupplier.class)
|
||||
)).thenAnswer(invocation -> {
|
||||
RenderType type = invocation.getArgument(4);
|
||||
BooleanSupplier cancelled = invocation.getArgument(5);
|
||||
if (type == RenderType.REGION) {
|
||||
staleStarted.countDown();
|
||||
while (!cancelled.getAsBoolean()) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
throw new java.util.concurrent.CancellationException();
|
||||
}
|
||||
latestStarted.countDown();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
latestStarted.countDown();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
passThroughRefinement(renderer);
|
||||
VisionRenderController controller = controller(2);
|
||||
try {
|
||||
controller.request(spec(renderer, RenderType.REGION, 1L, 0D, 0D, 4D, 64, 64));
|
||||
assertTrue(staleStarted.await(1L, TimeUnit.SECONDS));
|
||||
|
||||
controller.request(spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 64, 64));
|
||||
|
||||
assertTrue(latestStarted.await(1L, TimeUnit.SECONDS));
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completeBaseIsVisibleBeforeBlockedHydrologyRefinementStarts() throws Exception {
|
||||
IrisRenderer renderer = exactRenderer();
|
||||
CountDownLatch overlayStarted = new CountDownLatch(1);
|
||||
CountDownLatch releaseOverlay = new CountDownLatch(1);
|
||||
AtomicInteger baseCalls = new AtomicInteger();
|
||||
AtomicInteger overlays = new AtomicInteger();
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
baseCalls.incrementAndGet();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
overlays.incrementAndGet();
|
||||
overlayStarted.countDown();
|
||||
assertTrue(releaseOverlay.await(2L, TimeUnit.SECONDS));
|
||||
return invocation.getArgument(3);
|
||||
});
|
||||
VisionRenderController controller = controller(3);
|
||||
try {
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 192, 128);
|
||||
VisionRenderController.Frame frame = controller.request(spec);
|
||||
assertTrue(overlayStarted.await(2L, TimeUnit.SECONDS));
|
||||
assertEquals(frame.tiles().size(), baseCalls.get());
|
||||
assertEquals(1, overlays.get());
|
||||
assertEquals(0, controller.progress(frame).ready());
|
||||
for (VisionRenderController.VisibleTile tile : frame.tiles()) {
|
||||
assertEquals(64, controller.image(frame, tile).getWidth());
|
||||
}
|
||||
|
||||
releaseOverlay.countDown();
|
||||
Await.until("every biome overlay to complete", Duration.ofSeconds(2L),
|
||||
() -> controller.progress(frame).ready() == frame.tiles().size());
|
||||
assertEquals(frame.tiles().size(), overlays.get());
|
||||
|
||||
VisionRenderController.Frame cached = controller.request(spec);
|
||||
assertEquals(frame.tiles().size(), controller.progress(cached).ready());
|
||||
assertEquals(frame.tiles().size(), baseCalls.get());
|
||||
assertEquals(frame.tiles().size(), overlays.get());
|
||||
} finally {
|
||||
releaseOverlay.countDown();
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cancelledOverlayKeepsCachedBaseWithoutMarkingItComplete() throws Exception {
|
||||
IrisRenderer renderer = exactRenderer();
|
||||
CountDownLatch staleStarted = new CountDownLatch(1);
|
||||
CountDownLatch latestStarted = new CountDownLatch(1);
|
||||
CountDownLatch releaseLatest = new CountDownLatch(1);
|
||||
AtomicInteger baseCalls = new AtomicInteger();
|
||||
AtomicInteger overlayCalls = new AtomicInteger();
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
baseCalls.incrementAndGet();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
if (overlayCalls.getAndIncrement() == 0) {
|
||||
staleStarted.countDown();
|
||||
BooleanSupplier cancelled = invocation.getArgument(4);
|
||||
while (!cancelled.getAsBoolean()) {
|
||||
Thread.onSpinWait();
|
||||
}
|
||||
throw new CancellationException();
|
||||
}
|
||||
latestStarted.countDown();
|
||||
assertTrue(releaseLatest.await(2L, TimeUnit.SECONDS));
|
||||
return invocation.getArgument(3);
|
||||
});
|
||||
VisionRenderController controller = controller(2);
|
||||
try {
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.BIOME, 1L, 0D, 0D, 4D, 128, 128);
|
||||
VisionRenderController.Frame stale = controller.request(spec);
|
||||
assertTrue(staleStarted.await(2L, TimeUnit.SECONDS));
|
||||
|
||||
VisionRenderController.Frame latest = controller.request(spec);
|
||||
assertTrue(latestStarted.await(2L, TimeUnit.SECONDS));
|
||||
assertEquals(stale.tiles().size(), baseCalls.get());
|
||||
assertEquals(0, controller.progress(latest).ready());
|
||||
for (VisionRenderController.VisibleTile tile : latest.tiles()) {
|
||||
assertEquals(64, controller.image(latest, tile).getWidth());
|
||||
}
|
||||
|
||||
releaseLatest.countDown();
|
||||
Await.until("the replacement frame to finish its overlays", Duration.ofSeconds(2L),
|
||||
() -> controller.progress(latest).ready() == latest.tiles().size());
|
||||
assertEquals(0, controller.progress(stale).ready());
|
||||
} finally {
|
||||
releaseLatest.countDown();
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void provisionalTilesAreShownButNeverCachedAndRetriedUntilFinal() throws Exception {
|
||||
IrisRenderer renderer = mock(IrisRenderer.class);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
when(renderer.renderStudio(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(RenderType.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
if (calls.incrementAndGet() <= 2) {
|
||||
ProvisionalSampling.mark();
|
||||
}
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
VisionRenderController controller = controller(1);
|
||||
try {
|
||||
VisionRenderController.RenderSpec spec = spec(renderer, RenderType.RIVER, 1L, 32D, 32D, 1D, 64, 64);
|
||||
VisionRenderController.Frame frame = controller.request(spec);
|
||||
VisionRenderController.VisibleTile tile = frame.tiles().getFirst();
|
||||
Await.until("the provisional tile to show", Duration.ofSeconds(2L), () -> controller.image(frame, tile) != null);
|
||||
assertEquals(1, frame.tiles().size());
|
||||
assertEquals(0, controller.progress(frame).ready());
|
||||
|
||||
VisionRenderController.Frame again = controller.request(spec);
|
||||
Await.until("the uncached tile to render again", Duration.ofSeconds(2L), () -> calls.get() == 2
|
||||
&& controller.image(again, again.tiles().getFirst()) != null);
|
||||
assertEquals(0, controller.progress(again).ready());
|
||||
|
||||
Await.until("the retried tile to become final", Duration.ofSeconds(2L), () -> {
|
||||
controller.retryProvisional(again);
|
||||
return controller.progress(again).ready() == 1;
|
||||
});
|
||||
assertEquals(3, calls.get());
|
||||
VisionRenderController.Frame cached = controller.request(spec);
|
||||
assertEquals(1, controller.progress(cached).ready());
|
||||
controller.retryProvisional(cached);
|
||||
assertEquals(3, calls.get());
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void provisionalRefinementKeepsTheBaseTileAndRetries() throws Exception {
|
||||
IrisRenderer renderer = exactRenderer();
|
||||
AtomicInteger refinements = new AtomicInteger();
|
||||
BufferedImage refinedImage = new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
if (refinements.incrementAndGet() == 1) {
|
||||
ProvisionalSampling.mark();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
}
|
||||
return refinedImage;
|
||||
});
|
||||
VisionRenderController controller = controller(1);
|
||||
try {
|
||||
VisionRenderController.Frame frame = controller.request(spec(renderer, RenderType.BIOME, 1L, 32D, 32D, 1D, 64, 64));
|
||||
VisionRenderController.VisibleTile tile = frame.tiles().getFirst();
|
||||
Await.until("the provisional refinement to finish", Duration.ofSeconds(2L), () -> refinements.get() == 1
|
||||
&& controller.progress(frame).active() == 0);
|
||||
BufferedImage base = controller.image(frame, tile);
|
||||
assertNotEquals(refinedImage, base);
|
||||
assertEquals(0, controller.progress(frame).ready());
|
||||
|
||||
Await.until("the retried refinement to become final", Duration.ofSeconds(2L), () -> {
|
||||
controller.retryProvisional(frame);
|
||||
return controller.progress(frame).ready() == 1;
|
||||
});
|
||||
assertSame(refinedImage, controller.image(frame, tile));
|
||||
assertEquals(2, refinements.get());
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void provisionalTilesBackOffFromHalfASecondDoublingToEightSeconds() throws Exception {
|
||||
IrisRenderer renderer = mock(IrisRenderer.class);
|
||||
AtomicInteger calls = new AtomicInteger();
|
||||
when(renderer.renderStudio(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(RenderType.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> {
|
||||
calls.incrementAndGet();
|
||||
ProvisionalSampling.mark();
|
||||
return new BufferedImage(64, 64, BufferedImage.TYPE_INT_RGB);
|
||||
});
|
||||
AtomicLong now = new AtomicLong();
|
||||
VisionRenderController controller = controller(1, now::get);
|
||||
try {
|
||||
VisionRenderController.Frame frame = controller.request(spec(renderer, RenderType.RIVER, 1L, 32D, 32D, 1D, 64, 64));
|
||||
long[] delays = {500L, 1000L, 2000L, 4000L, 8000L, 8000L};
|
||||
long retryAt = 0L;
|
||||
for (int attempt = 0; attempt < delays.length; attempt++) {
|
||||
int expectedCalls = attempt + 1;
|
||||
long delay = TimeUnit.MILLISECONDS.toNanos(delays[attempt]);
|
||||
Await.until("the provisional render to settle", Duration.ofSeconds(2L), () -> calls.get() == expectedCalls
|
||||
&& controller.progress(frame).active() == 0);
|
||||
retryAt += delay;
|
||||
now.set(retryAt - 1L);
|
||||
controller.retryProvisional(frame);
|
||||
assertEquals(expectedCalls, calls.get());
|
||||
assertEquals(0, controller.progress(frame).active());
|
||||
now.set(retryAt);
|
||||
controller.retryProvisional(frame);
|
||||
}
|
||||
Await.until("the last retry to render", Duration.ofSeconds(2L), () -> calls.get() == delays.length + 1);
|
||||
assertEquals(0, controller.progress(frame).ready());
|
||||
} finally {
|
||||
controller.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static VisionRenderController controller(int workers) {
|
||||
return controller(workers, System::nanoTime);
|
||||
}
|
||||
|
||||
private static VisionRenderController controller(int workers, LongSupplier clock) {
|
||||
return new VisionRenderController(
|
||||
() -> {
|
||||
},
|
||||
new VisionRenderController.RuntimeOptions(workers, 8, false, clock)
|
||||
);
|
||||
}
|
||||
|
||||
private static IrisRenderer exactRenderer() {
|
||||
IrisRenderer renderer = mock(IrisRenderer.class);
|
||||
when(renderer.renderStudio(
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
anyDouble(),
|
||||
anyInt(),
|
||||
any(RenderType.class),
|
||||
any(BooleanSupplier.class)
|
||||
)).thenAnswer(invocation -> new BufferedImage(
|
||||
invocation.getArgument(3),
|
||||
invocation.getArgument(3),
|
||||
BufferedImage.TYPE_INT_RGB
|
||||
));
|
||||
when(renderer.renderStudioBase(anyDouble(), anyDouble(), anyDouble(), anyInt(),
|
||||
any(RenderType.class), any(BooleanSupplier.class))).thenAnswer(invocation -> new BufferedImage(
|
||||
invocation.getArgument(3), invocation.getArgument(3), BufferedImage.TYPE_INT_RGB));
|
||||
passThroughRefinement(renderer);
|
||||
return renderer;
|
||||
}
|
||||
|
||||
private static void passThroughRefinement(IrisRenderer renderer) {
|
||||
when(renderer.refineStudioBiome(anyDouble(), anyDouble(), anyDouble(), any(BufferedImage.class),
|
||||
any(BooleanSupplier.class))).thenAnswer(invocation -> invocation.getArgument(3));
|
||||
}
|
||||
|
||||
private static Set<VisionRenderController.TileKey> keys(VisionRenderController.Frame frame) {
|
||||
Set<VisionRenderController.TileKey> keys = new HashSet<>();
|
||||
for (VisionRenderController.VisibleTile tile : frame.tiles()) {
|
||||
keys.add(frame.key(tile));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private static VisionRenderController.VisibleTile tile(
|
||||
VisionRenderController.Frame frame,
|
||||
VisionRenderController.TileKey key
|
||||
) {
|
||||
for (VisionRenderController.VisibleTile tile : frame.tiles()) {
|
||||
if (frame.key(tile).equals(key)) {
|
||||
return tile;
|
||||
}
|
||||
}
|
||||
throw new AssertionError("Missing tile " + key);
|
||||
}
|
||||
|
||||
private static VisionRenderController.RenderSpec spec(
|
||||
RenderType type,
|
||||
long revision,
|
||||
double centerX,
|
||||
double centerZ,
|
||||
double blocksPerPixel,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
return new VisionRenderController.RenderSpec(
|
||||
mock(IrisRenderer.class),
|
||||
type,
|
||||
revision,
|
||||
centerX,
|
||||
centerZ,
|
||||
blocksPerPixel,
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
|
||||
private static VisionRenderController.RenderSpec spec(
|
||||
IrisRenderer renderer,
|
||||
RenderType type,
|
||||
long revision,
|
||||
double centerX,
|
||||
double centerZ,
|
||||
double blocksPerPixel,
|
||||
int width,
|
||||
int height
|
||||
) {
|
||||
return new VisionRenderController.RenderSpec(
|
||||
renderer,
|
||||
type,
|
||||
revision,
|
||||
centerX,
|
||||
centerZ,
|
||||
blocksPerPixel,
|
||||
width,
|
||||
height
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package art.arcane.iris.studio.view;
|
||||
|
||||
import art.arcane.iris.localization.DesktopUiMessages;
|
||||
import art.arcane.iris.studio.render.RenderType;
|
||||
import art.arcane.volmlib.util.localization.MessageKey;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
public class VisionRenderTypeTest {
|
||||
@Test
|
||||
public void everyRenderTypeHasALocalizedVisionLabel() {
|
||||
for (RenderType type : RenderType.values()) {
|
||||
MessageKey key = VisionGUI.modeKey(type);
|
||||
assertNotNull(type.name(), key);
|
||||
assertTrueCatalogMember(key);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void riverRenderTypeUsesTheRiverNetworkLabel() {
|
||||
assertSame(DesktopUiMessages.VISION_MODE_RIVER, VisionGUI.modeKey(RenderType.RIVER));
|
||||
assertEquals("iris.desktop.vision.mode.river", DesktopUiMessages.VISION_MODE_RIVER.id());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void teleportCoordinatesFloorAcrossTheNegativeOrigin() {
|
||||
assertEquals(-1, VisionGUI.floorWorldCoordinate(-0.01D));
|
||||
assertEquals(0, VisionGUI.floorWorldCoordinate(0.99D));
|
||||
assertEquals(1, VisionGUI.floorWorldCoordinate(1D));
|
||||
}
|
||||
|
||||
private static void assertTrueCatalogMember(MessageKey key) {
|
||||
assertEquals(key.id(), 1, DesktopUiMessages.keys().stream()
|
||||
.filter(candidate -> candidate.id().equals(key.id()))
|
||||
.count());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user