Redesigned pregeneration map

- Desktop pregeneration uses a proportional map with labeled controls and metrics
- Pixel updates remain bounded across large targets and minimized windows
- Preview lifecycle keeps pause controls attached to their owning job
- Bundled language catalogs include the revised desktop labels
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-06 14:36:32 -04:00
commit 28c715ff51
26 files changed
+1406 -375

No files matched your search

@@ -0,0 +1,109 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.core.pregenerator.PregenTask;
import art.arcane.volmlib.util.math.Position2;
import org.junit.Test;
import javax.swing.SwingUtilities;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.lang.reflect.Field;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class PregenMapPanelTest {
@Test
public void singleChunkAndInclusiveFinalEdgesReceivePixels() throws Exception {
PregenMapPanel single = panel(new PregenRenderSnapshot.Bounds(5, -9, 5, -9));
single.submit(5, -9, Color.GREEN);
flush(single);
assertEquals(Color.GREEN.getRGB(), image(single).getRGB(0, 0));
PregenMapPanel rectangle = panel(new PregenRenderSnapshot.Bounds(-2, -3, 1, -1));
rectangle.submit(1, -1, Color.RED);
rectangle.submit(-2, -3, Color.BLUE);
flush(rectangle);
assertEquals(Color.RED.getRGB(), image(rectangle).getRGB(3, 2));
assertEquals(Color.BLUE.getRGB(), image(rectangle).getRGB(0, 0));
}
@Test
public void largeMapsStayBoundedAndEverySubmittedChunkCanReachAPixel() throws Exception {
PregenMapPanel panel = panel(new PregenRenderSnapshot.Bounds(Integer.MIN_VALUE, -10, Integer.MAX_VALUE, 10));
panel.submit(Integer.MAX_VALUE, 10, Color.GREEN);
panel.submit(0, 0, Color.RED);
flush(panel);
assertEquals(1024, image(panel).getWidth());
assertEquals(21, image(panel).getHeight());
assertEquals(Color.GREEN.getRGB(), image(panel).getRGB(1023, 20));
assertEquals(Color.RED.getRGB(), image(panel).getRGB(512, 10));
}
@Test
public void duplicateAndOutOfBoundsSubmissionsDoNotRequestAnotherFrame() throws Exception {
PregenMapPanel panel = panel(new PregenRenderSnapshot.Bounds(0, 0, 1, 1));
panel.submit(2, 0, Color.GREEN);
SwingUtilities.invokeAndWait(() -> assertFalse(panel.flush()));
panel.submit(0, 0, Color.GREEN);
SwingUtilities.invokeAndWait(() -> assertTrue(panel.flush()));
panel.submit(0, 0, Color.GREEN);
SwingUtilities.invokeAndWait(() -> assertFalse(panel.flush()));
SwingUtilities.invokeAndWait(panel::disposeMap);
panel.submit(0, 0, Color.RED);
SwingUtilities.invokeAndWait(() -> assertFalse(panel.flush()));
}
@Test
public void pixelUpdatesRetainTheirLatestValueUntilTheViewFlushes() throws Exception {
PregenMapPanel panel = panel(new PregenRenderSnapshot.Bounds(0, 0, 10_000, 0));
for (int index = 0; index < 100_000; index++) {
panel.submit(index % 10_001, 0, Color.RED);
}
panel.submit(10_000, 0, Color.GREEN);
flush(panel);
assertEquals(Color.GREEN.getRGB(), image(panel).getRGB(1023, 0));
assertEquals(1024, image(panel).getWidth());
}
@Test
public void mapFitPreservesRectangularChunkAspect() {
Rectangle wide = PregenMapPanel.fit(new PregenRenderSnapshot.Bounds(0, 0, 3, 1), 802, 602);
assertEquals(new Rectangle(1, 101, 800, 400), wide);
Rectangle tall = PregenMapPanel.fit(new PregenRenderSnapshot.Bounds(0, 0, 1, 3), 802, 602);
assertEquals(new Rectangle(251, 1, 300, 600), tall);
}
@Test
public void arithmeticTaskBoundsMatchActualRectangularTraversal() {
PregenTask task = PregenTask.builder().center(new Position2(-17, 35)).radiusX(9).radiusZ(37).build();
int[] actual = {Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MIN_VALUE};
task.iterateAllChunks((x, z) -> {
actual[0] = Math.min(actual[0], x);
actual[1] = Math.min(actual[1], z);
actual[2] = Math.max(actual[2], x);
actual[3] = Math.max(actual[3], z);
});
assertArrayEquals(actual, task.chunkBounds());
}
private static PregenMapPanel panel(PregenRenderSnapshot.Bounds bounds) throws Exception {
PregenMapPanel[] result = new PregenMapPanel[1];
SwingUtilities.invokeAndWait(() -> result[0] = new PregenMapPanel(bounds));
return result[0];
}
private static void flush(PregenMapPanel panel) throws Exception {
SwingUtilities.invokeAndWait(panel::flush);
}
private static BufferedImage image(PregenMapPanel panel) throws Exception {
Field field = PregenMapPanel.class.getDeclaredField("image");
field.setAccessible(true);
return (BufferedImage) field.get(panel);
}
}
@@ -0,0 +1,120 @@
package art.arcane.iris.core.gui;
import art.arcane.iris.core.pregenerator.IrisPregenerator;
import art.arcane.iris.core.pregenerator.PregenPhaseTracker;
import art.arcane.iris.core.pregenerator.PregenRates;
import art.arcane.volmlib.util.format.MemoryMonitor;
import org.junit.Test;
import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verifyNoInteractions;
public class PregeneratorJobRenderSnapshotTest {
@Test
public void viewSnapshotPublishesAllRatesMemoryAndFailedCountsTogether() throws Exception {
Fixture fixture = new Fixture();
when(fixture.pregenerator.getFailedChunks()).thenReturn(2L);
fixture.tick();
PregenRenderSnapshot view = fixture.job.renderSnapshot();
assertEquals(PregenRenderSnapshot.Phase.GENERATING, view.phase());
assertEquals(12D, view.progress().chunksPerSecond(), 0D);
assertEquals(7D, view.progress().overallChunksPerSecond(), 0D);
assertEquals(18D, view.progress().thirtySecondChunksPerSecond(), 0D);
assertEquals(24D, view.progress().sixtySecondChunksPerSecond(), 0D);
assertEquals(2L, view.progress().failed());
assertEquals(1234L, view.usedMemoryBytes());
assertEquals(0.25D, view.memoryUsage(), 0D);
assertEquals(256L, view.allocationBytesPerSecond());
assertTrue(view.cached());
assertSame(view, fixture.job.renderSnapshot());
}
@Test
public void stoppingAndFinishedPhasesCannotBeOverwrittenByTickOrSave() throws Exception {
Fixture fixture = new Fixture();
fixture.stopping.set(true);
fixture.tick();
assertEquals(PregenRenderSnapshot.Phase.STOPPING, fixture.job.renderSnapshot().phase());
fixture.job.onSaving();
assertEquals(PregenRenderSnapshot.Phase.STOPPING, fixture.job.renderSnapshot().phase());
fixture.stopping.set(false);
fixture.set("finished", true);
fixture.tick();
assertEquals(PregenRenderSnapshot.Phase.STOPPING, fixture.job.renderSnapshot().phase());
}
@Test
public void pausedAndErrorStateKeepTheirPrecedence() throws Exception {
Fixture fixture = new Fixture();
when(fixture.pregenerator.paused()).thenReturn(true);
fixture.job.onSaving();
assertEquals(PregenRenderSnapshot.Phase.PAUSED, fixture.job.renderSnapshot().phase());
assertTrue(fixture.job.renderSnapshot().progress().paused());
fixture.set("failure", "Preparation failed");
fixture.tick();
assertEquals(PregenRenderSnapshot.Phase.ERROR, fixture.job.renderSnapshot().phase());
assertEquals("Preparation failed", fixture.job.renderSnapshot().failure());
}
@Test
public void finishedWindowCannotPauseItsReplacementJob() throws Exception {
Fixture previous = new Fixture();
Fixture replacement = new Fixture();
Runnable queuedPause = previous.job::togglePause;
previous.set("finished", true);
Field field = PregeneratorJob.class.getDeclaredField("instance");
field.setAccessible(true);
@SuppressWarnings("unchecked")
AtomicReference<PregeneratorJob> instance = (AtomicReference<PregeneratorJob>) field.get(null);
PregeneratorJob original = instance.getAndSet(replacement.job);
try {
queuedPause.run();
verifyNoInteractions(previous.pregenerator, replacement.pregenerator);
} finally {
instance.set(original);
}
}
private static final class Fixture {
private final PregeneratorJob job = mock(PregeneratorJob.class, CALLS_REAL_METHODS);
private final IrisPregenerator pregenerator = mock(IrisPregenerator.class);
private final AtomicBoolean stopping = new AtomicBoolean();
private Fixture() throws Exception {
MemoryMonitor monitor = mock(MemoryMonitor.class);
when(monitor.getUsedBytes()).thenReturn(1234L);
when(monitor.getUsagePercent()).thenReturn(0.25D);
when(monitor.getPressure()).thenReturn(256L);
when(pregenerator.getRates()).thenReturn(new PregenRates(7D, 12D, 18D, 24D));
set("bounds", new PregenRenderSnapshot.Bounds(-5, 2, 8, 12));
set("pregenerator", pregenerator);
set("monitor", monitor);
set("stopRequested", stopping);
set("saving", new AtomicBoolean());
set("onProgress", List.of());
set("apiPhases", new PregenPhaseTracker());
}
private void tick() {
job.onTick(12D, 720D, 1D, 0.5D, 5L, 10L, 3L, 1000L, 2500L, "Async", true);
}
private void set(String name, Object value) throws Exception {
Field field = PregeneratorJob.class.getDeclaredField(name);
field.setAccessible(true);
field.set(job, value);
}
}
}