mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 23:10:32 +00:00
Stabilized shutdown and Studio
- Shutdown protection initializes before enable and drains inline when retention fails - Cleanup preserves class loader retention and closes native providers in order - Error reports retain context and handle empty stack traces - Studio maps retain partial coverage and suspend hidden views - Hydrology previews refresh when plans and rasters complete - Terrain scratch hashing spreads lattice samples across cache slots - Plugin and probe artifacts include LWJGL libraries and native resources - Regression tests cover shutdown, error reporting, map progress, and terrain scratch
This commit is contained in:
1 parent
f64a7c6753
commit
3dc958edcf
31 files changed
+1970
-214
No files matched your search
@@ -57,6 +57,7 @@ import art.arcane.iris.localization.IrisLanguage;
|
|||||||
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
import art.arcane.volmlib.util.director.help.DirectorMiniMenu;
|
||||||
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
import art.arcane.volmlib.util.localization.BukkitLanguageSwitcher;
|
||||||
import art.arcane.iris.integration.MultiverseCoreLink;
|
import art.arcane.iris.integration.MultiverseCoreLink;
|
||||||
|
import art.arcane.iris.platform.agent.Agent;
|
||||||
import art.arcane.iris.platform.bukkit.nms.INMS;
|
import art.arcane.iris.platform.bukkit.nms.INMS;
|
||||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||||
import art.arcane.iris.studio.view.BukkitGuiHost;
|
import art.arcane.iris.studio.view.BukkitGuiHost;
|
||||||
@@ -201,10 +202,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private final AtomicBoolean postStopFinisherStarted = new AtomicBoolean(false);
|
private final AtomicBoolean postStopFinisherStarted = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false);
|
private final AtomicBoolean serverStopTeardownDeferred = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
|
private final AtomicBoolean servicesDisabled = new AtomicBoolean(false);
|
||||||
|
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
private final AtomicBoolean sharedRuntimeClosed = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
private final AtomicBoolean startupBoundaryRestart = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
private final AtomicBoolean terminalCleanupCompleted = new AtomicBoolean(false);
|
||||||
private final AtomicBoolean runtimeTeardownFailed = new AtomicBoolean(false);
|
|
||||||
private volatile boolean generatorDrainCompleted;
|
private volatile boolean generatorDrainCompleted;
|
||||||
private volatile boolean enableFailed;
|
private volatile boolean enableFailed;
|
||||||
private volatile PlaceholderRegistration papiRegistration;
|
private volatile PlaceholderRegistration papiRegistration;
|
||||||
@@ -225,6 +226,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
private BukkitDebugDump debugDump;
|
private BukkitDebugDump debugDump;
|
||||||
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
private volatile SettingsHotloadWatch settingsHotloadWatch;
|
||||||
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
private volatile ServerShutdownBoundary serverShutdownBoundary;
|
||||||
|
private boolean shutdownProtectionInitialized;
|
||||||
|
|
||||||
public static VolmitSender getSender() {
|
public static VolmitSender getSender() {
|
||||||
VolmitSender current = sender;
|
VolmitSender current = sender;
|
||||||
@@ -474,10 +476,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
String n = e.getClass().getCanonicalName() + "-" + e.getStackTrace()[0].getClassName() + "-" + e.getStackTrace()[0].getLineNumber();
|
String n = e.getClass().getName();
|
||||||
|
StackTraceElement[] trace = e.getStackTrace();
|
||||||
|
if (trace.length > 0) {
|
||||||
|
n += "-" + trace[0].getClassName() + "-" + trace[0].getLineNumber();
|
||||||
|
}
|
||||||
|
|
||||||
if (e.getCause() != null) {
|
if (e.getCause() != null) {
|
||||||
n += "-" + e.getCause().getStackTrace()[0].getClassName() + "-" + e.getCause().getStackTrace()[0].getLineNumber();
|
StackTraceElement[] causeTrace = e.getCause().getStackTrace();
|
||||||
|
n += "-" + (causeTrace.length == 0 ? e.getCause().getClass().getName()
|
||||||
|
: causeTrace[0].getClassName() + "-" + causeTrace[0].getLineNumber());
|
||||||
}
|
}
|
||||||
|
|
||||||
File f = instance.getDataFile("debug", "caught-exceptions", n + ".txt");
|
File f = instance.getDataFile("debug", "caught-exceptions", n + ".txt");
|
||||||
@@ -627,10 +635,10 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
postStopFinisherStarted.set(false);
|
postStopFinisherStarted.set(false);
|
||||||
serverStopTeardownDeferred.set(false);
|
serverStopTeardownDeferred.set(false);
|
||||||
servicesDisabled.set(false);
|
servicesDisabled.set(false);
|
||||||
|
runtimeTeardownFailed.set(false);
|
||||||
sharedRuntimeClosed.set(false);
|
sharedRuntimeClosed.set(false);
|
||||||
startupBoundaryRestart.set(false);
|
startupBoundaryRestart.set(false);
|
||||||
terminalCleanupCompleted.set(false);
|
terminalCleanupCompleted.set(false);
|
||||||
runtimeTeardownFailed.set(false);
|
|
||||||
generatorDrainCompleted = false;
|
generatorDrainCompleted = false;
|
||||||
deferredShutdownGenerators.clear();
|
deferredShutdownGenerators.clear();
|
||||||
MultiBurst.burst.reopen();
|
MultiBurst.burst.reopen();
|
||||||
@@ -911,6 +919,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
|
|
||||||
public void onEnable() {
|
public void onEnable() {
|
||||||
enableFailed = false;
|
enableFailed = false;
|
||||||
|
shutdownProtectionInitialized = false;
|
||||||
IrisStartupValidation.begin();
|
IrisStartupValidation.begin();
|
||||||
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
Bukkit.getPluginManager().registerEvents(new IrisStartupAdmissionListener(), this);
|
||||||
try {
|
try {
|
||||||
@@ -922,13 +931,14 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
refuseUnsupportedServer();
|
refuseUnsupportedServer();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
initializeShutdownProtection();
|
||||||
enable();
|
enable();
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
|
enableFailed = true;
|
||||||
if (!FailedEnableLock.engage(failure, FailedEnableLock.irisWorldsPresent(),
|
if (!FailedEnableLock.engage(failure, FailedEnableLock.irisWorldsPresent(),
|
||||||
generatorResolver.refusals().startupWorldsCreated())) {
|
generatorResolver.refusals().startupWorldsCreated())) {
|
||||||
throw failure;
|
throw failure;
|
||||||
}
|
}
|
||||||
enableFailed = true;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
BukkitGuiHost.install();
|
BukkitGuiHost.install();
|
||||||
@@ -946,6 +956,17 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
reportLockedRuntime();
|
reportLockedRuntime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeShutdownProtection() {
|
||||||
|
if (!INMS.get().supportsIrisWorldGeneration()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Agent.install()) {
|
||||||
|
throw new IllegalStateException("Iris Java agent is unavailable; shutdown protection cannot be installed.");
|
||||||
|
}
|
||||||
|
INMS.get().initializeLifecycle(getClass().getClassLoader());
|
||||||
|
shutdownProtectionInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
private void refuseUnsupportedServer() {
|
private void refuseUnsupportedServer() {
|
||||||
Throwable bindFailure = INMS.bindFailure();
|
Throwable bindFailure = INMS.bindFailure();
|
||||||
IllegalStateException failure = new IllegalStateException("Iris cannot start: " + (bindFailure == null
|
IllegalStateException failure = new IllegalStateException("Iris cannot start: " + (bindFailure == null
|
||||||
@@ -1008,7 +1029,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
IrisLanguage.shutdown();
|
IrisLanguage.shutdown();
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping())
|
boolean deferToServerStop = FailedEnableLock.defersTeardownToServerStop(enableFailed, IrisToolbelt.isServerStopping())
|
||||||
&& INMS.get().deferPluginClassLoaderClose();
|
&& tryDeferPluginClassLoaderClose();
|
||||||
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
boolean restartingAtStartupBoundary = startupBoundaryRestart.get();
|
||||||
if (restartingAtStartupBoundary) {
|
if (restartingAtStartupBoundary) {
|
||||||
teardownRuntime("startup-boundary-restart", 30L);
|
teardownRuntime("startup-boundary-restart", 30L);
|
||||||
@@ -1040,7 +1061,7 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
preservePendingBlockDrops();
|
preservePendingBlockDrops();
|
||||||
papiLifecycleActive = false;
|
papiLifecycleActive = false;
|
||||||
teardownPapi();
|
teardownPapi();
|
||||||
if (IrisToolbelt.isServerStopping() && INMS.get().deferPluginClassLoaderClose()) {
|
if (IrisToolbelt.isServerStopping() && tryDeferPluginClassLoaderClose()) {
|
||||||
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
quiesceRuntimeForServerShutdown("pre-unload:" + reason);
|
||||||
startPostStopFinisher();
|
startPostStopFinisher();
|
||||||
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
Iris.debug("Pre-unload hook deferred generator teardown until Paper closes its chunk schedulers.");
|
||||||
@@ -1055,6 +1076,19 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
return CompletableFuture.completedFuture(null);
|
return CompletableFuture.completedFuture(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean tryDeferPluginClassLoaderClose() {
|
||||||
|
if (!shutdownProtectionInitialized) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
INMS.get().deferPluginClassLoaderClose();
|
||||||
|
return true;
|
||||||
|
} catch (RuntimeException | LinkageError failure) {
|
||||||
|
Iris.reportError("Failed to retain the Iris plugin class loader; draining shutdown inline.", failure);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drains the world generators exactly once. Serialized against the JVM shutdown hook so a
|
* Drains the world generators exactly once. Serialized against the JVM shutdown hook so a
|
||||||
* second caller cannot rip the pools or services out from under an in-flight drain.
|
* second caller cannot rip the pools or services out from under an in-flight drain.
|
||||||
@@ -1101,19 +1135,16 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
try {
|
try {
|
||||||
MultiBurst.burst.close();
|
MultiBurst.burst.close();
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
runtimeTeardownFailed.set(true);
|
|
||||||
Iris.reportError("Failed to close Iris generation workers.", failure);
|
Iris.reportError("Failed to close Iris generation workers.", failure);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
MultiBurst.hydrology.close();
|
MultiBurst.hydrology.close();
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
runtimeTeardownFailed.set(true);
|
|
||||||
Iris.reportError("Failed to close Iris hydrology workers.", failure);
|
Iris.reportError("Failed to close Iris hydrology workers.", failure);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
MultiBurst.ioBurst.close();
|
MultiBurst.ioBurst.close();
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
runtimeTeardownFailed.set(true);
|
|
||||||
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
Iris.reportError("Failed to close Iris I/O workers.", failure);
|
||||||
}
|
}
|
||||||
clearQueues();
|
clearQueues();
|
||||||
@@ -1229,30 +1260,37 @@ public class Iris extends VolmitPlugin implements Listener, ReloadAware {
|
|||||||
if (!terminalCleanupCompleted.compareAndSet(false, true)) {
|
if (!terminalCleanupCompleted.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
J.attempt(() -> INMS.get().uninjectBukkit());
|
try {
|
||||||
|
if (INMS.isBound()) {
|
||||||
|
INMS.get().uninjectBukkit();
|
||||||
|
}
|
||||||
|
} catch (Throwable failure) {
|
||||||
|
Iris.reportError("Failed to remove Iris world-generation hooks.", failure);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
runPostShutdown();
|
runPostShutdown();
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
runtimeTeardownFailed.set(true);
|
runtimeTeardownFailed.set(true);
|
||||||
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
Iris.reportError("Failed to run Iris post-shutdown cleanup.", e);
|
||||||
} finally {
|
} finally {
|
||||||
if (serverStopTeardownDeferred.get() && hasActiveShutdownResources()) {
|
boolean activeResources = serverStopTeardownDeferred.get()
|
||||||
runtimeTeardownFailed.set(true);
|
&& (runtimeTeardownFailed.get() || hasActiveShutdownResources());
|
||||||
SHUTDOWN_ERRORS.println("[Iris] Owned background work remains active after cleanup; retaining the plugin class loader until JVM exit.");
|
|
||||||
}
|
|
||||||
IrisPlatforms.unbind();
|
IrisPlatforms.unbind();
|
||||||
if (serverStopTeardownDeferred.get() && runtimeTeardownFailed.get()) {
|
BukkitPlatform.releaseHost();
|
||||||
SHUTDOWN_ERRORS.println("[Iris] Runtime cleanup failed; retaining the plugin class loader until JVM exit.");
|
if (activeResources) {
|
||||||
|
SHUTDOWN_ERRORS.println("[Iris] Owned background work has not completed cleanup; retaining the plugin class loader until JVM exit.");
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
if (shutdownProtectionInitialized) {
|
||||||
INMS.get().releasePluginClassLoaderClose();
|
INMS.get().releasePluginClassLoaderClose();
|
||||||
|
} else {
|
||||||
SlimJar.closeNativeProviders();
|
SlimJar.closeNativeProviders();
|
||||||
|
}
|
||||||
} catch (Throwable failure) {
|
} catch (Throwable failure) {
|
||||||
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
|
SHUTDOWN_ERRORS.println("[Iris] Failed to release the plugin class loader after runtime cleanup.");
|
||||||
failure.printStackTrace(SHUTDOWN_ERRORS);
|
failure.printStackTrace(SHUTDOWN_ERRORS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
BukkitPlatform.releaseHost();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,9 @@ public class FailedEnableLockTest {
|
|||||||
try (EnableFixture fixture = new EnableFixture(false)) {
|
try (EnableFixture fixture = new EnableFixture(false)) {
|
||||||
IllegalStateException failure = assertThrows(IllegalStateException.class, plugin::onEnable);
|
IllegalStateException failure = assertThrows(IllegalStateException.class, plugin::onEnable);
|
||||||
assertTrue(failure.getMessage(), failure.getMessage().contains("already bound"));
|
assertTrue(failure.getMessage(), failure.getMessage().contains("already bound"));
|
||||||
|
Field failed = Iris.class.getDeclaredField("enableFailed");
|
||||||
|
failed.setAccessible(true);
|
||||||
|
assertTrue(failed.getBoolean(plugin));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package art.arcane.iris;
|
||||||
|
|
||||||
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.rules.TemporaryFolder;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class IrisErrorReportingTest {
|
||||||
|
@Rule
|
||||||
|
public TemporaryFolder temporaryFolder = new TemporaryFolder();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void debugReportingPreservesStacklessFailures() throws Exception {
|
||||||
|
ArrayIndexOutOfBoundsException failure = new ArrayIndexOutOfBoundsException("raster index");
|
||||||
|
failure.setStackTrace(new StackTraceElement[0]);
|
||||||
|
|
||||||
|
String output = reportWithDebug(failure);
|
||||||
|
|
||||||
|
assertEquals(1L, output.lines().filter(line -> line.contains("raster index")).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void debugReportingPreservesFailureWithStacklessCause() throws Exception {
|
||||||
|
ArrayIndexOutOfBoundsException cause = new ArrayIndexOutOfBoundsException("raster index");
|
||||||
|
cause.setStackTrace(new StackTraceElement[0]);
|
||||||
|
IllegalStateException failure = new IllegalStateException("Hydrology failed", cause);
|
||||||
|
|
||||||
|
String output = reportWithDebug(failure);
|
||||||
|
|
||||||
|
assertEquals(1L, output.lines().filter(line -> line.contains("Hydrology failed")).count());
|
||||||
|
assertTrue(output.contains("Caused by: java.lang.ArrayIndexOutOfBoundsException: raster index"));
|
||||||
|
assertTrue(output.contains("IrisErrorReportingTest.debugReportingPreservesFailureWithStacklessCause"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String reportWithDebug(Throwable failure) throws Exception {
|
||||||
|
Iris previousPlugin = Iris.instance;
|
||||||
|
IrisSettings previousSettings = IrisSettings.settings;
|
||||||
|
PrintStream previousError = System.err;
|
||||||
|
Iris plugin = mock(Iris.class);
|
||||||
|
Field deferred = Iris.class.getDeclaredField("serverStopTeardownDeferred");
|
||||||
|
deferred.setAccessible(true);
|
||||||
|
deferred.set(plugin, new AtomicBoolean());
|
||||||
|
File report = new File(temporaryFolder.getRoot(), "failure.txt");
|
||||||
|
when(plugin.getDataFile(any(String[].class))).thenReturn(report);
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
try (PrintStream errors = new PrintStream(output, true, StandardCharsets.UTF_8);
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
|
Iris.instance = plugin;
|
||||||
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
IrisSettings.settings.getGeneral().setDebug(true);
|
||||||
|
System.setErr(errors);
|
||||||
|
iris.when(() -> Iris.reportError(failure)).thenCallRealMethod();
|
||||||
|
|
||||||
|
Iris.reportError(failure);
|
||||||
|
|
||||||
|
assertTrue(Files.readString(report.toPath()).contains(failure.toString()));
|
||||||
|
return output.toString(StandardCharsets.UTF_8);
|
||||||
|
} finally {
|
||||||
|
System.setErr(previousError);
|
||||||
|
IrisSettings.settings = previousSettings;
|
||||||
|
Iris.instance = previousPlugin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+227
-5
@@ -1,7 +1,17 @@
|
|||||||
package art.arcane.iris;
|
package art.arcane.iris;
|
||||||
|
|
||||||
import art.arcane.iris.configuration.IrisSettings;
|
import art.arcane.iris.configuration.IrisSettings;
|
||||||
|
import art.arcane.iris.generation.concurrent.MultiBurst;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.platform.agent.Agent;
|
||||||
|
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||||
|
import art.arcane.iris.platform.bukkit.BukkitPlatform;
|
||||||
|
import art.arcane.iris.platform.bukkit.plugin.IrisService;
|
||||||
|
import art.arcane.iris.platform.bukkit.plugin.VolmitPlugin;
|
||||||
|
import art.arcane.iris.spi.IrisPlatforms;
|
||||||
|
import art.arcane.iris.world.lifecycle.IrisRuntimeStatics;
|
||||||
|
import art.arcane.volmlib.util.collection.KList;
|
||||||
|
import art.arcane.volmlib.util.collection.KMap;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
import art.arcane.iris.generation.runtime.GenerationSessionLease;
|
||||||
import art.arcane.iris.generation.runtime.GenerationSessionManager;
|
import art.arcane.iris.generation.runtime.GenerationSessionManager;
|
||||||
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
import art.arcane.iris.platform.bukkit.BukkitWorldBinding;
|
||||||
@@ -16,8 +26,8 @@ import art.arcane.iris.world.IrisWorld;
|
|||||||
import art.arcane.iris.world.WorldMaintenance;
|
import art.arcane.iris.world.WorldMaintenance;
|
||||||
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
|
import art.arcane.iris.world.runtime.BukkitEnginePlatformHooks;
|
||||||
import art.arcane.iris.world.task.J;
|
import art.arcane.iris.world.task.J;
|
||||||
import art.arcane.volmlib.integration.ReloadAware;
|
|
||||||
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
import art.arcane.volmlib.nativelib.terrain.ServerShutdownBoundary;
|
||||||
|
import art.arcane.volmlib.integration.ReloadAware;
|
||||||
import org.bukkit.Bukkit;
|
import org.bukkit.Bukkit;
|
||||||
import org.bukkit.NamespacedKey;
|
import org.bukkit.NamespacedKey;
|
||||||
import org.bukkit.World;
|
import org.bukkit.World;
|
||||||
@@ -27,10 +37,13 @@ import org.mockito.MockedStatic;
|
|||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
@@ -38,12 +51,16 @@ import java.util.concurrent.atomic.AtomicReference;
|
|||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyLong;
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
import static org.mockito.Mockito.CALLS_REAL_METHODS;
|
||||||
import static org.mockito.Mockito.doCallRealMethod;
|
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.doAnswer;
|
||||||
|
import static org.mockito.Mockito.doCallRealMethod;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
import static org.mockito.Mockito.mockStatic;
|
import static org.mockito.Mockito.mockStatic;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
@@ -152,13 +169,181 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startupEnsuresAgentReadinessBeforeLifecycleProtectionWithoutWorldInjection() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(); MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||||
|
when(fixture.binding.supportsIrisWorldGeneration()).thenReturn(true);
|
||||||
|
agent.when(Agent::install).thenReturn(true);
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
agent.verify(Agent::install);
|
||||||
|
assertSame(fixture.plugin.getClass().getClassLoader(), invocation.getArgument(0));
|
||||||
|
return null;
|
||||||
|
}).when(fixture.binding).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||||
|
invoke(fixture.plugin, "initializeShutdownProtection");
|
||||||
|
verify(fixture.binding).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||||
|
verify(fixture.binding, never()).injectBukkit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void unavailableAgentStopsLifecycleInitialization() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(); MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||||
|
when(fixture.binding.supportsIrisWorldGeneration()).thenReturn(true);
|
||||||
|
InvocationTargetException failure = assertThrows(InvocationTargetException.class,
|
||||||
|
() -> invoke(fixture.plugin, "initializeShutdownProtection"));
|
||||||
|
assertTrue(failure.getCause() instanceof IllegalStateException);
|
||||||
|
verify(fixture.binding, never()).initializeLifecycle(fixture.plugin.getClass().getClassLoader());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void worldHookTeardownFailureReleasesProtectionAfterServicesAndWorkersStop() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture();
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||||
|
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||||
|
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||||
|
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class);
|
||||||
|
MockedStatic<SlimJar> providers = mockStatic(SlimJar.class)) {
|
||||||
|
IrisService service = mock(IrisService.class);
|
||||||
|
IllegalStateException failure = new IllegalStateException("world hook teardown failed");
|
||||||
|
fixture.nms.when(INMS::isBound).thenReturn(true);
|
||||||
|
doThrow(failure).when(fixture.binding).uninjectBukkit();
|
||||||
|
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||||
|
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||||
|
doAnswer(invocation -> {
|
||||||
|
platforms.verify(IrisPlatforms::unbind);
|
||||||
|
bukkitPlatform.verify(BukkitPlatform::releaseHost);
|
||||||
|
assertTrue(MultiBurst.burst.isTerminated());
|
||||||
|
assertTrue(MultiBurst.hydrology.isTerminated());
|
||||||
|
assertTrue(MultiBurst.ioBurst.isTerminated());
|
||||||
|
return null;
|
||||||
|
}).when(fixture.binding).releasePluginClassLoaderClose();
|
||||||
|
fixture.quiesce();
|
||||||
|
try {
|
||||||
|
fixture.finish(5L);
|
||||||
|
fixture.finish(5L);
|
||||||
|
verify(service).onDisable();
|
||||||
|
iris.verify(() -> Iris.reportError("Failed to remove Iris world-generation hooks.", failure));
|
||||||
|
assertTrue(MultiBurst.burst.isTerminated());
|
||||||
|
assertTrue(MultiBurst.hydrology.isTerminated());
|
||||||
|
assertTrue(MultiBurst.ioBurst.isTerminated());
|
||||||
|
verify(fixture.binding).releasePluginClassLoaderClose();
|
||||||
|
platforms.verify(IrisPlatforms::unbind);
|
||||||
|
bukkitPlatform.verify(BukkitPlatform::releaseHost);
|
||||||
|
} finally {
|
||||||
|
MultiBurst.burst.reopen();
|
||||||
|
MultiBurst.hydrology.reopen();
|
||||||
|
MultiBurst.ioBurst.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedServiceWithAnOwnedExecutorRetainsTheClassLoader() throws Exception {
|
||||||
|
ExecutorService ownedExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
CountDownLatch started = new CountDownLatch(1);
|
||||||
|
CountDownLatch finish = new CountDownLatch(1);
|
||||||
|
ownedExecutor.submit(() -> {
|
||||||
|
started.countDown();
|
||||||
|
finish.await();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
try (Fixture fixture = new Fixture();
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||||
|
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||||
|
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||||
|
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class)) {
|
||||||
|
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||||
|
IrisService service = mock(IrisService.class);
|
||||||
|
IllegalStateException failure = new IllegalStateException("service teardown failed");
|
||||||
|
doThrow(failure).when(service).onDisable();
|
||||||
|
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||||
|
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||||
|
fixture.quiesce();
|
||||||
|
fixture.finish(5L);
|
||||||
|
verify(service).onDisable();
|
||||||
|
iris.verify(() -> Iris.reportError("Failed to disable " + service.getClass().getSimpleName() + ".", failure));
|
||||||
|
assertFalse(ownedExecutor.isTerminated());
|
||||||
|
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||||
|
} finally {
|
||||||
|
finish.countDown();
|
||||||
|
ownedExecutor.shutdownNow();
|
||||||
|
assertTrue(ownedExecutor.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
MultiBurst.burst.reopen();
|
||||||
|
MultiBurst.hydrology.reopen();
|
||||||
|
MultiBurst.ioBurst.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedPostShutdownCallbackRetainsTheClassLoaderForSkippedCleanup() throws Exception {
|
||||||
|
ExecutorService ownedExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
CountDownLatch started = new CountDownLatch(1);
|
||||||
|
CountDownLatch finish = new CountDownLatch(1);
|
||||||
|
ownedExecutor.submit(() -> {
|
||||||
|
started.countDown();
|
||||||
|
finish.await();
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
try (Fixture fixture = new Fixture();
|
||||||
|
MockedStatic<Iris> iris = mockStatic(Iris.class);
|
||||||
|
MockedStatic<IrisRuntimeStatics> statics = mockStatic(IrisRuntimeStatics.class);
|
||||||
|
MockedStatic<IrisPlatforms> platforms = mockStatic(IrisPlatforms.class);
|
||||||
|
MockedStatic<BukkitPlatform> bukkitPlatform = mockStatic(BukkitPlatform.class)) {
|
||||||
|
assertTrue(started.await(5, TimeUnit.SECONDS));
|
||||||
|
IllegalStateException failure = new IllegalStateException("post-shutdown callback failed");
|
||||||
|
AtomicBoolean cleanupAttempted = new AtomicBoolean();
|
||||||
|
KList<Runnable> callbacks = new KList<>();
|
||||||
|
callbacks.add(() -> { throw failure; });
|
||||||
|
callbacks.add(() -> {
|
||||||
|
cleanupAttempted.set(true);
|
||||||
|
ownedExecutor.shutdown();
|
||||||
|
});
|
||||||
|
Field callbacksField = VolmitPlugin.class.getDeclaredField("postShutdown");
|
||||||
|
callbacksField.setAccessible(true);
|
||||||
|
callbacksField.set(fixture.plugin, callbacks);
|
||||||
|
Method runCallbacks = VolmitPlugin.class.getDeclaredMethod("runPostShutdown");
|
||||||
|
runCallbacks.setAccessible(true);
|
||||||
|
runCallbacks.invoke(doCallRealMethod().when(fixture.plugin));
|
||||||
|
fixture.quiesce();
|
||||||
|
fixture.finish(5L);
|
||||||
|
iris.verify(() -> Iris.reportError("Failed to run Iris post-shutdown cleanup.", failure));
|
||||||
|
assertFalse(cleanupAttempted.get());
|
||||||
|
assertFalse(ownedExecutor.isShutdown());
|
||||||
|
assertFalse(ownedExecutor.isTerminated());
|
||||||
|
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||||
|
} finally {
|
||||||
|
finish.countDown();
|
||||||
|
ownedExecutor.shutdownNow();
|
||||||
|
assertTrue(ownedExecutor.awaitTermination(5, TimeUnit.SECONDS));
|
||||||
|
MultiBurst.burst.reopen();
|
||||||
|
MultiBurst.hydrology.reopen();
|
||||||
|
MultiBurst.ioBurst.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void incompleteDrainRetainsServicesWorkersAndClassLoader() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture(); MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
|
IrisService service = mock(IrisService.class);
|
||||||
|
setField(fixture.plugin, "services", new KMap<Class<? extends IrisService>, IrisService>());
|
||||||
|
setField(fixture.plugin, "enabledServices", List.of(service));
|
||||||
|
when(fixture.generator.closeAsync()).thenReturn(new CompletableFuture<>());
|
||||||
|
fixture.quiesce();
|
||||||
|
fixture.finish(0L);
|
||||||
|
verify(service, never()).onDisable();
|
||||||
|
verify(fixture.binding, never()).releasePluginClassLoaderClose();
|
||||||
|
verify(fixture.binding, never()).uninjectBukkit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void serverStopPreUnloadDefersTeardownWhileTheClassLoaderIsHeld() throws Exception {
|
public void serverStopPreUnloadDefersTeardownWhileTheClassLoaderIsHeld() throws Exception {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture()) {
|
||||||
when(fixture.binding.deferPluginClassLoaderClose()).thenReturn(true);
|
|
||||||
setField(fixture.plugin, "postStopFinisherStarted", new AtomicBoolean(true));
|
setField(fixture.plugin, "postStopFinisherStarted", new AtomicBoolean(true));
|
||||||
try {
|
try {
|
||||||
fixture.commitReloadDuringServerStop();
|
fixture.commitReloadDuringServerStop();
|
||||||
|
verify(fixture.binding).deferPluginClassLoaderClose();
|
||||||
verify(fixture.generator).quiesceForServerShutdown();
|
verify(fixture.generator).quiesceForServerShutdown();
|
||||||
verify(fixture.generator, never()).closeAsync();
|
verify(fixture.generator, never()).closeAsync();
|
||||||
assertTrue(fixture.serverStopTeardownDeferred());
|
assertTrue(fixture.serverStopTeardownDeferred());
|
||||||
@@ -170,19 +355,43 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void serverStopPreUnloadDrainsInlineWhenTheClassLoaderCannotBeHeld() throws Exception {
|
public void serverStopPreUnloadDrainsInlineWhenTheClassLoaderCannotBeHeld() throws Exception {
|
||||||
try (Fixture fixture = new Fixture()) {
|
try (Fixture fixture = new Fixture(); MockedStatic<Iris> iris = mockStatic(Iris.class)) {
|
||||||
when(fixture.binding.deferPluginClassLoaderClose()).thenReturn(false);
|
IllegalStateException failure = new IllegalStateException("class loader hold failed");
|
||||||
|
doThrow(failure).when(fixture.binding).deferPluginClassLoaderClose();
|
||||||
try {
|
try {
|
||||||
fixture.commitReloadDuringServerStop();
|
fixture.commitReloadDuringServerStop();
|
||||||
verify(fixture.generator).closeAsync();
|
verify(fixture.generator).closeAsync();
|
||||||
verify(fixture.generator, never()).quiesceForServerShutdown();
|
verify(fixture.generator, never()).quiesceForServerShutdown();
|
||||||
assertFalse(fixture.serverStopTeardownDeferred());
|
assertFalse(fixture.serverStopTeardownDeferred());
|
||||||
|
iris.verify(() -> Iris.reportError("Failed to retain the Iris plugin class loader; draining shutdown inline.", failure));
|
||||||
|
} finally {
|
||||||
|
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void serverStopPreUnloadDrainsInlineWithoutInitializedProtection() throws Exception {
|
||||||
|
try (Fixture fixture = new Fixture()) {
|
||||||
|
setField(fixture.plugin, "shutdownProtectionInitialized", false);
|
||||||
|
try {
|
||||||
|
fixture.commitReloadDuringServerStop();
|
||||||
|
verify(fixture.binding, never()).deferPluginClassLoaderClose();
|
||||||
|
verify(fixture.generator).closeAsync();
|
||||||
|
verify(fixture.generator, never()).quiesceForServerShutdown();
|
||||||
|
assertFalse(fixture.serverStopTeardownDeferred());
|
||||||
} finally {
|
} finally {
|
||||||
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
WorldMaintenance.endWorldMaintenance(fixture.identity, "pre-unload:HOT_UNLOAD", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void invoke(Iris plugin, String name) throws ReflectiveOperationException {
|
||||||
|
Method method = Iris.class.getDeclaredMethod(name);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
private static void setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
|
private static void setField(Iris plugin, String name, Object value) throws ReflectiveOperationException {
|
||||||
Field field = Iris.class.getDeclaredField(name);
|
Field field = Iris.class.getDeclaredField(name);
|
||||||
field.setAccessible(true);
|
field.setAccessible(true);
|
||||||
@@ -210,6 +419,12 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
|
|
||||||
private Fixture() throws ReflectiveOperationException {
|
private Fixture() throws ReflectiveOperationException {
|
||||||
IrisSettings.settings = new IrisSettings();
|
IrisSettings.settings = new IrisSettings();
|
||||||
|
setField(plugin, "shutdownProtectionInitialized", true);
|
||||||
|
setField(plugin, "alreadyDrained", new AtomicBoolean());
|
||||||
|
setField(plugin, "servicesDisabled", new AtomicBoolean());
|
||||||
|
setField(plugin, "runtimeTeardownFailed", new AtomicBoolean());
|
||||||
|
setField(plugin, "sharedRuntimeClosed", new AtomicBoolean());
|
||||||
|
setField(plugin, "terminalCleanupCompleted", new AtomicBoolean());
|
||||||
setField(plugin, "serverStopTeardownDeferred", new AtomicBoolean());
|
setField(plugin, "serverStopTeardownDeferred", new AtomicBoolean());
|
||||||
setField(plugin, "postStopFinisherStarted", new AtomicBoolean());
|
setField(plugin, "postStopFinisherStarted", new AtomicBoolean());
|
||||||
setField(plugin, "deferredShutdownGenerators", new CopyOnWriteArrayList<PlatformChunkGenerator>());
|
setField(plugin, "deferredShutdownGenerators", new CopyOnWriteArrayList<PlatformChunkGenerator>());
|
||||||
@@ -251,6 +466,13 @@ public class IrisShutdownMaintenanceTest {
|
|||||||
return ((AtomicBoolean) field.get(plugin)).get();
|
return ((AtomicBoolean) field.get(plugin)).get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void finish(long timeoutSeconds) throws ReflectiveOperationException {
|
||||||
|
Method method = Iris.class.getDeclaredMethod("finishDeferredRuntimeTeardown", String.class, long.class);
|
||||||
|
method.setAccessible(true);
|
||||||
|
method.invoke(plugin, "test_shutdown", timeoutSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
private void startFinisher() throws ReflectiveOperationException {
|
private void startFinisher() throws ReflectiveOperationException {
|
||||||
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
|
Method method = Iris.class.getDeclaredMethod("startPostStopFinisher");
|
||||||
method.setAccessible(true);
|
method.setAccessible(true);
|
||||||
|
|||||||
@@ -180,6 +180,13 @@ def jarJar = configurations.create('jarJar')
|
|||||||
dependencies {
|
dependencies {
|
||||||
add('included', project(path: ':core', configuration: 'shadow'))
|
add('included', project(path: ':core', configuration: 'shadow'))
|
||||||
add('included', project(path: ':adapters:bukkit:plugin', configuration: 'runtimeElements'))
|
add('included', project(path: ':adapters:bukkit:plugin', configuration: 'runtimeElements'))
|
||||||
|
// Shaded: the runtime downloader can neither fetch the natives classifiers nor relocate lwjgl's own jar.
|
||||||
|
add('included', libs.lwjgl)
|
||||||
|
add('included', libs.lwjgl.opengl)
|
||||||
|
add('included', libs.lwjgl.egl)
|
||||||
|
add('included', libs.lwjgl.jawt)
|
||||||
|
add('included', variantOf(libs.lwjgl) { classifier('natives-linux') })
|
||||||
|
add('included', variantOf(libs.lwjgl.opengl) { classifier('natives-linux') })
|
||||||
add('jarJar', project(':core:agent'))
|
add('jarJar', project(':core:agent'))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +201,7 @@ tasks.named('jar', Jar).configure {
|
|||||||
includeEmptyDirs = false
|
includeEmptyDirs = false
|
||||||
from(jarJar)
|
from(jarJar)
|
||||||
from(provider { included.resolve().collect { zipTree(it) } }) {
|
from(provider { included.resolve().collect { zipTree(it) } }) {
|
||||||
|
exclude('META-INF/INDEX.LIST')
|
||||||
exclude('languages/**')
|
exclude('languages/**')
|
||||||
exclude('com/fren_gor/**')
|
exclude('com/fren_gor/**')
|
||||||
|
|
||||||
@@ -734,6 +742,7 @@ pluginPackaging {
|
|||||||
policyName = 'Iris Bukkit'
|
policyName = 'Iris Bukkit'
|
||||||
packed = true
|
packed = true
|
||||||
packedJarAccessors = ['art/arcane/iris/platform/bukkit/plugin/VolmitPlugin#getJarFile']
|
packedJarAccessors = ['art/arcane/iris/platform/bukkit/plugin/VolmitPlugin#getJarFile']
|
||||||
|
packedPayloadOnlyPrefixes = ['linux/x64/org/lwjgl/', 'iris-language-guides/']
|
||||||
stripDirectories = true
|
stripDirectories = true
|
||||||
stripLocalVariables = true
|
stripLocalVariables = true
|
||||||
releaseCompression = providers.gradleProperty('compactRelease')
|
releaseCompression = providers.gradleProperty('compactRelease')
|
||||||
|
|||||||
+10
-7
@@ -108,6 +108,10 @@ dependencies {
|
|||||||
compileOnly(libs.log4j.api)
|
compileOnly(libs.log4j.api)
|
||||||
compileOnly(libs.log4j.core)
|
compileOnly(libs.log4j.core)
|
||||||
compileOnly(libs.guava)
|
compileOnly(libs.guava)
|
||||||
|
compileOnly(libs.lwjgl)
|
||||||
|
compileOnly(libs.lwjgl.opengl)
|
||||||
|
compileOnly(libs.lwjgl.egl)
|
||||||
|
compileOnly(libs.lwjgl.jawt)
|
||||||
|
|
||||||
// Third Party Integrations
|
// Third Party Integrations
|
||||||
compileOnly(libs.nexo)
|
compileOnly(libs.nexo)
|
||||||
@@ -136,13 +140,6 @@ dependencies {
|
|||||||
implementation('com.moandjiezana.toml:toml4j:0.7.2') {
|
implementation('com.moandjiezana.toml:toml4j:0.7.2') {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
// Shaded: the runtime downloader can neither fetch the natives classifiers nor relocate lwjgl's own jar.
|
|
||||||
implementation(libs.lwjgl)
|
|
||||||
implementation(libs.lwjgl.opengl)
|
|
||||||
implementation(libs.lwjgl.egl)
|
|
||||||
implementation(libs.lwjgl.jawt)
|
|
||||||
runtimeOnly(variantOf(libs.lwjgl) { classifier('natives-linux') })
|
|
||||||
runtimeOnly(variantOf(libs.lwjgl.opengl) { classifier('natives-linux') })
|
|
||||||
// Dynamically Loaded
|
// Dynamically Loaded
|
||||||
slim(libs.gson)
|
slim(libs.gson)
|
||||||
slim(libs.lru)
|
slim(libs.lru)
|
||||||
@@ -183,6 +180,12 @@ dependencies {
|
|||||||
testImplementation(libs.multiverseCore)
|
testImplementation(libs.multiverseCore)
|
||||||
testImplementation(libs.annotations)
|
testImplementation(libs.annotations)
|
||||||
testImplementation(libs.mythic)
|
testImplementation(libs.mythic)
|
||||||
|
testRuntimeOnly(libs.lwjgl)
|
||||||
|
testRuntimeOnly(libs.lwjgl.opengl)
|
||||||
|
testRuntimeOnly(libs.lwjgl.egl)
|
||||||
|
testRuntimeOnly(libs.lwjgl.jawt)
|
||||||
|
testRuntimeOnly(variantOf(libs.lwjgl) { classifier('natives-linux') })
|
||||||
|
testRuntimeOnly(variantOf(libs.lwjgl.opengl) { classifier('natives-linux') })
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named('test').configure {
|
tasks.named('test').configure {
|
||||||
|
|||||||
+9
-1
@@ -47,6 +47,7 @@ import java.util.concurrent.CancellationException;
|
|||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.concurrent.CompletionException;
|
import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.IntBinaryOperator;
|
import java.util.function.IntBinaryOperator;
|
||||||
@@ -79,6 +80,7 @@ public final class HydrologyRuntime {
|
|||||||
private final HydrologyBinding binding;
|
private final HydrologyBinding binding;
|
||||||
private final Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores;
|
private final Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores;
|
||||||
private final boolean caveFluidPolicies;
|
private final boolean caveFluidPolicies;
|
||||||
|
private final AtomicLong rasterRevision = new AtomicLong();
|
||||||
private volatile PreparedHydrologyCacheIdentity storeIdentity;
|
private volatile PreparedHydrologyCacheIdentity storeIdentity;
|
||||||
private boolean storeOpen;
|
private boolean storeOpen;
|
||||||
private volatile boolean closed;
|
private volatile boolean closed;
|
||||||
@@ -210,6 +212,10 @@ public final class HydrologyRuntime {
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long previewRevision() {
|
||||||
|
return cache == null ? 0L : cache.readyRevision() + rasterRevision.get();
|
||||||
|
}
|
||||||
|
|
||||||
public PreviewScope preview() {
|
public PreviewScope preview() {
|
||||||
return new PreviewScope(cache == null ? null : last.get());
|
return new PreviewScope(cache == null ? null : last.get());
|
||||||
}
|
}
|
||||||
@@ -696,7 +702,9 @@ public final class HydrologyRuntime {
|
|||||||
|
|
||||||
private ChunkWaterRaster remember(long key, ChunkWaterRaster raster) {
|
private ChunkWaterRaster remember(long key, ChunkWaterRaster raster) {
|
||||||
synchronized (rasters) {
|
synchronized (rasters) {
|
||||||
rasters.put(key, raster);
|
if (rasters.put(key, raster) == null) {
|
||||||
|
rasterRevision.incrementAndGet();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
last.get().remember(key, raster);
|
last.get().remember(key, raster);
|
||||||
return raster;
|
return raster;
|
||||||
|
|||||||
+8
-1
@@ -57,6 +57,7 @@ public final class HydrologyPlanCache {
|
|||||||
private final AtomicLong demands = new AtomicLong();
|
private final AtomicLong demands = new AtomicLong();
|
||||||
private final AtomicLong speculations = new AtomicLong();
|
private final AtomicLong speculations = new AtomicLong();
|
||||||
private final AtomicLong previewStamps = new AtomicLong(Long.MIN_VALUE);
|
private final AtomicLong previewStamps = new AtomicLong(Long.MIN_VALUE);
|
||||||
|
private final AtomicLong readyRevision = new AtomicLong();
|
||||||
private final Shelf<RegionKey, RegionPlan> regions = new Shelf<>(REGION_CAPACITY, this::startRegion);
|
private final Shelf<RegionKey, RegionPlan> regions = new Shelf<>(REGION_CAPACITY, this::startRegion);
|
||||||
private final Shelf<WindowKey, IndexedWindow> windows = new Shelf<>(WINDOW_CAPACITY, this::startWindow);
|
private final Shelf<WindowKey, IndexedWindow> windows = new Shelf<>(WINDOW_CAPACITY, this::startWindow);
|
||||||
private final Map<Object, Set<Slot<?, ?>>> epochSlots = new HashMap<>();
|
private final Map<Object, Set<Slot<?, ?>>> epochSlots = new HashMap<>();
|
||||||
@@ -112,6 +113,10 @@ public final class HydrologyPlanCache {
|
|||||||
return windows.evictions.get();
|
return windows.evictions.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long readyRevision() {
|
||||||
|
return readyRevision.get();
|
||||||
|
}
|
||||||
|
|
||||||
public RegionPlan region(RegionKey key) {
|
public RegionPlan region(RegionKey key) {
|
||||||
Objects.requireNonNull(key, "key");
|
Objects.requireNonNull(key, "key");
|
||||||
requireOffMainThread("region " + key.rx() + "," + key.rz());
|
requireOffMainThread("region " + key.rx() + "," + key.rz());
|
||||||
@@ -759,7 +764,9 @@ public final class HydrologyPlanCache {
|
|||||||
|
|
||||||
private void succeed(V value) {
|
private void succeed(V value) {
|
||||||
release();
|
release();
|
||||||
future.complete(value);
|
if (future.complete(value)) {
|
||||||
|
readyRevision.incrementAndGet();
|
||||||
|
}
|
||||||
shelf.evict();
|
shelf.evict();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,10 @@ public final class TerrainBatchScratch {
|
|||||||
int lattice(double x, double z) {
|
int lattice(double x, double z) {
|
||||||
long xBits = Double.doubleToRawLongBits(x);
|
long xBits = Double.doubleToRawLongBits(x);
|
||||||
long zBits = Double.doubleToRawLongBits(z);
|
long zBits = Double.doubleToRawLongBits(z);
|
||||||
long mixed = xBits * 0x9E3779B97F4A7C15L ^ Long.rotateLeft(zBits * 0xC2B2AE3D27D4EB4FL, 31);
|
long mixed = xBits ^ Long.rotateLeft(zBits, 32);
|
||||||
mixed ^= mixed >>> 29;
|
mixed = (mixed ^ (mixed >>> 33)) * 0xFF51AFD7ED558CCDL;
|
||||||
|
mixed = (mixed ^ (mixed >>> 33)) * 0xC4CEB9FE1A85EC53L;
|
||||||
|
mixed ^= mixed >>> 33;
|
||||||
int mask = latticeCapacity - 1;
|
int mask = latticeCapacity - 1;
|
||||||
int slot = (int) mixed & mask;
|
int slot = (int) mixed & mask;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -1668,7 +1668,6 @@ public class IrisEngine implements Engine {
|
|||||||
} catch (GenerationSessionException e) {
|
} catch (GenerationSessionException e) {
|
||||||
throw e;
|
throw e;
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
IrisLogging.reportError(e);
|
|
||||||
fail("Failed to generate " + x + ", " + z, e);
|
fail("Failed to generate " + x + ", " + z, e);
|
||||||
if (e instanceof WrongEngineBroException wrongEngine) {
|
if (e instanceof WrongEngineBroException wrongEngine) {
|
||||||
throw wrongEngine;
|
throw wrongEngine;
|
||||||
|
|||||||
@@ -365,4 +365,10 @@ public final class BukkitPlatform implements IrisPlatform {
|
|||||||
public void reportError(Throwable error) {
|
public void reportError(Throwable error) {
|
||||||
bridge().errorSink().accept(error);
|
bridge().errorSink().accept(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reportError(String context, Throwable error) {
|
||||||
|
log(LogLevel.ERROR, context == null || context.isBlank() ? "Unhandled Iris failure." : context);
|
||||||
|
reportError(error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+18
@@ -1,6 +1,7 @@
|
|||||||
package art.arcane.iris.platform.bukkit.nms;
|
package art.arcane.iris.platform.bukkit.nms;
|
||||||
|
|
||||||
import art.arcane.iris.platform.agent.Agent;
|
import art.arcane.iris.platform.agent.Agent;
|
||||||
|
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||||
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
import art.arcane.iris.platform.generation.PlatformChunkGenerator;
|
||||||
import art.arcane.iris.generation.runtime.EngineTarget;
|
import art.arcane.iris.generation.runtime.EngineTarget;
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
@@ -8,6 +9,7 @@ import art.arcane.iris.world.lifecycle.WorldLifecycleStaging;
|
|||||||
import art.arcane.volmlib.nativelib.terrain.NativeWorldLifecyclePolicy;
|
import art.arcane.volmlib.nativelib.terrain.NativeWorldLifecyclePolicy;
|
||||||
import org.bukkit.generator.ChunkGenerator;
|
import org.bukkit.generator.ChunkGenerator;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.instrument.Instrumentation;
|
import java.lang.instrument.Instrumentation;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@@ -41,7 +43,23 @@ public enum BukkitWorldLifecyclePolicy implements NativeWorldLifecyclePolicy {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void releaseClassLoader(ClassLoader loader) {
|
public void releaseClassLoader(ClassLoader loader) {
|
||||||
|
Throwable providerFailure = null;
|
||||||
|
try {
|
||||||
|
SlimJar.closeNativeProviders();
|
||||||
|
} catch (IOException | RuntimeException | Error failure) {
|
||||||
|
providerFailure = failure;
|
||||||
|
}
|
||||||
|
try {
|
||||||
Agent.releaseClassLoader(loader);
|
Agent.releaseClassLoader(loader);
|
||||||
|
} catch (RuntimeException | Error failure) {
|
||||||
|
if (providerFailure != null) {
|
||||||
|
failure.addSuppressed(providerFailure);
|
||||||
|
}
|
||||||
|
throw failure;
|
||||||
|
}
|
||||||
|
if (providerFailure != null) {
|
||||||
|
throw new IllegalStateException("Failed to close Iris native providers", providerFailure);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -290,6 +290,10 @@ public interface INMSBinding extends NativeTerrainAccess {
|
|||||||
|
|
||||||
boolean missingDimensionTypes(String... keys);
|
boolean missingDimensionTypes(String... keys);
|
||||||
|
|
||||||
|
default void initializeLifecycle(ClassLoader loader) {
|
||||||
|
throw new IllegalStateException("Plugin class loader shutdown protection is unavailable");
|
||||||
|
}
|
||||||
|
|
||||||
default boolean injectBukkit() {
|
default boolean injectBukkit() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -301,8 +305,8 @@ public interface INMSBinding extends NativeTerrainAccess {
|
|||||||
default void uninjectBukkit() {
|
default void uninjectBukkit() {
|
||||||
}
|
}
|
||||||
|
|
||||||
default boolean deferPluginClassLoaderClose() {
|
default void deferPluginClassLoaderClose() {
|
||||||
return false;
|
throw new IllegalStateException("Plugin class loader shutdown protection is unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
default void releasePluginClassLoaderClose() {
|
default void releasePluginClassLoaderClose() {
|
||||||
|
|||||||
@@ -276,9 +276,21 @@ final class MapCanvas extends JPanel {
|
|||||||
|
|
||||||
// Tiles keep loading in the background only for the view on screen; a hidden canvas stops asking for new ones.
|
// Tiles keep loading in the background only for the view on screen; a hidden canvas stops asking for new ones.
|
||||||
void setActive(boolean nextActive) {
|
void setActive(boolean nextActive) {
|
||||||
|
if (closed || active == nextActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
active = nextActive;
|
active = nextActive;
|
||||||
if (active) {
|
if (active) {
|
||||||
|
markerTimer.start();
|
||||||
|
provisionalTimer.start();
|
||||||
requestRender();
|
requestRender();
|
||||||
|
} else {
|
||||||
|
controller.suspend();
|
||||||
|
hoverTimer.stop();
|
||||||
|
legendTimer.stop();
|
||||||
|
markerTimer.stop();
|
||||||
|
provisionalTimer.stop();
|
||||||
|
clearHover();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1021,6 +1033,9 @@ final class MapCanvas extends JPanel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void onTilesChanged() {
|
private void onTilesChanged() {
|
||||||
|
if (closed || !active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
tileLayerStale = true;
|
tileLayerStale = true;
|
||||||
repaint();
|
repaint();
|
||||||
refreshStatus();
|
refreshStatus();
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
private double worldCenterX;
|
private double worldCenterX;
|
||||||
private double worldCenterZ;
|
private double worldCenterZ;
|
||||||
private MapView currentView;
|
private MapView currentView;
|
||||||
|
private boolean windowVisible = true;
|
||||||
private double paletteCenterX;
|
private double paletteCenterX;
|
||||||
private double paletteCenterZ;
|
private double paletteCenterZ;
|
||||||
|
|
||||||
@@ -190,11 +191,55 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
frame.setMinimumSize(StudioTheme.scaledSize(820, 520));
|
frame.setMinimumSize(StudioTheme.scaledSize(820, 520));
|
||||||
frame.setLocationRelativeTo(null);
|
frame.setLocationRelativeTo(null);
|
||||||
frame.addWindowListener(new WindowAdapter() {
|
frame.addWindowListener(new WindowAdapter() {
|
||||||
|
@Override
|
||||||
|
public void windowIconified(WindowEvent event) {
|
||||||
|
updateWindowVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void windowDeiconified(WindowEvent event) {
|
||||||
|
updateWindowVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void windowClosed(WindowEvent event) {
|
public void windowClosed(WindowEvent event) {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
frame.addComponentListener(new ComponentAdapter() {
|
||||||
|
@Override
|
||||||
|
public void componentShown(ComponentEvent event) {
|
||||||
|
updateWindowVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void componentHidden(ComponentEvent event) {
|
||||||
|
updateWindowVisibility();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWindowVisibility() {
|
||||||
|
boolean visible = isWindowVisible();
|
||||||
|
boolean wasVisible = windowVisible;
|
||||||
|
windowVisible = visible;
|
||||||
|
canvas.setActive(visible && currentView != null && currentView.isTileMap());
|
||||||
|
if (currentView == MapView.WORLD_3D && worldExplorer != null) {
|
||||||
|
if (visible && !wasVisible) {
|
||||||
|
worldExplorer.show(worldCenterX, worldCenterZ, false);
|
||||||
|
} else if (!visible) {
|
||||||
|
worldExplorer.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (visible) {
|
||||||
|
resourceUsage.start();
|
||||||
|
} else {
|
||||||
|
resourceUsage.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isWindowVisible() {
|
||||||
|
return frame.isShowing() && (frame.getExtendedState() & JFrame.ICONIFIED) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private JToolBar buildToolbar() {
|
private JToolBar buildToolbar() {
|
||||||
@@ -265,6 +310,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
}
|
}
|
||||||
frame.getRootPane().revalidate();
|
frame.getRootPane().revalidate();
|
||||||
frame.getRootPane().repaint();
|
frame.getRootPane().repaint();
|
||||||
|
updateWindowVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showMap(MapView previousView, MapView view) {
|
private void showMap(MapView previousView, MapView view) {
|
||||||
@@ -272,7 +318,7 @@ public final class MapWindow implements MapCanvas.Listener {
|
|||||||
canvas.centerOn(paletteExplorer.centerX(), paletteExplorer.centerZ());
|
canvas.centerOn(paletteExplorer.centerX(), paletteExplorer.centerZ());
|
||||||
}
|
}
|
||||||
canvas.showView(view);
|
canvas.showView(view);
|
||||||
canvas.setActive(true);
|
canvas.setActive(isWindowVisible());
|
||||||
showControls(mapControls);
|
showControls(mapControls);
|
||||||
waterToggle.setVisible(view == MapView.BIOMES);
|
waterToggle.setVisible(view == MapView.BIOMES);
|
||||||
if (view == MapView.REGIONS) {
|
if (view == MapView.REGIONS) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
package art.arcane.iris.studio.map;
|
package art.arcane.iris.studio.map;
|
||||||
|
|
||||||
import art.arcane.iris.spi.IrisLogging;
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
|
||||||
import art.arcane.iris.studio.map.components.MapTileRenderer;
|
import art.arcane.iris.studio.map.components.MapTileRenderer;
|
||||||
import art.arcane.iris.studio.map.components.MapLabels;
|
import art.arcane.iris.studio.map.components.MapLabels;
|
||||||
import art.arcane.iris.studio.map.components.MapLayer;
|
import art.arcane.iris.studio.map.components.MapLayer;
|
||||||
@@ -39,6 +38,7 @@ import java.util.Set;
|
|||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.Future;
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
import java.util.concurrent.RejectedExecutionException;
|
import java.util.concurrent.RejectedExecutionException;
|
||||||
import java.util.concurrent.ThreadFactory;
|
import java.util.concurrent.ThreadFactory;
|
||||||
@@ -48,6 +48,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.concurrent.atomic.AtomicLong;
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.LongSupplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the map as tiles on fixed zoom levels, so zooming within a level and panning reuse tiles that are already drawn.
|
* Renders the map as tiles on fixed zoom levels, so zooming within a level and panning reuse tiles that are already drawn.
|
||||||
@@ -67,6 +68,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
private static final long CACHE_BYTES = 256L * 1024L * 1024L;
|
private static final long CACHE_BYTES = 256L * 1024L * 1024L;
|
||||||
|
|
||||||
private final Runnable listener;
|
private final Runnable listener;
|
||||||
|
private final LongSupplier nanoTime;
|
||||||
private final int workerCount;
|
private final int workerCount;
|
||||||
private final ThreadPoolExecutor renderExecutor;
|
private final ThreadPoolExecutor renderExecutor;
|
||||||
private final ThreadPoolExecutor probeExecutor;
|
private final ThreadPoolExecutor probeExecutor;
|
||||||
@@ -79,9 +81,15 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
private final AtomicBoolean publicationQueued = new AtomicBoolean();
|
private final AtomicBoolean publicationQueued = new AtomicBoolean();
|
||||||
private final AtomicBoolean publicationDirty = new AtomicBoolean();
|
private final AtomicBoolean publicationDirty = new AtomicBoolean();
|
||||||
private volatile Frame currentFrame;
|
private volatile Frame currentFrame;
|
||||||
|
private Future<?> activeProbe;
|
||||||
|
|
||||||
VisionRenderController(Runnable listener) {
|
VisionRenderController(Runnable listener) {
|
||||||
|
this(listener, System::nanoTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
VisionRenderController(Runnable listener, LongSupplier nanoTime) {
|
||||||
this.listener = Objects.requireNonNull(listener, "listener");
|
this.listener = Objects.requireNonNull(listener, "listener");
|
||||||
|
this.nanoTime = Objects.requireNonNull(nanoTime, "nanoTime");
|
||||||
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
|
int processors = Math.max(1, Runtime.getRuntime().availableProcessors());
|
||||||
this.workerCount = Math.max(1, Math.min(6, processors / 2));
|
this.workerCount = Math.max(1, Math.min(6, processors / 2));
|
||||||
AtomicInteger threadSequence = new AtomicInteger();
|
AtomicInteger threadSequence = new AtomicInteger();
|
||||||
@@ -123,11 +131,10 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
List<VisibleTile> tiles = visibleTiles(spec, renderScale);
|
List<VisibleTile> tiles = visibleTiles(spec, renderScale);
|
||||||
Frame frame = new Frame(viewSequence.incrementAndGet(), spec, renderScale, tiles);
|
Frame frame = new Frame(viewSequence.incrementAndGet(), spec, renderScale, tiles);
|
||||||
for (VisibleTile tile : tiles) {
|
for (VisibleTile tile : tiles) {
|
||||||
tile.setTile(cachedTileServing(tile.key()));
|
tile.state = cachedTileServing(tile.key());
|
||||||
}
|
}
|
||||||
currentFrame = frame;
|
currentFrame = frame;
|
||||||
probeSequence.incrementAndGet();
|
cancelProbe();
|
||||||
probeExecutor.getQueue().clear();
|
|
||||||
startWorkers();
|
startWorkers();
|
||||||
publish();
|
publish();
|
||||||
return frame;
|
return frame;
|
||||||
@@ -141,25 +148,41 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
startWorkers();
|
startWorkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void suspend() {
|
||||||
|
currentFrame = null;
|
||||||
|
cancelProbe();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A tile from another zoom level covering the given one, to draw while the real tile renders.
|
* A tile from another zoom level covering the given one, to draw while the real tile renders.
|
||||||
*/
|
*/
|
||||||
Fallback fallback(Frame frame, VisibleTile tile) {
|
Fallback fallback(Frame frame, VisibleTile tile) {
|
||||||
MapTile withoutHeights = cache.get(tile.key().withHeights(false));
|
Fallback complete = fallback(tile.key(), false);
|
||||||
|
return complete != null || tile.tile() != null ? complete : fallback(tile.key(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Fallback fallback(TileKey key, boolean provisional) {
|
||||||
|
MapTile withoutHeights = fallbackTile(key.withHeights(false), provisional);
|
||||||
if (withoutHeights != null) {
|
if (withoutHeights != null) {
|
||||||
return new Fallback(List.of(new FallbackPiece(withoutHeights, 0, 0, TILE_PIXELS, 0D, 0D, 1D)));
|
return new Fallback(List.of(new FallbackPiece(withoutHeights, 0, 0, TILE_PIXELS, 0D, 0D, 1D)));
|
||||||
}
|
}
|
||||||
Fallback coarser = coarserFallback(frame, tile);
|
Fallback coarser = coarserFallback(key, provisional);
|
||||||
return coarser != null ? coarser : finerFallback(frame, tile);
|
return coarser != null ? coarser : finerFallback(key, provisional);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A tile drawn with heights serves just as well where no heights are needed, so turning shading off redraws nothing.
|
// A tile drawn with heights serves just as well where no heights are needed, so turning shading off redraws nothing.
|
||||||
private MapTile cachedTileServing(TileKey key) {
|
private TileState cachedTileServing(TileKey key) {
|
||||||
MapTile exact = cache.get(key);
|
CacheEntry exact = cache.get(key);
|
||||||
if (exact != null || key.heights()) {
|
if (exact != null || key.heights()) {
|
||||||
return exact;
|
return exact == null ? null : exact.state();
|
||||||
}
|
}
|
||||||
return cache.get(key.withHeights(true));
|
CacheEntry shaded = cache.get(key.withHeights(true));
|
||||||
|
return shaded != null && !shaded.state().provisional() ? shaded.state() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MapTile fallbackTile(TileKey key, boolean provisional) {
|
||||||
|
CacheEntry entry = cache.get(key);
|
||||||
|
return entry != null && entry.state().provisional() == provisional ? entry.state().tile() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Progress progress(Frame frame) {
|
Progress progress(Frame frame) {
|
||||||
@@ -168,34 +191,44 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
int ready = 0;
|
int ready = 0;
|
||||||
for (VisibleTile tile : frame.tiles()) {
|
for (VisibleTile tile : frame.tiles()) {
|
||||||
if (tile.tile() != null && !tile.provisional) {
|
if (tile.tile() != null && !tile.provisional()) {
|
||||||
ready++;
|
ready++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new Progress(frame.tiles().size(), ready, activeWorkers.get());
|
return new Progress(frame.tiles().size(), ready, activeWorkers.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
<T> void submitProbe(Frame frame, Callable<T> probe, Consumer<T> consumer) {
|
synchronized <T> void submitProbe(Frame frame, Callable<T> probe, Consumer<T> consumer) {
|
||||||
Objects.requireNonNull(probe, "probe");
|
Objects.requireNonNull(probe, "probe");
|
||||||
Objects.requireNonNull(consumer, "consumer");
|
Objects.requireNonNull(consumer, "consumer");
|
||||||
if (frame == null || closed.get() || currentFrame != frame) {
|
if (frame == null || closed.get() || currentFrame != frame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
long probeRevision = probeSequence.incrementAndGet();
|
cancelProbe();
|
||||||
probeExecutor.getQueue().clear();
|
long probeRevision = probeSequence.get();
|
||||||
try {
|
try {
|
||||||
probeExecutor.execute(() -> runProbe(probeRevision, probe, consumer));
|
activeProbe = probeExecutor.submit(() -> runProbe(probeRevision, probe, consumer));
|
||||||
} catch (RejectedExecutionException ignored) {
|
} catch (RejectedExecutionException ignored) {
|
||||||
probeExecutor.getQueue().clear();
|
probeExecutor.getQueue().clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void cancelProbe() {
|
||||||
|
probeSequence.incrementAndGet();
|
||||||
|
if (activeProbe != null) {
|
||||||
|
activeProbe.cancel(true);
|
||||||
|
activeProbe = null;
|
||||||
|
}
|
||||||
|
probeExecutor.getQueue().clear();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized void close() {
|
public synchronized void close() {
|
||||||
if (!closed.compareAndSet(false, true)) {
|
if (!closed.compareAndSet(false, true)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentFrame = null;
|
currentFrame = null;
|
||||||
|
cancelProbe();
|
||||||
renderExecutor.shutdownNow();
|
renderExecutor.shutdownNow();
|
||||||
probeExecutor.shutdownNow();
|
probeExecutor.shutdownNow();
|
||||||
cache.clear();
|
cache.clear();
|
||||||
@@ -285,7 +318,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
|
|
||||||
private synchronized boolean hasWork() {
|
private synchronized boolean hasWork() {
|
||||||
Frame frame = currentFrame;
|
Frame frame = currentFrame;
|
||||||
return frame != null && frame.nextMissing(tilesInFlight, false) != null;
|
return frame != null && frame.nextMissing(tilesInFlight, false, nanoTime.getAsLong()) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized TileJob claimNextTile() {
|
private synchronized TileJob claimNextTile() {
|
||||||
@@ -293,51 +326,63 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
if (frame == null || closed.get()) {
|
if (frame == null || closed.get()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
VisibleTile tile = frame.nextMissing(tilesInFlight, true);
|
VisibleTile tile = frame.nextMissing(tilesInFlight, true, nanoTime.getAsLong());
|
||||||
if (tile == null) {
|
if (tile == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
tilesInFlight.add(tile.key());
|
tilesInFlight.add(tile.key());
|
||||||
return new TileJob(frame.spec().renderer(), tile.key());
|
CacheEntry cached = cache.get(tile.key());
|
||||||
|
return new TileJob(frame.spec().renderer(), tile.key(), cached == null ? null : cached.work(), tile.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
private synchronized void release(TileKey key) {
|
private synchronized void release(TileKey key, boolean cancelled) {
|
||||||
tilesInFlight.remove(key);
|
tilesInFlight.remove(key);
|
||||||
|
Frame frame = currentFrame;
|
||||||
|
VisibleTile visible = frame == null ? null : frame.tile(key);
|
||||||
|
if (cancelled && visible != null && visible.needsRender(nanoTime.getAsLong(), frame.spec().renderer().previewRevision())) {
|
||||||
|
frame.nextCandidate = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void render(TileJob job) {
|
private void render(TileJob job) {
|
||||||
TileKey key = job.key();
|
TileKey key = job.key();
|
||||||
|
boolean cancelled = false;
|
||||||
try {
|
try {
|
||||||
|
long previewRevision = job.renderer().previewRevision();
|
||||||
double tileSpan = TILE_PIXELS * key.blocksPerPixel();
|
double tileSpan = TILE_PIXELS * key.blocksPerPixel();
|
||||||
long revision = ProvisionalSampling.revision();
|
MapTileRenderer.TileRender work = job.work();
|
||||||
MapTile tile = job.renderer().renderTile(
|
if (work == null) {
|
||||||
key.tileX() * tileSpan,
|
work = job.renderer().prepareTile(key.tileX() * tileSpan, key.tileZ() * tileSpan,
|
||||||
key.tileZ() * tileSpan,
|
key.blocksPerPixel(), TILE_PIXELS, key.layer(), key.heights());
|
||||||
key.blocksPerPixel(),
|
}
|
||||||
TILE_PIXELS,
|
MapTile tile = work.render(() -> !isWanted(key));
|
||||||
key.layer(),
|
boolean provisional = !work.complete();
|
||||||
key.heights(),
|
TileState previous = job.previous();
|
||||||
() -> !isWanted(key)
|
long delay = 0L;
|
||||||
);
|
if (provisional) {
|
||||||
boolean provisional = revision != ProvisionalSampling.revision();
|
delay = previous == null || previous.retryDelay() == 0L
|
||||||
|
? FIRST_RETRY_NANOS : Math.min(LAST_RETRY_NANOS, previous.retryDelay() * 2L);
|
||||||
|
}
|
||||||
|
long completedAt = nanoTime.getAsLong();
|
||||||
|
TileState state = new TileState(tile, provisional, delay, completedAt + delay,
|
||||||
|
completedAt + FIRST_RETRY_NANOS, previewRevision);
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
if (!isWanted(key)) {
|
if (closed.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
VisibleTile visible = currentFrame.tile(key);
|
cache.put(key, state, provisional ? work : null);
|
||||||
if (provisional) {
|
if (isWanted(key)) {
|
||||||
visible.markProvisional(tile, System.nanoTime());
|
currentFrame.tile(key).state = state;
|
||||||
} else {
|
|
||||||
cache.put(key, tile);
|
|
||||||
visible.setTile(tile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
publish();
|
publish();
|
||||||
} catch (CancellationException ignored) {
|
} catch (CancellationException ignored) {
|
||||||
|
cancelled = true;
|
||||||
|
cache.discardWork(key);
|
||||||
} catch (Throwable error) {
|
} catch (Throwable error) {
|
||||||
IrisLogging.debug("Vision tile render failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
|
IrisLogging.debug("Vision tile render failed: " + error.getClass().getSimpleName() + ": " + error.getMessage());
|
||||||
} finally {
|
} finally {
|
||||||
release(key);
|
release(key, cancelled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,8 +391,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
return !closed.get() && frame != null && frame.tile(key) != null;
|
return !closed.get() && frame != null && frame.tile(key) != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Fallback coarserFallback(Frame frame, VisibleTile tile) {
|
private Fallback coarserFallback(TileKey key, boolean provisional) {
|
||||||
TileKey key = tile.key();
|
|
||||||
for (int level = 1; level <= COARSER_FALLBACK_LEVELS; level++) {
|
for (int level = 1; level <= COARSER_FALLBACK_LEVELS; level++) {
|
||||||
long scale = 1L << level;
|
long scale = 1L << level;
|
||||||
int sourceSize = TILE_PIXELS >> level;
|
int sourceSize = TILE_PIXELS >> level;
|
||||||
@@ -357,8 +401,8 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
long parentX = Math.floorDiv(key.tileX(), scale);
|
long parentX = Math.floorDiv(key.tileX(), scale);
|
||||||
long parentZ = Math.floorDiv(key.tileZ(), scale);
|
long parentZ = Math.floorDiv(key.tileZ(), scale);
|
||||||
MapTile parent = cache.get(new TileKey(key.contentRevision(), key.layer(), key.heights(), parentScale, parentX,
|
MapTile parent = fallbackTile(new TileKey(key.contentRevision(), key.layer(), key.heights(), parentScale,
|
||||||
parentZ));
|
parentX, parentZ), provisional);
|
||||||
if (parent != null) {
|
if (parent != null) {
|
||||||
int sourceX = (int) (key.tileX() - parentX * scale) * sourceSize;
|
int sourceX = (int) (key.tileX() - parentX * scale) * sourceSize;
|
||||||
int sourceZ = (int) (key.tileZ() - parentZ * scale) * sourceSize;
|
int sourceZ = (int) (key.tileZ() - parentZ * scale) * sourceSize;
|
||||||
@@ -368,8 +412,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Fallback finerFallback(Frame frame, VisibleTile tile) {
|
private Fallback finerFallback(TileKey key, boolean provisional) {
|
||||||
TileKey key = tile.key();
|
|
||||||
double childScale = key.blocksPerPixel() / 2D;
|
double childScale = key.blocksPerPixel() / 2D;
|
||||||
if (childScale < MINIMUM_RENDER_BLOCKS_PER_PIXEL) {
|
if (childScale < MINIMUM_RENDER_BLOCKS_PER_PIXEL) {
|
||||||
return null;
|
return null;
|
||||||
@@ -377,8 +420,8 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
List<FallbackPiece> pieces = new ArrayList<>(4);
|
List<FallbackPiece> pieces = new ArrayList<>(4);
|
||||||
for (int childZ = 0; childZ < 2; childZ++) {
|
for (int childZ = 0; childZ < 2; childZ++) {
|
||||||
for (int childX = 0; childX < 2; childX++) {
|
for (int childX = 0; childX < 2; childX++) {
|
||||||
MapTile child = cache.get(new TileKey(key.contentRevision(), key.layer(), key.heights(), childScale,
|
MapTile child = fallbackTile(new TileKey(key.contentRevision(), key.layer(), key.heights(), childScale,
|
||||||
key.tileX() * 2L + childX, key.tileZ() * 2L + childZ));
|
key.tileX() * 2L + childX, key.tileZ() * 2L + childZ), provisional);
|
||||||
if (child != null) {
|
if (child != null) {
|
||||||
pieces.add(new FallbackPiece(child, 0, 0, TILE_PIXELS, childX * 0.5D, childZ * 0.5D, 0.5D));
|
pieces.add(new FallbackPiece(child, 0, 0, TILE_PIXELS, childX * 0.5D, childZ * 0.5D, 0.5D));
|
||||||
}
|
}
|
||||||
@@ -528,10 +571,11 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tiles are sorted centre first, and everything before the cursor is drawn or claimed, so each search resumes there.
|
// Tiles are sorted centre first, and everything before the cursor is drawn or claimed, so each search resumes there.
|
||||||
private VisibleTile nextMissing(Set<TileKey> inFlight, boolean advance) {
|
private VisibleTile nextMissing(Set<TileKey> inFlight, boolean advance, long now) {
|
||||||
|
long previewRevision = spec.renderer().previewRevision();
|
||||||
for (int index = nextCandidate; index < tiles.size(); index++) {
|
for (int index = nextCandidate; index < tiles.size(); index++) {
|
||||||
VisibleTile tile = tiles.get(index);
|
VisibleTile tile = tiles.get(index);
|
||||||
if (tile.needsRender(System.nanoTime()) && !inFlight.contains(tile.key())) {
|
if (tile.needsRender(now, previewRevision) && !inFlight.contains(tile.key())) {
|
||||||
if (advance) {
|
if (advance) {
|
||||||
nextCandidate = index + 1;
|
nextCandidate = index + 1;
|
||||||
}
|
}
|
||||||
@@ -548,10 +592,7 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
static final class VisibleTile {
|
static final class VisibleTile {
|
||||||
private final TileKey key;
|
private final TileKey key;
|
||||||
private final double distanceSquared;
|
private final double distanceSquared;
|
||||||
private volatile MapTile tile;
|
private volatile TileState state;
|
||||||
private volatile boolean provisional;
|
|
||||||
private long retryDelay;
|
|
||||||
private long retryAt;
|
|
||||||
|
|
||||||
private VisibleTile(TileKey key, double distanceSquared) {
|
private VisibleTile(TileKey key, double distanceSquared) {
|
||||||
this.key = key;
|
this.key = key;
|
||||||
@@ -575,28 +616,19 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MapTile tile() {
|
MapTile tile() {
|
||||||
return tile;
|
TileState current = state;
|
||||||
|
return current == null ? null : current.tile();
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean provisional() {
|
boolean provisional() {
|
||||||
return provisional;
|
TileState current = state;
|
||||||
|
return current != null && current.provisional();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTile(MapTile tile) {
|
private boolean needsRender(long now, long previewRevision) {
|
||||||
this.tile = tile;
|
TileState current = state;
|
||||||
provisional = false;
|
return current == null || current.provisional() && (now - current.retryAt() >= 0L
|
||||||
retryDelay = 0L;
|
|| previewRevision != current.previewRevision() && now - current.earliestRetryAt() >= 0L);
|
||||||
}
|
|
||||||
|
|
||||||
private void markProvisional(MapTile tile, long now) {
|
|
||||||
this.tile = tile;
|
|
||||||
retryDelay = retryDelay == 0L ? FIRST_RETRY_NANOS : Math.min(LAST_RETRY_NANOS, retryDelay * 2L);
|
|
||||||
retryAt = now + retryDelay;
|
|
||||||
provisional = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean needsRender(long now) {
|
|
||||||
return tile == null || provisional && now - retryAt >= 0L;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,7 +677,11 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record TileJob(MapTileRenderer renderer, TileKey key) {
|
private record TileJob(MapTileRenderer renderer, TileKey key, MapTileRenderer.TileRender work, TileState previous) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record TileState(MapTile tile, boolean provisional, long retryDelay, long retryAt, long earliestRetryAt,
|
||||||
|
long previewRevision) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final class WeightedTileCache {
|
private static final class WeightedTileCache {
|
||||||
@@ -658,14 +694,13 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
this.entries = new LinkedHashMap<>(128, 0.75F, true);
|
this.entries = new LinkedHashMap<>(128, 0.75F, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized MapTile get(TileKey key) {
|
synchronized CacheEntry get(TileKey key) {
|
||||||
CacheEntry entry = entries.get(key);
|
return entries.get(key);
|
||||||
return entry == null ? null : entry.tile();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized void put(TileKey key, MapTile tile) {
|
synchronized void put(TileKey key, TileState state, MapTileRenderer.TileRender work) {
|
||||||
long tileBytes = tile.bytes();
|
long tileBytes = state.tile().bytes() + (work == null ? 0L : work.bytes());
|
||||||
CacheEntry previous = entries.put(key, new CacheEntry(tile, tileBytes));
|
CacheEntry previous = entries.put(key, new CacheEntry(state, work, tileBytes));
|
||||||
if (previous != null) {
|
if (previous != null) {
|
||||||
bytes -= previous.bytes();
|
bytes -= previous.bytes();
|
||||||
}
|
}
|
||||||
@@ -682,8 +717,18 @@ final class VisionRenderController implements AutoCloseable {
|
|||||||
entries.clear();
|
entries.clear();
|
||||||
bytes = 0L;
|
bytes = 0L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
synchronized void discardWork(TileKey key) {
|
||||||
|
CacheEntry entry = entries.get(key);
|
||||||
|
if (entry == null || entry.work() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long retainedBytes = entry.state().tile().bytes();
|
||||||
|
entries.put(key, new CacheEntry(entry.state(), null, retainedBytes));
|
||||||
|
bytes -= entry.bytes() - retainedBytes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private record CacheEntry(MapTile tile, long bytes) {
|
private record CacheEntry(TileState state, MapTileRenderer.TileRender work, long bytes) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ package art.arcane.iris.studio.map.components;
|
|||||||
import art.arcane.iris.generation.runtime.IrisComplex;
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
import art.arcane.iris.generation.runtime.Engine;
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||||
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
import art.arcane.iris.generation.terrain.InferredType;
|
import art.arcane.iris.generation.terrain.InferredType;
|
||||||
import art.arcane.iris.generation.biome.IrisBiome;
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||||
@@ -30,7 +31,6 @@ import art.arcane.iris.studio.render.RenderType;
|
|||||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.awt.Color;
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.EnumMap;
|
import java.util.EnumMap;
|
||||||
import java.util.IdentityHashMap;
|
import java.util.IdentityHashMap;
|
||||||
@@ -73,6 +73,11 @@ public final class MapTileRenderer {
|
|||||||
return labels;
|
return labels;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long previewRevision() {
|
||||||
|
HydrologyRuntime runtime = engine.getComplex().getHydrologyRuntime();
|
||||||
|
return runtime == null ? 0L : runtime.previewRevision();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the square of {@code size} pixels whose top left pixel samples the given world position.
|
* Renders the square of {@code size} pixels whose top left pixel samples the given world position.
|
||||||
*
|
*
|
||||||
@@ -87,6 +92,13 @@ public final class MapTileRenderer {
|
|||||||
boolean withHeights,
|
boolean withHeights,
|
||||||
BooleanSupplier cancelled
|
BooleanSupplier cancelled
|
||||||
) {
|
) {
|
||||||
|
Objects.requireNonNull(cancelled, "cancelled");
|
||||||
|
checkCancelled(cancelled);
|
||||||
|
return prepareTile(startX, startZ, blocksPerPixel, size, layer, withHeights).render(cancelled);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TileRender prepareTile(double startX, double startZ, double blocksPerPixel, int size, MapLayer layer,
|
||||||
|
boolean withHeights) {
|
||||||
if (!Double.isFinite(startX) || !Double.isFinite(startZ) || !Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
if (!Double.isFinite(startX) || !Double.isFinite(startZ) || !Double.isFinite(blocksPerPixel) || blocksPerPixel <= 0D) {
|
||||||
throw new IllegalArgumentException("Map tile coordinates and scale must be finite");
|
throw new IllegalArgumentException("Map tile coordinates and scale must be finite");
|
||||||
}
|
}
|
||||||
@@ -94,35 +106,7 @@ public final class MapTileRenderer {
|
|||||||
throw new IllegalArgumentException("Map tile size must be positive");
|
throw new IllegalArgumentException("Map tile size must be positive");
|
||||||
}
|
}
|
||||||
Objects.requireNonNull(layer, "layer");
|
Objects.requireNonNull(layer, "layer");
|
||||||
Objects.requireNonNull(cancelled, "cancelled");
|
return new TileRender(new TileRequest(startX, startZ, blocksPerPixel, size, layer, withHeights));
|
||||||
TileType type = layer.type();
|
|
||||||
checkCancelled(cancelled);
|
|
||||||
|
|
||||||
TileGrid grid = new TileGrid(size + 2, startX - blocksPerPixel, startZ - blocksPerPixel, blocksPerPixel, cancelled);
|
|
||||||
int[] identities = sampleIdentities(grid, layer);
|
|
||||||
double[] heights = withHeights || type == TileType.HEIGHT ? sampleHeights(grid) : null;
|
|
||||||
short[] subjects = new short[identities.length];
|
|
||||||
short[] regions = new short[identities.length];
|
|
||||||
for (int index = 0; index < identities.length; index++) {
|
|
||||||
subjects[index] = subjectOf(identities[index]);
|
|
||||||
regions[index] = regionOf(identities[index]);
|
|
||||||
}
|
|
||||||
int[] colors = switch (type) {
|
|
||||||
case HEIGHT -> null;
|
|
||||||
case RIVER -> riverColors(startX, startZ, blocksPerPixel, size, cancelled);
|
|
||||||
default -> tileColors(size, type, subjects);
|
|
||||||
};
|
|
||||||
if (layer.display() != PickDisplay.PICKS) {
|
|
||||||
drawPickNoise(colors, grid, size, layer);
|
|
||||||
}
|
|
||||||
if (layer.waterOverlay()) {
|
|
||||||
BufferedImage base = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB);
|
|
||||||
base.setRGB(0, 0, size, size, colors, 0, size);
|
|
||||||
BufferedImage water = hydrologyRenderer.refineStudioBiome(startX, startZ, blocksPerPixel * size, base, cancelled);
|
|
||||||
colors = water.getRGB(0, 0, size, size, null, 0, size);
|
|
||||||
}
|
|
||||||
MapLabels.Kind subjectKind = type == TileType.REGION ? MapLabels.Kind.REGION : MapLabels.Kind.BIOME;
|
|
||||||
return new MapTile(size, blocksPerPixel, labels, subjectKind, colors, defaultHeightRange(), subjects, regions, heights);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,15 +150,15 @@ public final class MapTileRenderer {
|
|||||||
return sampler.identities;
|
return sampler.identities;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double[] sampleHeights(TileGrid grid) {
|
private double[] sampleHeights(HeightLattice lattice, BooleanSupplier cancelled) {
|
||||||
IrisComplex complex = engine.getComplex();
|
IrisComplex complex = engine.getComplex();
|
||||||
IrisDimension dimension = engine.getDimension();
|
IrisDimension dimension = engine.getDimension();
|
||||||
HydrologyRuntime water = complex.getHydrologyRuntime();
|
HydrologyRuntime water = complex.getHydrologyRuntime();
|
||||||
if (water == null || !water.enabled() || dimension != null && dimension.getTerrainTransform() != null) {
|
if (water == null || !water.enabled() || dimension != null && dimension.getTerrainTransform() != null) {
|
||||||
return new HeightLattice(grid, complex.getHeightStream()).interpolatedHeights();
|
return lattice.interpolatedHeights(cancelled);
|
||||||
}
|
}
|
||||||
try (HydrologyRuntime.PreviewScope ignored = water.preview()) {
|
try (HydrologyRuntime.PreviewScope ignored = water.preview()) {
|
||||||
return new HeightLattice(grid, complex.getHeightStream()).interpolatedHeights();
|
return lattice.interpolatedHeights(cancelled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,17 +261,6 @@ public final class MapTileRenderer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rivers are drawn by the hydrology renderer the engine ships; the biomes under them stay the tile's subjects.
|
|
||||||
private int[] riverColors(double startX, double startZ, double blocksPerPixel, int size, BooleanSupplier cancelled) {
|
|
||||||
BufferedImage rivers = hydrologyRenderer.renderStudio(startX, startZ, blocksPerPixel * size, size, RenderType.RIVER,
|
|
||||||
cancelled);
|
|
||||||
int[] colors = rivers.getRGB(0, 0, size, size, null, 0, size);
|
|
||||||
for (int index = 0; index < colors.length; index++) {
|
|
||||||
colors[index] &= 0xFFFFFF;
|
|
||||||
}
|
|
||||||
return colors;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int[] tileColors(int size, TileType type, short[] subjects) {
|
private int[] tileColors(int size, TileType type, short[] subjects) {
|
||||||
int[] colors = new int[size * size];
|
int[] colors = new int[size * size];
|
||||||
short previousSubject = MapLabels.NONE;
|
short previousSubject = MapLabels.NONE;
|
||||||
@@ -376,6 +349,124 @@ public final class MapTileRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class TileRender {
|
||||||
|
private final TileRequest request;
|
||||||
|
private short[] subjects;
|
||||||
|
private short[] regions;
|
||||||
|
private int[] baseColors;
|
||||||
|
private int[] finalColors;
|
||||||
|
private double[] heights;
|
||||||
|
private HeightLattice heightLattice;
|
||||||
|
private IrisRenderer.WaterTile water;
|
||||||
|
private boolean identitiesComplete;
|
||||||
|
private boolean baseComplete;
|
||||||
|
private boolean heightsComplete;
|
||||||
|
private boolean waterComplete;
|
||||||
|
private volatile boolean complete;
|
||||||
|
private volatile long bytes;
|
||||||
|
|
||||||
|
private TileRender(TileRequest request) {
|
||||||
|
this.request = request;
|
||||||
|
heightsComplete = !request.withHeights() && request.layer().type() != TileType.HEIGHT;
|
||||||
|
waterComplete = request.layer().type() != TileType.RIVER && !request.layer().waterOverlay();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MapTile render(BooleanSupplier cancelled) {
|
||||||
|
Objects.requireNonNull(cancelled, "cancelled");
|
||||||
|
checkCancelled(cancelled);
|
||||||
|
TileGrid grid = new TileGrid(request.size() + 2, request.startX() - request.blocksPerPixel(),
|
||||||
|
request.startZ() - request.blocksPerPixel(), request.blocksPerPixel(), cancelled);
|
||||||
|
sampleSubjects(grid);
|
||||||
|
sampleBaseColors(grid);
|
||||||
|
sampleTerrain(grid, cancelled);
|
||||||
|
int[] colors = sampleWater(cancelled);
|
||||||
|
checkCancelled(cancelled);
|
||||||
|
complete = baseComplete && heightsComplete && waterComplete;
|
||||||
|
bytes = (heightLattice == null ? 0L : heightLattice.bytes()) + (water == null ? 0L : water.bytes())
|
||||||
|
+ (baseColors == null || colors == baseColors ? 0L : (long) baseColors.length * Integer.BYTES);
|
||||||
|
if (!complete) {
|
||||||
|
ProvisionalSampling.mark();
|
||||||
|
}
|
||||||
|
MapLabels.Kind kind = request.layer().type() == TileType.REGION ? MapLabels.Kind.REGION : MapLabels.Kind.BIOME;
|
||||||
|
return new MapTile(request.size(), request.blocksPerPixel(), labels, kind, colors, defaultHeightRange(), subjects,
|
||||||
|
regions, heights);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean complete() {
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long bytes() {
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sampleSubjects(TileGrid grid) {
|
||||||
|
if (identitiesComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long revision = ProvisionalSampling.revision();
|
||||||
|
int[] identities = sampleIdentities(grid, request.layer());
|
||||||
|
subjects = new short[identities.length];
|
||||||
|
regions = new short[identities.length];
|
||||||
|
for (int index = 0; index < identities.length; index++) {
|
||||||
|
subjects[index] = subjectOf(identities[index]);
|
||||||
|
regions[index] = regionOf(identities[index]);
|
||||||
|
}
|
||||||
|
identitiesComplete = revision == ProvisionalSampling.revision();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sampleBaseColors(TileGrid grid) {
|
||||||
|
if (baseComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
long revision = ProvisionalSampling.revision();
|
||||||
|
baseColors = switch (request.layer().type()) {
|
||||||
|
case HEIGHT, RIVER -> null;
|
||||||
|
default -> tileColors(request.size(), request.layer().type(), subjects);
|
||||||
|
};
|
||||||
|
if (request.layer().display() != PickDisplay.PICKS) {
|
||||||
|
drawPickNoise(baseColors, grid, request.size(), request.layer());
|
||||||
|
}
|
||||||
|
baseComplete = identitiesComplete && revision == ProvisionalSampling.revision();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sampleTerrain(TileGrid grid, BooleanSupplier cancelled) {
|
||||||
|
if (heightsComplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (heightLattice == null) {
|
||||||
|
heightLattice = new HeightLattice(grid, engine.getComplex().getHeightStream());
|
||||||
|
}
|
||||||
|
heights = sampleHeights(heightLattice, cancelled);
|
||||||
|
heightsComplete = heightLattice.complete();
|
||||||
|
if (heightsComplete) {
|
||||||
|
heightLattice = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int[] sampleWater(BooleanSupplier cancelled) {
|
||||||
|
if (waterComplete) {
|
||||||
|
return finalColors == null ? baseColors : finalColors;
|
||||||
|
}
|
||||||
|
if (water == null) {
|
||||||
|
water = hydrologyRenderer.prepareStudioWater(request.startX(), request.startZ(), request.blocksPerPixel(),
|
||||||
|
request.size());
|
||||||
|
}
|
||||||
|
int[] colors = water.render(baseColors, request.layer().waterOverlay(), cancelled);
|
||||||
|
if (baseComplete && water.complete()) {
|
||||||
|
finalColors = colors;
|
||||||
|
baseColors = null;
|
||||||
|
waterComplete = true;
|
||||||
|
water = null;
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record TileRequest(double startX, double startZ, double blocksPerPixel, int size, MapLayer layer,
|
||||||
|
boolean withHeights) {
|
||||||
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
private interface IdentityShader {
|
private interface IdentityShader {
|
||||||
int identity(double x, double z);
|
int identity(double x, double z);
|
||||||
@@ -554,16 +645,20 @@ public final class MapTileRenderer {
|
|||||||
private final ProceduralStream<Double> stream;
|
private final ProceduralStream<Double> stream;
|
||||||
private final int pointsPerSide;
|
private final int pointsPerSide;
|
||||||
private final double[] points;
|
private final double[] points;
|
||||||
|
private final boolean[] resolved;
|
||||||
|
private int pending;
|
||||||
|
|
||||||
private HeightLattice(TileGrid grid, ProceduralStream<Double> stream) {
|
private HeightLattice(TileGrid grid, ProceduralStream<Double> stream) {
|
||||||
this.grid = grid;
|
this.grid = grid;
|
||||||
this.stream = stream;
|
this.stream = stream;
|
||||||
this.pointsPerSide = (grid.size() - 2) / LATTICE_PIXELS + LATTICE_MARGIN * 2 + 1;
|
this.pointsPerSide = (grid.size() - 2) / LATTICE_PIXELS + LATTICE_MARGIN * 2 + 1;
|
||||||
this.points = new double[pointsPerSide * pointsPerSide];
|
this.points = new double[pointsPerSide * pointsPerSide];
|
||||||
|
this.resolved = new boolean[points.length];
|
||||||
|
this.pending = points.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
double[] interpolatedHeights() {
|
double[] interpolatedHeights(BooleanSupplier cancelled) {
|
||||||
samplePoints();
|
samplePoints(cancelled);
|
||||||
int size = grid.size();
|
int size = grid.size();
|
||||||
double[] heights = new double[size * size];
|
double[] heights = new double[size * size];
|
||||||
for (int gridZ = 0; gridZ < size; gridZ++) {
|
for (int gridZ = 0; gridZ < size; gridZ++) {
|
||||||
@@ -574,16 +669,33 @@ public final class MapTileRenderer {
|
|||||||
return heights;
|
return heights;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean complete() {
|
||||||
|
return pending == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
long bytes() {
|
||||||
|
return (long) points.length * (Double.BYTES + 1L);
|
||||||
|
}
|
||||||
|
|
||||||
// Lattice point 0 sits on the tile's first pixel; the margin covers the border ring and the curve's reach.
|
// Lattice point 0 sits on the tile's first pixel; the margin covers the border ring and the curve's reach.
|
||||||
private void samplePoints() {
|
private void samplePoints(BooleanSupplier cancelled) {
|
||||||
double latticeStep = grid.step() * LATTICE_PIXELS;
|
double latticeStep = grid.step() * LATTICE_PIXELS;
|
||||||
double firstX = grid.worldX(1) - LATTICE_MARGIN * latticeStep;
|
double firstX = grid.worldX(1) - LATTICE_MARGIN * latticeStep;
|
||||||
double firstZ = grid.worldZ(1) - LATTICE_MARGIN * latticeStep;
|
double firstZ = grid.worldZ(1) - LATTICE_MARGIN * latticeStep;
|
||||||
for (int pointZ = 0; pointZ < pointsPerSide; pointZ++) {
|
for (int pointZ = 0; pointZ < pointsPerSide; pointZ++) {
|
||||||
checkCancelled(grid.cancelled());
|
checkCancelled(cancelled);
|
||||||
for (int pointX = 0; pointX < pointsPerSide; pointX++) {
|
for (int pointX = 0; pointX < pointsPerSide; pointX++) {
|
||||||
points[pointZ * pointsPerSide + pointX] = stream.getDouble(firstX + pointX * latticeStep,
|
int index = pointZ * pointsPerSide + pointX;
|
||||||
|
if (resolved[index]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
long revision = ProvisionalSampling.revision();
|
||||||
|
points[index] = stream.getDouble(firstX + pointX * latticeStep,
|
||||||
firstZ + pointZ * latticeStep);
|
firstZ + pointZ * latticeStep);
|
||||||
|
if (revision == ProvisionalSampling.revision()) {
|
||||||
|
resolved[index] = true;
|
||||||
|
pending--;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ public final class IrisRenderer {
|
|||||||
this.renderer = Objects.requireNonNull(renderer, "renderer");
|
this.renderer = Objects.requireNonNull(renderer, "renderer");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public WaterTile prepareStudioWater(double startX, double startZ, double step, int resolution) {
|
||||||
|
if (!Double.isFinite(startX) || !Double.isFinite(startZ) || !Double.isFinite(step) || step <= 0D || resolution < 1) {
|
||||||
|
throw new IllegalArgumentException("Water tile coordinates, scale and resolution must be valid");
|
||||||
|
}
|
||||||
|
return new WaterTile(startX, startZ, step, resolution);
|
||||||
|
}
|
||||||
|
|
||||||
public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) {
|
public BufferedImage render(double sx, double sz, double size, int resolution, RenderType currentType) {
|
||||||
return render(sx, sz, size, resolution, currentType, () -> false);
|
return render(sx, sz, size, resolution, currentType, () -> false);
|
||||||
}
|
}
|
||||||
@@ -445,6 +452,71 @@ public final class IrisRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class WaterTile {
|
||||||
|
private final double startX;
|
||||||
|
private final double startZ;
|
||||||
|
private final double step;
|
||||||
|
private final int resolution;
|
||||||
|
private final WaterMap map;
|
||||||
|
private boolean complete;
|
||||||
|
|
||||||
|
private WaterTile(double startX, double startZ, double step, int resolution) {
|
||||||
|
this.startX = startX;
|
||||||
|
this.startZ = startZ;
|
||||||
|
this.step = step;
|
||||||
|
this.resolution = resolution;
|
||||||
|
HydrologyRuntime runtime = renderer.getComplex().getHydrologyRuntime();
|
||||||
|
this.map = runtime == null || !runtime.enabled() || step > WaterMap.MAX_STEP
|
||||||
|
? null : new WaterMap(runtime, resolution, startX, startZ, step);
|
||||||
|
complete = map == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int[] render(int[] base, boolean composite, BooleanSupplier cancelled) {
|
||||||
|
Objects.requireNonNull(cancelled, "cancelled");
|
||||||
|
checkCancelled(cancelled);
|
||||||
|
if (composite && (base == null || base.length != resolution * resolution)) {
|
||||||
|
throw new IllegalArgumentException("Water overlay requires matching base pixels");
|
||||||
|
}
|
||||||
|
if (!complete) {
|
||||||
|
map.advance(cancelled);
|
||||||
|
map.drawCavePools(GenerationSemanticQueries.recordedCavePools(renderer,
|
||||||
|
Math.floorDiv((int) Math.floor(startX), 16), Math.floorDiv((int) Math.floor(startZ), 16),
|
||||||
|
Math.floorDiv((int) Math.ceil(startX + resolution * step) - 1, 16),
|
||||||
|
Math.floorDiv((int) Math.ceil(startZ + resolution * step) - 1, 16)));
|
||||||
|
complete = !map.provisional();
|
||||||
|
}
|
||||||
|
int[] colors = composite ? base.clone() : new int[resolution * resolution];
|
||||||
|
if (!composite) {
|
||||||
|
Arrays.fill(colors, NO_RIVER & 0xFFFFFF);
|
||||||
|
}
|
||||||
|
if (map != null) {
|
||||||
|
for (int z = 0; z < resolution; z++) {
|
||||||
|
checkCancelled(cancelled);
|
||||||
|
for (int x = 0; x < resolution; x++) {
|
||||||
|
HydrologyFeatureType type = map.at(x, z);
|
||||||
|
if (type != null) {
|
||||||
|
int index = z * resolution + x;
|
||||||
|
int color = hydrologyFeatureColor(type);
|
||||||
|
colors[index] = (composite ? blend(colors[index], color, RIVER_OVERLAY) : color) & 0xFFFFFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!complete) {
|
||||||
|
ProvisionalSampling.mark();
|
||||||
|
}
|
||||||
|
return colors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean complete() {
|
||||||
|
return complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long bytes() {
|
||||||
|
return map == null ? 0L : map.bytes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
private interface PixelShader {
|
private interface PixelShader {
|
||||||
int color(double x, double z);
|
int color(double x, double z);
|
||||||
|
|||||||
@@ -36,35 +36,85 @@ final class WaterMap {
|
|||||||
private final double startX;
|
private final double startX;
|
||||||
private final double startZ;
|
private final double startZ;
|
||||||
private final double step;
|
private final double step;
|
||||||
private final BooleanSupplier cancelled;
|
|
||||||
private final int[] ranks;
|
private final int[] ranks;
|
||||||
|
private final boolean[] resolved;
|
||||||
private final IdentityHashMap<HydrologyTier, HydrologyFeatureType> channels = new IdentityHashMap<>();
|
private final IdentityHashMap<HydrologyTier, HydrologyFeatureType> channels = new IdentityHashMap<>();
|
||||||
|
private final Set<Long> lakes = new HashSet<>();
|
||||||
|
private BooleanSupplier cancelled;
|
||||||
|
private int[] plannedRanks;
|
||||||
|
private boolean plansComplete;
|
||||||
private boolean provisional;
|
private boolean provisional;
|
||||||
|
|
||||||
private WaterMap(HydrologyRuntime runtime, int resolution, double startX, double startZ, double step, BooleanSupplier cancelled) {
|
WaterMap(HydrologyRuntime runtime, int resolution, double startX, double startZ, double step) {
|
||||||
this.runtime = runtime;
|
this.runtime = runtime;
|
||||||
this.resolution = resolution;
|
this.resolution = resolution;
|
||||||
this.startX = startX;
|
this.startX = startX;
|
||||||
this.startZ = startZ;
|
this.startZ = startZ;
|
||||||
this.step = step;
|
this.step = step;
|
||||||
this.cancelled = cancelled;
|
|
||||||
this.ranks = new int[resolution * resolution];
|
this.ranks = new int[resolution * resolution];
|
||||||
|
this.resolved = step <= RASTER_STEP ? new boolean[ranks.length] : null;
|
||||||
Arrays.fill(ranks, DRY);
|
Arrays.fill(ranks, DRY);
|
||||||
|
provisional = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static WaterMap draw(HydrologyRuntime runtime, int resolution, double startX, double startZ, double step, BooleanSupplier cancelled) {
|
static WaterMap draw(HydrologyRuntime runtime, int resolution, double startX, double startZ, double step, BooleanSupplier cancelled) {
|
||||||
WaterMap map = new WaterMap(runtime, resolution, startX, startZ, step, cancelled);
|
WaterMap map = new WaterMap(runtime, resolution, startX, startZ, step);
|
||||||
HydrologyPlanPreview plans = runtime.planPreview((int) Math.floor(startX), (int) Math.floor(startZ),
|
map.advance(cancelled);
|
||||||
(int) Math.ceil(startX + resolution * step) - 1, (int) Math.ceil(startZ + resolution * step) - 1);
|
|
||||||
map.provisional = !plans.complete();
|
|
||||||
if (step <= RASTER_STEP) {
|
|
||||||
map.drawRasters(plans);
|
|
||||||
} else {
|
|
||||||
map.drawPlans(plans);
|
|
||||||
}
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void advance(BooleanSupplier cancelled) {
|
||||||
|
if (!provisional) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.cancelled = cancelled;
|
||||||
|
HydrologyPlanPreview plans = null;
|
||||||
|
if (!plansComplete) {
|
||||||
|
plans = preview();
|
||||||
|
plannedRanks = null;
|
||||||
|
lakes.clear();
|
||||||
|
for (Pond lake : plans.lakes()) {
|
||||||
|
lakes.add(lake.id());
|
||||||
|
}
|
||||||
|
if (step > RASTER_STEP) {
|
||||||
|
prepareFallback(plans);
|
||||||
|
}
|
||||||
|
plansComplete = plans.complete();
|
||||||
|
}
|
||||||
|
if (step <= RASTER_STEP) {
|
||||||
|
provisional = drawRasters(plans) || !plansComplete;
|
||||||
|
} else {
|
||||||
|
prepareFallback(plans);
|
||||||
|
System.arraycopy(plannedRanks, 0, ranks, 0, ranks.length);
|
||||||
|
provisional = !plansComplete;
|
||||||
|
}
|
||||||
|
if (!provisional) {
|
||||||
|
plannedRanks = null;
|
||||||
|
lakes.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HydrologyPlanPreview preview() {
|
||||||
|
return runtime.planPreview((int) Math.floor(startX), (int) Math.floor(startZ),
|
||||||
|
(int) Math.ceil(startX + resolution * step) - 1, (int) Math.ceil(startZ + resolution * step) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareFallback(HydrologyPlanPreview plans) {
|
||||||
|
if (plannedRanks != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
WaterMap planned = new WaterMap(runtime, resolution, startX, startZ, step);
|
||||||
|
planned.cancelled = cancelled;
|
||||||
|
planned.drawPlans(plans == null ? preview() : plans);
|
||||||
|
plannedRanks = planned.ranks;
|
||||||
|
}
|
||||||
|
|
||||||
|
long bytes() {
|
||||||
|
return (long) ranks.length * Integer.BYTES + (resolved == null ? 0L : resolved.length)
|
||||||
|
+ (plannedRanks == null ? 0L : (long) plannedRanks.length * Integer.BYTES)
|
||||||
|
+ (long) lakes.size() * 64L;
|
||||||
|
}
|
||||||
|
|
||||||
HydrologyFeatureType at(int pixelX, int pixelZ) {
|
HydrologyFeatureType at(int pixelX, int pixelZ) {
|
||||||
int rank = ranks[pixelZ * resolution + pixelX];
|
int rank = ranks[pixelZ * resolution + pixelX];
|
||||||
return rank == DRY ? null : DRAWN.get(rank);
|
return rank == DRY ? null : DRAWN.get(rank);
|
||||||
@@ -84,37 +134,39 @@ final class WaterMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void drawRasters(HydrologyPlanPreview plans) {
|
private boolean drawRasters(HydrologyPlanPreview plans) {
|
||||||
Set<Long> lakes = new HashSet<>();
|
|
||||||
for (Pond lake : plans.lakes()) {
|
|
||||||
lakes.add(lake.id());
|
|
||||||
}
|
|
||||||
Map<Long, ChunkWaterRaster> rasters = new HashMap<>();
|
Map<Long, ChunkWaterRaster> rasters = new HashMap<>();
|
||||||
WaterMap planned = null;
|
boolean pending = false;
|
||||||
for (int pixelZ = 0; pixelZ < resolution; pixelZ++) {
|
for (int pixelZ = 0; pixelZ < resolution; pixelZ++) {
|
||||||
checkCancelled();
|
checkCancelled();
|
||||||
int minimumZ = (int) Math.floor(startZ + pixelZ * step);
|
int minimumZ = (int) Math.floor(startZ + pixelZ * step);
|
||||||
int maximumZ = Math.max(minimumZ, (int) Math.ceil(startZ + (pixelZ + 1) * step) - 1);
|
int maximumZ = Math.max(minimumZ, (int) Math.ceil(startZ + (pixelZ + 1) * step) - 1);
|
||||||
for (int pixelX = 0; pixelX < resolution; pixelX++) {
|
for (int pixelX = 0; pixelX < resolution; pixelX++) {
|
||||||
|
int index = pixelZ * resolution + pixelX;
|
||||||
|
if (resolved[index]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ranks[index] = DRY;
|
||||||
|
boolean ready = plansComplete;
|
||||||
int minimumX = (int) Math.floor(startX + pixelX * step);
|
int minimumX = (int) Math.floor(startX + pixelX * step);
|
||||||
int maximumX = Math.max(minimumX, (int) Math.ceil(startX + (pixelX + 1) * step) - 1);
|
int maximumX = Math.max(minimumX, (int) Math.ceil(startX + (pixelX + 1) * step) - 1);
|
||||||
for (int z = minimumZ; z <= maximumZ; z++) {
|
for (int z = minimumZ; z <= maximumZ; z++) {
|
||||||
for (int x = minimumX; x <= maximumX; x++) {
|
for (int x = minimumX; x <= maximumX; x++) {
|
||||||
ChunkWaterRaster raster = raster(rasters, x >> 4, z >> 4);
|
ChunkWaterRaster raster = raster(rasters, x >> 4, z >> 4);
|
||||||
if (raster.provisional()) {
|
if (raster.provisional()) {
|
||||||
provisional = true;
|
pending = true;
|
||||||
if (planned == null) {
|
ready = false;
|
||||||
planned = new WaterMap(runtime, resolution, startX, startZ, step, cancelled);
|
prepareFallback(plans);
|
||||||
planned.drawPlans(plans);
|
ranks[index] = Math.min(ranks[index], plannedRanks[index]);
|
||||||
}
|
|
||||||
stamp(pixelX, pixelZ, planned.at(pixelX, pixelZ));
|
|
||||||
} else if (!raster.empty()) {
|
} else if (!raster.empty()) {
|
||||||
stamp(pixelX, pixelZ, columnType(raster, x & 15, z & 15, lakes));
|
stamp(pixelX, pixelZ, columnType(raster, x & 15, z & 15, lakes));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
resolved[index] = ready;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ChunkWaterRaster raster(Map<Long, ChunkWaterRaster> rasters, int chunkX, int chunkZ) {
|
private ChunkWaterRaster raster(Map<Long, ChunkWaterRaster> rasters, int chunkX, int chunkZ) {
|
||||||
@@ -251,7 +303,7 @@ final class WaterMap {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkCancelled() {
|
private void checkCancelled() {
|
||||||
if (cancelled.getAsBoolean()) {
|
if (cancelled.getAsBoolean() || Thread.currentThread().isInterrupted()) {
|
||||||
throw new CancellationException("Vision render was cancelled");
|
throw new CancellationException("Vision render was cancelled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+92
@@ -70,6 +70,7 @@ import java.util.stream.Stream;
|
|||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertThrows;
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
@@ -143,6 +144,95 @@ public class HydrologyRuntimeTest {
|
|||||||
assertTrue(planners.plannerThreads().stream().noneMatch(name -> name.equals(Thread.currentThread().getName())));
|
assertTrue(planners.plannerThreads().stream().noneMatch(name -> name.equals(Thread.currentThread().getName())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 180_000L)
|
||||||
|
public void previewRevisionAdvancesOnlyWhenPlansOrRastersBecomeAvailable() throws Exception {
|
||||||
|
FakePlanners planners = new FakePlanners(settings(0, false));
|
||||||
|
ExecutorService pool = pool(planners, 1);
|
||||||
|
HydrologyRuntime runtime = runtime(planners, pool, () -> false);
|
||||||
|
CountDownLatch blocker = block(pool);
|
||||||
|
CompletableFuture<Void> plans = runtime.prepareWindowsAround(0, 0, 0, 0, 1);
|
||||||
|
|
||||||
|
assertEquals(0L, runtime.previewRevision());
|
||||||
|
assertFalse(plans.isDone());
|
||||||
|
blocker.countDown();
|
||||||
|
plans.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
long planned = runtime.previewRevision();
|
||||||
|
assertTrue(planned > 0L);
|
||||||
|
assertTrue(runtime.planPreview(0, 0, 15, 15).complete());
|
||||||
|
assertEquals(planned, runtime.previewRevision());
|
||||||
|
|
||||||
|
CountDownLatch rasterBlocker = block(pool);
|
||||||
|
for (int repeat = 0; repeat < 100; repeat++) {
|
||||||
|
assertTrue(runtime.rasterPreview(0, 0).provisional());
|
||||||
|
assertEquals(planned, runtime.previewRevision());
|
||||||
|
}
|
||||||
|
rasterBlocker.countDown();
|
||||||
|
await(() -> runtime.previewRevision() == planned + 1L, "raster publication advances preview readiness");
|
||||||
|
ChunkWaterRaster ready = runtime.rasterPreview(0, 0);
|
||||||
|
assertFalse(ready.provisional());
|
||||||
|
for (int repeat = 0; repeat < 100; repeat++) {
|
||||||
|
assertSame(ready, runtime.rasterPreview(0, 0));
|
||||||
|
assertSame(ready, runtime.raster(0, 0));
|
||||||
|
assertEquals(planned + 1L, runtime.previewRevision());
|
||||||
|
}
|
||||||
|
assertFalse(runtime.raster(1, 0).provisional());
|
||||||
|
assertEquals(planned + 2L, runtime.previewRevision());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 180_000L)
|
||||||
|
public void concurrentRasterPublicationAdvancesPreviewRevisionOnce() throws Exception {
|
||||||
|
FakePlanners planners = new FakePlanners(settings(0, false));
|
||||||
|
ExecutorService pool = pool(planners, 1);
|
||||||
|
HydrologyRuntime runtime = runtime(planners, pool, () -> false);
|
||||||
|
runtime.prepareWindowsAround(0, 0, 0, 0, 1).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
long planned = runtime.previewRevision();
|
||||||
|
CountDownLatch gate = new CountDownLatch(1);
|
||||||
|
planners.naturalGate = gate;
|
||||||
|
AtomicReference<Throwable> failure = new AtomicReference<>();
|
||||||
|
Thread demand = new Thread(() -> {
|
||||||
|
try {
|
||||||
|
runtime.raster(0, 0);
|
||||||
|
} catch (Throwable error) {
|
||||||
|
failure.set(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
demand.start();
|
||||||
|
try {
|
||||||
|
await(() -> planners.naturalSamples.get() > 0, "demanded raster starts");
|
||||||
|
assertTrue(runtime.rasterPreview(0, 0).provisional());
|
||||||
|
await(() -> planners.naturalSamples.get() >= 2, "preview raster starts beside demand");
|
||||||
|
assertEquals(planned, runtime.previewRevision());
|
||||||
|
} finally {
|
||||||
|
gate.countDown();
|
||||||
|
demand.join(TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS));
|
||||||
|
}
|
||||||
|
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
assertFalse(demand.isAlive());
|
||||||
|
assertNull(failure.get());
|
||||||
|
assertFalse(runtime.rasterPreview(0, 0).provisional());
|
||||||
|
assertEquals(planned + 1L, runtime.previewRevision());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(timeout = 180_000L)
|
||||||
|
public void failedRasterDoesNotAdvancePreviewRevision() throws Exception {
|
||||||
|
FakePlanners planners = new FakePlanners(settings(0, false));
|
||||||
|
ExecutorService pool = pool(planners, 1);
|
||||||
|
HydrologyPlanCache cache = new HydrologyPlanCache(planners.settings, planners::region, planners::window, pool, () -> false);
|
||||||
|
HydrologyRuntime runtime = new HydrologyRuntime(planners.settings, cache, (x, z) -> {
|
||||||
|
throw new IllegalStateException("terrain sampling failed");
|
||||||
|
}, () -> false, planners.ticker::get);
|
||||||
|
runtimes.add(runtime);
|
||||||
|
runtime.prepareWindowsAround(0, 0, 0, 0, 1).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
long planned = runtime.previewRevision();
|
||||||
|
|
||||||
|
assertThrows(IllegalStateException.class, () -> runtime.raster(0, 0));
|
||||||
|
|
||||||
|
assertEquals(planned, runtime.previewRevision());
|
||||||
|
}
|
||||||
|
|
||||||
@Test(timeout = 180_000L)
|
@Test(timeout = 180_000L)
|
||||||
public void rasterCachedPerChunk() throws Exception {
|
public void rasterCachedPerChunk() throws Exception {
|
||||||
FakePlanners planners = new FakePlanners(settings());
|
FakePlanners planners = new FakePlanners(settings());
|
||||||
@@ -868,6 +958,7 @@ public class HydrologyRuntimeTest {
|
|||||||
|
|
||||||
private static void assertDisabled(HydrologyRuntime runtime) {
|
private static void assertDisabled(HydrologyRuntime runtime) {
|
||||||
assertFalse(runtime.enabled());
|
assertFalse(runtime.enabled());
|
||||||
|
assertEquals(0L, runtime.previewRevision());
|
||||||
assertSame(ChunkWaterRaster.EMPTY, runtime.raster(0, 0));
|
assertSame(ChunkWaterRaster.EMPTY, runtime.raster(0, 0));
|
||||||
assertSame(ChunkWaterRaster.EMPTY, runtime.rasterSnapshot(0, 0));
|
assertSame(ChunkWaterRaster.EMPTY, runtime.rasterSnapshot(0, 0));
|
||||||
assertSame(ChunkWaterRaster.EMPTY, runtime.rasterAt(0, 0));
|
assertSame(ChunkWaterRaster.EMPTY, runtime.rasterAt(0, 0));
|
||||||
@@ -879,6 +970,7 @@ public class HydrologyRuntimeTest {
|
|||||||
assertSame(ChunkWaterRaster.EMPTY, runtime.rasterPreview(0, 0));
|
assertSame(ChunkWaterRaster.EMPTY, runtime.rasterPreview(0, 0));
|
||||||
assertEquals(HydrologyPlanPreview.EMPTY, runtime.planPreview(0, 0, 511, 511));
|
assertEquals(HydrologyPlanPreview.EMPTY, runtime.planPreview(0, 0, 511, 511));
|
||||||
runtime.close();
|
runtime.close();
|
||||||
|
assertEquals(0L, runtime.previewRevision());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void settle(FakePlanners planners, ExecutorService pool, int windows) throws Exception {
|
private static void settle(FakePlanners planners, ExecutorService pool, int windows) throws Exception {
|
||||||
|
|||||||
+67
@@ -139,6 +139,73 @@ public class HydrologyPlanCacheTest {
|
|||||||
assertEquals(KEY, results[0].plan().key());
|
assertEquals(KEY, results[0].plan().key());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void readyRevisionCountsSuccessfulPlanPublicationsWithoutCountingReads() throws Exception {
|
||||||
|
FakePlanners planners = new FakePlanners(unpersisted());
|
||||||
|
ExecutorService pool = pool(1);
|
||||||
|
HydrologyPlanCache cache = cache(planners, pool, () -> false);
|
||||||
|
RegionKey region = WindowPlanner.lakeRegions(KEY).getFirst();
|
||||||
|
Gate gate = new Gate();
|
||||||
|
planners.hook = key -> gate.pass();
|
||||||
|
CompletableFuture<RegionPlan> pending = cache.previewRegion(region);
|
||||||
|
await(() -> gate.arrivals() == 1, "region planning starts before publication");
|
||||||
|
assertEquals(0L, cache.readyRevision());
|
||||||
|
assertFalse(pending.isDone());
|
||||||
|
gate.release();
|
||||||
|
RegionPlan ready = pending.get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
assertEquals(1L, cache.readyRevision());
|
||||||
|
assertSame(ready, cache.regionIfReady(region).orElseThrow());
|
||||||
|
assertSame(ready, cache.previewRegion(region).get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
|
||||||
|
assertSame(ready, cache.region(region));
|
||||||
|
assertEquals(1L, cache.readyRevision());
|
||||||
|
|
||||||
|
cache.previewWindow(KEY).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
long published = WindowPlanner.lakeRegions(KEY).size() + 1L;
|
||||||
|
assertEquals(published, cache.readyRevision());
|
||||||
|
assertTrue(cache.windowIfReady(KEY).isPresent());
|
||||||
|
cache.window(KEY);
|
||||||
|
assertEquals(published, cache.readyRevision());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void failedPlansDoNotAdvanceReadyRevision() throws Exception {
|
||||||
|
FakePlanners planners = new FakePlanners(unpersisted());
|
||||||
|
ExecutorService pool = pool(1);
|
||||||
|
HydrologyPlanCache cache = cache(planners, pool, () -> false);
|
||||||
|
RegionKey region = new RegionKey(0, 0);
|
||||||
|
planners.hook = key -> {
|
||||||
|
throw new IllegalStateException("region planning failed");
|
||||||
|
};
|
||||||
|
|
||||||
|
CompletableFuture<RegionPlan> failed = cache.previewRegion(region);
|
||||||
|
assertThrows(ExecutionException.class, () -> failed.get(TIMEOUT_SECONDS, TimeUnit.SECONDS));
|
||||||
|
assertEquals(0L, cache.readyRevision());
|
||||||
|
planners.hook = key -> { };
|
||||||
|
cache.previewRegion(region).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
assertEquals(1L, cache.readyRevision());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cancelledPlanCompletionDoesNotAdvanceReadyRevision() throws Exception {
|
||||||
|
FakePlanners planners = new FakePlanners(unpersisted());
|
||||||
|
ExecutorService pool = pool(1);
|
||||||
|
HydrologyPlanCache cache = cache(planners, pool, () -> false);
|
||||||
|
Gate gate = new Gate();
|
||||||
|
planners.hook = key -> gate.pass();
|
||||||
|
CompletableFuture<RegionPlan> pending = cache.previewRegion(new RegionKey(0, 0));
|
||||||
|
await(() -> gate.arrivals() == 1, "region planning starts before cancellation");
|
||||||
|
|
||||||
|
cache.close();
|
||||||
|
gate.release();
|
||||||
|
pool.submit(() -> { }).get(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||||
|
|
||||||
|
assertTrue(pending.isCompletedExceptionally());
|
||||||
|
assertEquals(0L, cache.readyRevision());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void interruptedPlanIsNotCached() throws Exception {
|
public void interruptedPlanIsNotCached() throws Exception {
|
||||||
FakePlanners planners = new FakePlanners(unpersisted());
|
FakePlanners planners = new FakePlanners(unpersisted());
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package art.arcane.iris.generation.noise;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class TerrainBatchScratchTest {
|
||||||
|
@Test
|
||||||
|
public void integerLatticesDistributeWithoutLongLinearProbeClusters() throws Exception {
|
||||||
|
TerrainBatchScratch scratch = new TerrainBatchScratch();
|
||||||
|
for (int origin : new int[]{0, -1536, 1536, 16_777_212}) {
|
||||||
|
scratch.clearLattice(4096);
|
||||||
|
for (int index = 0; index < 1024; index++) {
|
||||||
|
double x = origin + (index & 31) * 6D;
|
||||||
|
double z = origin + (index >> 5) * 6D;
|
||||||
|
assertEquals(index, scratch.lattice(x, z));
|
||||||
|
}
|
||||||
|
assertEquals(1024, scratch.latticeCount());
|
||||||
|
for (int index = 1023; index >= 0; index--) {
|
||||||
|
double x = origin + (index & 31) * 6D;
|
||||||
|
double z = origin + (index >> 5) * 6D;
|
||||||
|
assertEquals(index, scratch.lattice(x, z));
|
||||||
|
assertEquals(x, scratch.latticeX(index), 0D);
|
||||||
|
assertEquals(z, scratch.latticeZ(index), 0D);
|
||||||
|
}
|
||||||
|
assertTrue("clustered lattice at " + origin, longestCluster(scratch) < 32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void rawCoordinateIdentityAndInsertionOrderSurviveRehashingAndReset() {
|
||||||
|
TerrainBatchScratch scratch = new TerrainBatchScratch();
|
||||||
|
long[] bits = {0L, Long.MIN_VALUE, Double.doubleToRawLongBits(1D), Double.doubleToRawLongBits(-1D),
|
||||||
|
Double.doubleToRawLongBits(0.125D), Double.doubleToRawLongBits(-0.125D),
|
||||||
|
Double.doubleToRawLongBits(Double.POSITIVE_INFINITY), Double.doubleToRawLongBits(Double.NEGATIVE_INFINITY),
|
||||||
|
0x7FF8000000000001L, 0x7FF8000000000002L};
|
||||||
|
for (int pass = 0; pass < 2; pass++) {
|
||||||
|
scratch.clearLattice(bits.length * bits.length);
|
||||||
|
for (int x = 0; x < bits.length; x++) {
|
||||||
|
for (int z = 0; z < bits.length; z++) {
|
||||||
|
int expected = x * bits.length + z;
|
||||||
|
double worldX = Double.longBitsToDouble(bits[x]);
|
||||||
|
double worldZ = Double.longBitsToDouble(bits[z]);
|
||||||
|
assertEquals(expected, scratch.lattice(worldX, worldZ));
|
||||||
|
assertEquals(expected, scratch.lattice(worldX, worldZ));
|
||||||
|
assertEquals(bits[x], Double.doubleToRawLongBits(scratch.latticeX(expected)));
|
||||||
|
assertEquals(bits[z], Double.doubleToRawLongBits(scratch.latticeZ(expected)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(bits.length * bits.length, scratch.latticeCount());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int longestCluster(TerrainBatchScratch scratch) throws Exception {
|
||||||
|
Field field = TerrainBatchScratch.class.getDeclaredField("latticeSlots");
|
||||||
|
field.setAccessible(true);
|
||||||
|
int[] slots = (int[]) field.get(scratch);
|
||||||
|
int longest = 0;
|
||||||
|
int length = 0;
|
||||||
|
for (int index = 0; index < slots.length * 2; index++) {
|
||||||
|
length = slots[index % slots.length] == 0 ? 0 : length + 1;
|
||||||
|
longest = Math.max(longest, length);
|
||||||
|
}
|
||||||
|
return longest;
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
package art.arcane.iris.generation.runtime;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.IrisLogging;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.doCallRealMethod;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
|
public class IrisEngineFailureReportingTest {
|
||||||
|
@Test
|
||||||
|
public void failedGenerationReportsOnceAndRetainsItsCause() throws Exception {
|
||||||
|
IrisEngine engine = mock(IrisEngine.class);
|
||||||
|
IllegalStateException failure = new IllegalStateException("Hydrology raster failed");
|
||||||
|
Field platformHooks = IrisEngine.class.getDeclaredField("platformHooks");
|
||||||
|
platformHooks.setAccessible(true);
|
||||||
|
platformHooks.set(engine, mock(EnginePlatformHooks.class));
|
||||||
|
doThrow(failure).when(engine).openGenerationHistoryCoordinateScope(-352, -144);
|
||||||
|
doCallRealMethod().when(engine).generate(-352, -144, null, null, false);
|
||||||
|
doCallRealMethod().when(engine).fail("Failed to generate -352, -144", failure);
|
||||||
|
doCallRealMethod().when(engine).hasFailed();
|
||||||
|
|
||||||
|
try (MockedStatic<IrisLogging> logging = mockStatic(IrisLogging.class)) {
|
||||||
|
WrongEngineBroException reported = assertThrows(WrongEngineBroException.class,
|
||||||
|
() -> engine.generate(-352, -144, null, null, false));
|
||||||
|
|
||||||
|
assertSame(failure, reported.getCause());
|
||||||
|
assertTrue(engine.hasFailed());
|
||||||
|
logging.verify(() -> IrisLogging.error("Failed to generate -352, -144"));
|
||||||
|
logging.verify(() -> IrisLogging.reportError(failure));
|
||||||
|
logging.verifyNoMoreInteractions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit;
|
||||||
|
|
||||||
|
import art.arcane.iris.spi.LogLevel;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class BukkitPlatformErrorReportingTest {
|
||||||
|
@After
|
||||||
|
public void releaseHost() {
|
||||||
|
BukkitPlatform.releaseHost();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void contextualReportEmitsOneCompleteTraceThroughTheHost() {
|
||||||
|
ArrayList<LogEntry> messages = new ArrayList<>();
|
||||||
|
ArrayList<Throwable> reported = new ArrayList<>();
|
||||||
|
BukkitPlatform.hostBridge(new BukkitPlatform.HostBridge(
|
||||||
|
(level, message) -> messages.add(new LogEntry(level, message)), null,
|
||||||
|
failure -> {
|
||||||
|
reported.add(failure);
|
||||||
|
failure.printStackTrace(System.err);
|
||||||
|
}, null, null, null, null, null, null));
|
||||||
|
IllegalStateException failure = new IllegalStateException("raster failure",
|
||||||
|
new ArrayIndexOutOfBoundsException("invalid sample"));
|
||||||
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
|
PrintStream previousError = System.err;
|
||||||
|
try (PrintStream errors = new PrintStream(output, true, StandardCharsets.UTF_8)) {
|
||||||
|
System.setErr(errors);
|
||||||
|
|
||||||
|
new BukkitPlatform().reportError("Generation failed", failure);
|
||||||
|
} finally {
|
||||||
|
System.setErr(previousError);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(List.of(new LogEntry(LogLevel.ERROR, "Generation failed")), messages);
|
||||||
|
assertEquals(List.of(failure), reported);
|
||||||
|
String trace = output.toString(StandardCharsets.UTF_8);
|
||||||
|
assertEquals(1L, trace.lines().filter(line -> line.contains("raster failure")).count());
|
||||||
|
assertEquals(1L, trace.lines().filter(line -> line.contains("Caused by:")).count());
|
||||||
|
assertTrue(trace.contains("BukkitPlatformErrorReportingTest.contextualReportEmitsOneCompleteTraceThroughTheHost"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private record LogEntry(LogLevel level, String message) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,10 @@ import org.junit.Test;
|
|||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
import static org.junit.Assert.assertTrue;
|
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.mock;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
@@ -38,13 +41,23 @@ public class BukkitBindingTest {
|
|||||||
Fixture fixture = new Fixture();
|
Fixture fixture = new Fixture();
|
||||||
when(fixture.lifecycle.isServerStopping()).thenReturn(true);
|
when(fixture.lifecycle.isServerStopping()).thenReturn(true);
|
||||||
assertTrue(fixture.binding.isServerStopping());
|
assertTrue(fixture.binding.isServerStopping());
|
||||||
when(fixture.lifecycle.deferPluginClassLoaderClose()).thenReturn(true);
|
fixture.binding.initializeLifecycle(getClass().getClassLoader());
|
||||||
assertTrue(fixture.binding.deferPluginClassLoaderClose());
|
verify(fixture.lifecycle).initializeLifecycle(getClass().getClassLoader());
|
||||||
|
fixture.binding.deferPluginClassLoaderClose();
|
||||||
fixture.binding.releasePluginClassLoaderClose();
|
fixture.binding.releasePluginClassLoaderClose();
|
||||||
verify(fixture.lifecycle).deferPluginClassLoaderClose();
|
verify(fixture.lifecycle).deferPluginClassLoaderClose();
|
||||||
verify(fixture.lifecycle).releasePluginClassLoaderClose();
|
verify(fixture.lifecycle).releasePluginClassLoaderClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void limitedBindingRejectsClassLoaderProtectionAndUsesSynchronousShutdown() {
|
||||||
|
INMSBinding binding = mock(INMSBinding.class, CALLS_REAL_METHODS);
|
||||||
|
assertFalse(binding.isServerStopping());
|
||||||
|
assertThrows(IllegalStateException.class,
|
||||||
|
() -> binding.initializeLifecycle(getClass().getClassLoader()));
|
||||||
|
assertThrows(IllegalStateException.class, binding::deferPluginClassLoaderClose);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void injectAttachesVanillaSpawnersOnlyToOverworldEnvironments() throws NoSuchFieldException, IllegalAccessException {
|
public void injectAttachesVanillaSpawnersOnlyToOverworldEnvironments() throws NoSuchFieldException, IllegalAccessException {
|
||||||
Fixture fixture = new Fixture();
|
Fixture fixture = new Fixture();
|
||||||
@@ -71,8 +84,8 @@ public class BukkitBindingTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static final class Fixture {
|
private static final class Fixture {
|
||||||
private final NativeTerrainAccess terrain = mock(NativeTerrainAccess.class);
|
|
||||||
private final NativeWorldGeneration generation = mock(NativeWorldGeneration.class);
|
private final NativeWorldGeneration generation = mock(NativeWorldGeneration.class);
|
||||||
|
private final NativeTerrainAccess terrain = mock(NativeTerrainAccess.class);
|
||||||
private final NativeWorldLifecycleFactory.Controller lifecycle = mock(NativeWorldLifecycleFactory.Controller.class);
|
private final NativeWorldLifecycleFactory.Controller lifecycle = mock(NativeWorldLifecycleFactory.Controller.class);
|
||||||
private final BukkitBinding binding;
|
private final BukkitBinding binding;
|
||||||
|
|
||||||
|
|||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
package art.arcane.iris.platform.bukkit.nms;
|
||||||
|
|
||||||
|
import art.arcane.iris.platform.agent.Agent;
|
||||||
|
import art.arcane.iris.platform.bootstrap.SlimJar;
|
||||||
|
import art.arcane.iris.testsupport.PlatformBinding;
|
||||||
|
import org.junit.ClassRule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertSame;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
|
||||||
|
public class BukkitWorldLifecyclePolicyTest {
|
||||||
|
@ClassRule
|
||||||
|
public static final PlatformBinding PLATFORM = PlatformBinding.mockPlatform();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nativeProvidersCloseBeforeThePluginClassLoaderIsReleased() throws Exception {
|
||||||
|
ClassLoader loader = mock(ClassLoader.class);
|
||||||
|
try (MockedStatic<SlimJar> providers = mockStatic(SlimJar.class);
|
||||||
|
MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||||
|
agent.when(() -> Agent.releaseClassLoader(loader)).thenAnswer(invocation -> {
|
||||||
|
providers.verify(SlimJar::closeNativeProviders);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
BukkitWorldLifecyclePolicy.INSTANCE.releaseClassLoader(loader);
|
||||||
|
agent.verify(() -> Agent.releaseClassLoader(loader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nativeProviderCloseFailureStillReleasesThePluginClassLoader() throws Exception {
|
||||||
|
ClassLoader loader = mock(ClassLoader.class);
|
||||||
|
IOException failure = new IOException("provider close failed");
|
||||||
|
try (MockedStatic<SlimJar> providers = mockStatic(SlimJar.class);
|
||||||
|
MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||||
|
providers.when(SlimJar::closeNativeProviders).thenThrow(failure);
|
||||||
|
IllegalStateException reported = assertThrows(IllegalStateException.class,
|
||||||
|
() -> BukkitWorldLifecyclePolicy.INSTANCE.releaseClassLoader(loader));
|
||||||
|
assertSame(failure, reported.getCause());
|
||||||
|
agent.verify(() -> Agent.releaseClassLoader(loader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
public void providerAndLoaderCloseFailuresAreBothPreserved() throws Exception {
|
||||||
|
ClassLoader loader = mock(ClassLoader.class);
|
||||||
|
IOException providerFailure = new IOException("provider close failed");
|
||||||
|
IllegalStateException loaderFailure = new IllegalStateException("loader close failed");
|
||||||
|
try (MockedStatic<SlimJar> providers = mockStatic(SlimJar.class);
|
||||||
|
MockedStatic<Agent> agent = mockStatic(Agent.class)) {
|
||||||
|
providers.when(SlimJar::closeNativeProviders).thenThrow(providerFailure);
|
||||||
|
agent.when(() -> Agent.releaseClassLoader(loader)).thenThrow(loaderFailure);
|
||||||
|
IllegalStateException reported = assertThrows(IllegalStateException.class,
|
||||||
|
() -> BukkitWorldLifecyclePolicy.INSTANCE.releaseClassLoader(loader));
|
||||||
|
assertSame(loaderFailure, reported);
|
||||||
|
assertEquals(1, reported.getSuppressed().length);
|
||||||
|
assertSame(providerFailure, reported.getSuppressed()[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
package art.arcane.iris.studio.map;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.biome.IrisBiome;
|
||||||
|
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanPreview;
|
||||||
|
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||||
|
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||||
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
|
import art.arcane.iris.generation.runtime.Engine;
|
||||||
|
import art.arcane.iris.generation.runtime.IrisComplex;
|
||||||
|
import art.arcane.iris.generation.stream.ProvisionalSampling;
|
||||||
|
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||||
|
import art.arcane.iris.studio.map.components.MapLayer;
|
||||||
|
import art.arcane.iris.studio.map.components.MapStyle;
|
||||||
|
import art.arcane.iris.studio.map.components.MapTile;
|
||||||
|
import art.arcane.iris.studio.map.components.MapTileRenderer;
|
||||||
|
import art.arcane.iris.studio.map.components.PickDisplay;
|
||||||
|
import art.arcane.iris.studio.map.components.TileType;
|
||||||
|
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||||
|
import art.arcane.volmlib.util.stream.interpolation.Interpolated;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertArrayEquals;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class MapTileProgressTest {
|
||||||
|
@Test
|
||||||
|
public void retriesOnlySampleUnresolvedHeightPointsAndReuseBiomeData() {
|
||||||
|
AtomicBoolean pending = new AtomicBoolean(true);
|
||||||
|
AtomicInteger heightSamples = new AtomicInteger();
|
||||||
|
AtomicInteger biomeSamples = new AtomicInteger();
|
||||||
|
Engine engine = engine(biomeSamples);
|
||||||
|
when(engine.getComplex().getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> {
|
||||||
|
heightSamples.incrementAndGet();
|
||||||
|
if (x == 0D && z == 0D && pending.get()) {
|
||||||
|
ProvisionalSampling.mark();
|
||||||
|
return 60D;
|
||||||
|
}
|
||||||
|
return 80D + x * 0.02D + z * 0.01D;
|
||||||
|
}));
|
||||||
|
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||||
|
MapLayer layer = MapLayer.biomes(PickDisplay.PICKS, false);
|
||||||
|
MapTileRenderer.TileRender progress = renderer.prepareTile(-8D, -8D, 0.5D, 16, layer, true);
|
||||||
|
|
||||||
|
progress.render(() -> false);
|
||||||
|
int initialHeights = heightSamples.get();
|
||||||
|
int initialBiomes = biomeSamples.get();
|
||||||
|
assertFalse(progress.complete());
|
||||||
|
assertTrue(progress.bytes() > 0L);
|
||||||
|
|
||||||
|
progress.render(() -> false);
|
||||||
|
assertEquals(initialHeights + 1, heightSamples.get());
|
||||||
|
assertEquals(initialBiomes, biomeSamples.get());
|
||||||
|
pending.set(false);
|
||||||
|
MapTile settled = progress.render(() -> false);
|
||||||
|
assertTrue(progress.complete());
|
||||||
|
assertEquals(initialHeights + 2, heightSamples.get());
|
||||||
|
assertEquals(initialBiomes, biomeSamples.get());
|
||||||
|
assertEquals(0L, progress.bytes());
|
||||||
|
progress.render(() -> false);
|
||||||
|
assertEquals(initialHeights + 2, heightSamples.get());
|
||||||
|
assertEquals(initialBiomes, biomeSamples.get());
|
||||||
|
|
||||||
|
MapTile expected = renderer.renderTile(-8D, -8D, 0.5D, 16, layer, true, () -> false);
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
assertEquals(expected.heightAt(x, z), settled.heightAt(x, z), 0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertArrayEquals(pixels(expected.image(MapStyle.PLAIN.withShading(true))),
|
||||||
|
pixels(settled.image(MapStyle.PLAIN.withShading(true))));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void completedWaterIsNotBlendedAgainWhileHeightsWait() {
|
||||||
|
AtomicBoolean pending = new AtomicBoolean(true);
|
||||||
|
Engine engine = engine(new AtomicInteger());
|
||||||
|
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||||
|
ChunkWaterRaster raster = mock(ChunkWaterRaster.class);
|
||||||
|
when(runtime.enabled()).thenReturn(true);
|
||||||
|
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(HydrologyPlanPreview.EMPTY);
|
||||||
|
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(raster);
|
||||||
|
when(raster.role(anyInt(), anyInt())).thenReturn(WaterRole.POND);
|
||||||
|
when(engine.getComplex().getHydrologyRuntime()).thenReturn(runtime);
|
||||||
|
when(engine.getComplex().getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> {
|
||||||
|
if (pending.get()) {
|
||||||
|
ProvisionalSampling.mark();
|
||||||
|
}
|
||||||
|
return 60D;
|
||||||
|
}));
|
||||||
|
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||||
|
MapTileRenderer.TileRender progress = renderer.prepareTile(0D, 0D, 1D, 16, MapLayer.of(TileType.BIOME), true);
|
||||||
|
|
||||||
|
MapTile first = progress.render(() -> false);
|
||||||
|
assertFalse(progress.complete());
|
||||||
|
assertNotEquals(0xFF738249, first.image(MapStyle.PLAIN).getRGB(0, 0));
|
||||||
|
MapTile second = progress.render(() -> false);
|
||||||
|
assertArrayEquals(pixels(first.image(MapStyle.PLAIN)), pixels(second.image(MapStyle.PLAIN)));
|
||||||
|
|
||||||
|
pending.set(false);
|
||||||
|
MapTile complete = progress.render(() -> false);
|
||||||
|
assertTrue(progress.complete());
|
||||||
|
assertEquals(0L, progress.bytes());
|
||||||
|
assertArrayEquals(pixels(first.image(MapStyle.PLAIN)), pixels(complete.image(MapStyle.PLAIN)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void twoBlockPixelsRefineMixedWaterAndHeightsToTheSameFinalTile() {
|
||||||
|
AtomicBoolean pending = new AtomicBoolean(true);
|
||||||
|
Engine engine = engine(new AtomicInteger());
|
||||||
|
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||||
|
ChunkWaterRaster raster = mock(ChunkWaterRaster.class);
|
||||||
|
when(runtime.enabled()).thenReturn(true);
|
||||||
|
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(HydrologyPlanPreview.EMPTY);
|
||||||
|
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(raster);
|
||||||
|
when(runtime.rasterPreview(0, 0)).thenAnswer(invocation -> pending.get() ? ChunkWaterRaster.PROVISIONAL : raster);
|
||||||
|
when(raster.role(anyInt(), anyInt())).thenAnswer(invocation -> {
|
||||||
|
int x = invocation.getArgument(0);
|
||||||
|
int z = invocation.getArgument(1);
|
||||||
|
if (x == 1 && z < 4) {
|
||||||
|
return WaterRole.POND;
|
||||||
|
}
|
||||||
|
if (x == 7 && z < 8) {
|
||||||
|
return WaterRole.WATERFALL;
|
||||||
|
}
|
||||||
|
return x < 4 && z == 9 ? WaterRole.INLET : WaterRole.NONE;
|
||||||
|
});
|
||||||
|
when(engine.getComplex().getHydrologyRuntime()).thenReturn(runtime);
|
||||||
|
when(engine.getComplex().getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> {
|
||||||
|
if (x == 0D && z == 0D && pending.get()) {
|
||||||
|
ProvisionalSampling.mark();
|
||||||
|
return 60D;
|
||||||
|
}
|
||||||
|
return 80D + x * 0.02D + z * 0.01D;
|
||||||
|
}));
|
||||||
|
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||||
|
MapLayer layer = MapLayer.of(TileType.BIOME);
|
||||||
|
MapTileRenderer.TileRender progress = renderer.prepareTile(-16D, -16D, 2D, 16, layer, true);
|
||||||
|
|
||||||
|
MapTile first = progress.render(() -> false);
|
||||||
|
int[] firstPixels = pixels(first.image(MapStyle.PLAIN.withShading(true)));
|
||||||
|
assertFalse(progress.complete());
|
||||||
|
assertEquals(0xFF738249, first.image(MapStyle.PLAIN).getRGB(8, 8));
|
||||||
|
MapTile waiting = progress.render(() -> false);
|
||||||
|
assertFalse(progress.complete());
|
||||||
|
assertArrayEquals(firstPixels, pixels(waiting.image(MapStyle.PLAIN.withShading(true))));
|
||||||
|
|
||||||
|
pending.set(false);
|
||||||
|
MapTile actual = progress.render(() -> false);
|
||||||
|
assertTrue(progress.complete());
|
||||||
|
assertEquals(0L, progress.bytes());
|
||||||
|
BufferedImage colors = actual.image(MapStyle.PLAIN);
|
||||||
|
assertEquals(0xFF738249, colors.getRGB(14, 14));
|
||||||
|
assertNotEquals(0xFF738249, colors.getRGB(8, 8));
|
||||||
|
assertNotEquals(colors.getRGB(8, 8), colors.getRGB(11, 8));
|
||||||
|
assertNotEquals(colors.getRGB(8, 8), colors.getRGB(8, 12));
|
||||||
|
assertNotEquals(colors.getRGB(11, 8), colors.getRGB(8, 12));
|
||||||
|
assertArrayEquals(firstPixels, pixels(first.image(MapStyle.PLAIN.withShading(true))));
|
||||||
|
|
||||||
|
MapTile expected = renderer.renderTile(-16D, -16D, 2D, 16, layer, true, () -> false);
|
||||||
|
assertArrayEquals(pixels(expected.image(MapStyle.PLAIN.withShading(true))),
|
||||||
|
pixels(actual.image(MapStyle.PLAIN.withShading(true))));
|
||||||
|
for (int z = 0; z < 16; z++) {
|
||||||
|
for (int x = 0; x < 16; x++) {
|
||||||
|
assertEquals(expected.heightAt(x, z), actual.heightAt(x, z), 0D);
|
||||||
|
assertEquals(expected.subjectAt(x, z), actual.subjectAt(x, z));
|
||||||
|
assertEquals(expected.regionAt(x, z), actual.regionAt(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cancelledPreparationCanResumeWithoutReusingIncompleteHeightSamples() {
|
||||||
|
Engine engine = engine(new AtomicInteger());
|
||||||
|
AtomicInteger samples = new AtomicInteger();
|
||||||
|
when(engine.getComplex().getHeightStream()).thenReturn(ProceduralStream.ofDouble((x, z) -> {
|
||||||
|
samples.incrementAndGet();
|
||||||
|
return x + z + 80D;
|
||||||
|
}));
|
||||||
|
MapTileRenderer renderer = new MapTileRenderer(engine);
|
||||||
|
MapLayer layer = MapLayer.biomes(PickDisplay.PICKS, false);
|
||||||
|
MapTileRenderer.TileRender progress = renderer.prepareTile(-16D, -16D, 1D, 16, layer, true);
|
||||||
|
assertThrows(CancellationException.class, () -> progress.render(() -> samples.get() >= 9));
|
||||||
|
assertFalse(progress.complete());
|
||||||
|
MapTile actual = progress.render(() -> false);
|
||||||
|
assertTrue(progress.complete());
|
||||||
|
assertEquals(81, samples.get());
|
||||||
|
MapTile expected = renderer.renderTile(-16D, -16D, 1D, 16, layer, true, () -> false);
|
||||||
|
assertArrayEquals(pixels(expected.image(MapStyle.PLAIN.withShading(true))),
|
||||||
|
pixels(actual.image(MapStyle.PLAIN.withShading(true))));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Engine engine(AtomicInteger biomeSamples) {
|
||||||
|
Engine engine = mock(Engine.class);
|
||||||
|
IrisComplex complex = mock(IrisComplex.class);
|
||||||
|
IrisBiome biome = new IrisBiome();
|
||||||
|
biome.setColor("#738249");
|
||||||
|
IrisRegion region = new IrisRegion();
|
||||||
|
when(engine.getComplex()).thenReturn(complex);
|
||||||
|
when(engine.getHeight()).thenReturn(256);
|
||||||
|
when(complex.getBaseBiomeStream()).thenReturn(ProceduralStream.of((x, z) -> {
|
||||||
|
biomeSamples.incrementAndGet();
|
||||||
|
return biome;
|
||||||
|
}, Interpolated.of(value -> 0D, value -> biome)));
|
||||||
|
when(complex.getRegionStream()).thenReturn(ProceduralStream.of((x, z) -> region,
|
||||||
|
Interpolated.of(value -> 0D, value -> region)));
|
||||||
|
return engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int[] pixels(BufferedImage image) {
|
||||||
|
return image.getRGB(0, 0, image.getWidth(), image.getHeight(), null, 0, image.getWidth());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,23 @@
|
|||||||
package art.arcane.iris.studio.map;
|
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.MapLayer;
|
||||||
import art.arcane.iris.studio.map.components.MapTile;
|
import art.arcane.iris.studio.map.components.MapTile;
|
||||||
import art.arcane.iris.studio.map.components.MapTileRenderer;
|
import art.arcane.iris.studio.map.components.MapTileRenderer;
|
||||||
import art.arcane.iris.studio.map.components.TileType;
|
import art.arcane.iris.studio.map.components.TileType;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.awt.EventQueue;
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.atomic.AtomicLong;
|
||||||
import java.util.function.BooleanSupplier;
|
import java.util.function.BooleanSupplier;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
import static org.junit.Assert.assertSame;
|
import static org.junit.Assert.assertSame;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
@@ -22,21 +25,23 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
|
|||||||
import static org.mockito.ArgumentMatchers.anyDouble;
|
import static org.mockito.ArgumentMatchers.anyDouble;
|
||||||
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.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
public class VisionRenderControllerTest {
|
public class VisionRenderControllerTest {
|
||||||
@Test
|
@Test
|
||||||
public void provisionalTilesRetryWithBackoffAndConvergeWithoutMoving() throws Exception {
|
public void provisionalTilesRetryWithBackoffAndConvergeWithoutMoving() throws Exception {
|
||||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||||
|
MapTileRenderer renderer = renderer(work);
|
||||||
MapTile tile = mock(MapTile.class);
|
MapTile tile = mock(MapTile.class);
|
||||||
AtomicInteger attempts = new AtomicInteger();
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
when(work.render(any()))
|
||||||
.thenAnswer(invocation -> {
|
.thenAnswer(invocation -> {
|
||||||
if (attempts.incrementAndGet() < 3) {
|
attempts.incrementAndGet();
|
||||||
ProvisionalSampling.mark();
|
|
||||||
}
|
|
||||||
return tile;
|
return tile;
|
||||||
});
|
});
|
||||||
|
when(work.complete()).thenAnswer(invocation -> attempts.get() >= 3);
|
||||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
VisionRenderController.Frame frame = controller.request(spec(renderer, 1L));
|
VisionRenderController.Frame frame = controller.request(spec(renderer, 1L));
|
||||||
await(() -> frame.tiles().getFirst().tile() != null);
|
await(() -> frame.tiles().getFirst().tile() != null);
|
||||||
@@ -55,30 +60,47 @@ public class VisionRenderControllerTest {
|
|||||||
});
|
});
|
||||||
assertEquals(3, attempts.get());
|
assertEquals(3, attempts.get());
|
||||||
assertSame(tile, controller.request(spec(renderer, 1L)).tiles().getFirst().tile());
|
assertSame(tile, controller.request(spec(renderer, 1L)).tiles().getFirst().tile());
|
||||||
|
verify(renderer).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void provisionalTilesAreShownButNeverReusedAsFinal() throws Exception {
|
public void resizeZoomAndSuspendRetainProvisionalImageWorkAndBackoff() throws Exception {
|
||||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||||
|
MapTileRenderer renderer = renderer(work);
|
||||||
MapTile incomplete = mock(MapTile.class);
|
MapTile incomplete = mock(MapTile.class);
|
||||||
MapTile complete = mock(MapTile.class);
|
MapTile complete = mock(MapTile.class);
|
||||||
AtomicInteger attempts = new AtomicInteger();
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
when(work.render(any()))
|
||||||
.thenAnswer(invocation -> {
|
.thenAnswer(invocation -> {
|
||||||
if (attempts.incrementAndGet() == 1) {
|
if (attempts.incrementAndGet() == 1) {
|
||||||
ProvisionalSampling.mark();
|
|
||||||
return incomplete;
|
return incomplete;
|
||||||
}
|
}
|
||||||
return complete;
|
return complete;
|
||||||
});
|
});
|
||||||
|
when(work.complete()).thenAnswer(invocation -> attempts.get() >= 2);
|
||||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||||
await(() -> first.tiles().getFirst().tile() != null);
|
await(() -> first.tiles().getFirst().tile() != null && controller.progress(first).activeWorkers() == 0);
|
||||||
assertSame(incomplete, first.tiles().getFirst().tile());
|
assertSame(incomplete, first.tiles().getFirst().tile());
|
||||||
|
VisionRenderController.Frame resized = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||||
|
MapLayer.of(TileType.RIVER), 1L, 32D, 32D, 1D, 48, 48, false));
|
||||||
|
assertSame(incomplete, resized.tiles().getFirst().tile());
|
||||||
|
assertTrue(resized.tiles().getFirst().provisional());
|
||||||
|
VisionRenderController.Frame zoomed = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||||
|
MapLayer.of(TileType.RIVER), 1L, 32D, 32D, 1.15D, 48, 48, false));
|
||||||
|
assertSame(incomplete, zoomed.tiles().getFirst().tile());
|
||||||
|
controller.suspend();
|
||||||
VisionRenderController.Frame second = controller.request(spec(renderer, 1L));
|
VisionRenderController.Frame second = controller.request(spec(renderer, 1L));
|
||||||
await(() -> second.tiles().getFirst().tile() == complete);
|
assertSame(incomplete, second.tiles().getFirst().tile());
|
||||||
|
assertEquals(0, controller.progress(second).ready());
|
||||||
|
assertEquals(1, attempts.get());
|
||||||
|
await(() -> {
|
||||||
|
controller.retryProvisional(second);
|
||||||
|
return controller.progress(second).ready() == 1;
|
||||||
|
});
|
||||||
assertEquals(2, attempts.get());
|
assertEquals(2, attempts.get());
|
||||||
|
verify(renderer).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||||
VisionRenderController.Frame third = controller.request(spec(renderer, 1L));
|
VisionRenderController.Frame third = controller.request(spec(renderer, 1L));
|
||||||
assertSame(complete, third.tiles().getFirst().tile());
|
assertSame(complete, third.tiles().getFirst().tile());
|
||||||
assertEquals(2, attempts.get());
|
assertEquals(2, attempts.get());
|
||||||
@@ -87,10 +109,11 @@ public class VisionRenderControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void contentRevisionInvalidatesFinalTiles() throws Exception {
|
public void contentRevisionInvalidatesFinalTiles() throws Exception {
|
||||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
|
||||||
MapTile tile = mock(MapTile.class);
|
MapTile tile = mock(MapTile.class);
|
||||||
|
MapTileRenderer.TileRender work = finished(tile);
|
||||||
|
MapTileRenderer renderer = renderer(work);
|
||||||
AtomicInteger attempts = new AtomicInteger();
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
when(work.render(any()))
|
||||||
.thenAnswer(invocation -> {
|
.thenAnswer(invocation -> {
|
||||||
attempts.incrementAndGet();
|
attempts.incrementAndGet();
|
||||||
return tile;
|
return tile;
|
||||||
@@ -109,14 +132,16 @@ public class VisionRenderControllerTest {
|
|||||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||||
MapTile overview = mock(MapTile.class);
|
MapTile overview = mock(MapTile.class);
|
||||||
MapTile incomplete = mock(MapTile.class);
|
MapTile incomplete = mock(MapTile.class);
|
||||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
MapTileRenderer.TileRender overviewWork = finished(overview);
|
||||||
|
MapTileRenderer.TileRender detailWork = mock(MapTileRenderer.TileRender.class);
|
||||||
|
when(detailWork.render(any())).thenReturn(incomplete);
|
||||||
|
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||||
.thenAnswer(invocation -> {
|
.thenAnswer(invocation -> {
|
||||||
double scale = invocation.getArgument(2);
|
double scale = invocation.getArgument(2);
|
||||||
if (scale < 4D) {
|
if (scale < 4D) {
|
||||||
ProvisionalSampling.mark();
|
return detailWork;
|
||||||
return incomplete;
|
|
||||||
}
|
}
|
||||||
return overview;
|
return overviewWork;
|
||||||
});
|
});
|
||||||
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
VisionRenderController.Frame coarse = controller.request(new VisionRenderController.RenderSpec(renderer,
|
VisionRenderController.Frame coarse = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||||
@@ -142,10 +167,11 @@ public class VisionRenderControllerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void closeRejectsLatePublication() throws Exception {
|
public void closeRejectsLatePublication() throws Exception {
|
||||||
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||||
|
MapTileRenderer renderer = renderer(work);
|
||||||
CountDownLatch entered = new CountDownLatch(1);
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
CountDownLatch release = new CountDownLatch(1);
|
CountDownLatch release = new CountDownLatch(1);
|
||||||
when(renderer.renderTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean(), any()))
|
when(work.render(any()))
|
||||||
.thenAnswer(invocation -> {
|
.thenAnswer(invocation -> {
|
||||||
entered.countDown();
|
entered.countDown();
|
||||||
while (release.getCount() != 0L) {
|
while (release.getCount() != 0L) {
|
||||||
@@ -167,7 +193,261 @@ public class VisionRenderControllerTest {
|
|||||||
controller.close();
|
controller.close();
|
||||||
}
|
}
|
||||||
await(() -> controller.progress(frame).activeWorkers() == 0);
|
await(() -> controller.progress(frame).activeWorkers() == 0);
|
||||||
assertEquals(null, frame.tiles().getFirst().tile());
|
assertNull(frame.tiles().getFirst().tile());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void resizeAndSameLevelZoomReuseFinalTiles() throws Exception {
|
||||||
|
MapTile tile = mock(MapTile.class);
|
||||||
|
MapTileRenderer.TileRender work = finished(tile);
|
||||||
|
MapTileRenderer renderer = renderer(work);
|
||||||
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
|
VisionRenderController.Frame first = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||||
|
MapLayer.of(TileType.RIVER), 1L, 32D, 32D, 1D, 48, 48, false));
|
||||||
|
await(() -> controller.progress(first).ready() == 1);
|
||||||
|
VisionRenderController.Frame resized = controller.request(spec(renderer, 1L));
|
||||||
|
assertSame(tile, resized.tiles().getFirst().tile());
|
||||||
|
VisionRenderController.Frame zoomed = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||||
|
MapLayer.of(TileType.RIVER), 1L, 32D, 32D, 0.9D, 64, 64, false));
|
||||||
|
assertSame(tile, zoomed.tiles().getFirst().tile());
|
||||||
|
assertEquals(1, controller.progress(zoomed).ready());
|
||||||
|
verify(work).render(any());
|
||||||
|
verify(renderer).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void contentRevisionInvalidatesProvisionalWork() throws Exception {
|
||||||
|
MapTile tile = mock(MapTile.class);
|
||||||
|
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||||
|
MapTileRenderer renderer = renderer(work);
|
||||||
|
when(work.render(any())).thenReturn(tile);
|
||||||
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
|
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||||
|
await(() -> first.tiles().getFirst().tile() != null);
|
||||||
|
VisionRenderController.Frame second = controller.request(spec(renderer, 2L));
|
||||||
|
await(() -> second.tiles().getFirst().tile() != null);
|
||||||
|
verify(renderer, times(2)).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void missingDetailUsesProvisionalOverviewWithoutMarkingItFinal() throws Exception {
|
||||||
|
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||||
|
MapTile overview = mock(MapTile.class);
|
||||||
|
MapTileRenderer.TileRender overviewWork = mock(MapTileRenderer.TileRender.class);
|
||||||
|
when(overviewWork.render(any())).thenReturn(overview);
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
MapTileRenderer.TileRender detailWork = cancellable(entered);
|
||||||
|
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||||
|
.thenAnswer(invocation -> (double) invocation.getArgument(2) < 4D ? detailWork : overviewWork);
|
||||||
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
|
VisionRenderController.Frame coarse = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||||
|
MapLayer.of(TileType.RIVER), 1L, 128D, 128D, 4D, 64, 64, false));
|
||||||
|
await(() -> coarse.tiles().getFirst().tile() != null);
|
||||||
|
VisionRenderController.Frame fine = controller.request(spec(renderer, 1L));
|
||||||
|
assertTrue(entered.await(5L, TimeUnit.SECONDS));
|
||||||
|
assertNull(fine.tiles().getFirst().tile());
|
||||||
|
VisionRenderController.Fallback fallback = controller.fallback(fine, fine.tiles().getFirst());
|
||||||
|
assertNotNull(fallback);
|
||||||
|
assertSame(overview, fallback.pieces().getFirst().tile());
|
||||||
|
assertEquals(0, controller.progress(fine).ready());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void zoomingOutKeepsCompleteFinerTilesOverProvisionalExactTile() throws Exception {
|
||||||
|
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||||
|
MapTile fineTile = mock(MapTile.class);
|
||||||
|
MapTile coarseTile = mock(MapTile.class);
|
||||||
|
MapTileRenderer.TileRender fineWork = finished(fineTile);
|
||||||
|
MapTileRenderer.TileRender coarseWork = mock(MapTileRenderer.TileRender.class);
|
||||||
|
when(coarseWork.render(any())).thenReturn(coarseTile);
|
||||||
|
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||||
|
.thenAnswer(invocation -> (double) invocation.getArgument(2) < 2D ? fineWork : coarseWork);
|
||||||
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
|
VisionRenderController.Frame fine = controller.request(spec(renderer, 1L));
|
||||||
|
await(() -> controller.progress(fine).ready() == 1);
|
||||||
|
VisionRenderController.Frame coarse = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||||
|
MapLayer.of(TileType.RIVER), 1L, 64D, 64D, 2D, 64, 64, false));
|
||||||
|
await(() -> coarse.tiles().getFirst().tile() != null);
|
||||||
|
VisionRenderController.Fallback fallback = controller.fallback(coarse, coarse.tiles().getFirst());
|
||||||
|
assertNotNull(fallback);
|
||||||
|
assertSame(fineTile, fallback.pieces().getFirst().tile());
|
||||||
|
assertEquals(0.5D, fallback.pieces().getFirst().size(), 0D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void suspendedViewCancelsSamplingAndProbeButKeepsCompletedTiles() throws Exception {
|
||||||
|
MapTile tile = mock(MapTile.class);
|
||||||
|
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||||
|
CountDownLatch sampling = new CountDownLatch(1);
|
||||||
|
MapTileRenderer.TileRender pending = cancellable(sampling);
|
||||||
|
MapTileRenderer.TileRender complete = finished(tile);
|
||||||
|
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||||
|
.thenReturn(complete, pending);
|
||||||
|
CountDownLatch probing = new CountDownLatch(1);
|
||||||
|
CountDownLatch interrupted = new CountDownLatch(1);
|
||||||
|
AtomicInteger published = new AtomicInteger();
|
||||||
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
|
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||||
|
await(() -> controller.progress(first).ready() == 1);
|
||||||
|
VisionRenderController.Frame next = controller.request(new VisionRenderController.RenderSpec(renderer,
|
||||||
|
MapLayer.of(TileType.RIVER), 1L, 96D, 32D, 1D, 64, 64, false));
|
||||||
|
assertTrue(sampling.await(5L, TimeUnit.SECONDS));
|
||||||
|
controller.submitProbe(next, () -> {
|
||||||
|
probing.countDown();
|
||||||
|
try {
|
||||||
|
new CountDownLatch(1).await();
|
||||||
|
return "late";
|
||||||
|
} catch (InterruptedException error) {
|
||||||
|
interrupted.countDown();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}, value -> published.incrementAndGet());
|
||||||
|
assertTrue(probing.await(5L, TimeUnit.SECONDS));
|
||||||
|
controller.suspend();
|
||||||
|
assertTrue(interrupted.await(5L, TimeUnit.SECONDS));
|
||||||
|
await(() -> controller.progress(next).activeWorkers() == 0);
|
||||||
|
EventQueue.invokeAndWait(() -> { });
|
||||||
|
assertEquals(0, published.get());
|
||||||
|
assertNull(next.tiles().getFirst().tile());
|
||||||
|
VisionRenderController.Frame resumed = controller.request(spec(renderer, 1L));
|
||||||
|
assertSame(tile, resumed.tiles().getFirst().tile());
|
||||||
|
verify(renderer, times(2)).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void retainedWorkCountsAgainstCacheBudgetAndIsReleasedWhenComplete() throws Exception {
|
||||||
|
MapTile tile = mock(MapTile.class);
|
||||||
|
when(tile.bytes()).thenReturn(1L);
|
||||||
|
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||||
|
MapTileRenderer renderer = renderer(work);
|
||||||
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
|
when(work.render(any())).thenAnswer(invocation -> {
|
||||||
|
attempts.incrementAndGet();
|
||||||
|
return tile;
|
||||||
|
});
|
||||||
|
when(work.complete()).thenAnswer(invocation -> attempts.get() >= 2);
|
||||||
|
when(work.bytes()).thenReturn(256L * 1024L * 1024L);
|
||||||
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
|
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||||
|
await(() -> first.tiles().getFirst().tile() != null);
|
||||||
|
VisionRenderController.Frame second = controller.request(spec(renderer, 1L));
|
||||||
|
await(() -> controller.progress(second).ready() == 1);
|
||||||
|
VisionRenderController.Frame third = controller.request(spec(renderer, 1L));
|
||||||
|
assertSame(tile, third.tiles().getFirst().tile());
|
||||||
|
verify(renderer, times(2)).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||||
|
assertEquals(2, attempts.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cancelledRetryReleasesWorkAndKeepsPublishedImage() throws Exception {
|
||||||
|
MapTile incomplete = mock(MapTile.class);
|
||||||
|
MapTile complete = mock(MapTile.class);
|
||||||
|
CountDownLatch entered = new CountDownLatch(1);
|
||||||
|
MapTileRenderer.TileRender partial = mock(MapTileRenderer.TileRender.class);
|
||||||
|
MapTileRenderer.TileRender replacement = finished(complete);
|
||||||
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
|
when(partial.render(any())).thenAnswer(invocation -> {
|
||||||
|
if (attempts.incrementAndGet() == 1) {
|
||||||
|
return incomplete;
|
||||||
|
}
|
||||||
|
BooleanSupplier cancelled = invocation.getArgument(0);
|
||||||
|
entered.countDown();
|
||||||
|
await(cancelled);
|
||||||
|
throw new CancellationException();
|
||||||
|
});
|
||||||
|
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||||
|
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean()))
|
||||||
|
.thenReturn(partial, replacement);
|
||||||
|
try (VisionRenderController controller = new VisionRenderController(() -> { })) {
|
||||||
|
VisionRenderController.Frame first = controller.request(spec(renderer, 1L));
|
||||||
|
await(() -> first.tiles().getFirst().tile() != null);
|
||||||
|
await(() -> {
|
||||||
|
controller.retryProvisional(first);
|
||||||
|
return entered.getCount() == 0L;
|
||||||
|
});
|
||||||
|
controller.suspend();
|
||||||
|
await(() -> controller.progress(first).activeWorkers() == 0);
|
||||||
|
assertSame(incomplete, first.tiles().getFirst().tile());
|
||||||
|
VisionRenderController.Frame resumed = controller.request(spec(renderer, 1L));
|
||||||
|
await(() -> controller.progress(resumed).ready() == 1);
|
||||||
|
assertSame(complete, resumed.tiles().getFirst().tile());
|
||||||
|
verify(renderer, times(2)).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void previewReadinessWakesBackoffAtBoundedCadenceIncludingCompletionDuringRender() throws Exception {
|
||||||
|
MapTile tile = mock(MapTile.class);
|
||||||
|
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||||
|
MapTileRenderer renderer = renderer(work);
|
||||||
|
AtomicLong time = new AtomicLong();
|
||||||
|
AtomicLong readiness = new AtomicLong();
|
||||||
|
AtomicInteger attempts = new AtomicInteger();
|
||||||
|
when(renderer.previewRevision()).thenAnswer(invocation -> readiness.get());
|
||||||
|
when(work.render(any())).thenAnswer(invocation -> {
|
||||||
|
if (attempts.incrementAndGet() == 3) {
|
||||||
|
readiness.incrementAndGet();
|
||||||
|
}
|
||||||
|
return tile;
|
||||||
|
});
|
||||||
|
when(work.complete()).thenAnswer(invocation -> attempts.get() >= 4);
|
||||||
|
try (VisionRenderController controller = new VisionRenderController(() -> { }, time::get)) {
|
||||||
|
VisionRenderController.Frame frame = controller.request(spec(renderer, 1L));
|
||||||
|
await(() -> attempts.get() == 1 && controller.progress(frame).activeWorkers() == 0);
|
||||||
|
time.set(VisionRenderController.FIRST_RETRY_NANOS);
|
||||||
|
controller.retryProvisional(frame);
|
||||||
|
await(() -> attempts.get() == 2 && controller.progress(frame).activeWorkers() == 0);
|
||||||
|
time.addAndGet(VisionRenderController.FIRST_RETRY_NANOS);
|
||||||
|
controller.retryProvisional(frame);
|
||||||
|
assertEquals(2, attempts.get());
|
||||||
|
readiness.incrementAndGet();
|
||||||
|
controller.retryProvisional(frame);
|
||||||
|
await(() -> attempts.get() == 3 && controller.progress(frame).activeWorkers() == 0);
|
||||||
|
time.addAndGet(VisionRenderController.FIRST_RETRY_NANOS - 1L);
|
||||||
|
controller.retryProvisional(frame);
|
||||||
|
assertEquals(3, attempts.get());
|
||||||
|
time.incrementAndGet();
|
||||||
|
controller.retryProvisional(frame);
|
||||||
|
await(() -> controller.progress(frame).ready() == 1);
|
||||||
|
assertEquals(4, attempts.get());
|
||||||
|
verify(renderer).prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MapTileRenderer.TileRender cancellable(CountDownLatch entered) {
|
||||||
|
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||||
|
when(work.render(any())).thenAnswer(invocation -> {
|
||||||
|
BooleanSupplier cancelled = invocation.getArgument(0);
|
||||||
|
entered.countDown();
|
||||||
|
while (!cancelled.getAsBoolean() && !Thread.currentThread().isInterrupted()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1L);
|
||||||
|
} catch (InterruptedException error) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new CancellationException();
|
||||||
|
});
|
||||||
|
return work;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MapTileRenderer renderer(MapTileRenderer.TileRender work) {
|
||||||
|
MapTileRenderer renderer = mock(MapTileRenderer.class);
|
||||||
|
when(renderer.prepareTile(anyDouble(), anyDouble(), anyDouble(), anyInt(), any(), anyBoolean())).thenReturn(work);
|
||||||
|
return renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MapTileRenderer.TileRender finished(MapTile tile) {
|
||||||
|
MapTileRenderer.TileRender work = mock(MapTileRenderer.TileRender.class);
|
||||||
|
when(work.render(any())).thenReturn(tile);
|
||||||
|
when(work.complete()).thenReturn(true);
|
||||||
|
return work;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static VisionRenderController.RenderSpec spec(MapTileRenderer renderer, long revision) {
|
private static VisionRenderController.RenderSpec spec(MapTileRenderer renderer, long revision) {
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package art.arcane.iris.studio.render;
|
||||||
|
|
||||||
|
import art.arcane.iris.generation.hydrology.engine.HydrologyPlanPreview;
|
||||||
|
import art.arcane.iris.generation.hydrology.engine.HydrologyRuntime;
|
||||||
|
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
|
||||||
|
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
|
||||||
|
import art.arcane.iris.generation.hydrology.raster.WaterRole;
|
||||||
|
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.concurrent.CancellationException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertThrows;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyInt;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verifyNoInteractions;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
public class WaterMapProgressTest {
|
||||||
|
@Test
|
||||||
|
public void completeDetailedRastersDoNotDrawPlanFallbacks() {
|
||||||
|
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||||
|
Reach reach = mock(Reach.class);
|
||||||
|
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt()))
|
||||||
|
.thenReturn(new HydrologyPlanPreview(List.of(reach), List.of(), List.of(), List.of(), true));
|
||||||
|
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(ChunkWaterRaster.EMPTY);
|
||||||
|
|
||||||
|
WaterMap map = WaterMap.draw(runtime, 32, 0D, 0D, 1D, () -> false);
|
||||||
|
|
||||||
|
assertFalse(map.provisional());
|
||||||
|
verifyNoInteractions(reach);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void completedPixelsSurviveRuntimeRasterEviction() {
|
||||||
|
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||||
|
ChunkWaterRaster pond = pond();
|
||||||
|
AtomicBoolean pending = new AtomicBoolean(true);
|
||||||
|
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(HydrologyPlanPreview.EMPTY);
|
||||||
|
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(ChunkWaterRaster.EMPTY);
|
||||||
|
when(runtime.rasterPreview(0, 0)).thenReturn(pond, ChunkWaterRaster.PROVISIONAL);
|
||||||
|
when(runtime.rasterPreview(1, 0)).thenAnswer(invocation -> pending.get() ? ChunkWaterRaster.PROVISIONAL : pond);
|
||||||
|
WaterMap map = new WaterMap(runtime, 32, 0D, 0D, 1D);
|
||||||
|
|
||||||
|
map.advance(() -> false);
|
||||||
|
assertTrue(map.provisional());
|
||||||
|
assertEquals(HydrologyFeatureType.POND, map.at(1, 1));
|
||||||
|
assertNull(map.at(17, 1));
|
||||||
|
pending.set(false);
|
||||||
|
map.advance(() -> false);
|
||||||
|
|
||||||
|
assertFalse(map.provisional());
|
||||||
|
assertEquals(HydrologyFeatureType.POND, map.at(1, 1));
|
||||||
|
assertEquals(HydrologyFeatureType.POND, map.at(17, 1));
|
||||||
|
assertNull(map.at(1, 17));
|
||||||
|
assertTrue(map.bytes() < 16_384L);
|
||||||
|
verify(runtime, times(1)).rasterPreview(0, 0);
|
||||||
|
verify(runtime, times(2)).rasterPreview(1, 0);
|
||||||
|
verify(runtime, times(1)).planPreview(anyInt(), anyInt(), anyInt(), anyInt());
|
||||||
|
|
||||||
|
map.advance(() -> false);
|
||||||
|
verify(runtime, times(2)).rasterPreview(1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cancelledWaterSamplingResumesToTheSameFinalPixels() {
|
||||||
|
HydrologyRuntime runtime = mock(HydrologyRuntime.class);
|
||||||
|
ChunkWaterRaster raster = pond();
|
||||||
|
when(runtime.planPreview(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(HydrologyPlanPreview.EMPTY);
|
||||||
|
when(runtime.rasterPreview(anyInt(), anyInt())).thenReturn(raster);
|
||||||
|
AtomicInteger rows = new AtomicInteger();
|
||||||
|
WaterMap map = new WaterMap(runtime, 32, -16.5D, -16.5D, 0.5D);
|
||||||
|
|
||||||
|
assertThrows(CancellationException.class, () -> map.advance(() -> rows.incrementAndGet() > 2));
|
||||||
|
assertTrue(map.provisional());
|
||||||
|
map.advance(() -> false);
|
||||||
|
assertFalse(map.provisional());
|
||||||
|
WaterMap expected = WaterMap.draw(runtime, 32, -16.5D, -16.5D, 0.5D, () -> false);
|
||||||
|
for (int z = 0; z < 32; z++) {
|
||||||
|
for (int x = 0; x < 32; x++) {
|
||||||
|
assertEquals(expected.at(x, z), map.at(x, z));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChunkWaterRaster pond() {
|
||||||
|
ChunkWaterRaster raster = mock(ChunkWaterRaster.class);
|
||||||
|
when(raster.role(anyInt(), anyInt())).thenReturn(WaterRole.POND);
|
||||||
|
return raster;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,10 @@ Configuration kernelDependencies = configurations.create('generationKernelDepend
|
|||||||
extendsFrom(configurations.implementation, configurations.slim, configurations.annotationProcessor)
|
extendsFrom(configurations.implementation, configurations.slim, configurations.annotationProcessor)
|
||||||
}
|
}
|
||||||
dependencies.add(kernelDependencies.name, libs.guava)
|
dependencies.add(kernelDependencies.name, libs.guava)
|
||||||
|
dependencies.add(kernelDependencies.name, libs.lwjgl)
|
||||||
|
dependencies.add(kernelDependencies.name, libs.lwjgl.opengl)
|
||||||
|
dependencies.add(kernelDependencies.name, libs.lwjgl.egl)
|
||||||
|
dependencies.add(kernelDependencies.name, libs.lwjgl.jawt)
|
||||||
List<String> nativeRevisionModules = ['native-common', 'native-minecraft26_2-sources']
|
List<String> nativeRevisionModules = ['native-common', 'native-minecraft26_2-sources']
|
||||||
for (String adapter : rootProject.extensions.extraProperties.get('nativeDevelopmentAdapters')) {
|
for (String adapter : rootProject.extensions.extraProperties.get('nativeDevelopmentAdapters')) {
|
||||||
nativeRevisionModules.add('native-' + adapter)
|
nativeRevisionModules.add('native-' + adapter)
|
||||||
|
|||||||
@@ -40,6 +40,12 @@ dependencies {
|
|||||||
runtimeOnly(libs.oshi)
|
runtimeOnly(libs.oshi)
|
||||||
runtimeOnly(libs.byteBuddy.core)
|
runtimeOnly(libs.byteBuddy.core)
|
||||||
runtimeOnly(libs.byteBuddy.agent)
|
runtimeOnly(libs.byteBuddy.agent)
|
||||||
|
runtimeOnly(libs.lwjgl)
|
||||||
|
runtimeOnly(libs.lwjgl.opengl)
|
||||||
|
runtimeOnly(libs.lwjgl.egl)
|
||||||
|
runtimeOnly(libs.lwjgl.jawt)
|
||||||
|
runtimeOnly(variantOf(libs.lwjgl) { classifier('natives-linux') })
|
||||||
|
runtimeOnly(variantOf(libs.lwjgl.opengl) { classifier('natives-linux') })
|
||||||
studioProbeApi(libs.spigot) {
|
studioProbeApi(libs.spigot) {
|
||||||
transitive = false
|
transitive = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user