mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-26 22:10:18 +00:00
Fixed saved biome previews
- Pregeneration previews wait for saved biome reads on the bounded renderer worker - Preview colors retain historical definitions through rendering - Existing chunk previews avoid biome reads while the window is hidden - Query completion handles cache eviction and capture refresh - Regression tests cover read failures, interruption, and shutdown ownership
This commit is contained in:
1 parent
5570923d30
commit
df717cf151
8 files changed
+617
-44
No files matched your search
@@ -33,6 +33,7 @@ import art.arcane.iris.core.pregenerator.PregenRates;
|
|||||||
import art.arcane.iris.core.pregenerator.PregenTask;
|
import art.arcane.iris.core.pregenerator.PregenTask;
|
||||||
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
import art.arcane.iris.core.pregenerator.PregeneratorMethod;
|
||||||
import art.arcane.iris.engine.framework.Engine;
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||||
import art.arcane.volmlib.util.format.Form;
|
import art.arcane.volmlib.util.format.Form;
|
||||||
import art.arcane.volmlib.util.format.MemoryMonitor;
|
import art.arcane.volmlib.util.format.MemoryMonitor;
|
||||||
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
import art.arcane.volmlib.util.mantle.runtime.Mantle;
|
||||||
@@ -492,18 +493,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChunkGenerated(int x, int z, boolean cached) {
|
public void onChunkGenerated(int x, int z, boolean cached) {
|
||||||
if (renderer == null || !renderer.isVisibleFrame()) return;
|
drawChunkPreview(x, z, COLOR_GENERATED);
|
||||||
if (service.isShutdown()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
service.execute(() -> {
|
|
||||||
if (engine != null) {
|
|
||||||
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(x, z, COLOR_GENERATED);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -578,12 +568,7 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onChunkExistsInRegionGen(int x, int z) {
|
public void onChunkExistsInRegionGen(int x, int z) {
|
||||||
if (engine != null) {
|
drawChunkPreview(x, z, COLOR_EXISTS);
|
||||||
draw(x, z, engine.draw((x << 4) + 8, (z << 4) + 8));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
draw(x, z, COLOR_EXISTS);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -606,6 +591,35 @@ public class PregeneratorJob implements PregenListener, PregenRenderSource {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void drawChunkPreview(int x, int z, Color statusColor) {
|
||||||
|
PregenRenderer activeRenderer = renderer;
|
||||||
|
if (activeRenderer == null || !activeRenderer.isVisibleFrame() || service.isShutdown()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
draw(x, z, statusColor);
|
||||||
|
if (engine != null) {
|
||||||
|
service.execute(() -> renderChunkPreview(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderChunkPreview(int x, int z) {
|
||||||
|
PregenRenderer activeRenderer = renderer;
|
||||||
|
if (activeRenderer == null || !activeRenderer.isVisibleFrame() || service.isShutdown() || engine.isClosing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
draw(x, z, engine.drawForPreview((x << 4) + 8, (z << 4) + 8));
|
||||||
|
} catch (InterruptedException interruption) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (SavedBiomeUnavailableException unavailable) {
|
||||||
|
if (!unavailable.isLoading()) {
|
||||||
|
IrisLogging.reportError("Unable to draw saved biome information for chunk " + x + "," + z + ".", unavailable);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException failure) {
|
||||||
|
IrisLogging.reportError("Unable to draw the pregeneration preview for chunk " + x + "," + z + ".", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public record Configuration(
|
public record Configuration(
|
||||||
PregenTask task,
|
PregenTask task,
|
||||||
PregeneratorMethod method,
|
PregeneratorMethod method,
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ import lombok.Data;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -547,6 +548,26 @@ public class IrisEngine implements Engine {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Color drawForPreview(int x, int z) throws InterruptedException {
|
||||||
|
try (GenerationSessionLease lease = acquireGenerationLease("pregen_preview")) {
|
||||||
|
if (!usesSavedBiomeEnvironment()) {
|
||||||
|
return Engine.super.draw(x, z);
|
||||||
|
}
|
||||||
|
return generationHistoryRuntimeRouter.biomes().readSurfaceBiome(x, z, saved -> {
|
||||||
|
if (saved.isPresent()) {
|
||||||
|
return drawBiomeEnvironment(x, z, saved.get());
|
||||||
|
}
|
||||||
|
try (GenerationHistoryRuntimeRouter.CoordinateScope ignored =
|
||||||
|
openGenerationHistoryCoordinateScopeUnchecked(x, z, "draw a pregeneration preview")) {
|
||||||
|
return Engine.super.draw(x, z);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (GenerationSessionException failure) {
|
||||||
|
throw new IllegalStateException("Pregeneration preview was rejected by the Iris lifecycle.", failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BiomeEnvironment getBiomeEnvironment(int x, int y, int z) {
|
public BiomeEnvironment getBiomeEnvironment(int x, int y, int z) {
|
||||||
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
|
Optional<BiomeEnvironment> saved = resolveSavedBiomeEnvironment(x, y, z, false);
|
||||||
|
|||||||
@@ -262,11 +262,21 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer,
|
|||||||
|
|
||||||
@BlockCoordinates
|
@BlockCoordinates
|
||||||
default Color draw(double x, double z) {
|
default Color draw(double x, double z) {
|
||||||
IrisRegion region = getRegion((int) x, (int) z);
|
return drawBiomeEnvironment((int) x, (int) z, getSurfaceBiomeEnvironment((int) x, (int) z));
|
||||||
IrisBiome biome = getSurfaceBiome((int) x, (int) z);
|
}
|
||||||
int height = getHeight((int) x, (int) z);
|
|
||||||
|
@BlockCoordinates
|
||||||
|
default Color drawForPreview(int x, int z) throws InterruptedException {
|
||||||
|
return draw(x, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BlockCoordinates
|
||||||
|
default Color drawBiomeEnvironment(int x, int z, BiomeEnvironment environment) {
|
||||||
|
IrisRegion region = environment.region();
|
||||||
|
IrisBiome biome = environment.biome();
|
||||||
|
int height = getHeight(x, z);
|
||||||
double heightFactor = M.lerpInverse(0, getTarget().getHeight(), height);
|
double heightFactor = M.lerpInverse(0, getTarget().getHeight(), height);
|
||||||
Color irc = region.getColor(this.getComplex(), RenderType.BIOME);
|
Color irc = region.getColor(environment::data, RenderType.BIOME);
|
||||||
Color ibc = biome.getColor(this, RenderType.BIOME);
|
Color ibc = biome.getColor(this, RenderType.BIOME);
|
||||||
Color rc = irc != null ? irc : Color.GREEN.darker();
|
Color rc = irc != null ? irc : Color.GREEN.darker();
|
||||||
Color bc = ibc != null ? ibc : biome.isAquatic() ? Color.BLUE : Color.YELLOW;
|
Color bc = ibc != null ? ibc : biome.isAquatic() ? Color.BLUE : Color.YELLOW;
|
||||||
|
|||||||
@@ -20,10 +20,14 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
public final class SavedBiomeRuntime implements AutoCloseable {
|
public final class SavedBiomeRuntime implements AutoCloseable {
|
||||||
private static final int MAXIMUM_QUERIES = 128;
|
private static final int MAXIMUM_QUERIES = 128;
|
||||||
@@ -36,6 +40,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
|||||||
private final Map<String, Definitions> definitions = new ConcurrentHashMap<>();
|
private final Map<String, Definitions> definitions = new ConcurrentHashMap<>();
|
||||||
private final LinkedHashMap<Long, PreparedQuery> queries = new LinkedHashMap<>(32, 0.75F, true);
|
private final LinkedHashMap<Long, PreparedQuery> queries = new LinkedHashMap<>(32, 0.75F, true);
|
||||||
private final Map<Long, PendingQuery> pending = new HashMap<>();
|
private final Map<Long, PendingQuery> pending = new HashMap<>();
|
||||||
|
private final ReentrantReadWriteLock consumption = new ReentrantReadWriteLock();
|
||||||
private long queryBytes;
|
private long queryBytes;
|
||||||
private final ExecutorService reads = Executors.newFixedThreadPool(2, task -> {
|
private final ExecutorService reads = Executors.newFixedThreadPool(2, task -> {
|
||||||
Thread thread = new Thread(task, "Iris Saved Biomes");
|
Thread thread = new Thread(task, "Iris Saved Biomes");
|
||||||
@@ -54,6 +59,31 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
|||||||
return resolve(blockX, worldY, blockZ, surface ? QueryKind.SURFACE : QueryKind.VOLUME);
|
return resolve(blockX, worldY, blockZ, surface ? QueryKind.SURFACE : QueryKind.VOLUME);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <T> T readSurfaceBiome(int blockX, int blockZ, Function<Optional<BiomeEnvironment>, T> reader)
|
||||||
|
throws InterruptedException {
|
||||||
|
Objects.requireNonNull(reader, "reader");
|
||||||
|
PreparedQuery prepared;
|
||||||
|
try {
|
||||||
|
prepared = queryAsync(Math.floorDiv(blockX, 16), Math.floorDiv(blockZ, 16)).get();
|
||||||
|
} catch (ExecutionException failure) {
|
||||||
|
Throwable cause = failure.getCause();
|
||||||
|
if (cause instanceof RuntimeException runtimeFailure) {
|
||||||
|
throw runtimeFailure;
|
||||||
|
}
|
||||||
|
if (cause instanceof Error error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
throw new IllegalStateException("Unable to prepare a saved biome preview.", cause);
|
||||||
|
}
|
||||||
|
consumption.readLock().lockInterruptibly();
|
||||||
|
try {
|
||||||
|
requireOpen();
|
||||||
|
return reader.apply(resolve(prepared, blockX, 0, blockZ, QueryKind.SURFACE));
|
||||||
|
} finally {
|
||||||
|
consumption.readLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public NativeBiomeSpawnSelection nativeSpawnSelection(int blockX, int worldY, int blockZ, String physicalBiomeKey) {
|
public NativeBiomeSpawnSelection nativeSpawnSelection(int blockX, int worldY, int blockZ, String physicalBiomeKey) {
|
||||||
PreparedQuery prepared = query(Math.floorDiv(blockX, 16), Math.floorDiv(blockZ, 16));
|
PreparedQuery prepared = query(Math.floorDiv(blockX, 16), Math.floorDiv(blockZ, 16));
|
||||||
if (prepared.failure() != null) {
|
if (prepared.failure() != null) {
|
||||||
@@ -89,7 +119,10 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
|||||||
private Optional<BiomeEnvironment> resolve(int blockX, int worldY, int blockZ, QueryKind kind) {
|
private Optional<BiomeEnvironment> resolve(int blockX, int worldY, int blockZ, QueryKind kind) {
|
||||||
int chunkX = Math.floorDiv(blockX, 16);
|
int chunkX = Math.floorDiv(blockX, 16);
|
||||||
int chunkZ = Math.floorDiv(blockZ, 16);
|
int chunkZ = Math.floorDiv(blockZ, 16);
|
||||||
PreparedQuery query = query(chunkX, chunkZ);
|
return resolve(query(chunkX, chunkZ), blockX, worldY, blockZ, kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<BiomeEnvironment> resolve(PreparedQuery query, int blockX, int worldY, int blockZ, QueryKind kind) {
|
||||||
if (query.failure() != null) {
|
if (query.failure() != null) {
|
||||||
throw query.failure();
|
throw query.failure();
|
||||||
}
|
}
|
||||||
@@ -184,22 +217,27 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
|||||||
pending.clear();
|
pending.clear();
|
||||||
queryBytes = 0L;
|
queryBytes = 0L;
|
||||||
}
|
}
|
||||||
for (Definitions source : definitions.values()) {
|
consumption.writeLock().lock();
|
||||||
try {
|
try {
|
||||||
|
for (Definitions source : definitions.values()) {
|
||||||
try {
|
try {
|
||||||
source.data().unregisterEngine(engine);
|
try {
|
||||||
} finally {
|
source.data().unregisterEngine(engine);
|
||||||
source.data().close();
|
} finally {
|
||||||
}
|
source.data().close();
|
||||||
} catch (Throwable closeFailure) {
|
}
|
||||||
if (failure == null) {
|
} catch (Throwable closeFailure) {
|
||||||
failure = closeFailure;
|
if (failure == null) {
|
||||||
} else {
|
failure = closeFailure;
|
||||||
failure.addSuppressed(closeFailure);
|
} else {
|
||||||
|
failure.addSuppressed(closeFailure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
definitions.clear();
|
||||||
|
} finally {
|
||||||
|
consumption.writeLock().unlock();
|
||||||
}
|
}
|
||||||
definitions.clear();
|
|
||||||
if (failure != null) {
|
if (failure != null) {
|
||||||
throw new IllegalStateException("Unable to close saved biome definitions", failure);
|
throw new IllegalStateException("Unable to close saved biome definitions", failure);
|
||||||
}
|
}
|
||||||
@@ -229,20 +267,41 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private PreparedQuery query(int chunkX, int chunkZ) {
|
private PreparedQuery query(int chunkX, int chunkZ) {
|
||||||
|
synchronized (queries) {
|
||||||
|
requireOpen();
|
||||||
|
PreparedQuery cached = queries.get(key(chunkX, chunkZ));
|
||||||
|
if (cached != null) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
queryAsync(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
throw loading(chunkX, chunkZ);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<PreparedQuery> queryAsync(int chunkX, int chunkZ) {
|
||||||
long key = key(chunkX, chunkZ);
|
long key = key(chunkX, chunkZ);
|
||||||
synchronized (queries) {
|
synchronized (queries) {
|
||||||
requireOpen();
|
requireOpen();
|
||||||
PreparedQuery cached = queries.get(key);
|
PreparedQuery cached = queries.get(key);
|
||||||
if (cached != null) {
|
if (cached != null) {
|
||||||
return cached;
|
return CompletableFuture.completedFuture(cached);
|
||||||
}
|
}
|
||||||
if (!pending.containsKey(key) && pending.size() < MAXIMUM_PENDING_QUERIES) {
|
PendingQuery loading = pending.get(key);
|
||||||
PendingQuery loading = new PendingQuery();
|
if (loading != null) {
|
||||||
pending.put(key, loading);
|
return loading.result;
|
||||||
reads.execute(() -> readQuery(chunkX, chunkZ, loading));
|
|
||||||
}
|
}
|
||||||
|
if (pending.size() >= MAXIMUM_PENDING_QUERIES) {
|
||||||
|
return CompletableFuture.failedFuture(loading(chunkX, chunkZ));
|
||||||
|
}
|
||||||
|
PendingQuery requested = new PendingQuery();
|
||||||
|
pending.put(key, requested);
|
||||||
|
reads.execute(() -> readQuery(chunkX, chunkZ, requested));
|
||||||
|
return requested.result;
|
||||||
}
|
}
|
||||||
throw new SavedBiomeUnavailableException("Saved biome information is loading at chunk "
|
}
|
||||||
|
|
||||||
|
private static SavedBiomeUnavailableException loading(int chunkX, int chunkZ) {
|
||||||
|
return new SavedBiomeUnavailableException("Saved biome information is loading at chunk "
|
||||||
+ chunkX + "," + chunkZ + ". Try again shortly.", true);
|
+ chunkX + "," + chunkZ + ". Try again shortly.", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,11 +328,28 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
prepared = new PreparedQuery(Optional.empty(), Map.of(), Map.of(), new SavedBiomeUnavailableException(
|
prepared = new PreparedQuery(Optional.empty(), Map.of(), Map.of(), new SavedBiomeUnavailableException(
|
||||||
"Unable to read the saved biome at chunk " + chunkX + "," + chunkZ + ".", failure), 512L);
|
"Unable to read the saved biome at chunk " + chunkX + "," + chunkZ + ".", failure), 512L);
|
||||||
|
} catch (Error failure) {
|
||||||
|
loading.result.completeExceptionally(failure);
|
||||||
|
throw failure;
|
||||||
} finally {
|
} finally {
|
||||||
|
boolean refresh;
|
||||||
synchronized (queries) {
|
synchronized (queries) {
|
||||||
pending.remove(key, loading);
|
refresh = !closed && loading.invalidated;
|
||||||
if (!closed && !loading.invalidated && prepared != null) {
|
if (refresh) {
|
||||||
cacheQuery(key, prepared);
|
loading.invalidated = false;
|
||||||
|
reads.execute(() -> readQuery(chunkX, chunkZ, loading));
|
||||||
|
} else {
|
||||||
|
pending.remove(key, loading);
|
||||||
|
if (!closed && prepared != null) {
|
||||||
|
cacheQuery(key, prepared);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!refresh) {
|
||||||
|
if (closed || prepared == null) {
|
||||||
|
loading.result.completeExceptionally(new IllegalStateException("Saved biome runtime is closed."));
|
||||||
|
} else {
|
||||||
|
loading.result.complete(prepared);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -399,6 +475,7 @@ public final class SavedBiomeRuntime implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class PendingQuery {
|
private static final class PendingQuery {
|
||||||
|
private final CompletableFuture<PreparedQuery> result = new CompletableFuture<>();
|
||||||
private boolean invalidated;
|
private boolean invalidated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package art.arcane.iris.core.gui;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.framework.Engine;
|
||||||
|
import art.arcane.iris.engine.history.SavedBiomeUnavailableException;
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class PregeneratorJobSavedBiomeTest {
|
||||||
|
@Test
|
||||||
|
public void generatedChunkLoadingDoesNotEscapeRendererWork() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
doThrow(new SavedBiomeUnavailableException("Saved biome information is loading.", true))
|
||||||
|
.when(fixture.engine).draw(anyDouble(), anyDouble());
|
||||||
|
|
||||||
|
fixture.job.onChunkGenerated(2, -3, false);
|
||||||
|
assertEquals(1, fixture.tasks.size());
|
||||||
|
fixture.tasks.removeFirst().run();
|
||||||
|
verify(fixture.renderer).submit(anyInt(), anyInt(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void existingChunkPreviewRunsOnTheRendererWorker() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
doThrow(new SavedBiomeUnavailableException("Saved biome information is loading.", true))
|
||||||
|
.when(fixture.engine).draw(anyDouble(), anyDouble());
|
||||||
|
|
||||||
|
fixture.job.onChunkExistsInRegionGen(2, -3);
|
||||||
|
verify(fixture.engine, never()).draw(anyDouble(), anyDouble());
|
||||||
|
assertEquals(1, fixture.tasks.size());
|
||||||
|
fixture.tasks.removeFirst().run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void existingChunksDoNotResolveBiomesWhileThePreviewIsHidden() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
when(fixture.renderer.isVisibleFrame()).thenReturn(false);
|
||||||
|
|
||||||
|
fixture.job.onChunkExistsInRegionGen(2, -3);
|
||||||
|
|
||||||
|
assertEquals(0, fixture.tasks.size());
|
||||||
|
verify(fixture.engine, never()).draw(anyDouble(), anyDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void queuedPreviewSkipsReadsWhenTheFrameBecomesHidden() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
fixture.job.onChunkGenerated(2, -3, false);
|
||||||
|
when(fixture.renderer.isVisibleFrame()).thenReturn(false);
|
||||||
|
|
||||||
|
fixture.tasks.removeFirst().run();
|
||||||
|
|
||||||
|
verify(fixture.engine, never()).draw(anyDouble(), anyDouble());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void actualDrawFailureStillReportsWhenShutdownStartsDuringRendering() throws Exception {
|
||||||
|
Fixture fixture = new Fixture();
|
||||||
|
IllegalStateException failure = new IllegalStateException("Palette failure");
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
when(fixture.engine.isClosing()).thenReturn(true);
|
||||||
|
throw failure;
|
||||||
|
}).when(fixture.engine).drawForPreview(anyInt(), anyInt());
|
||||||
|
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||||
|
fixture.job.onChunkGenerated(2, -3, false);
|
||||||
|
fixture.tasks.removeFirst().run();
|
||||||
|
|
||||||
|
logging.verify(() -> IrisLogging.reportError(
|
||||||
|
"Unable to draw the pregeneration preview for chunk 2,-3.", failure));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Fixture {
|
||||||
|
private final PregeneratorJob job = mock(PregeneratorJob.class, CALLS_REAL_METHODS);
|
||||||
|
private final Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||||
|
private final PregenRenderer renderer = mock(PregenRenderer.class);
|
||||||
|
private final ArrayList<Runnable> tasks = new ArrayList<>();
|
||||||
|
|
||||||
|
private Fixture() throws Exception {
|
||||||
|
ExecutorService service = mock(ExecutorService.class);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
tasks.add(invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(service).execute(any(Runnable.class));
|
||||||
|
when(renderer.isVisibleFrame()).thenReturn(true);
|
||||||
|
setField("engine", engine);
|
||||||
|
setField("renderer", renderer);
|
||||||
|
setField("service", service);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setField(String name, Object value) throws Exception {
|
||||||
|
Field field = PregeneratorJob.class.getDeclaredField(name);
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(job, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package art.arcane.iris.engine;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.framework.BiomeEnvironment;
|
||||||
|
import art.arcane.iris.engine.framework.GenerationSessionLease;
|
||||||
|
import art.arcane.iris.engine.history.GenerationHistoryRuntimeRouter;
|
||||||
|
import art.arcane.iris.engine.history.SavedBiomeRuntime;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.doCallRealMethod;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisEnginePreviewTest {
|
||||||
|
@Test
|
||||||
|
public void previewRendersThePreparedEnvironmentWhileItIsRetained() throws Exception {
|
||||||
|
IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
GenerationHistoryRuntimeRouter router = mock(GenerationHistoryRuntimeRouter.class);
|
||||||
|
SavedBiomeRuntime biomes = mock(SavedBiomeRuntime.class);
|
||||||
|
GenerationSessionLease lease = mock(GenerationSessionLease.class);
|
||||||
|
BiomeEnvironment environment = new BiomeEnvironment(1L, mock(IrisBiome.class), mock(IrisRegion.class),
|
||||||
|
mock(IrisDimension.class), mock(IrisData.class));
|
||||||
|
Field field = IrisEngine.class.getDeclaredField("generationHistoryRuntimeRouter");
|
||||||
|
field.setAccessible(true);
|
||||||
|
field.set(engine, router);
|
||||||
|
when(engine.acquireGenerationLease("pregen_preview")).thenReturn(lease);
|
||||||
|
when(router.biomes()).thenReturn(biomes);
|
||||||
|
when(biomes.readSurfaceBiome(eq(-24), eq(40), any())).thenAnswer(invocation -> {
|
||||||
|
Function<Optional<BiomeEnvironment>, Color> reader = invocation.getArgument(2);
|
||||||
|
Color color = reader.apply(Optional.of(environment));
|
||||||
|
verify(engine).drawBiomeEnvironment(-24, 40, environment);
|
||||||
|
return color;
|
||||||
|
});
|
||||||
|
when(engine.drawBiomeEnvironment(-24, 40, environment)).thenReturn(Color.RED);
|
||||||
|
doCallRealMethod().when(engine).drawForPreview(-24, 40);
|
||||||
|
|
||||||
|
assertSame(Color.RED, engine.drawForPreview(-24, 40));
|
||||||
|
|
||||||
|
verify(lease).close();
|
||||||
|
verify(engine, never()).getSurfaceBiomeEnvironment(anyInt(), anyInt());
|
||||||
|
verify(engine, never()).getRegion(anyInt(), anyInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package art.arcane.iris.engine.framework;
|
||||||
|
|
||||||
|
import art.arcane.iris.core.loader.IrisData;
|
||||||
|
import art.arcane.iris.engine.object.IrisBiome;
|
||||||
|
import art.arcane.iris.engine.object.IrisDimension;
|
||||||
|
import art.arcane.iris.engine.object.IrisRegion;
|
||||||
|
import art.arcane.iris.util.common.data.DataProvider;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
|
import static org.mockito.Mockito.doReturn;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class EnginePreviewColorTest {
|
||||||
|
@Test
|
||||||
|
public void previewUsesOneSurfaceEnvironmentAndItsHistoricalRegionDefinitions() {
|
||||||
|
Engine engine = mock(Engine.class, CALLS_REAL_METHODS);
|
||||||
|
IrisData data = mock(IrisData.class);
|
||||||
|
IrisRegion region = mock(IrisRegion.class);
|
||||||
|
IrisBiome biome = mock(IrisBiome.class);
|
||||||
|
BiomeEnvironment environment = new BiomeEnvironment(1L, biome, region, mock(IrisDimension.class), data);
|
||||||
|
EngineTarget target = mock(EngineTarget.class);
|
||||||
|
doReturn(environment).when(engine).getSurfaceBiomeEnvironment(-24, 40);
|
||||||
|
doReturn(target).when(engine).getTarget();
|
||||||
|
doReturn(100).when(engine).getHeight(-24, 40);
|
||||||
|
when(target.getHeight()).thenReturn(384);
|
||||||
|
when(region.getColor(any(), any())).thenAnswer(invocation -> {
|
||||||
|
DataProvider provider = invocation.getArgument(0);
|
||||||
|
assertSame(data, provider.getData());
|
||||||
|
return Color.GREEN;
|
||||||
|
});
|
||||||
|
when(biome.getColor(any(), any())).thenReturn(Color.YELLOW);
|
||||||
|
|
||||||
|
assertNotNull(engine.draw(-24, 40));
|
||||||
|
|
||||||
|
verify(engine).getSurfaceBiomeEnvironment(-24, 40);
|
||||||
|
verify(engine, never()).getRegion(anyInt(), anyInt());
|
||||||
|
verify(engine, never()).getSurfaceBiome(anyInt(), anyInt());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ import org.junit.Rule;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.junit.rules.TemporaryFolder;
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -22,6 +24,9 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
@@ -30,6 +35,8 @@ import static org.junit.Assert.assertTrue;
|
|||||||
import static org.mockito.ArgumentMatchers.anyInt;
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
public class SavedBiomeRuntimeTest {
|
public class SavedBiomeRuntimeTest {
|
||||||
@Rule
|
@Rule
|
||||||
@@ -217,9 +224,233 @@ public class SavedBiomeRuntimeTest {
|
|||||||
Files.delete(pack.resolve("regions/main.json"));
|
Files.delete(pack.resolve("regions/main.json"));
|
||||||
assertSame(environment, runtime.resolve(0, 0, 0, true).orElseThrow());
|
assertSame(environment, runtime.resolve(0, 0, 0, true).orElseThrow());
|
||||||
assertEquals("forest", environment.biome().getLoadKey());
|
assertEquals("forest", environment.biome().getLoadKey());
|
||||||
|
assertSame(environment, runtime.readSurfaceBiome(0, 0, Optional::orElseThrow));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void backgroundPreviewWaitsForTheReadWithoutRepeatingTheQuery() throws Exception {
|
||||||
|
allowUnownedChunks();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
when(store.get(0, 0)).thenAnswer(invocation -> {
|
||||||
|
entered.countDown();
|
||||||
|
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||||
|
ExecutorService caller = Executors.newSingleThreadExecutor()) {
|
||||||
|
Future<Boolean> rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||||
|
try {
|
||||||
|
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||||
|
assertThrows(TimeoutException.class, () -> rendered.get(100L, TimeUnit.MILLISECONDS));
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
}
|
||||||
|
assertTrue(rendered.get(5L, TimeUnit.SECONDS));
|
||||||
|
verify(store).get(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void backgroundPreviewRetainsItsResultAcrossQueryCacheEviction() throws Exception {
|
||||||
|
allowUnownedChunks();
|
||||||
|
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||||
|
ExecutorService caller = Executors.newSingleThreadExecutor()) {
|
||||||
|
Field field = SavedBiomeRuntime.class.getDeclaredField("consumption");
|
||||||
|
field.setAccessible(true);
|
||||||
|
ReentrantReadWriteLock consumption = (ReentrantReadWriteLock) field.get(runtime);
|
||||||
|
consumption.writeLock().lock();
|
||||||
|
Future<Boolean> rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||||
|
try {
|
||||||
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||||
|
while (runtime.cachedQueryCount() == 0 && System.nanoTime() < deadline) {
|
||||||
|
Thread.sleep(1L);
|
||||||
|
}
|
||||||
|
assertEquals(1, runtime.cachedQueryCount());
|
||||||
|
for (int chunkX = 1; chunkX <= 130; chunkX++) {
|
||||||
|
int requestedX = chunkX;
|
||||||
|
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
|
||||||
|
() -> runtime.prepareChunk(requestedX, 0)).isLoading());
|
||||||
|
awaitIdle(runtime);
|
||||||
|
}
|
||||||
|
assertThrows(TimeoutException.class, () -> rendered.get(100L, TimeUnit.MILLISECONDS));
|
||||||
|
} finally {
|
||||||
|
consumption.writeLock().unlock();
|
||||||
|
}
|
||||||
|
assertTrue(rendered.get(5L, TimeUnit.SECONDS));
|
||||||
|
verify(store).get(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void captureRefreshesThePendingPreviewWithoutAddingAnotherSlot() throws Exception {
|
||||||
|
allowUnownedChunks();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
AtomicInteger readCount = new AtomicInteger();
|
||||||
|
when(store.get(0, 0)).thenAnswer(invocation -> {
|
||||||
|
if (readCount.getAndIncrement() == 0) {
|
||||||
|
entered.countDown();
|
||||||
|
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||||
|
ExecutorService caller = Executors.newSingleThreadExecutor()) {
|
||||||
|
Future<Boolean> rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||||
|
try {
|
||||||
|
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||||
|
runtime.capture(unresolvedChunk(1));
|
||||||
|
assertEquals(1, runtime.pendingQueryCount());
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
}
|
||||||
|
assertTrue(rendered.get(5L, TimeUnit.SECONDS));
|
||||||
|
verify(store, times(2)).get(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closeWaitsForBackgroundConsumptionBeforeClosingDefinitions() throws Exception {
|
||||||
|
allowUnownedChunks();
|
||||||
|
CountDownLatch consuming = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||||
|
try (ExecutorService callers = Executors.newFixedThreadPool(2)) {
|
||||||
|
Future<Boolean> rendered = callers.submit(() -> runtime.readSurfaceBiome(0, 0, environment -> {
|
||||||
|
consuming.countDown();
|
||||||
|
try {
|
||||||
|
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||||
|
} catch (InterruptedException failure) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new AssertionError(failure);
|
||||||
|
}
|
||||||
|
return environment.isEmpty();
|
||||||
|
}));
|
||||||
|
assertTrue(consuming.await(5L, TimeUnit.SECONDS));
|
||||||
|
Future<?> closing = callers.submit(runtime::close);
|
||||||
|
try {
|
||||||
|
assertThrows(TimeoutException.class, () -> closing.get(100L, TimeUnit.MILLISECONDS));
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
}
|
||||||
|
assertTrue(rendered.get(5L, TimeUnit.SECONDS));
|
||||||
|
closing.get(5L, TimeUnit.SECONDS);
|
||||||
|
assertThrows(IllegalStateException.class, () -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
runtime.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void backgroundPreviewPreservesActualReadFailure() throws Exception {
|
||||||
|
IOException failure = new IOException("Saved biome read failed");
|
||||||
|
when(store.get(0, 0)).thenThrow(failure);
|
||||||
|
try (SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history)) {
|
||||||
|
SavedBiomeUnavailableException unavailable = assertThrows(SavedBiomeUnavailableException.class,
|
||||||
|
() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||||
|
assertEquals(false, unavailable.isLoading());
|
||||||
|
assertSame(failure, unavailable.getCause());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void closeCompletesWaitingPreviewWithoutConsumingClosedDefinitions() throws Exception {
|
||||||
|
allowUnownedChunks();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
when(store.get(0, 0)).thenAnswer(invocation -> {
|
||||||
|
entered.countDown();
|
||||||
|
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||||
|
AtomicInteger consumed = new AtomicInteger();
|
||||||
|
try (ExecutorService callers = Executors.newFixedThreadPool(2)) {
|
||||||
|
Future<Integer> rendered = callers.submit(() -> runtime.readSurfaceBiome(0, 0,
|
||||||
|
environment -> consumed.incrementAndGet()));
|
||||||
|
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||||
|
Future<?> closing = callers.submit(runtime::close);
|
||||||
|
try {
|
||||||
|
assertThrows(TimeoutException.class, () -> closing.get(100L, TimeUnit.MILLISECONDS));
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
}
|
||||||
|
ExecutionException unavailable = assertThrows(ExecutionException.class,
|
||||||
|
() -> rendered.get(5L, TimeUnit.SECONDS));
|
||||||
|
assertTrue(unavailable.getCause() instanceof IllegalStateException);
|
||||||
|
assertEquals(0, consumed.get());
|
||||||
|
closing.get(5L, TimeUnit.SECONDS);
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
runtime.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void fullReadQueueLeavesThePreviewPendingWithoutAddingWork() throws Exception {
|
||||||
|
allowUnownedChunks();
|
||||||
|
CountDownLatch entered = new CountDownLatch(2);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
when(store.get(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||||
|
entered.countDown();
|
||||||
|
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||||
|
try {
|
||||||
|
for (int chunkX = 0; chunkX < 256; chunkX++) {
|
||||||
|
int requestedX = chunkX;
|
||||||
|
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
|
||||||
|
() -> runtime.prepareChunk(requestedX, 0)).isLoading());
|
||||||
|
}
|
||||||
|
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||||
|
assertEquals(256, runtime.pendingQueryCount());
|
||||||
|
assertTrue(assertThrows(SavedBiomeUnavailableException.class,
|
||||||
|
() -> runtime.readSurfaceBiome(256 * 16, 0, Optional::isEmpty)).isLoading());
|
||||||
|
assertEquals(256, runtime.pendingQueryCount());
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
runtime.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void interruptingPreviewDoesNotCancelTheSharedRead() throws Exception {
|
||||||
|
allowUnownedChunks();
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
|
when(store.get(0, 0)).thenAnswer(invocation -> {
|
||||||
|
entered.countDown();
|
||||||
|
assertTrue(release.await(5L, TimeUnit.SECONDS));
|
||||||
|
return Optional.empty();
|
||||||
|
});
|
||||||
|
SavedBiomeRuntime runtime = new SavedBiomeRuntime(engine, history);
|
||||||
|
try (ExecutorService caller = Executors.newSingleThreadExecutor()) {
|
||||||
|
Future<Boolean> rendered = caller.submit(() -> runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||||
|
try {
|
||||||
|
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||||
|
assertTrue(rendered.cancel(true));
|
||||||
|
assertEquals(1, runtime.pendingQueryCount());
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
}
|
||||||
|
assertTrue(runtime.readSurfaceBiome(0, 0, Optional::isEmpty));
|
||||||
|
verify(store).get(0, 0);
|
||||||
|
} finally {
|
||||||
|
release.countDown();
|
||||||
|
runtime.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void allowUnownedChunks() throws IOException {
|
||||||
|
when(store.get(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||||
|
when(history.isActiveUnowned(anyInt(), anyInt())).thenReturn(true);
|
||||||
|
when(history.semantics(anyInt(), anyInt())).thenReturn(Optional.empty());
|
||||||
|
}
|
||||||
|
|
||||||
private static void awaitIdle(SavedBiomeRuntime runtime) throws InterruptedException {
|
private static void awaitIdle(SavedBiomeRuntime runtime) throws InterruptedException {
|
||||||
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5L);
|
||||||
while (runtime.pendingQueryCount() != 0 && System.nanoTime() < deadline) {
|
while (runtime.pendingQueryCount() != 0 && System.nanoTime() < deadline) {
|
||||||
|
|||||||
Reference in new issue
Block a user