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:
Brian Neumann-Fopiano committed 2026-10-25 16:56:49 -04:00
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));
}
}
}
@@ -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,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,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;
}
}
@@ -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,4 +1,4 @@
package art.arcane.iris.studio.view;
package art.arcane.iris.studio.map;
import org.junit.Test;
@@ -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,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,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());
}
}