diff --git a/core/src/main/java/art/arcane/iris/Iris.java b/core/src/main/java/art/arcane/iris/Iris.java index 84ce532b0..fd7b2c403 100644 --- a/core/src/main/java/art/arcane/iris/Iris.java +++ b/core/src/main/java/art/arcane/iris/Iris.java @@ -515,6 +515,7 @@ public class Iris extends VolmitPlugin implements Listener { services.values().forEach(this::registerListener); addShutdownHook(); processPendingStartupWorldDeletes(); + IrisToolbelt.applyPregenPerformanceProfile(); if (J.isFolia()) { checkForBukkitWorlds(s -> true); 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 0b4b00375..469c7cbe5 100644 --- a/core/src/main/java/art/arcane/iris/core/IrisSettings.java +++ b/core/src/main/java/art/arcane/iris/core/IrisSettings.java @@ -152,6 +152,11 @@ public class IrisSettings { public boolean useVirtualThreads = false; public boolean useTicketQueue = true; public int maxConcurrency = 256; + public boolean startupNoisemapPrebake = true; + public boolean enablePregenPerformanceProfile = true; + public int pregenProfileNoiseCacheSize = 4_096; + public boolean pregenProfileEnableFastCache = true; + public boolean pregenProfileLogJvmHints = true; } @Data @@ -210,6 +215,9 @@ public class IrisSettings { public boolean commandSounds = true; public boolean debug = false; public boolean dumpMantleOnError = false; + public boolean validatePacksOnStartup = true; + public boolean stopStartupOnPackValidationFailure = false; + public int maxPackValidationErrorsPerPack = 200; public boolean disableNMS = false; public boolean pluginMetrics = true; public boolean splashLogoStartup = true; diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java b/core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java index 9ea35dd95..e2e392982 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandLazyPregen.java @@ -24,6 +24,7 @@ import art.arcane.iris.core.gui.PregeneratorJob; import art.arcane.iris.core.pregenerator.LazyPregenerator; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.common.director.DirectorExecutor; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -68,6 +69,11 @@ public class CommandLazyPregen implements DirectorExecutor { sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example."); } + PlatformChunkGenerator platform = IrisToolbelt.access(world); + if (platform != null) { + IrisToolbelt.applyPregenPerformanceProfile(platform.getEngine()); + } + LazyPregenerator.LazyPregenJob pregenJob = LazyPregenerator.LazyPregenJob.builder() .world(worldName) .healingPosition(0) diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java index 3dbd8da31..0376c18f2 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandStudio.java @@ -27,7 +27,9 @@ import art.arcane.iris.core.project.IrisProject; import art.arcane.iris.core.service.ConversionSVC; import art.arcane.iris.core.service.StudioSVC; import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.IrisNoisemapPrebakePipeline; import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.SeedManager; import art.arcane.iris.engine.object.*; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.collection.KList; @@ -63,6 +65,7 @@ import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import art.arcane.iris.util.common.scheduling.jobs.ParallelRadiusJob; import io.papermc.lib.PaperLib; import org.bukkit.*; +import org.bukkit.entity.Player; import org.bukkit.event.inventory.InventoryType; import org.bukkit.inventory.Inventory; import org.bukkit.util.BlockVector; @@ -76,8 +79,11 @@ import java.nio.file.Files; import java.nio.file.attribute.FileTime; import java.time.Duration; import java.time.temporal.ChronoUnit; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Date; +import java.util.LinkedHashSet; import java.util.Objects; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; @@ -512,6 +518,18 @@ public class CommandStudio implements DirectorExecutor { File report = Iris.instance.getDataFile("profile.txt"); IrisProject project = new IrisProject(pack); IrisData data = IrisData.get(pack); + PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension); + Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine(); + long profileSeed = IrisNoisemapPrebakePipeline.dynamicStartupSeed(); + + if (activeEngine != null) { + profileSeed = activeEngine.getSeedManager().getSeed(); + IrisToolbelt.applyPregenPerformanceProfile(activeEngine); + } else { + IrisToolbelt.applyPregenPerformanceProfile(); + } + + IrisNoisemapPrebakePipeline.prebake(data, new SeedManager(profileSeed), "studio-profile", dimension.getLoadKey()); KList fileText = new KList<>(); @@ -691,6 +709,116 @@ public class CommandStudio implements DirectorExecutor { sender().sendMessage(C.GREEN + "Done! " + report.getPath()); } + @Director(description = "Profiles a dimension with a cache warm-up pass", origin = DirectorOrigin.PLAYER) + public void profilecache( + @Param(description = "The dimension to profile", contextual = true, defaultValue = "default", customHandler = DimensionHandler.class) + IrisDimension dimension + ) { + File pack = dimension.getLoadFile().getParentFile().getParentFile(); + IrisData data = IrisData.get(pack); + PlatformChunkGenerator activeGenerator = resolveProfileGenerator(dimension); + Engine activeEngine = activeGenerator == null ? null : activeGenerator.getEngine(); + long profileSeed = IrisNoisemapPrebakePipeline.dynamicStartupSeed(); + + if (activeEngine != null) { + profileSeed = activeEngine.getSeedManager().getSeed(); + IrisToolbelt.applyPregenPerformanceProfile(activeEngine); + } else { + IrisToolbelt.applyPregenPerformanceProfile(); + } + + sender().sendMessage(C.YELLOW + "Warming noisemap cache for profile..."); + IrisNoisemapPrebakePipeline.prebakeForced(data, new SeedManager(profileSeed), "studio-profilecache", dimension.getLoadKey()); + sender().sendMessage(C.YELLOW + "Running measured profile pass..."); + profile(dimension); + } + + @Director(description = "List pack noise generators as pack/generator", aliases = {"pack-noise", "packnoises"}) + public void packnoise() { + LinkedHashSet packFolders = new LinkedHashSet<>(); + File packsFolder = Iris.instance.getDataFolder("packs"); + File[] children = packsFolder.listFiles(); + if (children != null) { + for (File child : children) { + if (child != null && child.isDirectory()) { + packFolders.add(child); + } + } + } + + StudioSVC studioService = Iris.service(StudioSVC.class); + if (studioService != null && studioService.isProjectOpen()) { + IrisProject activeProject = studioService.getActiveProject(); + if (activeProject != null && activeProject.getPath() != null && activeProject.getPath().isDirectory()) { + packFolders.add(activeProject.getPath()); + } + } + + ArrayList entries = new ArrayList<>(); + + for (File packFolder : packFolders) { + IrisData packData = IrisData.get(packFolder); + String packName = packFolder.getName(); + String[] keys = packData.getGeneratorLoader().getPossibleKeys(); + for (String key : keys) { + entries.add(packName + "/" + key); + } + } + + if (entries.isEmpty()) { + sender().sendMessage(C.YELLOW + "No pack noise generators were found."); + return; + } + + Collections.sort(entries); + sender().sendMessage(C.GREEN + "Pack noise generators: " + C.GOLD + entries.size()); + for (String entry : entries) { + sender().sendMessage(C.GRAY + entry); + } + } + + private PlatformChunkGenerator resolveProfileGenerator(IrisDimension dimension) { + StudioSVC studioService = Iris.service(StudioSVC.class); + if (studioService != null && studioService.isProjectOpen()) { + IrisProject activeProject = studioService.getActiveProject(); + if (activeProject != null) { + PlatformChunkGenerator activeProvider = activeProject.getActiveProvider(); + if (isGeneratorDimension(activeProvider, dimension)) { + return activeProvider; + } + } + } + + if (!sender().isPlayer()) { + return null; + } + + Player player = sender().player(); + if (player == null) { + return null; + } + + PlatformChunkGenerator worldAccess = IrisToolbelt.access(player.getWorld()); + if (isGeneratorDimension(worldAccess, dimension)) { + return worldAccess; + } + + return null; + } + + private boolean isGeneratorDimension(PlatformChunkGenerator generator, IrisDimension dimension) { + if (generator == null || generator.getEngine() == null || dimension == null || dimension.getLoadKey() == null) { + return false; + } + + IrisDimension engineDimension = generator.getEngine().getDimension(); + if (engineDimension == null || engineDimension.getLoadKey() == null) { + return false; + } + + return engineDimension.getLoadKey().equalsIgnoreCase(dimension.getLoadKey()); + } + @Director(description = "Spawn an Iris entity", aliases = "summon", origin = DirectorOrigin.PLAYER) public void spawn( @Param(description = "The entity to spawn") diff --git a/core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java b/core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java index b3f80e01a..69b11c1e8 100644 --- a/core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java +++ b/core/src/main/java/art/arcane/iris/core/commands/CommandTurboPregen.java @@ -21,7 +21,8 @@ package art.arcane.iris.core.commands; import art.arcane.iris.Iris; import art.arcane.iris.core.pregenerator.LazyPregenerator; import art.arcane.iris.core.pregenerator.TurboPregenerator; -import art.arcane.iris.core.pregenerator.TurboPregenerator; +import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.iris.util.common.director.DirectorExecutor; import art.arcane.volmlib.util.director.annotations.Director; import art.arcane.volmlib.util.director.annotations.Param; @@ -70,6 +71,11 @@ public class CommandTurboPregen implements DirectorExecutor { sender().sendMessage(C.RED + "Please make sure the world is loaded & the engine is initialized. Generate a new chunk, for example."); } + PlatformChunkGenerator platform = IrisToolbelt.access(world); + if (platform != null) { + IrisToolbelt.applyPregenPerformanceProfile(platform.getEngine()); + } + TurboPregenerator.TurboPregenJob pregenJob = TurboPregenerator.TurboPregenJob.builder() .world(worldName) .radiusBlocks(radius) diff --git a/core/src/main/java/art/arcane/iris/core/service/PackValidationSVC.java b/core/src/main/java/art/arcane/iris/core/service/PackValidationSVC.java new file mode 100644 index 000000000..1a969d0ba --- /dev/null +++ b/core/src/main/java/art/arcane/iris/core/service/PackValidationSVC.java @@ -0,0 +1,576 @@ +package art.arcane.iris.core.service; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.object.annotations.ArrayType; +import art.arcane.iris.engine.object.annotations.Snippet; +import art.arcane.iris.util.common.data.registry.KeyedRegistry; +import art.arcane.iris.util.common.data.registry.RegistryUtil; +import art.arcane.iris.util.common.plugin.IrisService; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.json.JSONArray; +import art.arcane.volmlib.util.json.JSONObject; +import org.bukkit.NamespacedKey; + +import java.io.File; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class PackValidationSVC implements IrisService { + private final Map, Map> fieldCache = new ConcurrentHashMap<>(); + private final Map, Boolean> keyedTypeCache = new ConcurrentHashMap<>(); + + @Override + public void onEnable() { + IrisSettings.IrisSettingsGeneral general = IrisSettings.get().getGeneral(); + if (!general.isValidatePacksOnStartup()) { + return; + } + + File packsFolder = Iris.instance.getDataFolder("packs"); + File[] packFolders = packsFolder.listFiles(File::isDirectory); + if (packFolders == null || packFolders.length == 0) { + Iris.info("Startup pack validation skipped: no pack folders found."); + return; + } + + int maxLoggedIssues = Math.max(1, general.getMaxPackValidationErrorsPerPack()); + int totalPacks = 0; + int totalFiles = 0; + int totalErrors = 0; + int packsWithErrors = 0; + long started = System.currentTimeMillis(); + + Iris.info("Startup pack validation started for %d pack(s).", packFolders.length); + + for (File packFolder : packFolders) { + totalPacks++; + PackValidationReport report = validatePack(packFolder, maxLoggedIssues); + totalFiles += report.filesChecked; + totalErrors += report.errorCount; + + if (report.errorCount > 0) { + packsWithErrors++; + Iris.error("Pack \"%s\" has %d validation issue(s) across %d file(s).", report.packName, report.errorCount, report.filesChecked); + for (String issue : report.sampleIssues) { + Iris.error(" - %s", issue); + } + + int hiddenIssues = report.errorCount - report.sampleIssues.size(); + if (hiddenIssues > 0) { + Iris.error(" - ... %d additional issue(s) not shown", hiddenIssues); + } + } + } + + long elapsed = System.currentTimeMillis() - started; + if (totalErrors == 0) { + Iris.info("Startup pack validation finished: %d pack(s), %d file(s), no issues (%dms).", totalPacks, totalFiles, elapsed); + return; + } + + Iris.error("Startup pack validation finished: %d issue(s) in %d pack(s), %d file(s) checked (%dms).", totalErrors, packsWithErrors, totalFiles, elapsed); + if (general.isStopStartupOnPackValidationFailure()) { + throw new IllegalStateException("Pack validation failed with " + totalErrors + " issue(s)."); + } + } + + @Override + public void onDisable() { + + } + + private PackValidationReport validatePack(File packFolder, int maxLoggedIssues) { + PackValidationReport report = new PackValidationReport(packFolder.getName(), maxLoggedIssues); + IrisData data = IrisData.get(packFolder); + Collection> loaders = data.getLoaders().values(); + + for (ResourceLoader loader : loaders) { + Class rootType = loader.getObjectClass(); + if (rootType == null) { + continue; + } + + List folders = loader.getFolders(); + for (File folder : folders) { + validateFolder(data, packFolder, folder, rootType, report); + } + } + + validateSnippetFolders(data, packFolder, report); + return report; + } + + private void validateSnippetFolders(IrisData data, File packFolder, PackValidationReport report) { + File snippetRoot = new File(packFolder, "snippet"); + if (!snippetRoot.isDirectory()) { + return; + } + + Map> snippetTypes = new HashMap<>(); + for (Class snippetType : data.resolveSnippets()) { + Snippet snippet = snippetType.getDeclaredAnnotation(Snippet.class); + if (snippet == null) { + continue; + } + snippetTypes.put(snippet.value(), snippetType); + } + + for (Map.Entry> entry : snippetTypes.entrySet()) { + File typeFolder = new File(snippetRoot, entry.getKey()); + if (!typeFolder.isDirectory()) { + continue; + } + + validateFolder(data, packFolder, typeFolder, entry.getValue(), report); + } + } + + private void validateFolder(IrisData data, File packFolder, File folder, Class rootType, PackValidationReport report) { + List jsonFiles = new ArrayList<>(); + collectJsonFiles(folder, jsonFiles); + + for (File jsonFile : jsonFiles) { + report.filesChecked++; + validateJsonFile(data, packFolder, jsonFile, rootType, report); + } + } + + private void collectJsonFiles(File folder, List output) { + File[] files = folder.listFiles(); + if (files == null) { + return; + } + + for (File file : files) { + if (file.isDirectory()) { + collectJsonFiles(file, output); + continue; + } + + if (file.isFile() && file.getName().endsWith(".json")) { + output.add(file); + } + } + } + + private void validateJsonFile(IrisData data, File packFolder, File file, Class rootType, PackValidationReport report) { + String content; + try { + content = IO.readAll(file); + } catch (Throwable e) { + report.addIssue(packFolder, file, "$", "Unable to read file: " + simpleMessage(e)); + return; + } + + JSONObject object; + try { + object = new JSONObject(content); + } catch (Throwable e) { + report.addIssue(packFolder, file, "$", "Invalid JSON syntax: " + simpleMessage(e)); + return; + } + + try { + Object decoded = data.getGson().fromJson(content, rootType); + if (decoded == null) { + report.addIssue(packFolder, file, "$", "Decoded value is null for root type " + rootType.getSimpleName()); + } + } catch (Throwable e) { + report.addIssue(packFolder, file, "$", "Deserializer rejected file for " + rootType.getSimpleName() + ": " + simpleMessage(e)); + } + + validateObject(packFolder, file, "$", object, rootType, report); + } + + private void validateObject(File packFolder, File file, String path, JSONObject object, Class type, PackValidationReport report) { + if (type == null) { + return; + } + + Map fields = getSerializableFields(type); + for (String key : object.keySet()) { + Field field = fields.get(key); + String keyPath = path + "." + key; + + if (field == null) { + report.addIssue(packFolder, file, keyPath, "Unknown or misplaced key for type " + type.getSimpleName()); + continue; + } + + Object value = object.get(key); + validateValue(packFolder, file, keyPath, value, field.getType(), field.getGenericType(), field, report); + } + } + + private void validateValue(File packFolder, File file, String path, Object value, Class expectedType, Type genericType, Field sourceField, PackValidationReport report) { + Class normalizedType = normalizeType(expectedType); + if (value == JSONObject.NULL) { + if (normalizedType.isPrimitive()) { + report.addIssue(packFolder, file, path, "Null value is not allowed for primitive type " + normalizedType.getSimpleName()); + } + return; + } + + if (Collection.class.isAssignableFrom(normalizedType)) { + validateCollection(packFolder, file, path, value, genericType, sourceField, report); + return; + } + + if (normalizedType.isArray()) { + validateArray(packFolder, file, path, value, normalizedType, report); + return; + } + + if (Map.class.isAssignableFrom(normalizedType)) { + validateMap(packFolder, file, path, value, genericType, report); + return; + } + + if (normalizedType.isEnum()) { + validateEnum(packFolder, file, path, value, normalizedType, report); + return; + } + + if (isKeyedType(normalizedType)) { + validateKeyed(packFolder, file, path, value, normalizedType, report); + return; + } + + if (normalizedType == String.class) { + if (!(value instanceof String)) { + report.addIssue(packFolder, file, path, "Expected string value"); + } + return; + } + + if (normalizedType == Boolean.class) { + if (!(value instanceof Boolean)) { + report.addIssue(packFolder, file, path, "Expected boolean value"); + } + return; + } + + if (Number.class.isAssignableFrom(normalizedType)) { + if (!(value instanceof Number)) { + report.addIssue(packFolder, file, path, "Expected numeric value"); + } + return; + } + + if (normalizedType == Character.class) { + if (!(value instanceof String text) || text.length() != 1) { + report.addIssue(packFolder, file, path, "Expected single-character string value"); + } + return; + } + + Snippet snippet = normalizedType.getDeclaredAnnotation(Snippet.class); + if (snippet != null) { + if (value instanceof String reference) { + if (!reference.startsWith("snippet/")) { + report.addIssue(packFolder, file, path, "Snippet reference must start with snippet/"); + } + return; + } + + if (value instanceof JSONObject jsonObject) { + validateObject(packFolder, file, path, jsonObject, normalizedType, report); + return; + } + + report.addIssue(packFolder, file, path, "Snippet value must be an object or snippet reference string"); + return; + } + + if (value instanceof JSONObject jsonObject) { + if (shouldValidateNestedObject(normalizedType)) { + validateObject(packFolder, file, path, jsonObject, normalizedType, report); + } + return; + } + + if (value instanceof JSONArray) { + report.addIssue(packFolder, file, path, "Unexpected array value for type " + normalizedType.getSimpleName()); + return; + } + + if (shouldValidateNestedObject(normalizedType)) { + report.addIssue(packFolder, file, path, "Expected object value for type " + normalizedType.getSimpleName()); + } + } + + private void validateCollection(File packFolder, File file, String path, Object value, Type genericType, Field sourceField, PackValidationReport report) { + if (!(value instanceof JSONArray array)) { + report.addIssue(packFolder, file, path, "Expected array value"); + return; + } + + Class elementType = resolveCollectionElementType(genericType, sourceField); + if (elementType == null || elementType == Object.class) { + return; + } + + for (int i = 0; i < array.length(); i++) { + Object element = array.get(i); + validateValue(packFolder, file, path + "[" + i + "]", element, elementType, null, null, report); + } + } + + private void validateArray(File packFolder, File file, String path, Object value, Class expectedType, PackValidationReport report) { + if (!(value instanceof JSONArray array)) { + report.addIssue(packFolder, file, path, "Expected array value"); + return; + } + + Class componentType = normalizeType(expectedType.getComponentType()); + for (int i = 0; i < array.length(); i++) { + Object element = array.get(i); + validateValue(packFolder, file, path + "[" + i + "]", element, componentType, null, null, report); + } + } + + private void validateMap(File packFolder, File file, String path, Object value, Type genericType, PackValidationReport report) { + if (!(value instanceof JSONObject object)) { + report.addIssue(packFolder, file, path, "Expected object value"); + return; + } + + Class valueType = resolveMapValueType(genericType); + if (valueType == null || valueType == Object.class) { + return; + } + + for (String key : object.keySet()) { + Object child = object.get(key); + validateValue(packFolder, file, path + "." + key, child, valueType, null, null, report); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void validateEnum(File packFolder, File file, String path, Object value, Class expectedType, PackValidationReport report) { + if (!(value instanceof String text)) { + report.addIssue(packFolder, file, path, "Expected enum string for " + expectedType.getSimpleName()); + return; + } + + try { + Enum.valueOf((Class) expectedType, text); + } catch (Throwable e) { + report.addIssue(packFolder, file, path, "Unknown enum value \"" + text + "\" for " + expectedType.getSimpleName()); + } + } + + @SuppressWarnings("unchecked") + private void validateKeyed(File packFolder, File file, String path, Object value, Class expectedType, PackValidationReport report) { + if (!(value instanceof String text)) { + report.addIssue(packFolder, file, path, "Expected namespaced key string for " + expectedType.getSimpleName()); + return; + } + + NamespacedKey key = NamespacedKey.fromString(text); + if (key == null) { + report.addIssue(packFolder, file, path, "Invalid namespaced key format \"" + text + "\""); + return; + } + + KeyedRegistry registry; + try { + registry = RegistryUtil.lookup((Class) expectedType); + } catch (Throwable e) { + report.addIssue(packFolder, file, path, "Unable to resolve keyed registry for " + expectedType.getSimpleName() + ": " + simpleMessage(e)); + return; + } + + if (registry.isEmpty()) { + return; + } + + if (registry.get(key) == null) { + report.addIssue(packFolder, file, path, "Unknown registry key \"" + text + "\" for " + expectedType.getSimpleName()); + } + } + + private Map getSerializableFields(Class type) { + return fieldCache.computeIfAbsent(type, this::buildSerializableFields); + } + + private Map buildSerializableFields(Class type) { + Map fields = new LinkedHashMap<>(); + Class cursor = type; + + while (cursor != null && cursor != Object.class) { + Field[] declared = cursor.getDeclaredFields(); + for (Field field : declared) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers) || field.isSynthetic()) { + continue; + } + + fields.putIfAbsent(field.getName(), field); + } + cursor = cursor.getSuperclass(); + } + + return fields; + } + + private Class resolveCollectionElementType(Type genericType, Field sourceField) { + if (sourceField != null) { + ArrayType arrayType = sourceField.getDeclaredAnnotation(ArrayType.class); + if (arrayType != null && arrayType.type() != Object.class) { + return arrayType.type(); + } + } + + if (genericType instanceof ParameterizedType parameterizedType) { + Type[] arguments = parameterizedType.getActualTypeArguments(); + if (arguments.length > 0) { + Class resolved = resolveType(arguments[0]); + if (resolved != null) { + return resolved; + } + } + } + + return Object.class; + } + + private Class resolveMapValueType(Type genericType) { + if (genericType instanceof ParameterizedType parameterizedType) { + Type[] arguments = parameterizedType.getActualTypeArguments(); + if (arguments.length > 1) { + Class resolved = resolveType(arguments[1]); + if (resolved != null) { + return resolved; + } + } + } + + return Object.class; + } + + private Class resolveType(Type type) { + if (type instanceof Class clazz) { + return normalizeType(clazz); + } + + if (type instanceof ParameterizedType parameterizedType && parameterizedType.getRawType() instanceof Class clazz) { + return normalizeType(clazz); + } + + if (type instanceof WildcardType wildcardType) { + Type[] upperBounds = wildcardType.getUpperBounds(); + if (upperBounds.length > 0) { + return resolveType(upperBounds[0]); + } + } + + if (type instanceof GenericArrayType genericArrayType) { + Class component = resolveType(genericArrayType.getGenericComponentType()); + if (component != null) { + return normalizeType(Array.newInstance(component, 0).getClass()); + } + } + + return null; + } + + private Class normalizeType(Class type) { + if (type == null) { + return Object.class; + } + + if (!type.isPrimitive()) { + return type; + } + + if (type == int.class) return Integer.class; + if (type == long.class) return Long.class; + if (type == double.class) return Double.class; + if (type == float.class) return Float.class; + if (type == short.class) return Short.class; + if (type == byte.class) return Byte.class; + if (type == boolean.class) return Boolean.class; + if (type == char.class) return Character.class; + return type; + } + + private boolean shouldValidateNestedObject(Class type) { + String name = type.getName(); + return name.startsWith("art.arcane.iris."); + } + + private boolean isKeyedType(Class type) { + return keyedTypeCache.computeIfAbsent(type, this::resolveKeyedType); + } + + @SuppressWarnings("unchecked") + private boolean resolveKeyedType(Class type) { + try { + KeyedRegistry registry = RegistryUtil.lookup((Class) type); + return !registry.isEmpty(); + } catch (Throwable ignored) { + return false; + } + } + + private String simpleMessage(Throwable throwable) { + String message = throwable.getMessage(); + if (message == null || message.trim().isEmpty()) { + return throwable.getClass().getSimpleName(); + } + + return throwable.getClass().getSimpleName() + ": " + message; + } + + private String relativePath(File packFolder, File file) { + try { + Path packPath = packFolder.toPath(); + Path filePath = file.toPath(); + return packPath.relativize(filePath).toString().replace(File.separatorChar, '/'); + } catch (Throwable ignored) { + return file.getPath(); + } + } + + private final class PackValidationReport { + private final String packName; + private final int maxIssues; + private final List sampleIssues; + private int filesChecked; + private int errorCount; + + private PackValidationReport(String packName, int maxIssues) { + this.packName = packName; + this.maxIssues = maxIssues; + this.sampleIssues = new ArrayList<>(); + } + + private void addIssue(File packFolder, File file, String path, String message) { + errorCount++; + if (sampleIssues.size() >= maxIssues) { + return; + } + + String relative = relativePath(packFolder, file); + sampleIssues.add(relative + " " + path + " -> " + message); + } + } +} 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 aefbe9e8f..cfb6fcf4a 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 @@ -23,10 +23,13 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.link.FoliaWorldsLink; +import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.nms.INMS; import art.arcane.iris.core.pregenerator.PregenTask; import art.arcane.iris.core.service.BoardSVC; import art.arcane.iris.core.service.StudioSVC; +import art.arcane.iris.engine.IrisNoisemapPrebakePipeline; +import art.arcane.iris.engine.framework.SeedManager; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.exceptions.IrisException; @@ -147,6 +150,7 @@ public class IrisCreator { if (!studio() || benchmark) { Iris.service(StudioSVC.class).installIntoWorld(sender, d.getLoadKey(), new File(Bukkit.getWorldContainer(), name())); } + prebakeNoisemapsBeforeWorldCreate(d); AtomicDouble pp = new AtomicDouble(0); O done = new O<>(); @@ -279,6 +283,30 @@ public class IrisCreator { return world; } + private void prebakeNoisemapsBeforeWorldCreate(IrisDimension dimension) { + IrisSettings.IrisSettingsPregen pregenSettings = IrisSettings.get().getPregen(); + if (!pregenSettings.isStartupNoisemapPrebake()) { + return; + } + + try { + File targetDataFolder = new File(Bukkit.getWorldContainer(), name()); + if (studio() && !benchmark) { + IrisData studioData = dimension.getLoader(); + if (studioData != null) { + targetDataFolder = studioData.getDataFolder(); + } + } + + IrisData targetData = IrisData.get(targetDataFolder); + SeedManager seedManager = new SeedManager(seed()); + IrisNoisemapPrebakePipeline.prebake(targetData, seedManager, name(), dimension.getLoadKey()); + } catch (Throwable throwable) { + Iris.warn("Failed pre-create noisemap pre-bake for " + name() + "/" + dimension.getLoadKey() + ": " + throwable.getMessage()); + Iris.reportError(throwable); + } + } + private Location resolveStudioEntryLocation(World world) { CompletableFuture locationFuture = J.sfut(() -> { Location spawnLocation = world.getSpawnLocation(); 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 99b7117dd..cb148b600 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 @@ -48,6 +48,7 @@ import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; /** @@ -60,6 +61,7 @@ public class IrisToolbelt { private static final Map worldMaintenanceDepth = new ConcurrentHashMap<>(); private static final Map worldMaintenanceMantleBypassDepth = new ConcurrentHashMap<>(); private static final Method BUKKIT_IS_STOPPING_METHOD = resolveBukkitIsStoppingMethod(); + private static final AtomicBoolean PREGEN_PROFILE_JVM_HINT_LOGGED = new AtomicBoolean(false); /** * Will find / download / search for the dimension or return null @@ -230,10 +232,51 @@ public class IrisToolbelt { * @return the pregenerator job (already started) */ public static PregeneratorJob pregenerate(PregenTask task, PregeneratorMethod method, Engine engine, boolean cached) { + applyPregenPerformanceProfile(engine); boolean useCachedWrapper = cached && engine != null && !J.isFolia(); return new PregeneratorJob(task, useCachedWrapper ? new CachedPregenMethod(method, engine.getWorld().name()) : method, engine); } + public static boolean applyPregenPerformanceProfile() { + IrisSettings.IrisSettingsPregen pregen = IrisSettings.get().getPregen(); + if (!pregen.isEnablePregenPerformanceProfile()) { + return false; + } + + IrisSettings.IrisSettingsPerformance performance = IrisSettings.get().getPerformance(); + int previousNoiseCacheSize = performance.getNoiseCacheSize(); + int targetNoiseCacheSize = Math.max(previousNoiseCacheSize, Math.max(1, pregen.getPregenProfileNoiseCacheSize())); + boolean fastCacheEnabledBefore = Boolean.getBoolean("iris.cache.fast"); + boolean changed = false; + + if (targetNoiseCacheSize != previousNoiseCacheSize) { + performance.setNoiseCacheSize(targetNoiseCacheSize); + changed = true; + } + + if (pregen.isPregenProfileEnableFastCache() && !fastCacheEnabledBefore) { + System.setProperty("iris.cache.fast", "true"); + changed = true; + } + + if (pregen.isPregenProfileLogJvmHints() + && pregen.isPregenProfileEnableFastCache() + && PREGEN_PROFILE_JVM_HINT_LOGGED.compareAndSet(false, true) + && !fastCacheEnabledBefore) { + Iris.info("For startup-wide cache-fast coverage, set JVM argument: -Diris.cache.fast=true"); + } + + return changed; + } + + public static void applyPregenPerformanceProfile(Engine engine) { + boolean changed = applyPregenPerformanceProfile(); + if (changed && engine != null) { + engine.hotloadComplex(); + Iris.info("Pregen profile applied: noiseCacheSize=" + IrisSettings.get().getPerformance().getNoiseCacheSize() + " iris.cache.fast=" + Boolean.getBoolean("iris.cache.fast")); + } + } + /** * Start a pregenerator task. If the supplied generator is headless, headless mode is used, * otherwise Hybrid mode is used. diff --git a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java index 5b3bf57ee..6f7c358a2 100644 --- a/core/src/main/java/art/arcane/iris/engine/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/engine/IrisEngine.java @@ -21,6 +21,7 @@ package art.arcane.iris.engine; import com.google.common.util.concurrent.AtomicDouble; import com.google.gson.Gson; import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.ServerConfigurator; import art.arcane.iris.core.events.IrisEngineHotloadEvent; import art.arcane.iris.core.gui.PregeneratorJob; @@ -91,6 +92,7 @@ public class IrisEngine implements Engine { private final int art; private final AtomicCache engineData = new AtomicCache<>(); private final AtomicBoolean cleaning; + private final AtomicBoolean noisemapPrebakeRunning; private final ChronoLatch cleanLatch; private final SeedManager seedManager; private CompletableFuture hash32; @@ -127,6 +129,7 @@ public class IrisEngine implements Engine { mantle = new IrisEngineMantle(this); context = new IrisContext(this); cleaning = new AtomicBoolean(false); + noisemapPrebakeRunning = new AtomicBoolean(false); execution = getData().getEnvironment().with(this); if (studio) { getData().dump(); @@ -195,6 +198,7 @@ public class IrisEngine implements Engine { .toArray(File[]::new); hash32.complete(IO.hashRecursive(roots)); }); + scheduleStartupNoisemapPrebake(); } catch (Throwable e) { Iris.error("FAILED TO SETUP ENGINE!"); e.printStackTrace(); @@ -211,6 +215,26 @@ public class IrisEngine implements Engine { mode = getDimension().getMode().create(this); } + private void scheduleStartupNoisemapPrebake() { + if (!IrisSettings.get().getPregen().isStartupNoisemapPrebake()) { + return; + } + + if (!noisemapPrebakeRunning.compareAndSet(false, true)) { + return; + } + + J.a(() -> { + try { + IrisNoisemapPrebakePipeline.prebake(this); + } catch (Throwable throwable) { + Iris.reportError(throwable); + } finally { + noisemapPrebakeRunning.set(false); + } + }); + } + @Override public void generateMatter(int x, int z, boolean multicore, ChunkContext context) { getMantle().generateMatter(x, z, multicore, context); diff --git a/core/src/main/java/art/arcane/iris/engine/IrisNoisemapPrebakePipeline.java b/core/src/main/java/art/arcane/iris/engine/IrisNoisemapPrebakePipeline.java new file mode 100644 index 000000000..8155330cf --- /dev/null +++ b/core/src/main/java/art/arcane/iris/engine/IrisNoisemapPrebakePipeline.java @@ -0,0 +1,973 @@ +package art.arcane.iris.engine; + +import art.arcane.iris.Iris; +import art.arcane.iris.core.IrisSettings; +import art.arcane.iris.core.loader.IrisData; +import art.arcane.iris.core.loader.IrisRegistrant; +import art.arcane.iris.core.loader.ResourceLoader; +import art.arcane.iris.engine.framework.Engine; +import art.arcane.iris.engine.framework.SeedManager; +import art.arcane.iris.engine.object.IrisGeneratorStyle; +import art.arcane.iris.util.common.misc.ServerProperties; +import art.arcane.iris.util.common.parallel.BurstExecutor; +import art.arcane.iris.util.common.parallel.MultiBurst; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.format.Form; +import art.arcane.volmlib.util.io.IO; +import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; +import org.bukkit.Bukkit; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; + +public final class IrisNoisemapPrebakePipeline { + private static final long[] NO_SEEDS = new long[0]; + private static final long STARTUP_PROGRESS_INTERVAL_MS = Long.getLong("iris.prebake.progress.interval", 30000L); + private static final int STATE_VERSION = 1; + private static final String STATE_FILE = "noisemap-prebake.state"; + private static final AtomicInteger STARTUP_WORKER_SEQUENCE = new AtomicInteger(); + private static final ConcurrentHashMap, Field[]> FIELD_CACHE = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap SKIP_ONCE = new ConcurrentHashMap<>(); + private static final Set PREBAKE_LOADERS = Set.of( + "dimensions", + "regions", + "biomes", + "generators", + "caves", + "ravines", + "jigsaw-structures", + "mods", + "expressions" + ); + private static final Set PREBAKE_STATE_FOLDERS = Set.of( + "dimensions", + "regions", + "biomes", + "generators", + "caves", + "ravines", + "jigsaw-structures", + "jigsaw-pools", + "jigsaw-pieces", + "mods", + "expressions", + "scripts", + "images", + "snippet" + ); + + private IrisNoisemapPrebakePipeline() { + } + + public static void prebakeInstalledPacksAtStartup() { + IrisSettings.IrisSettingsPregen settings = IrisSettings.get().getPregen(); + + List targets = collectStartupTargets(); + if (targets.isEmpty()) { + Iris.info("Startup noisemap pre-bake skipped (no installed or self-contained packs found)."); + return; + } + + PrecisionStopwatch stopwatch = PrecisionStopwatch.start(); + long startupSeed = dynamicStartupSeed(); + SeedManager seedManager = new SeedManager(startupSeed); + int targetCount = targets.size(); + int workerCount = Math.min(targetCount, Math.max(1, IrisSettings.getThreadCount(IrisSettings.get().getConcurrency().getParallelism()))); + ExecutorService workers = Executors.newFixedThreadPool(workerCount, new StartupPrebakeThreadFactory()); + CompletionService completion = new ExecutorCompletionService<>(workers); + StartupProgress progress = new StartupProgress(targetCount); + Iris.info("Startup pack noisemap pre-bake running in background targets=" + targetCount + " workers=" + workerCount + " seed=" + startupSeed); + + for (PrebakeTarget target : targets) { + completion.submit(() -> new StartupTargetResult(target.label, + prebake(IrisData.get(target.folder), seedManager, "startup", target.label, () -> false, false, false, progress))); + } + + int completedTargets = 0; + long lastProgress = System.currentTimeMillis(); + while (completedTargets < targetCount) { + try { + Future future = completion.poll(2, TimeUnit.SECONDS); + if (future != null) { + StartupTargetResult result = future.get(); + completedTargets++; + progress.onTargetCompleted(result.result); + } + } catch (Throwable throwable) { + completedTargets++; + progress.onTargetFailed(); + Iris.reportError(throwable); + } + + long now = System.currentTimeMillis(); + if (completedTargets < targetCount && now - lastProgress >= STARTUP_PROGRESS_INTERVAL_MS) { + logStartupProgress(progress, stopwatch); + lastProgress = now; + } + } + + workers.shutdownNow(); + + Iris.info("Startup pack noisemap pre-bake scan completed targets=" + + targetCount + + " executed=" + + progress.executedTargets.get() + + " unchanged=" + + progress.unchangedTargets.get() + + " failed=" + + progress.failedTargets.get() + + " styles=" + + progress.stylesFinished.get() + + "/" + + progress.stylesDiscovered.get() + + " seed=" + + startupSeed + + " in " + + Form.duration(stopwatch.getMilliseconds(), 2)); + } + + public static int clearInstalledPackPrebakeStates() { + int cleared = 0; + List targets = collectStartupTargets(); + for (PrebakeTarget target : targets) { + File state = stateFile(target.folder); + if (state.exists() && state.delete()) { + cleared++; + } + } + + return cleared; + } + + private static void logStartupProgress(StartupProgress progress, PrecisionStopwatch stopwatch) { + int targetCount = progress.targetTotal; + if (targetCount <= 0) { + return; + } + + int completedTargets = progress.targetsCompleted.get(); + int discoveredStyles = progress.stylesDiscovered.get(); + int finishedStyles = progress.stylesFinished.get(); + int executedTargets = progress.executedTargets.get(); + int unchangedTargets = progress.unchangedTargets.get(); + int failedTargets = progress.failedTargets.get(); + double elapsed = stopwatch.getMilliseconds(); + if (discoveredStyles > 0) { + int remainingStyles = Math.max(0, discoveredStyles - finishedStyles); + int percent = (int) Math.round((finishedStyles * 100D) / discoveredStyles); + String eta = "estimating"; + if (finishedStyles > 0) { + long etaMillis = Math.max(0L, Math.round((elapsed / finishedStyles) * remainingStyles)); + eta = Form.duration(etaMillis, 2); + } + + Iris.info("Startup noisemap pre-bake progress " + + percent + + "% styles=" + + finishedStyles + + "/" + + discoveredStyles + + " targets=" + + completedTargets + + "/" + + targetCount + + " remaining=" + + remainingStyles + + " executed=" + + executedTargets + + " unchanged=" + + unchangedTargets + + " failed=" + + failedTargets + + " elapsed=" + + Form.duration(elapsed, 2) + + " eta=" + + eta); + return; + } + + int remainingTargets = Math.max(0, targetCount - completedTargets); + int percent = (int) Math.round((completedTargets * 100D) / targetCount); + String eta = "estimating"; + if (completedTargets > 0) { + long etaMillis = Math.max(0L, Math.round((elapsed / completedTargets) * remainingTargets)); + eta = Form.duration(etaMillis, 2); + } + + Iris.info("Startup noisemap pre-bake progress " + + percent + + "% targets=" + + completedTargets + + "/" + + targetCount + + " remaining=" + + remainingTargets + + " executed=" + + executedTargets + + " unchanged=" + + unchangedTargets + + " failed=" + + failedTargets + + " elapsed=" + + Form.duration(elapsed, 2) + + " eta=" + + eta); + } + + public static long dynamicStartupSeed() { + if (!Bukkit.getWorlds().isEmpty()) { + return Bukkit.getWorlds().get(0).getSeed(); + } + + String configuredSeed = ServerProperties.DATA.getProperty("level-seed", "").trim(); + if (!configuredSeed.isEmpty()) { + try { + return Long.parseLong(configuredSeed); + } catch (NumberFormatException ignored) { + return mixSeed(0x9E3779B97F4A7C15L, configuredSeed.hashCode()); + } + } + + return mixSeed(0x94D049BB133111EBL, ServerProperties.LEVEL_NAME.hashCode()); + } + + public static void prebake(Engine engine) { + if (engine == null || engine.isClosed()) { + return; + } + + if (!IrisSettings.get().getPregen().isStartupNoisemapPrebake()) { + return; + } + + prebake(engine.getData(), + engine.getSeedManager(), + engine.getWorld().name(), + engine.getDimension().getLoadKey(), + engine::isClosed, + false, + true, + null); + } + + public static void prebake(IrisData data, SeedManager seedManager, String worldName, String dimensionKey) { + if (!IrisSettings.get().getPregen().isStartupNoisemapPrebake()) { + return; + } + + prebake(data, seedManager, worldName, dimensionKey, () -> false, true, true, null); + } + + public static void prebakeForced(IrisData data, SeedManager seedManager, String worldName, String dimensionKey) { + prebake(data, seedManager, worldName, dimensionKey, () -> false, false, true, null); + } + + private static PrebakeRunResult prebake(IrisData data, + SeedManager seedManager, + String worldName, + String dimensionKey, + BooleanSupplier shouldAbort, + boolean primeSkipOnce, + boolean logResult, + StartupProgress progress) { + if (data == null || seedManager == null) { + return PrebakeRunResult.SKIPPED; + } + + if (shouldAbort != null && shouldAbort.getAsBoolean()) { + return PrebakeRunResult.SKIPPED; + } + + PrecisionStopwatch stopwatch = PrecisionStopwatch.start(); + String safeWorldName = worldName == null ? "unknown" : worldName; + String safeDimensionKey = dimensionKey == null ? "unknown" : dimensionKey; + boolean exhaustive = dynamicExhaustivePrebakeMode(); + int fallbackCacheSize = dynamicFallbackCacheSize(); + String key = prebakeKey(data, seedManager, safeDimensionKey, exhaustive, fallbackCacheSize); + + if (SKIP_ONCE.remove(key) != null) { + if (logResult) { + Iris.info("Startup noisemap pre-bake skipped for " + safeWorldName + "/" + safeDimensionKey + " (already pre-baked before engine init)."); + } + return PrebakeRunResult.SKIPPED; + } + + String stateToken = prebakeStateToken(data, seedManager, exhaustive, fallbackCacheSize); + if (isCurrentState(data, stateToken)) { + if (primeSkipOnce) { + SKIP_ONCE.put(key, Boolean.TRUE); + } + if (logResult) { + Iris.info("Startup noisemap pre-bake skipped for " + safeWorldName + "/" + safeDimensionKey + " (unchanged pack state)."); + } + return PrebakeRunResult.UNCHANGED; + } + + List styles = collectStyles(data, fallbackCacheSize, progress); + int styleCount = styles.size(); + + if (styleCount == 0) { + writeCurrentState(data, stateToken); + if (primeSkipOnce) { + SKIP_ONCE.put(key, Boolean.TRUE); + } + if (logResult) { + Iris.info("Startup noisemap pre-bake skipped for " + safeWorldName + "/" + safeDimensionKey + " (no cacheable styles found)."); + } + return PrebakeRunResult.SKIPPED; + } + + long[] domainSeeds = collectDomainSeeds(seedManager); + if (domainSeeds.length == 0) { + if (logResult) { + Iris.warn("Startup noisemap pre-bake skipped for " + safeWorldName + "/" + safeDimensionKey + " (no seed domains)."); + } + return PrebakeRunResult.SKIPPED; + } + + AtomicInteger prebakedStyles = new AtomicInteger(); + AtomicInteger prebakedVariants = new AtomicInteger(); + AtomicInteger failures = new AtomicInteger(); + BurstExecutor burst = MultiBurst.burst.burst(styleCount); + + for (StyleReference reference : styles) { + burst.queue(() -> { + if (shouldAbort != null && shouldAbort.getAsBoolean()) { + if (progress != null) { + progress.onStyleFinished(); + } + return; + } + + try { + int baked = prebakeStyle(reference, data, domainSeeds, exhaustive, fallbackCacheSize); + if (baked <= 0) { + return; + } + + prebakedStyles.incrementAndGet(); + prebakedVariants.addAndGet(baked); + } catch (Throwable throwable) { + failures.incrementAndGet(); + Iris.reportError(throwable); + } finally { + if (progress != null) { + progress.onStyleFinished(); + } + } + }); + } + + burst.complete(); + + if (failures.get() == 0) { + writeCurrentState(data, stateToken); + if (primeSkipOnce) { + SKIP_ONCE.put(key, Boolean.TRUE); + } + } + + if (logResult) { + Iris.info("Startup noisemap pre-bake completed for " + + safeWorldName + + "/" + + safeDimensionKey + + " styles=" + + styleCount + + " prebaked=" + + prebakedStyles.get() + + " variants=" + + prebakedVariants.get() + + " failures=" + + failures.get() + + " mode=" + + (exhaustive ? "exhaustive" : "targeted") + + " fallback=" + + fallbackCacheSize + + " in " + + Form.duration(stopwatch.getMilliseconds(), 2)); + } + return new PrebakeRunResult(true, false, failures.get()); + } + + private static boolean dynamicExhaustivePrebakeMode() { + int cores = Runtime.getRuntime().availableProcessors(); + long memoryGiB = Runtime.getRuntime().maxMemory() / (1024L * 1024L * 1024L); + return cores >= 24 && memoryGiB >= 64; + } + + private static int dynamicFallbackCacheSize() { + int cores = Runtime.getRuntime().availableProcessors(); + long memoryGiB = Runtime.getRuntime().maxMemory() / (1024L * 1024L * 1024L); + + if (memoryGiB >= 24 && cores >= 12) { + return 64; + } + + if (memoryGiB >= 16 && cores >= 8) { + return 48; + } + + if (memoryGiB >= 8 && cores >= 6) { + return 40; + } + + if (memoryGiB >= 4 && cores >= 4) { + return 32; + } + + return 24; + } + + private static String prebakeStateToken(IrisData data, SeedManager seedManager, boolean exhaustive, int fallbackCacheSize) { + File[] roots = prebakeStateRoots(data); + long stateHash = roots.length == 0 ? 0L : IO.hashRecursive(roots); + long seedHash = hashSeeds(collectDomainSeeds(seedManager)); + return STATE_VERSION + + "|" + + Long.toUnsignedString(stateHash) + + "|" + + Long.toUnsignedString(seedHash) + + "|" + + (exhaustive ? "X" : "T") + + "|" + + fallbackCacheSize; + } + + private static File[] prebakeStateRoots(IrisData data) { + File dataFolder = data.getDataFolder(); + List roots = new ArrayList<>(); + + for (String folder : PREBAKE_STATE_FOLDERS) { + File candidate = new File(dataFolder, folder); + if (candidate.exists()) { + roots.add(candidate); + } + } + + roots.sort(Comparator.comparing(File::getName)); + return roots.toArray(new File[0]); + } + + private static long hashSeeds(long[] seeds) { + long hash = 1125899906842597L; + for (long seed : seeds) { + hash = (hash * 31L) ^ seed; + } + return hash; + } + + private static File stateFile(IrisData data) { + return stateFile(data.getDataFolder()); + } + + private static File stateFile(File dataFolder) { + return new File(new File(dataFolder, ".cache"), STATE_FILE); + } + + private static boolean isCurrentState(IrisData data, String token) { + File stateFile = stateFile(data); + if (!stateFile.exists()) { + return false; + } + + Properties properties = new Properties(); + try (FileInputStream input = new FileInputStream(stateFile)) { + properties.load(input); + } catch (IOException e) { + return false; + } + + String previous = properties.getProperty("token"); + return token.equals(previous); + } + + private static void writeCurrentState(IrisData data, String token) { + File stateFile = stateFile(data); + File parent = stateFile.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs()) { + return; + } + + Properties properties = new Properties(); + properties.setProperty("token", token); + properties.setProperty("updated", Long.toString(System.currentTimeMillis())); + + try (FileOutputStream output = new FileOutputStream(stateFile)) { + properties.store(output, "Iris noisemap prebake state"); + } catch (IOException ignored) { + } + } + + private static String prebakeKey(IrisData data, SeedManager seedManager, String dimensionKey, boolean exhaustive, int fallbackCacheSize) { + return data.getDataFolder().getAbsolutePath() + + "|" + + seedManager.getSeed() + + "|" + + dimensionKey + + "|" + + fallbackCacheSize + + "|" + + (exhaustive ? "X" : "T"); + } + + private static int prebakeStyle(StyleReference reference, IrisData data, long[] domainSeeds, boolean exhaustive, int fallbackCacheSize) { + IrisGeneratorStyle style = reference.style; + if (!isCacheable(style, fallbackCacheSize)) { + return 0; + } + + int styleHash = reference.hash; + + if (exhaustive) { + int variants = 0; + + for (int i = 0; i < domainSeeds.length; i++) { + long mixedSeed = mixSeed(domainSeeds[i], styleHash + (i * 131)); + CNG baked = style.createForPrebake(new RNG(mixedSeed), data, fallbackCacheSize); + if (baked != null) { + variants++; + } + } + + return variants; + } + + int index = Math.floorMod(styleHash, domainSeeds.length); + long primarySeed = mixSeed(domainSeeds[index], styleHash); + int variants = 0; + + CNG primary = style.createForPrebake(new RNG(primarySeed), data, fallbackCacheSize); + if (primary != null) { + variants++; + } + + return variants; + } + + private static boolean isCacheable(IrisGeneratorStyle style, int fallbackCacheSize) { + if (style == null) { + return false; + } + return style.getCacheSize() > 0 || fallbackCacheSize > 0; + } + + private static List collectStyles(IrisData data, int fallbackCacheSize, StartupProgress progress) { + LinkedHashMap styles = new LinkedHashMap<>(); + Collection> loaders = data.getLoaders().values(); + + for (ResourceLoader loader : loaders) { + if (!PREBAKE_LOADERS.contains(loader.getFolderName())) { + continue; + } + + String[] keys = loader.getPossibleKeys(); + + for (String key : keys) { + IrisRegistrant registrant = loader.load(key, false); + if (registrant == null) { + continue; + } + + String rootPath = loader.getFolderName() + ":" + key; + collectFromObject(registrant, rootPath, styles, fallbackCacheSize, progress); + } + } + + return new ArrayList<>(styles.values()); + } + + private static void collectFromObject(Object root, String rootPath, LinkedHashMap styles, int fallbackCacheSize, StartupProgress progress) { + if (root == null) { + return; + } + + IdentityHashMap visited = new IdentityHashMap<>(); + ArrayDeque queue = new ArrayDeque<>(); + queue.add(new Node(root, rootPath)); + + while (!queue.isEmpty()) { + Node node = queue.removeFirst(); + Object value = node.value; + if (value == null) { + continue; + } + + if (value instanceof IrisGeneratorStyle style) { + if (isCacheable(style, fallbackCacheSize)) { + int styleSignature = style.prebakeSignature(); + if (styles.putIfAbsent(styleSignature, new StyleReference(style, styleSignature)) == null && progress != null) { + progress.onStylesDiscovered(1); + } + } + } + + Class type = value.getClass(); + if (isLeafType(type)) { + continue; + } + + if (visited.put(value, Boolean.TRUE) != null) { + continue; + } + + if (type.isArray()) { + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + Object element = Array.get(value, i); + queue.addLast(new Node(element, node.path + "[" + i + "]")); + } + continue; + } + + if (value instanceof Iterable iterable) { + int index = 0; + for (Object element : iterable) { + queue.addLast(new Node(element, node.path + "[" + index + "]")); + index++; + } + continue; + } + + if (value instanceof Map map) { + for (Map.Entry entry : map.entrySet()) { + Object key = entry.getKey(); + Object entryValue = entry.getValue(); + if (key != null) { + queue.addLast(new Node(key, node.path + "{k}")); + } + if (entryValue != null) { + queue.addLast(new Node(entryValue, node.path + "{v}")); + } + } + continue; + } + + Field[] fields = fieldsOf(type); + for (Field field : fields) { + if (skipField(field)) { + continue; + } + + Object fieldValue; + try { + if (!field.canAccess(value)) { + field.setAccessible(true); + } + fieldValue = field.get(value); + } catch (Throwable ignored) { + continue; + } + + if (fieldValue == null) { + continue; + } + + queue.addLast(new Node(fieldValue, node.path + "." + field.getName())); + } + } + } + + private static List collectStartupTargets() { + LinkedHashMap targets = new LinkedHashMap<>(); + File packsFolder = Iris.instance.getDataFolder("packs"); + File[] packs = packsFolder.listFiles(); + if (packs != null) { + Arrays.sort(packs, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); + for (File pack : packs) { + addStartupTarget(targets, pack); + } + } + + File worldContainer = Bukkit.getWorldContainer(); + File[] worlds = worldContainer.listFiles(); + if (worlds != null) { + Arrays.sort(worlds, Comparator.comparing(File::getName, String.CASE_INSENSITIVE_ORDER)); + for (File world : worlds) { + if (world == null || !world.isDirectory()) { + continue; + } + + File selfContainedPack = new File(world, "iris/pack"); + addStartupTarget(targets, selfContainedPack); + } + } + + return new ArrayList<>(targets.values()); + } + + private static void addStartupTarget(LinkedHashMap targets, File folder) { + if (!isPrebakeDataFolder(folder)) { + return; + } + + String canonicalPath = toCanonicalPath(folder); + if (targets.containsKey(canonicalPath)) { + return; + } + + targets.put(canonicalPath, new PrebakeTarget(folder, startupLabel(folder))); + } + + private static boolean isPrebakeDataFolder(File folder) { + if (folder == null || !folder.exists() || !folder.isDirectory()) { + return false; + } + + for (String loaderFolder : PREBAKE_LOADERS) { + File candidate = new File(folder, loaderFolder); + if (candidate.exists() && candidate.isDirectory()) { + return true; + } + } + + return false; + } + + private static String startupLabel(File folder) { + File parent = folder.getParentFile(); + if (parent != null && "iris".equalsIgnoreCase(parent.getName())) { + File worldFolder = parent.getParentFile(); + if (worldFolder != null) { + return worldFolder.getName() + "/self-contained"; + } + } + + return folder.getName(); + } + + private static String toCanonicalPath(File folder) { + try { + return folder.getCanonicalPath(); + } catch (IOException e) { + return folder.getAbsolutePath(); + } + } + + private static long[] collectDomainSeeds(SeedManager seedManager) { + if (seedManager == null) { + return NO_SEEDS; + } + + return new long[]{ + seedManager.getSeed(), + seedManager.getComplex(), + seedManager.getComplexStreams(), + seedManager.getBasic(), + seedManager.getHeight(), + seedManager.getComponent(), + seedManager.getScript(), + seedManager.getMantle(), + seedManager.getEntity(), + seedManager.getBiome(), + seedManager.getDecorator(), + seedManager.getTerrain(), + seedManager.getSpawn(), + seedManager.getJigsaw(), + seedManager.getCarve(), + seedManager.getDeposit(), + seedManager.getPost(), + seedManager.getBodies(), + seedManager.getMode() + }; + } + + private static Field[] fieldsOf(Class type) { + return FIELD_CACHE.computeIfAbsent(type, IrisNoisemapPrebakePipeline::resolveFields); + } + + private static Field[] resolveFields(Class type) { + List fields = new ArrayList<>(); + Class cursor = type; + while (cursor != null && cursor != Object.class) { + Field[] declared = cursor.getDeclaredFields(); + for (Field field : declared) { + fields.add(field); + } + cursor = cursor.getSuperclass(); + } + return fields.toArray(new Field[0]); + } + + private static boolean skipField(Field field) { + int modifiers = field.getModifiers(); + if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers) || field.isSynthetic()) { + return true; + } + + Class type = field.getType(); + if (type.isPrimitive()) { + return true; + } + + return isLeafType(type); + } + + private static boolean isLeafType(Class type) { + if (type.isPrimitive() || type.isEnum()) { + return true; + } + + if (type == String.class + || type == Boolean.class + || type == Character.class + || Number.class.isAssignableFrom(type) + || type == Class.class + || type == Locale.class + || type == File.class) { + return true; + } + + String name = type.getName(); + return name.startsWith("java.time.") + || name.startsWith("java.util.concurrent.atomic.") + || name.startsWith("org.bukkit.") + || name.startsWith("net.minecraft.") + || name.startsWith("art.arcane.iris.engine.data.cache.") + || name.equals("art.arcane.volmlib.util.math.RNG"); + } + + private static long mixSeed(long seed, int hash) { + long mixed = seed ^ ((long) hash * 0x9E3779B97F4A7C15L); + mixed ^= mixed >>> 33; + mixed *= 0xff51afd7ed558ccdL; + mixed ^= mixed >>> 33; + mixed *= 0xc4ceb9fe1a85ec53L; + mixed ^= mixed >>> 33; + return mixed; + } + + private static final class Node { + private final Object value; + private final String path; + + private Node(Object value, String path) { + this.value = value; + this.path = path; + } + } + + private static final class StyleReference { + private final IrisGeneratorStyle style; + private final int hash; + + private StyleReference(IrisGeneratorStyle style, int hash) { + this.style = style; + this.hash = hash; + } + } + + private static final class PrebakeRunResult { + private static final PrebakeRunResult SKIPPED = new PrebakeRunResult(false, false, 0); + private static final PrebakeRunResult UNCHANGED = new PrebakeRunResult(false, true, 0); + + private final boolean executed; + private final boolean unchanged; + private final int failures; + + private PrebakeRunResult(boolean executed, boolean unchanged, int failures) { + this.executed = executed; + this.unchanged = unchanged; + this.failures = failures; + } + } + + private static final class PrebakeTarget { + private final File folder; + private final String label; + + private PrebakeTarget(File folder, String label) { + this.folder = folder; + this.label = label; + } + } + + private static final class StartupTargetResult { + private final String label; + private final PrebakeRunResult result; + + private StartupTargetResult(String label, PrebakeRunResult result) { + this.label = label; + this.result = result; + } + } + + private static final class StartupProgress { + private final int targetTotal; + private final AtomicInteger targetsCompleted = new AtomicInteger(); + private final AtomicInteger executedTargets = new AtomicInteger(); + private final AtomicInteger unchangedTargets = new AtomicInteger(); + private final AtomicInteger failedTargets = new AtomicInteger(); + private final AtomicInteger stylesDiscovered = new AtomicInteger(); + private final AtomicInteger stylesFinished = new AtomicInteger(); + + private StartupProgress(int targetTotal) { + this.targetTotal = targetTotal; + } + + private void onStylesDiscovered(int styleCount) { + if (styleCount > 0) { + stylesDiscovered.addAndGet(styleCount); + } + } + + private void onStyleFinished() { + stylesFinished.incrementAndGet(); + } + + private void onTargetCompleted(PrebakeRunResult result) { + targetsCompleted.incrementAndGet(); + if (result.executed) { + executedTargets.incrementAndGet(); + } + if (result.unchanged) { + unchangedTargets.incrementAndGet(); + } + if (result.failures > 0) { + failedTargets.incrementAndGet(); + } + } + + private void onTargetFailed() { + targetsCompleted.incrementAndGet(); + failedTargets.incrementAndGet(); + } + } + + private static final class StartupPrebakeThreadFactory implements ThreadFactory { + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "Iris-NoisemapPrebake-" + STARTUP_WORKER_SEQUENCE.incrementAndGet()); + thread.setDaemon(true); + return thread; + } + } +} diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java index afd3b3742..17a82c0a7 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisGeneratorStyle.java @@ -88,7 +88,11 @@ public class IrisGeneratorStyle { } public CNG createNoCache(RNG rng, IrisData data) { - return createNoCache(rng, data, false); + return createNoCache(rng, data, false, 0, false); + } + + public CNG createForPrebake(RNG rng, IrisData data, int fallbackCacheSize) { + return createNoCache(rng, data, false, Math.max(0, fallbackCacheSize), true); } @@ -110,14 +114,19 @@ public class IrisGeneratorStyle { return Objects.hash(expression, imageMapHash(), script, multiplier, axialFracturing, fracture != null ? fracture.hash() : 0, exponent, cacheSize, zoom, cellularZoom, cellularFrequency, style); } - private String cachePrefix(RNG rng) { + public int prebakeSignature() { + return hash(); + } + + private String cachePrefix(RNG rng, int effectiveCacheSize) { return "style-" + Integer.toUnsignedString(hash()) + "-seed-" + Long.toUnsignedString(rng.getSeed()) + + "-sz-" + effectiveCacheSize + "-src-"; } - private String cacheKey(RNG rng, long sourceStamp) { - return cachePrefix(rng) + Long.toUnsignedString(sourceStamp); + private String cacheKey(RNG rng, long sourceStamp, int effectiveCacheSize) { + return cachePrefix(rng, effectiveCacheSize) + Long.toUnsignedString(sourceStamp); } private long scriptStamp(IrisData data) { @@ -153,6 +162,10 @@ public class IrisGeneratorStyle { } public CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached) { + return createNoCache(rng, data, actuallyCached, 0, false); + } + + private CNG createNoCache(RNG rng, IrisData data, boolean actuallyCached, int fallbackCacheSize, boolean quietCacheLog) { CNG cng = null; long sourceStamp = 0L; if (getExpression() != null) { @@ -182,17 +195,18 @@ public class IrisGeneratorStyle { cng.setTrueFracturing(axialFracturing); if (fracture != null) { - cng.fractureWith(fracture.create(rng.nextParallelRNG(2934), data), fracture.getMultiplier()); + cng.fractureWith(fracture.createNoCache(rng.nextParallelRNG(2934), data, false, fallbackCacheSize, quietCacheLog), fracture.getMultiplier()); } if (cellularFrequency > 0) { cng = cng.cellularize(rng.nextParallelRNG(884466), cellularFrequency).scale(1D / cellularZoom).bake(); } - if (cacheSize > 0) { - String key = cacheKey(rng, sourceStamp); - clearStaleCacheEntries(data, cachePrefix(rng), key); - cng = cng.cached(cacheSize, key, data.getDataFolder()); + int effectiveCacheSize = cacheSize > 0 ? cacheSize : Math.max(0, fallbackCacheSize); + if (effectiveCacheSize > 0) { + String key = cacheKey(rng, sourceStamp, effectiveCacheSize); + clearStaleCacheEntries(data, cachePrefix(rng, effectiveCacheSize), key); + cng = cng.cached(effectiveCacheSize, key, data.getDataFolder(), quietCacheLog); } return cng; 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 64ee9c471..9da8d837d 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 @@ -31,10 +31,12 @@ import art.arcane.iris.util.project.noise.CNG; import art.arcane.volmlib.util.scheduling.PrecisionStopwatch; import java.math.BigDecimal; +import java.util.Arrays; 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)); public static double bezier(double t) { return t * t * (3.0d - 2.0d * t); @@ -286,35 +288,9 @@ public class IrisInterpolation { } public static int getRadiusFactor(int coord, double radius) { - if (radius == 2) { - return coord >> 1; - } - if (radius == 4) { - return coord >> 2; - } - if (radius == 8) { - return coord >> 3; - } - if (radius == 16) { - return coord >> 4; - } - if (radius == 32) { - return coord >> 5; - } - if (radius == 64) { - return coord >> 6; - } - if (radius == 128) { - return coord >> 7; - } - if (radius == 256) { - return coord >> 8; - } - if (radius == 512) { - return coord >> 9; - } - if (radius == 1024) { - return coord >> 10; + int radiusInt = (int) radius; + if (radius == radiusInt && radiusInt > 0 && (radiusInt & (radiusInt - 1)) == 0) { + return coord >> Integer.numberOfTrailingZeros(radiusInt); } return (int) Math.floor(coord / radius); } @@ -999,8 +975,14 @@ public class IrisInterpolation { } public static double getNoise(InterpolationMethod method, int x, int z, double h, NoiseProvider noise) { - NoiseSampleCache2D cache = new NoiseSampleCache2D(64); - NoiseProvider n = (x1, z1) -> cache.getOrSample(x1 - x, z1 - z, x1, z1, noise); + final NoiseProvider n; + if (usesSampleCache(method)) { + NoiseSampleCache2D cache = NOISE_SAMPLE_CACHE_2D.get(); + cache.clear(); + n = (x1, z1) -> cache.getOrSample(x1 - x, z1 - z, x1, z1, noise); + } else { + n = noise; + } if (method.equals(InterpolationMethod.BILINEAR)) { return getBilinearNoise(x, z, h, n); @@ -1059,53 +1041,138 @@ public class IrisInterpolation { return n.noise(x, z); } + private static boolean usesSampleCache(InterpolationMethod method) { + return switch (method) { + case BILINEAR_STARCAST_3, + BILINEAR_STARCAST_6, + BILINEAR_STARCAST_9, + BILINEAR_STARCAST_12, + HERMITE_STARCAST_3, + HERMITE_STARCAST_6, + HERMITE_STARCAST_9, + HERMITE_STARCAST_12 -> true; + default -> false; + }; + } + private static class NoiseSampleCache2D { private long[] xBits; private long[] zBits; private double[] values; + private byte[] states; + private int mask; + private int resizeThreshold; private int size; public NoiseSampleCache2D(int initialCapacity) { - xBits = new long[initialCapacity]; - zBits = new long[initialCapacity]; - values = new double[initialCapacity]; + int minimumCapacity = Math.max(8, initialCapacity); + int tableSize = tableSizeFor((minimumCapacity << 1) + minimumCapacity); + xBits = new long[tableSize]; + zBits = new long[tableSize]; + values = 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 getOrSample(double relativeX, double relativeZ, double sampleX, double sampleZ, NoiseProvider provider) { long rx = Double.doubleToLongBits(relativeX); long rz = Double.doubleToLongBits(relativeZ); - - for (int i = 0; i < size; i++) { - if (xBits[i] == rx && zBits[i] == rz) { - return values[i]; - } + int slot = findSlot(rx, rz); + if (states[slot] != 0) { + return values[slot]; } double value = provider.noise(sampleX, sampleZ); - if (size >= xBits.length) { - grow(); - } - - xBits[size] = rx; - zBits[size] = rz; - values[size] = value; - size++; - + insert(slot, rx, rz, value); return value; } + private int findSlot(long rx, long rz) { + int slot = mix(rx, rz) & mask; + while (states[slot] != 0) { + if (xBits[slot] == rx && zBits[slot] == rz) { + break; + } + slot = (slot + 1) & mask; + } + return slot; + } + + private void insert(int slot, long rx, long rz, double value) { + xBits[slot] = rx; + zBits[slot] = rz; + values[slot] = value; + states[slot] = 1; + size++; + if (size >= resizeThreshold) { + grow(); + } + } + + private int mix(long rx, long rz) { + long hash = rx * 0x9E3779B97F4A7C15L; + hash ^= Long.rotateLeft(rz * 0xC2B2AE3D27D4EB4FL, 32); + hash ^= (hash >>> 33); + hash *= 0xff51afd7ed558ccdL; + hash ^= (hash >>> 33); + return (int) hash; + } + private void grow() { + long[] previousXBits = xBits; + long[] previousZBits = zBits; + double[] previousValues = values; + byte[] previousStates = states; + int nextLength = xBits.length << 1; long[] nextXBits = new long[nextLength]; long[] nextZBits = new long[nextLength]; double[] nextValues = new double[nextLength]; - System.arraycopy(xBits, 0, nextXBits, 0, size); - System.arraycopy(zBits, 0, nextZBits, 0, size); - System.arraycopy(values, 0, nextValues, 0, size); + byte[] nextStates = new byte[nextLength]; + xBits = nextXBits; zBits = nextZBits; values = nextValues; + 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]; + values[slot] = previousValues[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 size = n + 1; + if (size < 8) { + return 8; + } + return size; } } 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 ebbb01b8a..0e830a745 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 @@ -238,6 +238,10 @@ public class CNG { } public CNG cached(int size, String key, File cacheFolder) { + return cached(size, key, cacheFolder, false); + } + + public CNG cached(int size, String key, File cacheFolder, boolean quiet) { if (size <= 0) { return this; } @@ -271,7 +275,9 @@ public class CNG { DataOutputStream dos = new DataOutputStream(fos); fbc.writeCache(dos); dos.close(); - Iris.info("Saved Noise Cache " + f.getName()); + if (!quiet) { + Iris.info("Saved Noise Cache " + f.getName()); + } } catch (IOException e) { throw new RuntimeException(e); }