diff --git a/core/build.gradle b/core/build.gradle index 0ea5d3099..79da097c7 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,6 +1,8 @@ import io.github.slimjar.resolver.data.Mirror import org.ajoberstar.grgit.Grgit +import org.gradle.api.Task import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.compile.JavaCompile import org.gradle.jvm.tasks.Jar import org.gradle.jvm.toolchain.JavaLanguageVersion @@ -228,6 +230,28 @@ tasks.named('processResources').configure { } } +def runningTestTasks = gradle.startParameter.taskNames.any { String taskName -> taskName.toLowerCase().contains('test') } +if (runningTestTasks) { + TaskProvider processResourcesTask = tasks.named('processResources') + tasks.named('classes').configure { Task classesTask -> + Set dependencies = new LinkedHashSet(classesTask.getDependsOn()) + dependencies.removeIf { Object dependency -> + if (dependency instanceof TaskProvider) { + return ((TaskProvider) dependency).name == processResourcesTask.name + } + if (dependency instanceof Task) { + return ((Task) dependency).name == processResourcesTask.name + } + String dependencyName = String.valueOf(dependency) + return dependencyName == 'processResources' || dependencyName.endsWith(':processResources') + } + classesTask.setDependsOn(dependencies) + } + processResourcesTask.configure { Task task -> + task.enabled = false + } +} + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).configure { dependsOn(embeddedAgentJar) mergeServiceFiles() diff --git a/core/src/main/java/art/arcane/iris/core/IrisHotPathMetricsMode.java b/core/src/main/java/art/arcane/iris/core/IrisHotPathMetricsMode.java new file mode 100644 index 000000000..f0d409175 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisHotPathMetricsMode.java @@ -0,0 +1,7 @@ +package art.arcane.iris.core; + +public enum IrisHotPathMetricsMode { + SAMPLED, + EXACT, + DISABLED +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisPaperLikeBackendMode.java b/core/src/main/java/art/arcane/iris/core/IrisPaperLikeBackendMode.java new file mode 100644 index 000000000..9b068a863 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisPaperLikeBackendMode.java @@ -0,0 +1,7 @@ +package art.arcane.iris.core; + +public enum IrisPaperLikeBackendMode { + AUTO, + TICKET, + SERVICE +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisRuntimeSchedulerMode.java b/core/src/main/java/art/arcane/iris/core/IrisRuntimeSchedulerMode.java new file mode 100644 index 000000000..bc026f6f9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/IrisRuntimeSchedulerMode.java @@ -0,0 +1,70 @@ +package art.arcane.iris.core; + +import art.arcane.volmlib.util.scheduling.FoliaScheduler; +import org.bukkit.Bukkit; +import org.bukkit.Server; + +import java.util.Locale; + +public enum IrisRuntimeSchedulerMode { + AUTO, + PAPER_LIKE, + FOLIA; + + public static IrisRuntimeSchedulerMode resolve(IrisSettings.IrisSettingsPregen pregen) { + Server server = Bukkit.getServer(); + boolean regionizedRuntime = FoliaScheduler.isRegionizedRuntime(server); + if (regionizedRuntime) { + return FOLIA; + } + + IrisRuntimeSchedulerMode configuredMode = pregen == null ? null : pregen.getRuntimeSchedulerMode(); + if (configuredMode != null && configuredMode != AUTO) { + if (configuredMode == FOLIA) { + return PAPER_LIKE; + } + return configuredMode; + } + + String bukkitName = Bukkit.getName(); + String bukkitVersion = Bukkit.getVersion(); + String serverClassName = server == null ? "" : server.getClass().getName(); + if (containsIgnoreCase(bukkitName, "folia") + || containsIgnoreCase(bukkitVersion, "folia") + || containsIgnoreCase(serverClassName, "folia")) { + return FOLIA; + } + + if (containsIgnoreCase(bukkitName, "purpur") + || containsIgnoreCase(bukkitVersion, "purpur") + || containsIgnoreCase(serverClassName, "purpur") + || containsIgnoreCase(bukkitName, "paper") + || containsIgnoreCase(bukkitVersion, "paper") + || containsIgnoreCase(serverClassName, "paper") + || containsIgnoreCase(bukkitName, "pufferfish") + || containsIgnoreCase(bukkitVersion, "pufferfish") + || containsIgnoreCase(serverClassName, "pufferfish") + || containsIgnoreCase(bukkitName, "spigot") + || containsIgnoreCase(bukkitVersion, "spigot") + || containsIgnoreCase(serverClassName, "spigot") + || containsIgnoreCase(bukkitName, "craftbukkit") + || containsIgnoreCase(bukkitVersion, "craftbukkit") + || containsIgnoreCase(serverClassName, "craftbukkit")) { + return PAPER_LIKE; + } + + if (regionizedRuntime) { + return FOLIA; + } + + return PAPER_LIKE; + } + + private static boolean containsIgnoreCase(String value, String contains) { + if (value == null || contains == null || contains.isEmpty()) { + return false; + } + + return value.toLowerCase(Locale.ROOT).contains(contains.toLowerCase(Locale.ROOT)); + } +} diff --git a/core/src/main/java/art/arcane/iris/core/IrisSettings.java b/core/src/main/java/art/arcane/iris/core/IrisSettings.java index a37322bdf..14cc02cb9 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -151,9 +151,16 @@ public class IrisSettings { public boolean useHighPriority = false; public boolean useVirtualThreads = false; public boolean useTicketQueue = true; + public IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO; + public IrisPaperLikeBackendMode paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO; + public IrisHotPathMetricsMode hotPathMetricsMode = IrisHotPathMetricsMode.SAMPLED; + public int hotPathMetricsSampleStride = 1024; public int maxConcurrency = 256; + public int paperLikeMaxConcurrency = 96; + public int foliaMaxConcurrency = 32; public int chunkLoadTimeoutSeconds = 15; public int timeoutWarnIntervalMs = 500; + public int saveIntervalMs = 120_000; public boolean startupNoisemapPrebake = true; public boolean enablePregenPerformanceProfile = true; public int pregenProfileNoiseCacheSize = 4_096; @@ -167,6 +174,40 @@ public class IrisSettings { public int getTimeoutWarnIntervalMs() { return Math.max(timeoutWarnIntervalMs, 250); } + + public int getPaperLikeMaxConcurrency() { + return Math.max(1, paperLikeMaxConcurrency); + } + + public int getFoliaMaxConcurrency() { + return Math.max(1, foliaMaxConcurrency); + } + + public IrisPaperLikeBackendMode getPaperLikeBackendMode() { + if (paperLikeBackendMode == null) { + return IrisPaperLikeBackendMode.AUTO; + } + + return paperLikeBackendMode; + } + + public IrisHotPathMetricsMode getHotPathMetricsMode() { + if (hotPathMetricsMode == null) { + return IrisHotPathMetricsMode.SAMPLED; + } + + return hotPathMetricsMode; + } + + public int getHotPathMetricsSampleStride() { + int stride = Math.max(1, Math.min(hotPathMetricsSampleStride, 65_536)); + int normalized = Integer.highestOneBit(stride); + return normalized <= 0 ? 1 : normalized; + } + + public int getSaveIntervalMs() { + return Math.max(5_000, Math.min(saveIntervalMs, 900_000)); + } } @Data diff --git a/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java b/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java index aaaa839b5..931e2c16f 100644 --- a/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java +++ b/core/src/main/java/art/arcane/iris/core/link/FoliaWorldsLink.java @@ -116,6 +116,10 @@ public class FoliaWorldsLink { } public boolean isActive() { + if (!J.isFolia()) { + return false; + } + return isWorldsProviderActive() || isPaperWorldLoaderActive(); } diff --git a/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java index 6df4831ee..d032f1e07 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ImageResourceLoader.java @@ -29,6 +29,8 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; import java.io.File; +import java.io.IOException; +import java.util.HashSet; import java.util.Set; public class ImageResourceLoader extends ResourceLoader { @@ -67,12 +69,28 @@ public class ImageResourceLoader extends ResourceLoader { } } - void getPNGFiles(File directory, Set m) { - for (File file : directory.listFiles()) { + void getPNGFiles(File directory, Set m, HashSet visitedDirectories) { + if (directory == null || !directory.exists()) { + return; + } + + if (directory.isDirectory()) { + String canonicalDirectory = toCanonicalPath(directory); + if (canonicalDirectory != null && !visitedDirectories.add(canonicalDirectory)) { + return; + } + } + + File[] listedFiles = directory.listFiles(); + if (listedFiles == null) { + return; + } + + for (File file : listedFiles) { if (file.isFile() && file.getName().endsWith(".png")) { m.add(file.getName().replaceAll("\\Q.png\\E", "")); } else if (file.isDirectory()) { - getPNGFiles(file, m); + getPNGFiles(file, m, visitedDirectories); } } } @@ -85,10 +103,11 @@ public class ImageResourceLoader extends ResourceLoader { Iris.debug("Building " + resourceTypeName + " Possibility Lists"); KSet m = new KSet<>(); + HashSet visitedDirectories = new HashSet<>(); for (File i : getFolders()) { - getPNGFiles(i, m); + getPNGFiles(i, m, visitedDirectories); } // for (File i : getFolders()) { @@ -116,6 +135,14 @@ public class ImageResourceLoader extends ResourceLoader { return possibleKeys; } + private String toCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException ignored) { + return null; + } + } + public File findFile(String name) { for (File i : getFolders(name)) { for (File j : i.listFiles()) { diff --git a/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java index cae771600..57912c71b 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/MatterObjectResourceLoader.java @@ -27,6 +27,8 @@ import art.arcane.volmlib.util.data.KCache; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.io.File; +import java.io.IOException; +import java.util.HashSet; public class MatterObjectResourceLoader extends ResourceLoader { private String[] possibleKeys; @@ -65,12 +67,28 @@ public class MatterObjectResourceLoader extends ResourceLoader } } - private void findMatFiles(File dir, KSet m) { - for (File file : dir.listFiles()) { + private void findMatFiles(File dir, KSet m, HashSet visitedDirectories) { + if (dir == null || !dir.exists()) { + return; + } + + if (dir.isDirectory()) { + String canonicalDirectory = toCanonicalPath(dir); + if (canonicalDirectory != null && !visitedDirectories.add(canonicalDirectory)) { + return; + } + } + + File[] listedFiles = dir.listFiles(); + if (listedFiles == null) { + return; + } + + for (File file : listedFiles) { if (file.isFile() && file.getName().endsWith(".mat")) { m.add(file.getName().replaceAll("\\Q.mat\\E", "")); } else if (file.isDirectory()) { - findMatFiles(file, m); + findMatFiles(file, m, visitedDirectories); } } } @@ -82,9 +100,10 @@ public class MatterObjectResourceLoader extends ResourceLoader Iris.debug("Building " + resourceTypeName + " Possibility Lists"); KSet m = new KSet<>(); + HashSet visitedDirectories = new HashSet<>(); for (File folder : getFolders()) { - findMatFiles(folder, m); + findMatFiles(folder, m, visitedDirectories); } KList v = new KList<>(m); @@ -92,6 +111,14 @@ public class MatterObjectResourceLoader extends ResourceLoader return possibleKeys; } + private String toCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException ignored) { + return null; + } + } + // public String[] getPossibleKeys() { // if (possibleKeys != null) { diff --git a/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java index a986f9c94..65b6542e1 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ObjectResourceLoader.java @@ -27,6 +27,8 @@ import art.arcane.volmlib.util.data.KCache; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.io.File; +import java.io.IOException; +import java.util.HashSet; public class ObjectResourceLoader extends ResourceLoader { public ObjectResourceLoader(File root, IrisData idm, String folderName, String resourceTypeName) { @@ -75,26 +77,51 @@ public class ObjectResourceLoader extends ResourceLoader { } Iris.debug("Building " + resourceTypeName + " Possibility Lists"); KSet m = new KSet<>(); + HashSet visitedDirectories = new HashSet<>(); for (File i : getFolders()) { - m.addAll(getFiles(i, ".iob", true)); + m.addAll(getFiles(i, ".iob", true, visitedDirectories)); } possibleKeys = m.toArray(new String[0]); return possibleKeys; } - private KList getFiles(File dir, String ext, boolean skipDirName) { + private KList getFiles(File dir, String ext, boolean skipDirName, HashSet visitedDirectories) { KList paths = new KList<>(); + if (dir == null || !dir.exists()) { + return paths; + } + + if (dir.isDirectory()) { + String canonicalDirectory = toCanonicalPath(dir); + if (canonicalDirectory != null && !visitedDirectories.add(canonicalDirectory)) { + return paths; + } + } + + File[] listedFiles = dir.listFiles(); + if (listedFiles == null) { + return paths; + } + String name = skipDirName ? "" : dir.getName() + "/"; - for (File f : dir.listFiles()) { + for (File f : listedFiles) { if (f.isFile() && f.getName().endsWith(ext)) { paths.add(name + f.getName().replaceAll("\\Q" + ext + "\\E", "")); } else if (f.isDirectory()) { - getFiles(f, ext, false).forEach(e -> paths.add(name + e)); + getFiles(f, ext, false, visitedDirectories).forEach(e -> paths.add(name + e)); } } return paths; } + private String toCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException ignored) { + return null; + } + } + public File findFile(String name) { for (File i : getFolders(name)) { for (File j : i.listFiles()) { diff --git a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java index f3897033a..62a6cf407 100644 --- a/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java +++ b/core/src/main/java/art/arcane/iris/core/loader/ResourceLoader.java @@ -48,6 +48,10 @@ import java.io.*; import java.util.Arrays; import java.util.HashSet; import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Predicate; @@ -61,6 +65,13 @@ import java.util.zip.GZIPOutputStream; public class ResourceLoader implements MeteredCache { public static final AtomicDouble tlt = new AtomicDouble(0); private static final int CACHE_SIZE = 100000; + private static final ExecutorService schemaBuildExecutor = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "Iris-Schema-Builder"); + thread.setDaemon(true); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + private static final Set schemaBuildQueue = ConcurrentHashMap.newKeySet(); protected final AtomicCache> folderCache; protected KSet firstAccess; protected File root; @@ -102,7 +113,18 @@ public class ResourceLoader implements MeteredCache { o.put("fileMatch", new JSONArray(fm.toArray())); o.put("url", "./.iris/schema/" + getFolderName() + "-schema.json"); File a = new File(getManager().getDataFolder(), ".iris/schema/" + getFolderName() + "-schema.json"); - J.attemptAsync(() -> IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4))); + String schemaPath = a.getAbsolutePath(); + if (!a.exists() && schemaBuildQueue.add(schemaPath)) { + schemaBuildExecutor.execute(() -> { + try { + IO.writeAll(a, new SchemaBuilder(objectClass, manager).construct().toString(4)); + } catch (Throwable e) { + Iris.reportError(e); + } finally { + schemaBuildQueue.remove(schemaPath); + } + }); + } return o; } @@ -149,20 +171,44 @@ public class ResourceLoader implements MeteredCache { } private KList matchAllFiles(File root, Predicate f) { - KList fx = new KList<>(); - matchFiles(root, fx, f); - return fx; + KList files = new KList<>(); + HashSet visitedDirectories = new HashSet<>(); + matchFiles(root, files, f, visitedDirectories); + return files; } - private void matchFiles(File at, KList files, Predicate f) { + private void matchFiles(File at, KList files, Predicate f, HashSet visitedDirectories) { + if (at == null || !at.exists()) { + return; + } + if (at.isDirectory()) { - for (File i : at.listFiles()) { - matchFiles(i, files, f); + String canonicalPath = toCanonicalPath(at); + if (canonicalPath != null && !visitedDirectories.add(canonicalPath)) { + return; } - } else { - if (f.test(at)) { - files.add(at); + + File[] listedFiles = at.listFiles(); + if (listedFiles == null) { + return; } + + for (File listedFile : listedFiles) { + matchFiles(listedFile, files, f, visitedDirectories); + } + return; + } + + if (f.test(at)) { + files.add(at); + } + } + + private String toCanonicalPath(File file) { + try { + return file.getCanonicalPath(); + } catch (IOException ignored) { + return null; } } diff --git a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java index 51694f0ca..35a2f11af 100644 --- a/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java +++ b/core/src/main/java/art/arcane/iris/core/nms/INMSBinding.java @@ -24,6 +24,7 @@ import art.arcane.iris.core.nms.container.BiomeColor; import art.arcane.iris.core.nms.container.BlockProperty; import art.arcane.iris.core.nms.container.StructurePlacement; import art.arcane.iris.core.nms.datapack.DataVersion; +import art.arcane.iris.util.common.scheduling.J; import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.collection.KList; @@ -108,11 +109,13 @@ public interface INMSBinding { return CompletableFuture.failedFuture(new IllegalStateException("Missing dimension types to create world")); } - FoliaWorldsLink link = FoliaWorldsLink.get(); - if (link.isActive()) { - CompletableFuture future = link.createWorld(c); - if (future != null) { - return future; + if (J.isFolia()) { + FoliaWorldsLink link = FoliaWorldsLink.get(); + if (link.isActive()) { + CompletableFuture future = link.createWorld(c); + if (future != null) { + return future; + } } } return CompletableFuture.completedFuture(createWorld(c)); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java index ed52b2f25..bb3c009a6 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/IrisPregenerator.java @@ -19,6 +19,7 @@ package art.arcane.iris.core.pregenerator; import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.tools.IrisPackBenchmarking; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KSet; @@ -66,13 +67,14 @@ public class IrisPregenerator { private final KSet retry; private final KSet net; private final ChronoLatch cl; - private final ChronoLatch saveLatch = new ChronoLatch(30000); + private final ChronoLatch saveLatch; private final IrisPackBenchmarking benchmarking; public IrisPregenerator(PregenTask task, PregeneratorMethod generator, PregenListener listener) { benchmarking = IrisPackBenchmarking.getInstance(); this.listener = listenify(listener); cl = new ChronoLatch(5000); + saveLatch = new ChronoLatch(IrisSettings.get().getPregen().getSaveIntervalMs()); generatedRegions = new KSet<>(); this.shutdown = new AtomicBoolean(false); this.paused = new AtomicBoolean(false); diff --git a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java index 4a045396d..5880a9969 100644 --- a/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java +++ b/core/src/main/java/art/arcane/iris/core/pregenerator/methods/AsyncPregenMethod.java @@ -19,10 +19,13 @@ package art.arcane.iris.core.pregenerator.methods; import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisPaperLikeBackendMode; +import art.arcane.iris.core.IrisRuntimeSchedulerMode; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.pregenerator.PregenListener; import art.arcane.iris.core.pregenerator.PregeneratorMethod; import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.framework.Engine; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.math.M; @@ -33,6 +36,7 @@ import org.bukkit.Chunk; import org.bukkit.World; import java.lang.reflect.InvocationTargetException; +import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; @@ -44,11 +48,13 @@ import java.util.concurrent.atomic.AtomicLong; public class AsyncPregenMethod implements PregeneratorMethod { private static final AtomicInteger THREAD_COUNT = new AtomicInteger(); - private static final int FOLIA_MAX_CONCURRENCY = 32; - private static final int NON_FOLIA_MAX_CONCURRENCY = 96; - private static final int NON_FOLIA_CONCURRENCY_FACTOR = 2; private static final int ADAPTIVE_TIMEOUT_STEP = 3; private final World world; + private final IrisRuntimeSchedulerMode runtimeSchedulerMode; + private final IrisPaperLikeBackendMode paperLikeBackendMode; + private final boolean foliaRuntime; + private final String backendMode; + private final int workerPoolThreads; private final Executor executor; private final Semaphore semaphore; private final int threads; @@ -68,6 +74,8 @@ public class AsyncPregenMethod implements PregeneratorMethod { private final AtomicLong failed = new AtomicLong(); private final AtomicLong lastProgressAt = new AtomicLong(M.ms()); private final AtomicLong lastPermitWaitLog = new AtomicLong(0L); + private final Object permitMonitor = new Object(); + private volatile Engine metricsEngine; public AsyncPregenMethod(World world, int unusedThreads) { if (!PaperLib.isPaper()) { @@ -75,20 +83,31 @@ public class AsyncPregenMethod implements PregeneratorMethod { } this.world = world; - if (J.isFolia()) { + IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); + this.runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(pregen); + this.foliaRuntime = runtimeSchedulerMode == IrisRuntimeSchedulerMode.FOLIA; + if (foliaRuntime) { + this.paperLikeBackendMode = IrisPaperLikeBackendMode.AUTO; + this.backendMode = "folia-region"; this.executor = new FoliaRegionExecutor(); } else { - boolean useTicketQueue = IrisSettings.get().getPregen().isUseTicketQueue(); - this.executor = useTicketQueue ? new TicketExecutor() : new ServiceExecutor(); + this.paperLikeBackendMode = resolvePaperLikeBackendMode(pregen); + if (paperLikeBackendMode == IrisPaperLikeBackendMode.SERVICE) { + this.executor = new ServiceExecutor(); + this.backendMode = "paper-service"; + } else { + this.executor = new TicketExecutor(); + this.backendMode = "paper-ticket"; + } } - IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); int configuredThreads = pregen.getMaxConcurrency(); - if (J.isFolia()) { - configuredThreads = Math.min(configuredThreads, FOLIA_MAX_CONCURRENCY); + if (foliaRuntime) { + configuredThreads = Math.min(configuredThreads, pregen.getFoliaMaxConcurrency()); } else { - configuredThreads = Math.min(configuredThreads, resolveNonFoliaConcurrencyCap()); + configuredThreads = Math.min(configuredThreads, resolvePaperLikeConcurrencyCap(pregen.getPaperLikeMaxConcurrency())); } this.threads = Math.max(1, configuredThreads); + this.workerPoolThreads = resolveWorkerPoolThreads(); this.semaphore = new Semaphore(this.threads, true); this.timeoutSeconds = pregen.getChunkLoadTimeoutSeconds(); this.timeoutWarnIntervalMs = pregen.getTimeoutWarnIntervalMs(); @@ -98,8 +117,32 @@ public class AsyncPregenMethod implements PregeneratorMethod { this.adaptiveMinInFlightLimit = Math.max(4, Math.min(16, Math.max(1, this.threads / 4))); } + private IrisPaperLikeBackendMode resolvePaperLikeBackendMode(IrisSettings.IrisSettingsPregen pregen) { + IrisPaperLikeBackendMode configuredMode = pregen.getPaperLikeBackendMode(); + if (configuredMode != IrisPaperLikeBackendMode.AUTO) { + return configuredMode; + } + + return pregen.isUseVirtualThreads() ? IrisPaperLikeBackendMode.SERVICE : IrisPaperLikeBackendMode.TICKET; + } + + private int resolveWorkerPoolThreads() { + try { + Class moonriseCommonClass = Class.forName("ca.spottedleaf.moonrise.common.util.MoonriseCommon"); + java.lang.reflect.Field workerPoolField = moonriseCommonClass.getDeclaredField("WORKER_POOL"); + Object workerPool = workerPoolField.get(null); + Object coreThreads = workerPool.getClass().getDeclaredMethod("getCoreThreads").invoke(workerPool); + if (coreThreads instanceof Thread[] threadsArray) { + return threadsArray.length; + } + } catch (Throwable ignored) { + } + + return -1; + } + private void unloadAndSaveAllChunks() { - if (J.isFolia()) { + if (foliaRuntime) { // Folia requires world/chunk mutations to be region-owned; periodic global unload/save is unsafe. lastUse.clear(); return; @@ -190,6 +233,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { int next = Math.max(adaptiveMinInFlightLimit, current - 1); if (adaptiveInFlightLimit.compareAndSet(current, next)) { logAdaptiveLimit("decrease", next); + notifyPermitWaiters(); return; } } @@ -205,6 +249,7 @@ public class AsyncPregenMethod implements PregeneratorMethod { int next = Math.min(threads, current + 1); if (adaptiveInFlightLimit.compareAndSet(current, next)) { logAdaptiveLimit("increase", next); + notifyPermitWaiters(); return; } } @@ -222,11 +267,8 @@ public class AsyncPregenMethod implements PregeneratorMethod { } } - private int resolveNonFoliaConcurrencyCap() { - int worldGenThreads = Math.max(1, IrisSettings.get().getConcurrency().getWorldGenThreads()); - int recommended = worldGenThreads * NON_FOLIA_CONCURRENCY_FACTOR; - int bounded = Math.max(8, Math.min(NON_FOLIA_MAX_CONCURRENCY, recommended)); - return bounded; + private int resolvePaperLikeConcurrencyCap(int configuredCap) { + return Math.max(8, configuredCap); } private String metricsSnapshot() { @@ -259,6 +301,48 @@ public class AsyncPregenMethod implements PregeneratorMethod { if (after < 0) { inFlight.compareAndSet(after, 0); } + notifyPermitWaiters(); + } + + private void notifyPermitWaiters() { + synchronized (permitMonitor) { + permitMonitor.notifyAll(); + } + } + + private void recordAdaptiveWait(long waitedMs) { + Engine engine = resolveMetricsEngine(); + if (engine != null) { + engine.getMetrics().getPregenWaitAdaptive().put(waitedMs); + } + } + + private void recordPermitWait(long waitedMs) { + Engine engine = resolveMetricsEngine(); + if (engine != null) { + engine.getMetrics().getPregenWaitPermit().put(waitedMs); + } + } + + private Engine resolveMetricsEngine() { + Engine cachedEngine = metricsEngine; + if (cachedEngine != null) { + return cachedEngine; + } + + if (!IrisToolbelt.isIrisWorld(world)) { + return null; + } + + try { + Engine resolvedEngine = IrisToolbelt.access(world).getEngine(); + if (resolvedEngine != null) { + metricsEngine = resolvedEngine; + } + return resolvedEngine; + } catch (Throwable ignored) { + return null; + } } private void logPermitWaitIfNeeded(int x, int z, long waitedMs) { @@ -276,9 +360,11 @@ public class AsyncPregenMethod implements PregeneratorMethod { @Override public void init() { Iris.info("Async pregen init: world=" + world.getName() - + ", mode=" + (J.isFolia() ? "folia" : "paper") + + ", mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT) + + ", backend=" + backendMode + ", threads=" + threads + ", adaptiveLimit=" + adaptiveInFlightLimit.get() + + ", workerPoolThreads=" + workerPoolThreads + ", urgent=" + urgent + ", timeout=" + timeoutSeconds + "s"); unloadAndSaveAllChunks(); @@ -318,17 +404,26 @@ public class AsyncPregenMethod implements PregeneratorMethod { listener.onChunkGenerating(x, z); try { long waitStart = M.ms(); - while (inFlight.get() >= adaptiveInFlightLimit.get()) { - long waited = Math.max(0L, M.ms() - waitStart); - logPermitWaitIfNeeded(x, z, waited); - if (!J.sleep(5)) { - return; + synchronized (permitMonitor) { + while (inFlight.get() >= adaptiveInFlightLimit.get()) { + long waited = Math.max(0L, M.ms() - waitStart); + logPermitWaitIfNeeded(x, z, waited); + permitMonitor.wait(5000L); } } + long adaptiveWait = Math.max(0L, M.ms() - waitStart); + if (adaptiveWait > 0L) { + recordAdaptiveWait(adaptiveWait); + } + long permitWaitStart = M.ms(); while (!semaphore.tryAcquire(5, TimeUnit.SECONDS)) { logPermitWaitIfNeeded(x, z, Math.max(0L, M.ms() - waitStart)); } + long permitWait = Math.max(0L, M.ms() - permitWaitStart); + if (permitWait > 0L) { + recordPermitWait(permitWait); + } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; diff --git a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java index 4cd283992..9ef11c3e5 100644 --- a/core/src/main/java/art/arcane/iris/core/project/IrisProject.java +++ b/core/src/main/java/art/arcane/iris/core/project/IrisProject.java @@ -33,6 +33,7 @@ import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.collection.KMap; import art.arcane.volmlib.util.collection.KSet; import art.arcane.volmlib.util.exceptions.IrisException; +import art.arcane.iris.util.common.format.C; import art.arcane.volmlib.util.format.Form; import art.arcane.volmlib.util.io.IO; import art.arcane.volmlib.util.json.JSONArray; @@ -61,6 +62,8 @@ import java.util.Objects; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; @SuppressWarnings("ALL") @@ -165,6 +168,11 @@ public class IrisProject { J.attemptAsync(() -> { try { + if (d == null) { + sender.sendMessage("Could not load dimension \"" + getName() + "\""); + return; + } + if (d.getLoader() == null) { sender.sendMessage("Could not get dimension loader"); return; @@ -176,12 +184,11 @@ public class IrisProject { Iris.warn("Project missing code-workspace: " + ff.getAbsolutePath() + " Re-creating code workspace."); try { - IO.writeAll(ff, createCodeWorkspaceConfig()); + IO.writeAll(ff, createCodeWorkspaceConfig(false)); } catch (IOException e1) { Iris.reportError(e1); e1.printStackTrace(); } - updateWorkspace(); if (!doOpenVSCode(f)) { Iris.warn("Tried creating code workspace but failed a second time. Your project is likely corrupt."); } @@ -198,16 +205,20 @@ public class IrisProject { for (File i : Objects.requireNonNull(f.listFiles())) { if (i.getName().endsWith(".code-workspace")) { foundWork = true; - J.a(() -> - { - updateWorkspace(); - }); if (IrisSettings.get().getStudio().isOpenVSCode()) { if (!GraphicsEnvironment.isHeadless()) { Iris.msg("Opening VSCode. You may see the output from VSCode."); Iris.msg("VSCode output always starts with: '(node:#####) electron'"); - Desktop.getDesktop().open(i); + Thread launcherThread = new Thread(() -> { + try { + Desktop.getDesktop().open(i); + } catch (Throwable e) { + Iris.reportError(e); + } + }, "Iris-VSCode-Launcher"); + launcherThread.setDaemon(true); + launcherThread.start(); } } @@ -222,30 +233,121 @@ public class IrisProject { close(); } - J.a(() -> { - IrisDimension d = IrisData.loadAnyDimension(getName(), null); - if (d == null) { - sender.sendMessage("Can't find dimension: " + getName()); - return; - } else if (sender.isPlayer()) { - J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.SPECTATOR)); - } + AtomicReference stage = new AtomicReference<>("Queued"); + AtomicReference progress = new AtomicReference<>(0.01D); + AtomicBoolean complete = new AtomicBoolean(false); + AtomicBoolean failed = new AtomicBoolean(false); + startStudioOpenReporter(sender, stage, progress, complete, failed); + J.a(() -> { + World maintenanceWorld = null; + boolean maintenanceActive = false; try { + stage.set("Loading dimension"); + progress.set(0.05D); + IrisDimension d = IrisData.loadAnyDimension(getName(), null); + if (d == null) { + failed.set(true); + sender.sendMessage(C.RED + "Can't find dimension: " + getName()); + return; + } else if (sender.isPlayer()) { + J.runEntity(sender.player(), () -> sender.player().setGameMode(GameMode.SPECTATOR)); + } + + stage.set("Creating world"); + progress.set(0.12D); activeProvider = (PlatformChunkGenerator) IrisToolbelt.createWorld() .seed(seed) .sender(sender) .studio(true) .name("iris-" + UUID.randomUUID()) .dimension(d.getLoadKey()) + .studioProgressConsumer((value, currentStage) -> { + if (currentStage != null && !currentStage.isBlank()) { + stage.set(currentStage); + } + progress.set(Math.max(0D, Math.min(0.99D, value))); + }) .create().getGenerator(); - onDone.accept(activeProvider.getTarget().getWorld().realWorld()); + + if (activeProvider != null) { + maintenanceWorld = activeProvider.getTarget().getWorld().realWorld(); + if (maintenanceWorld != null) { + IrisToolbelt.beginWorldMaintenance(maintenanceWorld, "studio-open"); + maintenanceActive = true; + } + onDone.accept(maintenanceWorld); + } } catch (IrisException e) { - e.printStackTrace(); + failed.set(true); + Iris.reportError(e); + sender.sendMessage(C.RED + "Failed to open studio world: " + e.getMessage()); + } catch (Throwable e) { + failed.set(true); + Iris.reportError(e); + sender.sendMessage(C.RED + "Studio open failed: " + e.getMessage()); + } finally { + if (activeProvider != null) { + stage.set("Opening workspace"); + progress.set(Math.max(progress.get(), 0.95D)); + openVSCode(sender); + } + + if (maintenanceActive && maintenanceWorld != null) { + World worldToRelease = maintenanceWorld; + J.a(() -> { + J.sleep(15000); + IrisToolbelt.endWorldMaintenance(worldToRelease, "studio-open"); + }); + maintenanceActive = false; + } + + if (maintenanceActive && maintenanceWorld != null) { + IrisToolbelt.endWorldMaintenance(maintenanceWorld, "studio-open"); + } + complete.set(true); + } + }); + } + + private void startStudioOpenReporter(VolmitSender sender, AtomicReference stage, AtomicReference progress, AtomicBoolean complete, AtomicBoolean failed) { + J.a(() -> { + String[] spinner = {"|", "/", "-", "\\"}; + int spinIndex = 0; + long nextConsoleUpdate = 0L; + + while (!complete.get()) { + double currentProgress = Math.max(0D, Math.min(0.97D, progress.get())); + String currentStage = stage.get(); + String currentSpinner = spinner[spinIndex % spinner.length]; + + if (sender.isPlayer()) { + sender.sendProgress(currentProgress, "Studio " + currentSpinner + " " + currentStage); + } else { + long now = System.currentTimeMillis(); + if (now >= nextConsoleUpdate) { + sender.sendMessage(C.WHITE + "Studio " + Form.pc(currentProgress, 0) + C.GRAY + " - " + currentStage); + nextConsoleUpdate = now + 1500L; + } + } + + spinIndex++; + J.sleep(120); } - if (activeProvider != null) { - openVSCode(sender); + if (failed.get()) { + if (sender.isPlayer()) { + sender.sendProgress(1D, "Studio open failed"); + } else { + sender.sendMessage(C.RED + "Studio open failed."); + } + return; + } + + if (sender.isPlayer()) { + sender.sendProgress(1D, "Studio ready"); + } else { + sender.sendMessage(C.GREEN + "Studio ready."); } }); } @@ -361,6 +463,10 @@ public class IrisProject { } public JSONObject createCodeWorkspaceConfig() { + return createCodeWorkspaceConfig(true); + } + + private JSONObject createCodeWorkspaceConfig(boolean includeSchemas) { JSONObject ws = new JSONObject(); JSONArray folders = new JSONArray(); JSONObject folder = new JSONObject(); @@ -391,43 +497,50 @@ public class IrisProject { settings.put("[json]", jc); settings.put("json.maxItemsComputed", 30000); JSONArray schemas = new JSONArray(); - IrisData dm = IrisData.get(getPath()); - - for (ResourceLoader r : dm.getLoaders().v()) { - if (r.supportsSchemas()) { - schemas.put(r.buildSchema()); - } - } - - for (Class i : dm.resolveSnippets()) { - try { - String snipType = i.getDeclaredAnnotation(Snippet.class).value(); - JSONObject o = new JSONObject(); - KList fm = new KList<>(); - - for (int g = 1; g < 8; g++) { - fm.add("/snippet/" + snipType + Form.repeat("/*", g) + ".json"); + IrisData dm = null; + if (includeSchemas) { + dm = IrisData.get(getPath()); + for (ResourceLoader r : dm.getLoaders().v()) { + if (r.supportsSchemas()) { + schemas.put(r.buildSchema()); } + } - o.put("fileMatch", new JSONArray(fm.toArray())); - o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json"); - schemas.put(o); - File a = new File(dm.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json"); - J.attemptAsync(() -> { - try { - IO.writeAll(a, new SchemaBuilder(i, dm).construct().toString(4)); - } catch (Throwable e) { - e.printStackTrace(); + for (Class i : dm.resolveSnippets()) { + try { + String snipType = i.getDeclaredAnnotation(Snippet.class).value(); + JSONObject o = new JSONObject(); + KList fm = new KList<>(); + + for (int g = 1; g < 8; g++) { + fm.add("/snippet/" + snipType + Form.repeat("/*", g) + ".json"); } - }); - } catch (Throwable e) { - e.printStackTrace(); + + o.put("fileMatch", new JSONArray(fm.toArray())); + o.put("url", "./.iris/schema/snippet/" + snipType + "-schema.json"); + schemas.put(o); + IrisData snippetData = dm; + File a = new File(snippetData.getDataFolder(), ".iris/schema/snippet/" + snipType + "-schema.json"); + J.attemptAsync(() -> { + try { + IO.writeAll(a, new SchemaBuilder(i, snippetData).construct().toString(4)); + } catch (Throwable e) { + e.printStackTrace(); + } + }); + } catch (Throwable e) { + e.printStackTrace(); + } } } settings.put("json.schemas", schemas); ws.put("settings", settings); + if (!includeSchemas) { + return ws; + } + File schemasFile = new File(path, ".idea" + File.separator + "jsonSchemas.xml"); Document doc = IO.read(schemasFile); Element mappings = (Element) doc.selectSingleNode("//component[@name='JsonSchemaMappingsProjectConfiguration']"); diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java index 99bcd1378..0f19f8dd3 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisCreator.java @@ -20,6 +20,7 @@ package art.arcane.iris.core.tools; import com.google.common.util.concurrent.AtomicDouble; import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisRuntimeSchedulerMode; import art.arcane.iris.core.IrisWorlds; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; @@ -42,6 +43,7 @@ import art.arcane.volmlib.util.io.IO; import art.arcane.iris.util.common.plugin.VolmitSender; import art.arcane.iris.util.common.scheduling.J; import art.arcane.volmlib.util.scheduling.O; +import art.arcane.volmlib.util.scheduling.FoliaScheduler; import io.papermc.lib.PaperLib; import lombok.Data; import lombok.experimental.Accessors; @@ -60,6 +62,7 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.function.IntSupplier; @@ -102,6 +105,7 @@ public class IrisCreator { * Benchmark mode */ private boolean benchmark = false; + private BiConsumer studioProgressConsumer; public static boolean removeFromBukkitYml(String name) throws IOException { YamlConfiguration yml = YamlConfiguration.loadConfiguration(BUKKIT_YML); @@ -132,6 +136,8 @@ public class IrisCreator { throw new IrisException("You cannot invoke create() on the main thread."); } + reportStudioProgress(0.02D, "Preparing studio open"); + if (studio()) { World existing = Bukkit.getWorld(name()); if (existing == null) { @@ -141,6 +147,7 @@ public class IrisCreator { } } + reportStudioProgress(0.08D, "Resolving dimension"); IrisDimension d = IrisToolbelt.getDimension(dimension()); if (d == null) { @@ -150,11 +157,18 @@ public class IrisCreator { if (sender == null) sender = Iris.getSender(); + reportStudioProgress(0.16D, "Preparing world pack"); if (!studio() || benchmark) { Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name())); } + if (studio()) { + IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen()); + Iris.info("Studio create scheduling: mode=" + runtimeSchedulerMode.name().toLowerCase(Locale.ROOT) + + ", regionizedRuntime=" + FoliaScheduler.isRegionizedRuntime(Bukkit.getServer())); + } prebakeNoisemapsBeforeWorldCreate(d); + reportStudioProgress(0.28D, "Installing datapacks"); AtomicDouble pp = new AtomicDouble(0); O done = new O<>(); done.set(false); @@ -180,6 +194,7 @@ public class IrisCreator { if (ServerConfigurator.installDataPacks(verifyDataPacks, includeExternalDataPacks, extraWorldDatapackFoldersByPack)) { throw new IrisException("Datapacks were missing!"); } + reportStudioProgress(0.40D, "Datapacks ready"); PlatformChunkGenerator access = (PlatformChunkGenerator) wc.generator(); if (access == null) throw new IrisException("Access is null. Something bad happened."); @@ -195,7 +210,10 @@ public class IrisCreator { int req = access.getSpawnChunks().join(); for (int c = 0; c < req && !done.get(); c = g.getAsInt()) { double v = (double) c / req; - if (sender.isPlayer()) { + if (studioProgressConsumer != null) { + reportStudioProgress(0.40D + (0.42D * v), "Generating spawn"); + J.sleep(16); + } else if (sender.isPlayer()) { sender.sendProgress(v, "Generating"); J.sleep(16); } else { @@ -208,6 +226,7 @@ public class IrisCreator { World world; + reportStudioProgress(0.46D, "Creating world"); try { world = J.sfut(() -> INMS.get().createWorldAsync(wc)) .thenCompose(Function.identity()) @@ -224,6 +243,7 @@ public class IrisCreator { } done.set(true); + reportStudioProgress(0.86D, "World created"); if (sender.isPlayer() && !benchmark) { Player senderPlayer = sender.player(); @@ -267,6 +287,7 @@ public class IrisCreator { addToBukkitYml(); J.s(() -> Iris.linkMultiverseCore.updateWorld(world, dimension)); } + reportStudioProgress(0.93D, "Applying world settings"); if (pregen != null) { CompletableFuture ff = new CompletableFuture<>(); @@ -296,9 +317,24 @@ public class IrisCreator { e.printStackTrace(); } } + reportStudioProgress(0.98D, "Finalizing"); return world; } + private void reportStudioProgress(double progress, String stage) { + BiConsumer consumer = studioProgressConsumer; + if (consumer == null) { + return; + } + + double clamped = Math.max(0D, Math.min(1D, progress)); + try { + consumer.accept(clamped, stage); + } catch (Throwable e) { + Iris.reportError(e); + } + } + private void prebakeNoisemapsBeforeWorldCreate(IrisDimension dimension) { IrisSettings.IrisSettingsPregen pregenSettings = IrisSettings.get().getPregen(); if (!pregenSettings.isStartupNoisemapPrebake()) { diff --git a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java index 0c105ee13..3f36c734d 100644 --- a/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java +++ b/core/src/main/java/art/arcane/iris/core/tools/IrisToolbelt.java @@ -19,6 +19,7 @@ package art.arcane.iris.core.tools; import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisRuntimeSchedulerMode; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.loader.IrisData; @@ -233,7 +234,11 @@ public class IrisToolbelt { */ public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) { applyPregenPerformanceProfile(engine); - boolean useCachedWrapper = cached && engine != null && !J.isFolia(); + boolean useCachedWrapper = false; + if (cached && engine != null) { + IrisRuntimeSchedulerMode runtimeSchedulerMode = IrisRuntimeSchedulerMode.resolve(IrisSettings.get().getPregen()); + useCachedWrapper = runtimeSchedulerMode != IrisRuntimeSchedulerMode.FOLIA; + } return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine); } diff --git a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java index 3de022964..b628278d0 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisComplex.java @@ -19,6 +19,7 @@ package art.arcane.iris.engine; import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisHotPathMetricsMode; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.engine.data.cache.Cache; @@ -29,6 +30,7 @@ import art.arcane.iris.util.project.context.IrisContext; import art.arcane.iris.util.common.data.DataProvider; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.RNG; +import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds; import art.arcane.iris.util.project.noise.CNG; import art.arcane.iris.util.project.stream.ProceduralStream; import art.arcane.iris.util.project.stream.interpolation.Interpolated; @@ -47,6 +49,9 @@ import java.util.*; @ToString(exclude = "data") public class IrisComplex implements DataProvider { private static final BlockData AIR = Material.AIR.createBlockData(); + private static final NoiseBounds ZERO_NOISE_BOUNDS = new NoiseBounds(0D, 0D); + private static final int HOT_PATH_METRICS_FLUSH_SIZE = 64; + private static final ThreadLocal HOT_PATH_METRICS = ThreadLocal.withInitial(HotPathMetricsState::new); private RNG rng; private double fluidHeight; private IrisData data; @@ -84,6 +89,7 @@ public class IrisComplex implements DataProvider { private IrisRegion focusRegion; private Map> generatorBounds; private Set generatorBiomes; + private final Map childSelectionPlans = Collections.synchronizedMap(new IdentityHashMap<>()); public IrisComplex(Engine engine) { this(engine, false); @@ -318,10 +324,15 @@ public class IrisComplex implements DataProvider { return 0; } + IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); + IrisHotPathMetricsMode metricsMode = pregen.getHotPathMetricsMode(); + HotPathMetricsState metricsState = metricsMode == IrisHotPathMetricsMode.DISABLED ? null : HOT_PATH_METRICS.get(); + boolean sampleMetrics = metricsState != null && metricsState.shouldSample(metricsMode, pregen.getHotPathMetricsSampleStride()); + long interpolateStartNanos = sampleMetrics ? System.nanoTime() : 0L; CoordinateBiomeCache sampleCache = new CoordinateBiomeCache(64); IdentityHashMap cachedBounds = generatorBounds.get(interpolator); IdentityHashMap localBounds = new IdentityHashMap<>(8); - double hi = interpolator.interpolate(x, z, (xx, zz) -> { + NoiseBounds sampledBounds = interpolator.interpolateBounds(x, z, (xx, zz) -> { try { IrisBiome bx = sampleCache.get(xx, zz); if (bx == null) { @@ -329,57 +340,32 @@ public class IrisComplex implements DataProvider { sampleCache.put(xx, zz, bx); } - GeneratorBounds bounds = cachedBounds == null ? null : cachedBounds.get(bx); - if (bounds == null) { - bounds = localBounds.get(bx); - if (bounds == null) { - bounds = computeGeneratorBounds(engine, generators, bx); - localBounds.put(bx, bounds); - } - } - - return bounds.max; + GeneratorBounds bounds = resolveGeneratorBounds(engine, generators, bx, cachedBounds, localBounds); + return bounds.noiseBounds; } catch (Throwable e) { Iris.reportError(e); e.printStackTrace(); - Iris.error("Failed to sample hi biome at " + xx + " " + zz + "..."); + Iris.error("Failed to sample interpolated biome bounds at " + xx + " " + zz + "..."); } - return 0; + return ZERO_NOISE_BOUNDS; }); + if (sampleMetrics) { + metricsState.recordInterpolate(engine, System.nanoTime() - interpolateStartNanos); + } - double lo = interpolator.interpolate(x, z, (xx, zz) -> { - try { - IrisBiome bx = sampleCache.get(xx, zz); - if (bx == null) { - bx = baseBiomeStream.get(xx, zz); - sampleCache.put(xx, zz, bx); - } - - GeneratorBounds bounds = cachedBounds == null ? null : cachedBounds.get(bx); - if (bounds == null) { - bounds = localBounds.get(bx); - if (bounds == null) { - bounds = computeGeneratorBounds(engine, generators, bx); - localBounds.put(bx, bounds); - } - } - - return bounds.min; - } catch (Throwable e) { - Iris.reportError(e); - e.printStackTrace(); - Iris.error("Failed to sample lo biome at " + xx + " " + zz + "..."); - } - - return 0; - }); + double hi = sampledBounds.max(); + double lo = sampledBounds.min(); + long generatorStartNanos = sampleMetrics ? System.nanoTime() : 0L; double d = 0; for (IrisGenerator i : generators) { d += M.lerp(lo, hi, i.getHeight(x, z, seed + 239945)); } + if (sampleMetrics) { + metricsState.recordGenerator(engine, System.nanoTime() - generatorStartNanos); + } return d / generators.size(); } @@ -443,6 +429,28 @@ public class IrisComplex implements DataProvider { return new GeneratorBounds(min, max); } + private GeneratorBounds resolveGeneratorBounds( + Engine engine, + Set generators, + IrisBiome biome, + IdentityHashMap cachedBounds, + IdentityHashMap localBounds + ) { + GeneratorBounds bounds = cachedBounds == null ? null : cachedBounds.get(biome); + if (bounds != null) { + return bounds; + } + + GeneratorBounds local = localBounds.get(biome); + if (local != null) { + return local; + } + + GeneratorBounds computed = computeGeneratorBounds(engine, generators, biome); + localBounds.put(biome, computed); + return computed; + } + private IrisBiome implode(IrisBiome b, Double x, Double z) { if (b.getChildren().isEmpty()) { return b; @@ -461,20 +469,48 @@ public class IrisComplex implements DataProvider { } CNG childCell = b.getChildrenGenerator(rng, 123, b.getChildShrinkFactor()); - KList chx = b.getRealChildren(this).copy(); - chx.add(b); - IrisBiome biome = childCell.fitRarity(chx, x, z); + ChildSelectionPlan childSelectionPlan = resolveChildSelectionPlan(b); + IrisBiome biome = childSelectionPlan.select(childCell, x, z); biome.setInferredType(b.getInferredType()); return implode(biome, x, z, max - 1); } + private ChildSelectionPlan resolveChildSelectionPlan(IrisBiome biome) { + ChildSelectionPlan cachedPlan = childSelectionPlans.get(biome); + if (cachedPlan != null) { + return cachedPlan; + } + + synchronized (childSelectionPlans) { + ChildSelectionPlan synchronizedPlan = childSelectionPlans.get(biome); + if (synchronizedPlan != null) { + return synchronizedPlan; + } + + KList children = biome.getRealChildren(this); + KList options = new KList<>(); + for (IrisBiome child : children) { + if (child != null) { + options.add(child); + } + } + options.add(biome); + + ChildSelectionPlan createdPlan = ChildSelectionPlan.create(options); + childSelectionPlans.put(biome, createdPlan); + return createdPlan; + } + } + private static class GeneratorBounds { private final double min; private final double max; + private final NoiseBounds noiseBounds; private GeneratorBounds(double min, double max) { this.min = min; this.max = max; + this.noiseBounds = new NoiseBounds(min, max); } } @@ -528,6 +564,141 @@ public class IrisComplex implements DataProvider { } } + private static class ChildSelectionPlan { + private final IrisBiome[] mappedBiomes; + private final int maxIndex; + + private ChildSelectionPlan(IrisBiome[] mappedBiomes) { + this.mappedBiomes = mappedBiomes; + this.maxIndex = mappedBiomes.length - 1; + } + + private static ChildSelectionPlan create(KList options) { + if (options.isEmpty()) { + return new ChildSelectionPlan(new IrisBiome[0]); + } + + int maxRarity = 1; + for (IrisBiome biome : options) { + if (biome != null && biome.getRarity() > maxRarity) { + maxRarity = biome.getRarity(); + } + } + + int rarityMax = maxRarity + 1; + boolean flip = false; + KList mapped = new KList<>(); + for (IrisBiome biome : options) { + if (biome == null) { + continue; + } + + int rarity = Math.max(1, biome.getRarity()); + int count = rarityMax - rarity; + for (int index = 0; index < count; index++) { + flip = !flip; + if (flip) { + mapped.add(biome); + } else { + mapped.add(0, biome); + } + } + } + + if (mapped.isEmpty()) { + IrisBiome[] fallback = new IrisBiome[]{options.get(0)}; + return new ChildSelectionPlan(fallback); + } + + IrisBiome[] mappedBiomes = mapped.toArray(new IrisBiome[0]); + return new ChildSelectionPlan(mappedBiomes); + } + + private IrisBiome select(CNG childCell, double x, double z) { + if (mappedBiomes.length == 0) { + return null; + } + + if (mappedBiomes.length == 1) { + return mappedBiomes[0]; + } + + int selectedIndex = childCell.fit2D(0, maxIndex, x, z); + if (selectedIndex < 0) { + return mappedBiomes[0]; + } + + if (selectedIndex > maxIndex) { + return mappedBiomes[maxIndex]; + } + + return mappedBiomes[selectedIndex]; + } + } + + private static class HotPathMetricsState { + private long callCounter; + private long interpolateNanos; + private int interpolateSamples; + private long generatorNanos; + private int generatorSamples; + + private boolean shouldSample(IrisHotPathMetricsMode mode, int sampleStride) { + if (mode == IrisHotPathMetricsMode.EXACT) { + return true; + } + + long current = callCounter++; + return (current & (sampleStride - 1L)) == 0L; + } + + private void recordInterpolate(Engine engine, long nanos) { + if (nanos < 0L) { + return; + } + + interpolateNanos += nanos; + interpolateSamples++; + if (interpolateSamples >= HOT_PATH_METRICS_FLUSH_SIZE) { + flushInterpolate(engine); + } + } + + private void recordGenerator(Engine engine, long nanos) { + if (nanos < 0L) { + return; + } + + generatorNanos += nanos; + generatorSamples++; + if (generatorSamples >= HOT_PATH_METRICS_FLUSH_SIZE) { + flushGenerator(engine); + } + } + + private void flushInterpolate(Engine engine) { + if (interpolateSamples <= 0) { + return; + } + + double averageMs = (interpolateNanos / (double) interpolateSamples) / 1_000_000D; + engine.getMetrics().getNoiseHeightInterpolate().put(averageMs); + interpolateNanos = 0L; + interpolateSamples = 0; + } + + private void flushGenerator(Engine engine) { + if (generatorSamples <= 0) { + return; + } + + double averageMs = (generatorNanos / (double) generatorSamples) / 1_000_000D; + engine.getMetrics().getNoiseHeightGenerator().put(averageMs); + generatorNanos = 0L; + generatorSamples = 0; + } + } + public void close() { } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java index 3c6e51301..82d5455b3 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/Engine.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/Engine.java @@ -324,6 +324,18 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat return; } + if (!J.isFolia() && !J.isPrimaryThread()) { + CompletableFuture scheduled = J.sfut(() -> updateChunk(c)); + if (scheduled != null) { + try { + scheduled.join(); + } catch (Throwable e) { + Iris.reportError(e); + } + } + return; + } + var chunk = mantle.getChunk(c).use(); try { Runnable tileTask = () -> { @@ -424,7 +436,7 @@ public interface Engine extends DataProvider, Fallible, LootProvider, BlockUpdat } if (!J.isFolia()) { - return J.isPrimaryThread(); + return true; } return J.isOwnedByCurrentRegion(chunk.getWorld(), chunk.getX(), chunk.getZ()); diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java index 294edfff7..587a343bc 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineMetrics.java @@ -39,6 +39,17 @@ public class EngineMetrics { private final AtomicRollingSequence deposit; private final AtomicRollingSequence carveResolve; private final AtomicRollingSequence carveApply; + private final AtomicRollingSequence noiseHeightInterpolate; + private final AtomicRollingSequence noiseHeightGenerator; + private final AtomicRollingSequence contextPrefill; + private final AtomicRollingSequence contextPrefillHeight; + private final AtomicRollingSequence contextPrefillBiome; + private final AtomicRollingSequence contextPrefillRock; + private final AtomicRollingSequence contextPrefillFluid; + private final AtomicRollingSequence contextPrefillRegion; + private final AtomicRollingSequence contextPrefillCave; + private final AtomicRollingSequence pregenWaitPermit; + private final AtomicRollingSequence pregenWaitAdaptive; public EngineMetrics(int mem) { this.total = new AtomicRollingSequence(mem); @@ -56,6 +67,17 @@ public class EngineMetrics { this.deposit = new AtomicRollingSequence(mem); this.carveResolve = new AtomicRollingSequence(mem); this.carveApply = new AtomicRollingSequence(mem); + this.noiseHeightInterpolate = new AtomicRollingSequence(mem); + this.noiseHeightGenerator = new AtomicRollingSequence(mem); + this.contextPrefill = new AtomicRollingSequence(mem); + this.contextPrefillHeight = new AtomicRollingSequence(mem); + this.contextPrefillBiome = new AtomicRollingSequence(mem); + this.contextPrefillRock = new AtomicRollingSequence(mem); + this.contextPrefillFluid = new AtomicRollingSequence(mem); + this.contextPrefillRegion = new AtomicRollingSequence(mem); + this.contextPrefillCave = new AtomicRollingSequence(mem); + this.pregenWaitPermit = new AtomicRollingSequence(mem); + this.pregenWaitAdaptive = new AtomicRollingSequence(mem); } public KMap pull() { @@ -75,6 +97,17 @@ public class EngineMetrics { v.put("deposit", deposit.getAverage()); v.put("carve.resolve", carveResolve.getAverage()); v.put("carve.apply", carveApply.getAverage()); + v.put("noise.height.interpolate", noiseHeightInterpolate.getAverage()); + v.put("noise.height.generator", noiseHeightGenerator.getAverage()); + v.put("context.prefill", contextPrefill.getAverage()); + v.put("context.prefill.height", contextPrefillHeight.getAverage()); + v.put("context.prefill.biome", contextPrefillBiome.getAverage()); + v.put("context.prefill.rock", contextPrefillRock.getAverage()); + v.put("context.prefill.fluid", contextPrefillFluid.getAverage()); + v.put("context.prefill.region", contextPrefillRegion.getAverage()); + v.put("context.prefill.cave", contextPrefillCave.getAverage()); + v.put("pregen.wait.permit", pregenWaitPermit.getAverage()); + v.put("pregen.wait.adaptive", pregenWaitAdaptive.getAverage()); return v; } diff --git a/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java index ea2f64b2a..bfa2832bc 100644 --- a/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java +++ b/core/src/main/java/art/arcane/iris/engine/framework/EngineMode.java @@ -78,7 +78,8 @@ public interface EngineMode extends Staged { cacheContext = false; } } - ChunkContext ctx = new ChunkContext(x, z, getComplex(), cacheContext); + ChunkContext.PrefillPlan prefillPlan = cacheContext ? ChunkContext.PrefillPlan.NO_CAVE : ChunkContext.PrefillPlan.NONE; + ChunkContext ctx = new ChunkContext(x, z, getComplex(), cacheContext, prefillPlan, getEngine().getMetrics()); IrisContext.getOr(getEngine()).setChunkContext(ctx); EngineStage[] stages = getStages().toArray(new EngineStage[0]); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index 1c3466b69..7ec701d7e 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -25,12 +25,13 @@ import art.arcane.iris.engine.object.IrisCaveFieldModule; import art.arcane.iris.engine.object.IrisCaveProfile; import art.arcane.iris.engine.object.IrisRange; import art.arcane.iris.util.project.noise.CNG; -import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; public class IrisCaveCarver3D { private static final byte LIQUID_AIR = 0; @@ -46,7 +47,7 @@ public class IrisCaveCarver3D { private final CNG warpDensity; private final CNG surfaceBreakDensity; private final RNG thresholdRng; - private final KList modules; + private final ModuleState[] modules; private final double normalization; private final MatterCavern carveAir; private final MatterCavern carveLava; @@ -64,7 +65,7 @@ public class IrisCaveCarver3D { this.carveAir = new MatterCavern(true, "", LIQUID_AIR); this.carveLava = new MatterCavern(true, "", LIQUID_LAVA); this.carveForcedAir = new MatterCavern(true, "", LIQUID_FORCED_AIR); - this.modules = new KList<>(); + List moduleStates = new ArrayList<>(); RNG baseRng = new RNG(engine.getSeedManager().getCarve()); this.baseDensity = profile.getBaseDensityStyle().create(baseRng.nextParallelRNG(934_447), data); @@ -82,13 +83,14 @@ public class IrisCaveCarver3D { for (IrisCaveFieldModule module : profile.getModules()) { CNG moduleDensity = module.getStyle().create(baseRng.nextParallelRNG(1_000_003L + (index * 65_537L)), data); ModuleState state = new ModuleState(module, moduleDensity); - modules.add(state); + moduleStates.add(state); weight += Math.abs(state.weight); index++; } + this.modules = moduleStates.toArray(new ModuleState[0]); normalization = weight <= 0 ? 1 : weight; - hasModules = !modules.isEmpty(); + hasModules = modules.length > 0; } public int carve(MantleWriter writer, int chunkX, int chunkZ) { @@ -171,7 +173,7 @@ public class IrisCaveCarver3D { int columnSurfaceY = engine.getHeight(x, z); int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance)); boolean breakColumn = allowSurfaceBreak - && signed(surfaceBreakDensity.noise(x, z)) >= surfaceBreakNoiseThreshold; + && signed(surfaceBreakDensity.noiseFast2D(x, z)) >= surfaceBreakNoiseThreshold; int columnTopY = breakColumn ? Math.min(maxY, Math.max(minY, columnSurfaceY)) : clearanceTopY; @@ -329,8 +331,8 @@ public class IrisCaveCarver3D { private double sampleDensity(int x, int y, int z) { if (!hasWarp && !hasModules) { - double density = signed(baseDensity.noise(x, y, z)) * baseWeight; - density += signed(detailDensity.noise(x, y, z)) * detailWeight; + double density = signed(baseDensity.noiseFast3D(x, y, z)) * baseWeight; + density += signed(detailDensity.noiseFast3D(x, y, z)) * detailWeight; return density / normalization; } @@ -338,8 +340,8 @@ public class IrisCaveCarver3D { double warpedY = y; double warpedZ = z; if (hasWarp) { - double warpA = signed(warpDensity.noise(x, y, z)); - double warpB = signed(warpDensity.noise(x + 31.37D, y - 17.21D, z + 23.91D)); + double warpA = signed(warpDensity.noiseFast3D(x, y, z)); + double warpB = signed(warpDensity.noiseFast3D(x + 31.37D, y - 17.21D, z + 23.91D)); double offsetX = warpA * warpStrength; double offsetY = warpB * warpStrength; double offsetZ = (warpA - warpB) * 0.5D * warpStrength; @@ -348,16 +350,17 @@ public class IrisCaveCarver3D { warpedZ += offsetZ; } - double density = signed(baseDensity.noise(warpedX, warpedY, warpedZ)) * baseWeight; - density += signed(detailDensity.noise(warpedX, warpedY, warpedZ)) * detailWeight; + double density = signed(baseDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * baseWeight; + density += signed(detailDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * detailWeight; if (hasModules) { - for (ModuleState module : modules) { + for (int moduleIndex = 0; moduleIndex < modules.length; moduleIndex++) { + ModuleState module = modules[moduleIndex]; if (y < module.minY || y > module.maxY) { continue; } - double moduleDensity = signed(module.density.noise(warpedX, warpedY, warpedZ)) - module.threshold; + double moduleDensity = signed(module.density.noiseFast3D(warpedX, warpedY, warpedZ)) - module.threshold; if (module.invert) { moduleDensity = -moduleDensity; } diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index 3c058a731..9c89d8ea3 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -18,6 +18,7 @@ package art.arcane.iris.engine.mantle.components; +import art.arcane.iris.engine.data.cache.Cache; import art.arcane.iris.engine.mantle.ComponentFlag; import art.arcane.iris.engine.mantle.EngineMantle; import art.arcane.iris.engine.mantle.IrisMantleComponent; @@ -32,6 +33,7 @@ import art.arcane.iris.util.project.context.ChunkContext; import art.arcane.volmlib.util.documentation.ChunkCoordinates; import art.arcane.volmlib.util.mantle.flag.ReservedFlag; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import java.util.ArrayList; import java.util.Comparator; @@ -75,8 +77,9 @@ public class MantleCarvingComponent extends IrisMantleComponent { @Override public void generateLayer(MantleWriter writer, int x, int z, ChunkContext context) { IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(FIELD_SIZE * FIELD_SIZE); PrecisionStopwatch resolveStopwatch = PrecisionStopwatch.start(); - List weightedProfiles = resolveWeightedProfiles(x, z, resolverState); + List weightedProfiles = resolveWeightedProfiles(x, z, resolverState, caveBiomeCache); getEngineMantle().getEngine().getMetrics().getCarveResolve().put(resolveStopwatch.getMilliseconds()); for (WeightedProfile weightedProfile : weightedProfiles) { carveProfile(weightedProfile, writer, x, z); @@ -89,8 +92,8 @@ public class MantleCarvingComponent extends IrisMantleComponent { carver.carve(writer, cx, cz, weightedProfile.columnWeights, MIN_WEIGHT, THRESHOLD_PENALTY, weightedProfile.worldYRange); } - private List resolveWeightedProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) { - IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ, resolverState); + private List resolveWeightedProfiles(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { + IrisCaveProfile[] profileField = buildProfileField(chunkX, chunkZ, resolverState, caveBiomeCache); Map profileWeights = new IdentityHashMap<>(); IrisCaveProfile[] columnProfiles = new IrisCaveProfile[KERNEL_SIZE]; double[] columnProfileWeights = new double[KERNEL_SIZE]; @@ -215,7 +218,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { return weightedProfiles; } - private IrisCaveProfile[] buildProfileField(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState) { + private IrisCaveProfile[] buildProfileField(int chunkX, int chunkZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE]; int startX = (chunkX << 4) - BLEND_RADIUS; int startZ = (chunkZ << 4) - BLEND_RADIUS; @@ -224,7 +227,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { int worldX = startX + fieldX; for (int fieldZ = 0; fieldZ < FIELD_SIZE; fieldZ++) { int worldZ = startZ + fieldZ; - profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ, resolverState); + profileField[(fieldX * FIELD_SIZE) + fieldZ] = resolveColumnProfile(worldX, worldZ, resolverState, caveBiomeCache); } } @@ -241,7 +244,7 @@ public class MantleCarvingComponent extends IrisMantleComponent { return -1; } - private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState) { + private IrisCaveProfile resolveColumnProfile(int worldX, int worldZ, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { IrisCaveProfile resolved = null; IrisCaveProfile dimensionProfile = getDimension().getCaveProfile(); if (isProfileEnabled(dimensionProfile)) { @@ -266,7 +269,14 @@ public class MantleCarvingComponent extends IrisMantleComponent { int surfaceY = getEngineMantle().getEngine().getHeight(worldX, worldZ, true); int sampleY = Math.max(1, surfaceY - 56); - IrisBiome caveBiome = getEngineMantle().getEngine().getCaveBiome(worldX, sampleY, worldZ, resolverState); + long cacheKey = Cache.key(worldX, worldZ); + IrisBiome caveBiome = caveBiomeCache.get(cacheKey); + if (caveBiome == null) { + caveBiome = getEngineMantle().getEngine().getCaveBiome(worldX, sampleY, worldZ, resolverState); + if (caveBiome != null) { + caveBiomeCache.put(cacheKey, caveBiome); + } + } if (caveBiome != null) { IrisCaveProfile caveProfile = caveBiome.getCaveProfile(); if (isProfileEnabled(caveProfile)) { diff --git a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java index a96737ed7..9fb35c43b 100644 --- a/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java +++ b/core/src/main/java/art/arcane/iris/engine/modifier/IrisCarveModifier.java @@ -33,12 +33,14 @@ import art.arcane.iris.util.project.hunk.Hunk; import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.BlockPosition; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.slices.MarkerMatter; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import lombok.Data; import org.bukkit.Material; import org.bukkit.block.data.BlockData; @@ -64,6 +66,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { Mantle mantle = getEngine().getMantle().getMantle(); MantleChunk mc = mantle.getChunk(x, z).use(); IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + Long2ObjectOpenHashMap caveBiomeCache = new Long2ObjectOpenHashMap<>(2048); int[][] columnHeights = new int[256][]; int[] columnHeightSizes = new int[256]; PackedWallBuffer walls = new PackedWallBuffer(512); @@ -129,7 +132,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { int worldX = rx + (x << 4); int worldZ = rz + (z << 4); IrisBiome biome = cavern.getCustomBiome().isEmpty() - ? getEngine().getCaveBiome(worldX, yy, worldZ, resolverState) + ? resolveCaveBiome(caveBiomeCache, worldX, yy, worldZ, resolverState) : getEngine().getData().getBiomeLoader().load(cavern.getCustomBiome()); if (biome != null) { @@ -166,7 +169,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { buf = y; zone.ceiling = buf; } else if (zone.isValid(getEngine())) { - processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState); + processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState, caveBiomeCache); zone = new CaveZone(); zone.setFloor(y); buf = y; @@ -178,7 +181,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { } if (zone.isValid(getEngine())) { - processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState); + processZone(output, mc, mantle, zone, rx, rz, rx + (x << 4), rz + (z << 4), resolverState, caveBiomeCache); } } } finally { @@ -190,7 +193,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { } } - private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState) { + private void processZone(Hunk output, MantleChunk mc, Mantle mantle, CaveZone zone, int rx, int rz, int xx, int zz, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { int center = (zone.floor + zone.ceiling) / 2; String customBiome = ""; @@ -221,7 +224,7 @@ public class IrisCarveModifier extends EngineAssignedModifier { } IrisBiome biome = customBiome.isEmpty() - ? getEngine().getCaveBiome(xx, center, zz, resolverState) + ? resolveCaveBiome(caveBiomeCache, xx, center, zz, resolverState) : getEngine().getData().getBiomeLoader().load(customBiome); if (biome == null) { @@ -286,6 +289,20 @@ public class IrisCarveModifier extends EngineAssignedModifier { } } + private IrisBiome resolveCaveBiome(Long2ObjectOpenHashMap caveBiomeCache, int x, int y, int z, IrisDimensionCarvingResolver.State resolverState) { + long key = BlockPosition.toLong(x, y, z); + IrisBiome cachedBiome = caveBiomeCache.get(key); + if (cachedBiome != null) { + return cachedBiome; + } + + IrisBiome resolvedBiome = getEngine().getCaveBiome(x, y, z, resolverState); + if (resolvedBiome != null) { + caveBiomeCache.put(key, resolvedBiome); + } + return resolvedBiome; + } + private void appendColumnHeight(int[][] heights, int[] sizes, int columnIndex, int y) { int[] column = heights[columnIndex]; int size = sizes[columnIndex]; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java index eb1679119..0e65e2566 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolver.java @@ -1,6 +1,7 @@ package art.arcane.iris.engine.object; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.util.project.interpolation.IrisInterpolation; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.util.project.noise.CNG; @@ -153,7 +154,8 @@ public final class IrisDimensionCarvingResolver { long seed = resolveChildSeed(engine, state); CNG childGenerator = parent.getChildrenGenerator(seed, engine.getData()); - int selectedIndex = childGenerator.fit(0, selectionPlan.maxIndex, worldX, worldZ); + double sample = childGenerator.noiseFast2D(worldX, worldZ); + int selectedIndex = (int) Math.round(IrisInterpolation.lerp(0, selectionPlan.maxIndex, sample)); CarvingChoice selected = selectionPlan.get(selectedIndex); if (selected == null || selected.entry == null) { return parent; diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java index 79c9fe6f1..f2607cfd9 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisInterpolator.java @@ -25,6 +25,8 @@ import art.arcane.iris.engine.object.annotations.Required; import art.arcane.volmlib.util.function.NoiseProvider; import art.arcane.iris.util.project.interpolation.InterpolationMethod; import art.arcane.iris.util.project.interpolation.IrisInterpolation; +import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBounds; +import art.arcane.iris.util.project.interpolation.IrisInterpolation.NoiseBoundsProvider; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -71,4 +73,12 @@ public class IrisInterpolator { public double interpolate(int x, int z, NoiseProvider provider) { return IrisInterpolation.getNoise(getFunction(), x, z, getHorizontalScale(), provider); } + + public NoiseBounds interpolateBounds(double x, double z, NoiseBoundsProvider provider) { + return interpolateBounds((int) Math.round(x), (int) Math.round(z), provider); + } + + public NoiseBounds interpolateBounds(int x, int z, NoiseBoundsProvider provider) { + return IrisInterpolation.getNoiseBounds(getFunction(), x, z, getHorizontalScale(), provider); + } } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java b/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java index 9beecb09d..29e5f9b98 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisNoiseGenerator.java @@ -106,7 +106,8 @@ public class IrisNoiseGenerator { g += 819; } - double n = getGenerator(superSeed, data).fitDouble(0, opacity, (x / zoom) + offsetX, (z / zoom) + offsetZ); + CNG cng = getGenerator(superSeed, data); + double n = cng.noiseFast2D((x / zoom) + offsetX, (z / zoom) + offsetZ) * opacity; n = negative ? (-n + opacity) : n; n = (exponent != 1 ? n < 0 ? -Math.pow(-n, exponent) : Math.pow(n, exponent) : n) + offsetY; n = parametric ? IrisInterpolation.parametric(n, 1) : n; diff --git a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java index f65643b75..fd7c86b9b 100644 --- a/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java +++ b/core/src/main/java/art/arcane/iris/util/common/scheduling/J.java @@ -168,7 +168,7 @@ public class J { } public static boolean isFolia() { - return FoliaScheduler.isFolia(Iris.instance); + return FoliaScheduler.isFolia(Bukkit.getServer()); } public static boolean isPrimaryThread() { @@ -176,10 +176,26 @@ public class J { } public static boolean isOwnedByCurrentRegion(Entity entity) { + if (entity == null) { + return false; + } + + if (!isFolia()) { + return isPrimaryThread(); + } + return FoliaScheduler.isOwnedByCurrentRegion(entity); } public static boolean isOwnedByCurrentRegion(World world, int chunkX, int chunkZ) { + if (world == null) { + return false; + } + + if (!isFolia()) { + return isPrimaryThread(); + } + return FoliaScheduler.isOwnedByCurrentRegion(world, chunkX, chunkZ); } @@ -531,34 +547,66 @@ public class J { } private static boolean runGlobalImmediate(Runnable runnable) { + if (!isFolia()) { + return false; + } + return FoliaScheduler.runGlobal(Iris.instance, runnable); } private static boolean runGlobalDelayed(Runnable runnable, int delayTicks) { + if (!isFolia()) { + return false; + } + return FoliaScheduler.runGlobal(Iris.instance, runnable, Math.max(0, delayTicks)); } private static boolean runRegionImmediate(World world, int chunkX, int chunkZ, Runnable runnable) { + if (!isFolia()) { + return false; + } + return FoliaScheduler.runRegion(Iris.instance, world, chunkX, chunkZ, runnable); } private static boolean runRegionDelayed(World world, int chunkX, int chunkZ, Runnable runnable, int delayTicks) { + if (!isFolia()) { + return false; + } + return FoliaScheduler.runRegion(Iris.instance, world, chunkX, chunkZ, runnable, Math.max(0, delayTicks)); } private static boolean runAsyncImmediate(Runnable runnable) { + if (!isFolia()) { + return false; + } + return FoliaScheduler.runAsync(Iris.instance, runnable); } private static boolean runAsyncDelayed(Runnable runnable, int delayTicks) { + if (!isFolia()) { + return false; + } + return FoliaScheduler.runAsync(Iris.instance, runnable, Math.max(0, delayTicks)); } private static boolean runEntityImmediate(Entity entity, Runnable runnable) { + if (!isFolia()) { + return false; + } + return FoliaScheduler.runEntity(Iris.instance, entity, runnable); } private static boolean runEntityDelayed(Entity entity, Runnable runnable, int delayTicks) { + if (!isFolia()) { + return false; + } + return FoliaScheduler.runEntity(Iris.instance, entity, runnable, Math.max(0, delayTicks)); } diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java index 9cc2cd2cc..2cdf6508f 100644 --- a/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkContext.java @@ -1,17 +1,19 @@ package art.arcane.iris.util.project.context; +import art.arcane.iris.core.IrisHotPathMetricsMode; +import art.arcane.iris.core.IrisSettings; import art.arcane.iris.engine.IrisComplex; +import art.arcane.iris.engine.framework.EngineMetrics; import art.arcane.iris.engine.object.IrisBiome; import art.arcane.iris.engine.object.IrisRegion; -import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.volmlib.util.atomics.AtomicRollingSequence; import org.bukkit.block.data.BlockData; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Executor; +import java.util.IdentityHashMap; public class ChunkContext { + private static final int PREFILL_METRICS_FLUSH_SIZE = 64; + private static final ThreadLocal PREFILL_METRICS = ThreadLocal.withInitial(PrefillMetricsState::new); private final int x; private final int z; private final ChunkedDataCache height; @@ -22,10 +24,18 @@ public class ChunkContext { private final ChunkedDataCache region; public ChunkContext(int x, int z, IrisComplex complex) { - this(x, z, complex, true); + this(x, z, complex, true, PrefillPlan.NO_CAVE, null); } public ChunkContext(int x, int z, IrisComplex complex, boolean cache) { + this(x, z, complex, cache, PrefillPlan.NO_CAVE, null); + } + + public ChunkContext(int x, int z, IrisComplex complex, boolean cache, EngineMetrics metrics) { + this(x, z, complex, cache, PrefillPlan.NO_CAVE, metrics); + } + + public ChunkContext(int x, int z, IrisComplex complex, boolean cache, PrefillPlan prefillPlan, EngineMetrics metrics) { this.x = x; this.z = z; this.height = new ChunkedDataCache<>(complex.getHeightStream(), x, z, cache); @@ -36,17 +46,42 @@ public class ChunkContext { this.region = new ChunkedDataCache<>(complex.getRegionStream(), x, z, cache); if (cache) { - Executor executor = MultiBurst.burst; - List> tasks = new ArrayList<>(6); - tasks.add(CompletableFuture.runAsync(() -> height.fill(executor), executor)); - tasks.add(CompletableFuture.runAsync(() -> biome.fill(executor), executor)); - tasks.add(CompletableFuture.runAsync(() -> cave.fill(executor), executor)); - tasks.add(CompletableFuture.runAsync(() -> rock.fill(executor), executor)); - tasks.add(CompletableFuture.runAsync(() -> fluid.fill(executor), executor)); - tasks.add(CompletableFuture.runAsync(() -> region.fill(executor), executor)); - for (CompletableFuture task : tasks) { - task.join(); + PrefillPlan resolvedPlan = prefillPlan == null ? PrefillPlan.NO_CAVE : prefillPlan; + PrefillMetricsState metricsState = PREFILL_METRICS.get(); + IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); + IrisHotPathMetricsMode metricsMode = pregen.getHotPathMetricsMode(); + boolean sampleMetrics = metricsMode != IrisHotPathMetricsMode.DISABLED + && metricsState.shouldSample(metricsMode, pregen.getHotPathMetricsSampleStride()); + long totalStartNanos = sampleMetrics ? System.nanoTime() : 0L; + if (resolvedPlan.height) { + fill(height, metrics == null ? null : metrics.getContextPrefillHeight(), sampleMetrics, metricsState); } + if (resolvedPlan.biome) { + fill(biome, metrics == null ? null : metrics.getContextPrefillBiome(), sampleMetrics, metricsState); + } + if (resolvedPlan.rock) { + fill(rock, metrics == null ? null : metrics.getContextPrefillRock(), sampleMetrics, metricsState); + } + if (resolvedPlan.fluid) { + fill(fluid, metrics == null ? null : metrics.getContextPrefillFluid(), sampleMetrics, metricsState); + } + if (resolvedPlan.region) { + fill(region, metrics == null ? null : metrics.getContextPrefillRegion(), sampleMetrics, metricsState); + } + if (resolvedPlan.cave) { + fill(cave, metrics == null ? null : metrics.getContextPrefillCave(), sampleMetrics, metricsState); + } + if (metrics != null && sampleMetrics) { + metricsState.record(metrics.getContextPrefill(), System.nanoTime() - totalStartNanos); + } + } + } + + private void fill(ChunkedDataCache dataCache, AtomicRollingSequence metrics, boolean sampleMetrics, PrefillMetricsState metricsState) { + long startNanos = sampleMetrics ? System.nanoTime() : 0L; + dataCache.fill(); + if (metrics != null && sampleMetrics) { + metricsState.record(metrics, System.nanoTime() - startNanos); } } @@ -81,4 +116,66 @@ public class ChunkContext { public ChunkedDataCache getRegion() { return region; } + + public enum PrefillPlan { + ALL(true, true, true, true, true, true), + NO_CAVE(true, true, false, true, true, true), + NONE(false, false, false, false, false, false); + + private final boolean height; + private final boolean biome; + private final boolean cave; + private final boolean rock; + private final boolean fluid; + private final boolean region; + + PrefillPlan(boolean height, boolean biome, boolean cave, boolean rock, boolean fluid, boolean region) { + this.height = height; + this.biome = biome; + this.cave = cave; + this.rock = rock; + this.fluid = fluid; + this.region = region; + } + } + + private static final class PrefillMetricsState { + private long callCounter; + private final IdentityHashMap buckets = new IdentityHashMap<>(); + + private boolean shouldSample(IrisHotPathMetricsMode mode, int sampleStride) { + if (mode == IrisHotPathMetricsMode.EXACT) { + return true; + } + + long current = callCounter++; + return (current & (sampleStride - 1L)) == 0L; + } + + private void record(AtomicRollingSequence sequence, long nanos) { + if (sequence == null || nanos < 0L) { + return; + } + + MetricBucket bucket = buckets.get(sequence); + if (bucket == null) { + bucket = new MetricBucket(); + buckets.put(sequence, bucket); + } + + bucket.nanos += nanos; + bucket.samples++; + if (bucket.samples >= PREFILL_METRICS_FLUSH_SIZE) { + double averageMs = (bucket.nanos / (double) bucket.samples) / 1_000_000D; + sequence.put(averageMs); + bucket.nanos = 0L; + bucket.samples = 0; + } + } + } + + private static final class MetricBucket { + private long nanos; + private int samples; + } } diff --git a/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDataCache.java b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDataCache.java index f0f5472f2..0d180c973 100644 --- a/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDataCache.java +++ b/core/src/main/java/art/arcane/iris/util/project/context/ChunkedDataCache.java @@ -1,13 +1,9 @@ package art.arcane.iris.util.project.context; import art.arcane.iris.util.project.stream.ProceduralStream; +import art.arcane.iris.util.project.stream.utility.CachedStream2D; import art.arcane.volmlib.util.documentation.BlockCoordinates; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; -import java.util.concurrent.ForkJoinPool; public class ChunkedDataCache { private final int x; @@ -31,7 +27,7 @@ public class ChunkedDataCache { } public void fill() { - fill(ForkJoinPool.commonPool()); + fill(null); } public void fill(Executor executor) { @@ -39,20 +35,17 @@ public class ChunkedDataCache { return; } - List> tasks = new ArrayList<>(16); - for (int j = 0; j < 16; j++) { - int row = j; - tasks.add(CompletableFuture.runAsync(() -> { - int rowOffset = row * 16; - double zz = (z + row); - for (int i = 0; i < 16; i++) { - data[rowOffset + i] = stream.get(x + i, zz); - } - }, executor)); + if (stream instanceof CachedStream2D cachedStream) { + cachedStream.fillChunk(x, z, data); + return; } - for (CompletableFuture task : tasks) { - task.join(); + for (int row = 0; row < 16; row++) { + int rowOffset = row * 16; + int worldZ = z + row; + for (int column = 0; column < 16; column++) { + data[rowOffset + column] = stream.get(x + column, worldZ); + } } } @@ -63,11 +56,14 @@ public class ChunkedDataCache { return stream.get(this.x + x, this.z + z); } - T value = (T) data[(z * 16) + x]; + int index = (z * 16) + x; + T value = (T) data[index]; if (value != null) { return value; } - return stream.get(this.x + x, this.z + z); + T sampled = stream.get(this.x + x, this.z + z); + data[index] = sampled; + return sampled; } } diff --git a/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java index 9da8d837d..443f0d4d6 100644 --- a/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java +++ b/core/src/main/java/art/arcane/iris/util/project/interpolation/IrisInterpolation.java @@ -37,6 +37,7 @@ import java.util.HashMap; public class IrisInterpolation { public static CNG cng = NoiseStyle.SIMPLEX.create(new RNG()); private static final ThreadLocal NOISE_SAMPLE_CACHE_2D = ThreadLocal.withInitial(() -> new NoiseSampleCache2D(64)); + private static final ThreadLocal NOISE_BOUNDS_SAMPLE_CACHE_2D = ThreadLocal.withInitial(() -> new NoiseBoundsSampleCache2D(64)); public static double bezier(double t) { return t * t * (3.0d - 2.0d * t); @@ -1041,6 +1042,16 @@ public class IrisInterpolation { return n.noise(x, z); } + public static NoiseBounds getNoiseBounds(InterpolationMethod method, int x, int z, double h, NoiseBoundsProvider noise) { + NoiseBoundsSampleCache2D cache = NOISE_BOUNDS_SAMPLE_CACHE_2D.get(); + cache.clear(); + NoiseProvider minProvider = (sampleX, sampleZ) -> cache.getOrSampleMin(sampleX, sampleZ, noise); + NoiseProvider maxProvider = (sampleX, sampleZ) -> cache.getOrSampleMax(sampleX, sampleZ, noise); + double min = getNoise(method, x, z, h, minProvider); + double max = getNoise(method, x, z, h, maxProvider); + return new NoiseBounds(min, max); + } + private static boolean usesSampleCache(InterpolationMethod method) { return switch (method) { case BILINEAR_STARCAST_3, @@ -1176,6 +1187,170 @@ public class IrisInterpolation { } } + @FunctionalInterface + public interface NoiseBoundsProvider { + NoiseBounds noise(double x, double z); + } + + public static final class NoiseBounds { + private final double min; + private final double max; + + public NoiseBounds(double min, double max) { + this.min = min; + this.max = max; + } + + public double min() { + return min; + } + + public double max() { + return max; + } + } + + private static class NoiseBoundsSampleCache2D { + private long[] xBits; + private long[] zBits; + private double[] minValues; + private double[] maxValues; + private byte[] states; + private int mask; + private int resizeThreshold; + private int size; + + public NoiseBoundsSampleCache2D(int initialCapacity) { + int minimumCapacity = Math.max(8, initialCapacity); + int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); + xBits = new long[tableSize]; + zBits = new long[tableSize]; + minValues = new double[tableSize]; + maxValues = new double[tableSize]; + states = new byte[tableSize]; + mask = tableSize - 1; + resizeThreshold = Math.max(1, (tableSize * 3) >> 2); + size = 0; + } + + public void clear() { + if (size == 0) { + return; + } + Arrays.fill(states, (byte) 0); + size = 0; + } + + public double getOrSampleMin(double sampleX, double sampleZ, NoiseBoundsProvider provider) { + long xBitsValue = Double.doubleToLongBits(sampleX); + long zBitsValue = Double.doubleToLongBits(sampleZ); + int slot = findSlot(xBitsValue, zBitsValue); + if (states[slot] != 0) { + return minValues[slot]; + } + + NoiseBounds bounds = provider.noise(sampleX, sampleZ); + insert(slot, xBitsValue, zBitsValue, bounds.min(), bounds.max()); + return bounds.min(); + } + + public double getOrSampleMax(double sampleX, double sampleZ, NoiseBoundsProvider provider) { + long xBitsValue = Double.doubleToLongBits(sampleX); + long zBitsValue = Double.doubleToLongBits(sampleZ); + int slot = findSlot(xBitsValue, zBitsValue); + if (states[slot] != 0) { + return maxValues[slot]; + } + + NoiseBounds bounds = provider.noise(sampleX, sampleZ); + insert(slot, xBitsValue, zBitsValue, bounds.min(), bounds.max()); + return bounds.max(); + } + + private int findSlot(long xb, long zb) { + int slot = mix(xb, zb) & mask; + while (states[slot] != 0) { + if (xBits[slot] == xb && zBits[slot] == zb) { + break; + } + slot = (slot + 1) & mask; + } + return slot; + } + + private void insert(int slot, long xb, long zb, double min, double max) { + xBits[slot] = xb; + zBits[slot] = zb; + minValues[slot] = min; + maxValues[slot] = max; + states[slot] = 1; + size++; + if (size >= resizeThreshold) { + grow(); + } + } + + private int mix(long xb, long zb) { + long hash = xb * 0x9E3779B97F4A7C15L; + hash ^= Long.rotateLeft(zb * 0xC2B2AE3D27D4EB4FL, 32); + hash ^= (hash >>> 33); + hash *= 0xff51afd7ed558ccdL; + hash ^= (hash >>> 33); + return (int) hash; + } + + private void grow() { + long[] previousXBits = xBits; + long[] previousZBits = zBits; + double[] previousMin = minValues; + double[] previousMax = maxValues; + byte[] previousStates = states; + + int nextLength = xBits.length << 1; + long[] nextXBits = new long[nextLength]; + long[] nextZBits = new long[nextLength]; + double[] nextMin = new double[nextLength]; + double[] nextMax = new double[nextLength]; + byte[] nextStates = new byte[nextLength]; + + xBits = nextXBits; + zBits = nextZBits; + minValues = nextMin; + maxValues = nextMax; + states = nextStates; + mask = nextLength - 1; + resizeThreshold = Math.max(1, (nextLength * 3) >> 2); + size = 0; + + for (int i = 0; i < previousStates.length; i++) { + if (previousStates[i] == 0) { + continue; + } + int slot = findSlot(previousXBits[i], previousZBits[i]); + xBits[slot] = previousXBits[i]; + zBits[slot] = previousZBits[i]; + minValues[slot] = previousMin[i]; + maxValues[slot] = previousMax[i]; + states[slot] = 1; + size++; + } + } + + private int tableSizeFor(int value) { + int n = value - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + int tableSize = n + 1; + if (tableSize < 8) { + return 8; + } + return tableSize; + } + } + public static double rangeScale(double amin, double amax, double bmin, double bmax, double b) { return amin + ((amax - amin) * ((b - bmin) / (bmax - bmin))); } diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java index 0e830a745..3a3f6b8c5 100644 --- a/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CNG.java @@ -63,6 +63,7 @@ public class CNG { private FloatCache cache; private NoiseGenerator generator; private NoiseInjector injector; + private InjectorMode injectorMode; private RNG rng; private boolean noscale; private int oct; @@ -106,6 +107,7 @@ public class CNG { this.generator = generator; this.opacity = opacity; this.injector = ADD; + this.injectorMode = InjectorMode.ADD; if (generator instanceof OctaveNoise) { ((OctaveNoise) generator).setOctaves(octaves); @@ -345,11 +347,60 @@ public class CNG { } public CNG injectWith(NoiseInjector i) { - injector = i; + injector = i == null ? ADD : i; + injectorMode = resolveInjectorMode(injector); return this; } + private InjectorMode resolveInjectorMode(NoiseInjector i) { + if (i == ADD) { + return InjectorMode.ADD; + } + + if (i == SRC_SUBTRACT) { + return InjectorMode.SRC_SUBTRACT; + } + + if (i == DST_SUBTRACT) { + return InjectorMode.DST_SUBTRACT; + } + + if (i == MULTIPLY) { + return InjectorMode.MULTIPLY; + } + + if (i == MAX) { + return InjectorMode.MAX; + } + + if (i == MIN) { + return InjectorMode.MIN; + } + + if (i == SRC_MOD) { + return InjectorMode.SRC_MOD; + } + + if (i == SRC_POW) { + return InjectorMode.SRC_POW; + } + + if (i == DST_MOD) { + return InjectorMode.DST_MOD; + } + + if (i == DST_POW) { + return InjectorMode.DST_POW; + } + + return InjectorMode.CUSTOM; + } + public T fitRarity(KList b, double... dim) { + if (dim.length == 2) { + return fitRarity2D(b, dim[0], dim[1]); + } + if (b.size() == 0) { return null; } @@ -358,27 +409,7 @@ public class CNG { return b.get(0); } - KList rarityMapped = new KList<>(); - boolean o = false; - int max = 1; - for (T i : b) { - if (i.getRarity() > max) { - max = i.getRarity(); - } - } - - max++; - - for (T i : b) { - for (int j = 0; j < max - i.getRarity(); j++) { - //noinspection AssignmentUsedAsCondition - if (o = !o) { - rarityMapped.add(i); - } else { - rarityMapped.add(0, i); - } - } - } + KList rarityMapped = buildRarityMapped(b); if (rarityMapped.size() == 1) { return rarityMapped.get(0); @@ -391,6 +422,92 @@ public class CNG { return fit(rarityMapped, dim); } + public T fitRarity2D(KList b, double x, double z) { + if (b.size() == 0) { + return null; + } + + if (b.size() == 1) { + return b.get(0); + } + + KList rarityMapped = buildRarityMapped(b); + if (rarityMapped.size() == 1) { + return rarityMapped.get(0); + } + + if (rarityMapped.isEmpty()) { + throw new RuntimeException("BAD RARITY MAP! RELATED TO: " + b.toString(", or possibly ")); + } + + return fit2D(rarityMapped, x, z); + } + + private KList buildRarityMapped(KList values) { + KList rarityMapped = new KList<>(); + boolean flip = false; + int max = 1; + for (T value : values) { + if (value.getRarity() > max) { + max = value.getRarity(); + } + } + + max++; + for (T value : values) { + int count = max - value.getRarity(); + for (int j = 0; j < count; j++) { + flip = !flip; + if (flip) { + rarityMapped.add(value); + } else { + rarityMapped.add(0, value); + } + } + } + + return rarityMapped; + } + + public T fit2D(T[] values, double x, double z) { + if (values.length == 0) { + return null; + } + + if (values.length == 1) { + return values[0]; + } + + return values[fit2D(0, values.length - 1, x, z)]; + } + + public T fit2D(List values, double x, double z) { + if (values.isEmpty()) { + return null; + } + + if (values.size() == 1) { + return values.get(0); + } + + try { + return values.get(fit2D(0, values.size() - 1, x, z)); + } catch (Throwable e) { + Iris.reportError(e); + } + + return values.get(0); + } + + public int fit2D(int min, int max, double x, double z) { + if (min == max) { + return min; + } + + double noise = noiseFast2D(x, z); + return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + } + public T fit(T[] v, double... dim) { if (v.length == 0) { return null; @@ -432,13 +549,7 @@ public class CNG { } public int fit(int min, int max, double x, double z) { - if (min == max) { - return min; - } - - double noise = noise(x, z); - - return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); + return fit2D(min, max, x, z); } public int fit(int min, int max, double x, double y, double z) { @@ -466,7 +577,7 @@ public class CNG { return (int) Math.round(min); } - double noise = noise(x, z); + double noise = noiseFast2D(x, z); return (int) Math.round(IrisInterpolation.lerp(min, max, noise)); } @@ -610,9 +721,34 @@ public class CNG { if (children != null) { for (CNG i : children) { - double[] r = injector.combine(n, i.noise(x)); - n = r[0]; - m += r[1]; + double source = n; + double value = i.noise(x); + switch (injectorMode) { + case ADD -> { + n = source + value; + m += 1D; + } + case SRC_SUBTRACT -> { + n = source - value < 0D ? 0D : source - value; + m -= 1D; + } + case DST_SUBTRACT -> { + n = value - source < 0D ? 0D : source - value; + m -= 1D; + } + case MULTIPLY -> n = source * value; + case MAX -> n = Math.max(source, value); + case MIN -> n = Math.min(source, value); + case SRC_MOD -> n = source % value; + case SRC_POW -> n = Math.pow(source, value); + case DST_MOD -> n = value % source; + case DST_POW -> n = Math.pow(value, source); + case CUSTOM -> { + double[] combined = injector.combine(source, value); + n = combined[0]; + m += combined[1]; + } + } } } @@ -626,9 +762,34 @@ public class CNG { if (children != null) { for (CNG i : children) { - double[] r = injector.combine(n, i.noise(x, z)); - n = r[0]; - m += r[1]; + double source = n; + double value = i.noise(x, z); + switch (injectorMode) { + case ADD -> { + n = source + value; + m += 1D; + } + case SRC_SUBTRACT -> { + n = source - value < 0D ? 0D : source - value; + m -= 1D; + } + case DST_SUBTRACT -> { + n = value - source < 0D ? 0D : source - value; + m -= 1D; + } + case MULTIPLY -> n = source * value; + case MAX -> n = Math.max(source, value); + case MIN -> n = Math.min(source, value); + case SRC_MOD -> n = source % value; + case SRC_POW -> n = Math.pow(source, value); + case DST_MOD -> n = value % source; + case DST_POW -> n = Math.pow(value, source); + case CUSTOM -> { + double[] combined = injector.combine(source, value); + n = combined[0]; + m += combined[1]; + } + } } } @@ -642,9 +803,34 @@ public class CNG { if (children != null) { for (CNG i : children) { - double[] r = injector.combine(n, i.noise(x, y, z)); - n = r[0]; - m += r[1]; + double source = n; + double value = i.noise(x, y, z); + switch (injectorMode) { + case ADD -> { + n = source + value; + m += 1D; + } + case SRC_SUBTRACT -> { + n = source - value < 0D ? 0D : source - value; + m -= 1D; + } + case DST_SUBTRACT -> { + n = value - source < 0D ? 0D : source - value; + m -= 1D; + } + case MULTIPLY -> n = source * value; + case MAX -> n = Math.max(source, value); + case MIN -> n = Math.min(source, value); + case SRC_MOD -> n = source % value; + case SRC_POW -> n = Math.pow(source, value); + case DST_MOD -> n = value % source; + case DST_POW -> n = Math.pow(value, source); + case CUSTOM -> { + double[] combined = injector.combine(source, value); + n = combined[0]; + m += combined[1]; + } + } } } @@ -673,9 +859,34 @@ public class CNG { } for (CNG i : children) { - double[] r = injector.combine(n, i.noise(dim)); - n = r[0]; - m += r[1]; + double source = n; + double value = i.noise(dim); + switch (injectorMode) { + case ADD -> { + n = source + value; + m += 1D; + } + case SRC_SUBTRACT -> { + n = source - value < 0D ? 0D : source - value; + m -= 1D; + } + case DST_SUBTRACT -> { + n = value - source < 0D ? 0D : source - value; + m -= 1D; + } + case MULTIPLY -> n = source * value; + case MAX -> n = Math.max(source, value); + case MIN -> n = Math.min(source, value); + case SRC_MOD -> n = source % value; + case SRC_POW -> n = Math.pow(source, value); + case DST_MOD -> n = value % source; + case DST_POW -> n = Math.pow(value, source); + case CUSTOM -> { + double[] combined = injector.combine(source, value); + n = combined[0]; + m += combined[1]; + } + } } return ((n / m) - down + up) * patch; @@ -685,6 +896,10 @@ public class CNG { return applyPost(getNoise(x), x); } + public double noiseFast1D(double x) { + return applyPost(getNoise(x), x); + } + public double noise(double x, double z) { if (cache != null && isWholeCoordinate(x) && isWholeCoordinate(z)) { return cache.get((int) x, (int) z); @@ -693,10 +908,22 @@ public class CNG { return applyPost(getNoise(x, z), x, z); } + public double noiseFast2D(double x, double z) { + if (cache != null && isWholeCoordinate(x) && isWholeCoordinate(z)) { + return cache.get((int) x, (int) z); + } + + return applyPost(getNoise(x, z), x, z); + } + public double noise(double x, double y, double z) { return applyPost(getNoise(x, y, z), x, y, z); } + public double noiseFast3D(double x, double y, double z) { + return applyPost(getNoise(x, y, z), x, y, z); + } + public CNG pow(double power) { this.power = power; return this; @@ -714,4 +941,18 @@ public class CNG { public boolean isStatic() { return generator != null && generator.isStatic(); } + + private enum InjectorMode { + ADD, + SRC_SUBTRACT, + DST_SUBTRACT, + MULTIPLY, + MAX, + MIN, + SRC_MOD, + SRC_POW, + DST_MOD, + DST_POW, + CUSTOM + } } diff --git a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java index 08e9ff0c6..e99275e27 100644 --- a/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java +++ b/core/src/main/java/art/arcane/iris/util/project/stream/utility/CachedStream2D.java @@ -53,7 +53,6 @@ public class CachedStream2D extends BasicStream implements ProceduralStrea @Override public T get(double x, double z) { - //return stream.get(x, z); return cache.get((int) x, (int) z); } @@ -81,4 +80,10 @@ public class CachedStream2D extends BasicStream implements ProceduralStrea public boolean isClosed() { return engine.isClosed(); } + + public void fillChunk(int worldX, int worldZ, Object[] target) { + int chunkX = worldX >> 4; + int chunkZ = worldZ >> 4; + cache.fillChunk(chunkX, chunkZ, target); + } } diff --git a/core/src/test/java/art/arcane/iris/core/IrisRuntimeSchedulerModeRoutingTest.java b/core/src/test/java/art/arcane/iris/core/IrisRuntimeSchedulerModeRoutingTest.java new file mode 100644 index 000000000..659df6d0b --- /dev/null +++ b/core/src/test/java/art/arcane/iris/core/IrisRuntimeSchedulerModeRoutingTest.java @@ -0,0 +1,75 @@ +package art.arcane.iris.core; + +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.block.data.BlockData; +import org.junit.Test; + +import java.util.logging.Logger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockingDetails; + +public class IrisRuntimeSchedulerModeRoutingTest { + @Test + public void autoResolvesToPaperLikeOnPurpurBranding() { + installServer("Purpur", "git-Purpur-2562 (MC: 1.21.11)"); + IrisSettings.IrisSettingsPregen pregen = new IrisSettings.IrisSettingsPregen(); + pregen.runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO; + + IrisRuntimeSchedulerMode resolved = IrisRuntimeSchedulerMode.resolve(pregen); + assertEquals(IrisRuntimeSchedulerMode.PAPER_LIKE, resolved); + } + + @Test + public void autoResolvesToFoliaWhenBrandingContainsFolia() { + installServer("Folia", "git-Folia-123 (MC: 1.21.11)"); + IrisSettings.IrisSettingsPregen pregen = new IrisSettings.IrisSettingsPregen(); + pregen.runtimeSchedulerMode = IrisRuntimeSchedulerMode.AUTO; + + IrisRuntimeSchedulerMode resolved = IrisRuntimeSchedulerMode.resolve(pregen); + assertEquals(IrisRuntimeSchedulerMode.FOLIA, resolved); + } + + @Test + public void explicitModeBypassesAutoDetection() { + installServer("Purpur", "git-Purpur-2562 (MC: 1.21.11)"); + IrisSettings.IrisSettingsPregen pregen = new IrisSettings.IrisSettingsPregen(); + + pregen.runtimeSchedulerMode = IrisRuntimeSchedulerMode.FOLIA; + IrisRuntimeSchedulerMode foliaResolved = IrisRuntimeSchedulerMode.resolve(pregen); + assertEquals(IrisRuntimeSchedulerMode.PAPER_LIKE, foliaResolved); + + pregen.runtimeSchedulerMode = IrisRuntimeSchedulerMode.PAPER_LIKE; + IrisRuntimeSchedulerMode paperResolved = IrisRuntimeSchedulerMode.resolve(pregen); + assertEquals(IrisRuntimeSchedulerMode.PAPER_LIKE, paperResolved); + } + + private void installServer(String name, String version) { + Server server = Bukkit.getServer(); + if (server == null) { + server = mock(Server.class); + try { + Bukkit.setServer(server); + } catch (Throwable ignored) { + server = Bukkit.getServer(); + } + } + + assumeTrue(server != null && mockingDetails(server).isMock()); + + BlockData emptyBlockData = mock(BlockData.class); + doReturn(Logger.getLogger("IrisTest")).when(server).getLogger(); + doReturn(name).when(server).getName(); + doReturn(version).when(server).getVersion(); + doReturn(version).when(server).getBukkitVersion(); + doReturn(emptyBlockData).when(server).createBlockData(any(Material.class)); + doReturn(emptyBlockData).when(server).createBlockData(anyString()); + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/IrisComplexImplodeParityTest.java b/core/src/test/java/art/arcane/iris/engine/IrisComplexImplodeParityTest.java new file mode 100644 index 000000000..4f7a38b8b --- /dev/null +++ b/core/src/test/java/art/arcane/iris/engine/IrisComplexImplodeParityTest.java @@ -0,0 +1,111 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.engine.object.IrisBiome; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.block.data.BlockData; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class IrisComplexImplodeParityTest { + private static Method childSelectionCreateMethod; + private static Method childSelectionSelectMethod; + + @BeforeClass + public static void setup() throws Exception { + if (Bukkit.getServer() == null) { + Server server = mock(Server.class); + BlockData emptyBlockData = mock(BlockData.class); + doReturn(Logger.getLogger("IrisTest")).when(server).getLogger(); + doReturn("IrisTestServer").when(server).getName(); + doReturn("1.0").when(server).getVersion(); + doReturn("1.0").when(server).getBukkitVersion(); + doReturn(emptyBlockData).when(server).createBlockData(any(Material.class)); + doReturn(emptyBlockData).when(server).createBlockData(anyString()); + Bukkit.setServer(server); + } + + Class childSelectionClass = Class.forName("art.arcane.iris.engine.IrisComplex$ChildSelectionPlan"); + childSelectionCreateMethod = childSelectionClass.getDeclaredMethod("create", KList.class); + childSelectionCreateMethod.setAccessible(true); + childSelectionSelectMethod = childSelectionClass.getDeclaredMethod("select", CNG.class, double.class, double.class); + childSelectionSelectMethod.setAccessible(true); + } + + @Test + public void selectionPlanMatchesLegacyFitRarityAcrossSeedAndCoordinateGrid() throws Exception { + List> scenarios = buildScenarios(); + for (int scenarioIndex = 0; scenarioIndex < scenarios.size(); scenarioIndex++) { + KList options = scenarios.get(scenarioIndex); + Object selectionPlan = childSelectionCreateMethod.invoke(null, options); + for (long seed = 1L; seed <= 7L; seed++) { + CNG generator = new CNG(new RNG(seed), 4); + for (int x = -512; x <= 512; x += 37) { + for (int z = -512; z <= 512; z += 41) { + IrisBiome expected = generator.fitRarity(options, x, z); + IrisBiome actual = (IrisBiome) childSelectionSelectMethod.invoke(selectionPlan, generator, (double) x, (double) z); + assertSame("scenario=" + scenarioIndex + " seed=" + seed + " x=" + x + " z=" + z, expected, actual); + } + } + } + } + } + + @Test + public void emptySelectionPlanMatchesLegacyEmptyBehavior() throws Exception { + KList options = new KList<>(); + CNG generator = new CNG(new RNG(9L), 2); + Object selectionPlan = childSelectionCreateMethod.invoke(null, options); + IrisBiome expected = generator.fitRarity(options, 12D, -32D); + IrisBiome actual = (IrisBiome) childSelectionSelectMethod.invoke(selectionPlan, generator, 12D, -32D); + assertNull(expected); + assertNull(actual); + } + + private List> buildScenarios() { + List> scenarios = new ArrayList<>(); + + KList scenarioA = new KList<>(); + scenarioA.add(createBiome(1)); + scenarioA.add(createBiome(3)); + scenarioA.add(createBiome(5)); + scenarioA.add(createBiome(2)); + scenarios.add(scenarioA); + + KList scenarioB = new KList<>(); + scenarioB.add(createBiome(7)); + scenarioB.add(createBiome(2)); + scenarioB.add(createBiome(2)); + scenarioB.add(createBiome(6)); + scenarioB.add(createBiome(1)); + scenarios.add(scenarioB); + + KList scenarioC = new KList<>(); + scenarioC.add(createBiome(4)); + scenarios.add(scenarioC); + + return scenarios; + } + + private IrisBiome createBiome(int rarity) { + IrisBiome biome = mock(IrisBiome.class); + doReturn(rarity).when(biome).getRarity(); + return biome; + } +} diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java index b993be395..a98c385bf 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java @@ -57,8 +57,8 @@ public class IrisDimensionCarvingResolverParityTest { IrisDimensionCarvingEntry statefulRoot = IrisDimensionCarvingResolver.resolveRootEntry(fixture.engine, worldY, state); assertSame("root mismatch at worldY=" + worldY, legacyRoot, statefulRoot); - for (int worldX = -192; worldX <= 192; worldX += 31) { - for (int worldZ = -192; worldZ <= 192; worldZ += 37) { + for (int worldX = -384; worldX <= 384; worldX += 29) { + for (int worldZ = -384; worldZ <= 384; worldZ += 31) { IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ); IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state); assertSame("entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved); @@ -67,6 +67,26 @@ public class IrisDimensionCarvingResolverParityTest { } } + @Test + public void resolverStatefulOverloadsMatchLegacyResolverAcrossMixedDepthGraph() { + Fixture fixture = createMixedDepthFixture(); + IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); + + for (int worldY = -64; worldY <= 320; worldY += 17) { + IrisDimensionCarvingEntry legacyRoot = legacyResolveRootEntry(fixture.engine, worldY); + IrisDimensionCarvingEntry statefulRoot = IrisDimensionCarvingResolver.resolveRootEntry(fixture.engine, worldY, state); + assertSame("mixed root mismatch at worldY=" + worldY, legacyRoot, statefulRoot); + + for (int worldX = -640; worldX <= 640; worldX += 79) { + for (int worldZ = -640; worldZ <= 640; worldZ += 83) { + IrisDimensionCarvingEntry legacyResolved = legacyResolveFromRoot(fixture.engine, legacyRoot, worldX, worldZ); + IrisDimensionCarvingEntry statefulResolved = IrisDimensionCarvingResolver.resolveFromRoot(fixture.engine, statefulRoot, worldX, worldZ, state); + assertSame("mixed entry mismatch at worldY=" + worldY + " worldX=" + worldX + " worldZ=" + worldZ, legacyResolved, statefulResolved); + } + } + } + } + @Test public void caveBiomeStateOverloadMatchesDefaultOverloadAcrossSampleGrid() { Fixture fixture = createFixture(); @@ -145,6 +165,92 @@ public class IrisDimensionCarvingResolverParityTest { return new Fixture(engine); } + private Fixture createMixedDepthFixture() { + IrisBiome rootLowBiome = mock(IrisBiome.class); + IrisBiome rootHighBiome = mock(IrisBiome.class); + IrisBiome childABiome = mock(IrisBiome.class); + IrisBiome childBBiome = mock(IrisBiome.class); + IrisBiome childCBiome = mock(IrisBiome.class); + IrisBiome childDBiome = mock(IrisBiome.class); + IrisBiome childEBiome = mock(IrisBiome.class); + IrisBiome childFBiome = mock(IrisBiome.class); + IrisBiome childGBiome = mock(IrisBiome.class); + IrisBiome fallbackBiome = mock(IrisBiome.class); + IrisBiome surfaceBiome = mock(IrisBiome.class); + + doReturn(7).when(rootLowBiome).getRarity(); + doReturn(5).when(rootHighBiome).getRarity(); + doReturn(2).when(childABiome).getRarity(); + doReturn(3).when(childBBiome).getRarity(); + doReturn(6).when(childCBiome).getRarity(); + doReturn(1).when(childDBiome).getRarity(); + doReturn(4).when(childEBiome).getRarity(); + doReturn(8).when(childFBiome).getRarity(); + doReturn(2).when(childGBiome).getRarity(); + doReturn(0).when(fallbackBiome).getCaveMinDepthBelowSurface(); + + @SuppressWarnings("unchecked") + ResourceLoader biomeLoader = mock(ResourceLoader.class); + doReturn(rootLowBiome).when(biomeLoader).load("root-low"); + doReturn(rootHighBiome).when(biomeLoader).load("root-high"); + doReturn(childABiome).when(biomeLoader).load("child-a"); + doReturn(childBBiome).when(biomeLoader).load("child-b"); + doReturn(childCBiome).when(biomeLoader).load("child-c"); + doReturn(childDBiome).when(biomeLoader).load("child-d"); + doReturn(childEBiome).when(biomeLoader).load("child-e"); + doReturn(childFBiome).when(biomeLoader).load("child-f"); + doReturn(childGBiome).when(biomeLoader).load("child-g"); + + IrisData data = mock(IrisData.class); + doReturn(biomeLoader).when(data).getBiomeLoader(); + + IrisDimensionCarvingEntry rootLow = buildEntry("root-low", "root-low", new IrisRange(-64, 120), 7, List.of("child-a", "child-d", "child-e")); + IrisDimensionCarvingEntry rootHigh = buildEntry("root-high", "root-high", new IrisRange(121, 320), 6, List.of("child-b", "child-c", "child-f")); + IrisDimensionCarvingEntry childA = buildEntry("child-a", "child-a", new IrisRange(-4096, 4096), 5, List.of("child-b", "child-g")); + IrisDimensionCarvingEntry childB = buildEntry("child-b", "child-b", new IrisRange(-4096, 4096), 1, List.of("child-c")); + IrisDimensionCarvingEntry childC = buildEntry("child-c", "child-c", new IrisRange(-4096, 4096), 0, List.of()); + IrisDimensionCarvingEntry childD = buildEntry("child-d", "child-d", new IrisRange(-4096, 4096), 6, List.of("child-e", "child-f")); + IrisDimensionCarvingEntry childE = buildEntry("child-e", "child-e", new IrisRange(-4096, 4096), 2, List.of("child-a")); + IrisDimensionCarvingEntry childF = buildEntry("child-f", "child-f", new IrisRange(-4096, 4096), 8, List.of("child-g", "child-c")); + IrisDimensionCarvingEntry childG = buildEntry("child-g", "child-g", new IrisRange(-4096, 4096), 3, List.of("child-d")); + + KList carvingEntries = new KList<>(); + carvingEntries.add(rootLow); + carvingEntries.add(rootHigh); + carvingEntries.add(childA); + carvingEntries.add(childB); + carvingEntries.add(childC); + carvingEntries.add(childD); + carvingEntries.add(childE); + carvingEntries.add(childF); + carvingEntries.add(childG); + + Map index = new HashMap<>(); + index.put(rootLow.getId(), rootLow); + index.put(rootHigh.getId(), rootHigh); + index.put(childA.getId(), childA); + index.put(childB.getId(), childB); + index.put(childC.getId(), childC); + index.put(childD.getId(), childD); + index.put(childE.getId(), childE); + index.put(childF.getId(), childF); + index.put(childG.getId(), childG); + + IrisDimension dimension = mock(IrisDimension.class); + doReturn(carvingEntries).when(dimension).getCarving(); + doReturn(index).when(dimension).getCarvingEntryIndex(); + + Engine engine = mock(Engine.class, CALLS_REAL_METHODS); + doReturn(dimension).when(engine).getDimension(); + doReturn(data).when(engine).getData(); + doReturn(new SeedManager(4_627_991_643L)).when(engine).getSeedManager(); + doReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()).when(engine).getWorld(); + doReturn(surfaceBiome).when(engine).getSurfaceBiome(anyInt(), anyInt()); + doReturn(fallbackBiome).when(engine).getCaveBiome(anyInt(), anyInt()); + + return new Fixture(engine); + } + private IrisDimensionCarvingEntry buildEntry(String id, String biome, IrisRange worldRange, int depth, List children) { IrisDimensionCarvingEntry entry = new IrisDimensionCarvingEntry(); entry.setId(id); diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/CNGInjectorParityTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/CNGInjectorParityTest.java new file mode 100644 index 000000000..12d7b59d2 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/CNGInjectorParityTest.java @@ -0,0 +1,208 @@ +package art.arcane.iris.util.project.noise; + +import art.arcane.volmlib.util.function.NoiseInjector; +import art.arcane.volmlib.util.math.RNG; +import org.bukkit.Bukkit; +import org.bukkit.Material; +import org.bukkit.Server; +import org.bukkit.block.data.BlockData; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +public class CNGInjectorParityTest { + @BeforeClass + public static void setupBukkit() { + if (Bukkit.getServer() != null) { + return; + } + + Server server = mock(Server.class); + BlockData emptyBlockData = mock(BlockData.class); + doReturn(Logger.getLogger("IrisTest")).when(server).getLogger(); + doReturn("IrisTestServer").when(server).getName(); + doReturn("1.0").when(server).getVersion(); + doReturn("1.0").when(server).getBukkitVersion(); + doReturn(emptyBlockData).when(server).createBlockData(any(Material.class)); + doReturn(emptyBlockData).when(server).createBlockData(anyString()); + Bukkit.setServer(server); + } + + @Test + public void builtInInjectorsMatchLegacyCombineFor1D() { + List injectors = builtInInjectors(); + for (NoiseInjector injector : injectors) { + CompositeFixture fixture = createFixture(injector); + for (int x = -300; x <= 300; x += 17) { + double expected = legacyCombined1D(fixture, x); + double actual = fixture.root.noise(x); + assertEquals("injector=" + injector + " x=" + x, expected, actual, 1.0E-12D); + } + } + } + + @Test + public void builtInInjectorsMatchLegacyCombineFor2D() { + List injectors = builtInInjectors(); + for (NoiseInjector injector : injectors) { + CompositeFixture fixture = createFixture(injector); + for (int x = -160; x <= 160; x += 19) { + for (int z = -160; z <= 160; z += 23) { + double expected = legacyCombined2D(fixture, x, z); + double actual = fixture.root.noise(x, z); + assertEquals("injector=" + injector + " x=" + x + " z=" + z, expected, actual, 1.0E-12D); + } + } + } + } + + @Test + public void builtInInjectorsMatchLegacyCombineFor3D() { + List injectors = builtInInjectors(); + for (NoiseInjector injector : injectors) { + CompositeFixture fixture = createFixture(injector); + for (int x = -64; x <= 64; x += 11) { + for (int y = -32; y <= 32; y += 13) { + for (int z = -64; z <= 64; z += 17) { + double expected = legacyCombined3D(fixture, x, y, z); + double actual = fixture.root.noise(x, y, z); + assertEquals("injector=" + injector + " x=" + x + " y=" + y + " z=" + z, expected, actual, 1.0E-12D); + } + } + } + } + } + + private CompositeFixture createFixture(NoiseInjector injector) { + DeterministicNoiseGenerator rootGenerator = new DeterministicNoiseGenerator(0.17D); + DeterministicNoiseGenerator childGeneratorA = new DeterministicNoiseGenerator(0.43D); + DeterministicNoiseGenerator childGeneratorB = new DeterministicNoiseGenerator(0.79D); + + CNG childA = new CNG(new RNG(11L), childGeneratorA, 1.0D, 1).bake(); + CNG childB = new CNG(new RNG(12L), childGeneratorB, 1.0D, 1).bake(); + + CNG root = new CNG(new RNG(9L), rootGenerator, 1.0D, 1).bake(); + root.child(childA); + root.child(childB); + root.injectWith(injector); + + return new CompositeFixture(root, rootGenerator, childA, childB, injector); + } + + private List builtInInjectors() { + List injectors = new ArrayList<>(); + injectors.add(CNG.ADD); + injectors.add(CNG.SRC_SUBTRACT); + injectors.add(CNG.DST_SUBTRACT); + injectors.add(CNG.MULTIPLY); + injectors.add(CNG.MAX); + injectors.add(CNG.MIN); + injectors.add(CNG.SRC_MOD); + injectors.add(CNG.SRC_POW); + injectors.add(CNG.DST_MOD); + injectors.add(CNG.DST_POW); + return injectors; + } + + private double legacyCombined1D(CompositeFixture fixture, double x) { + double n = fixture.rootGenerator.noise(x, 0D, 0D); + double m = 1D; + + double valueA = fixture.childA.noise(x); + double[] combinedA = fixture.injector.combine(n, valueA); + n = combinedA[0]; + m += combinedA[1]; + + double valueB = fixture.childB.noise(x); + double[] combinedB = fixture.injector.combine(n, valueB); + n = combinedB[0]; + m += combinedB[1]; + + return n / m; + } + + private double legacyCombined2D(CompositeFixture fixture, double x, double z) { + double n = fixture.rootGenerator.noise(x, z, 0D); + double m = 1D; + + double valueA = fixture.childA.noise(x, z); + double[] combinedA = fixture.injector.combine(n, valueA); + n = combinedA[0]; + m += combinedA[1]; + + double valueB = fixture.childB.noise(x, z); + double[] combinedB = fixture.injector.combine(n, valueB); + n = combinedB[0]; + m += combinedB[1]; + + return n / m; + } + + private double legacyCombined3D(CompositeFixture fixture, double x, double y, double z) { + double n = fixture.rootGenerator.noise(x, y, z); + double m = 1D; + + double valueA = fixture.childA.noise(x, y, z); + double[] combinedA = fixture.injector.combine(n, valueA); + n = combinedA[0]; + m += combinedA[1]; + + double valueB = fixture.childB.noise(x, y, z); + double[] combinedB = fixture.injector.combine(n, valueB); + n = combinedB[0]; + m += combinedB[1]; + + return n / m; + } + + private static class CompositeFixture { + private final CNG root; + private final DeterministicNoiseGenerator rootGenerator; + private final CNG childA; + private final CNG childB; + private final NoiseInjector injector; + + private CompositeFixture(CNG root, DeterministicNoiseGenerator rootGenerator, CNG childA, CNG childB, NoiseInjector injector) { + this.root = root; + this.rootGenerator = rootGenerator; + this.childA = childA; + this.childB = childB; + this.injector = injector; + } + } + + private static class DeterministicNoiseGenerator implements NoiseGenerator { + private final double offset; + + private DeterministicNoiseGenerator(double offset) { + this.offset = offset; + } + + @Override + public double noise(double x) { + double angle = (x * 0.013D) + offset; + return 0.2D + (((Math.sin(angle) + 1D) * 0.5D) * 0.6D); + } + + @Override + public double noise(double x, double z) { + double angle = (x * 0.011D) + (z * 0.017D) + offset; + return 0.2D + (((Math.sin(angle) + 1D) * 0.5D) * 0.6D); + } + + @Override + public double noise(double x, double y, double z) { + double angle = (x * 0.007D) + (y * 0.013D) + (z * 0.019D) + offset; + return 0.2D + (((Math.sin(angle) + 1D) * 0.5D) * 0.6D); + } + } +}