diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java index 568c92088..4cdbcb534 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java @@ -342,15 +342,24 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, @BlockCoordinates default SubterrainCell getSubterrainCell(int x, int y, int z) { - if (getComplex() == null || getDimension() == null - || !getDimension().allowsSubterrainFeatures() - || getDimension().getSubterrainFeatures() == null || getDimension().getSubterrainFeatures().isEmpty()) { + IrisComplex complex = getComplex(); + IrisDimension dimension = getDimension(); + if (complex == null || dimension == null + || !dimension.allowsSubterrainFeatures() + || dimension.getSubterrainFeatures() == null || dimension.getSubterrainFeatures().isEmpty()) { return SubterrainCell.OUTSIDE; } - SubterrainPlanner planner = getComplex().getSubterrainPlanner(); + SubterrainPlanner planner = complex.getSubterrainPlanner(); return planner == null ? SubterrainCell.OUTSIDE : planner.sample(x, y + getWorld().minHeight(), z); } + @ChunkCoordinates + default boolean subterrainAbsentInChunk(int chunkX, int chunkZ) { + IrisComplex complex = getComplex(); + SubterrainPlanner planner = complex == null ? null : complex.getSubterrainPlanner(); + return planner == null || !planner.hasCandidates(chunkX, chunkZ); + } + @BlockCoordinates default IrisBiome getSubterrainBiome(int x, int y, int z) { SubterrainCell cell = getSubterrainCell(x, y, z); @@ -410,6 +419,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, return; } IrisComplex complex = getComplex(); + boolean subterrain = !subterrainAbsentInChunk(x >> 4, z >> 4); int caveTop = getHeight(x, z) - 2; IrisRegion region = getRegion(x, 0, z); IrisBiome surface = null; @@ -423,7 +433,7 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, for (int index = 0; index < biomes.length; index++) { int y = index * step; regions[index] = region; - IrisBiome featureBiome = getSubterrainBiome(x, y, z); + IrisBiome featureBiome = subterrain ? getSubterrainBiome(x, y, z) : null; if (featureBiome != null) { biomes[index] = featureBiome; continue; diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java index 709d50b7c..327765faf 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisEngine.java @@ -623,6 +623,15 @@ public class IrisEngine implements Engine { return Engine.super.getSubterrainCell(x, y, z); } + @ChunkCoordinates + @Override + public boolean subterrainAbsentInChunk(int chunkX, int chunkZ) { + if (!hasGenerationRuntimeScope() && generationHistoryRuntimeRouter != null) { + return false; + } + return Engine.super.subterrainAbsentInChunk(chunkX, chunkZ); + } + @BlockCoordinates @Override public IrisBiome getSubterrainBiome(int x, int y, int z) { diff --git a/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java b/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java index 03548a3d8..d25ccfcd1 100644 --- a/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java +++ b/core/src/main/java/art/arcane/iris/generation/subterrain/SubterrainPlanner.java @@ -106,6 +106,10 @@ public final class SubterrainPlanner { return List.copyOf(result); } + public boolean hasCandidates(int chunkX, int chunkZ) { + return !isEmpty() && !candidates(chunkX, chunkZ).isEmpty(); + } + public SubterrainCell sample(int x, int y, int z) { if (isEmpty() || y < minY || y >= maxYExclusive) { return SubterrainCell.OUTSIDE; diff --git a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java index 91d6354f3..e57dd6095 100644 --- a/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java +++ b/core/src/main/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomePolicy.java @@ -8,6 +8,7 @@ import art.arcane.iris.spi.IrisLogging; import art.arcane.iris.generation.runtime.DimensionStackContext; import art.arcane.iris.generation.runtime.DimensionStackLayout; import art.arcane.iris.generation.runtime.DimensionTerrainContext; +import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisEngine; import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.stream.ProvisionalSampling; @@ -27,6 +28,7 @@ import org.bukkit.block.Biome; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.IdentityHashMap; @@ -46,7 +48,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy private static final int STRONGHOLD_RING_SEARCH_RADIUS = 112; private static final int STRONGHOLD_RING_SEARCH_QUART_STEP = 4; - private final long seed; + private final RNG biomeRng; private final Engine engine; private final BukkitChunkGenerator platformGenerator; private final NativeBiomeRegistry biomeCustomRegistry; @@ -64,7 +66,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy this.engine = engine; this.platformGenerator = options.platformGenerator(); this.possibleBiomeRegistry = options.possibleBiomeRegistry(); - this.seed = options.seed(); + this.biomeRng = new RNG(options.seed()); this.biomeCustomRegistry = registry; this.biomeRegistry = biomeCustomRegistry; this.fallbackBiome = resolveFallbackBiome(this.biomeRegistry, this.biomeCustomRegistry); @@ -826,7 +828,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy int z, IrisDimensionCarvingResolver.State resolverState ) { - return resolveVisibleBiomeHolder(x, y, z, resolverState, null); + return resolveVisibleBiomeHolder(x, y, z, resolverState, null, null); } private H resolveVisibleBiomeHolder( @@ -835,32 +837,44 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy int z, IrisDimensionCarvingResolver.State resolverState, DimensionStackLayout stackLayout + ) { + return resolveVisibleBiomeHolder(x, y, z, resolverState, stackLayout, null); + } + + private H resolveVisibleBiomeHolder( + int x, + int y, + int z, + IrisDimensionCarvingResolver.State resolverState, + DimensionStackLayout stackLayout, + ChunkResolution chunk ) { int blockX = x << 2; int blockY = y << 2; int blockZ = z << 2; - Optional historicalKey = engine.getComplex().historicalPhysicalBiomeKeyAt( + IrisComplex complex = chunk == null ? engine.getComplex() : chunk.complex; + Optional historicalKey = complex.historicalPhysicalBiomeKeyAt( blockX, blockY, blockZ); if (historicalKey.isPresent()) { return resolvePhysicalBiomeHolder(historicalKey.get()); } BiomeResolution resolution = resolveBiomeResolution( - x, y, z, resolverState, true, stackLayout); + x, y, z, resolverState, true, stackLayout, chunk); if (resolution == null) { return getFallbackBiome(); } - return resolveVisibleBiomeHolder(resolution); + return resolveVisibleBiomeHolder(resolution, chunk); } - private H resolveVisibleBiomeHolder(BiomeResolution resolution) { + private H resolveVisibleBiomeHolder(BiomeResolution resolution, ChunkResolution chunk) { if (resolution.irisBiome.isCustom()) { - return resolveCustomHolder(resolution); + return resolveCustomHolder(resolution, chunk); } Biome vanillaBiome = resolution.underground - ? resolution.irisBiome.getGroundBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ) - : resolution.irisBiome.getSkyBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); + ? resolution.irisBiome.getGroundBiome(biomeRng, engine, resolution.blockX, resolution.blockY, resolution.blockZ) + : resolution.irisBiome.getSkyBiome(biomeRng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); H holder = biomeRegistry.lookup(vanillaBiome); if (holder != null) { return holder; @@ -869,11 +883,12 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy return getFallbackBiome(); } - private H resolveCustomHolder(BiomeResolution resolution) { - IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(resolution.rng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); + private H resolveCustomHolder(BiomeResolution resolution, ChunkResolution chunk) { + IrisBiomeCustom customBiome = resolution.irisBiome.getCustomBiome(biomeRng, engine, resolution.blockX, resolution.blockY, resolution.blockZ); if (customBiome != null) { - H holder = runtimeBiomeState().customBiomes().get(customBiomeKey( - resolution.dimension, customBiome.getId())); + H holder = chunk == null + ? runtimeBiomeState().customBiomes().get(customBiomeKey(resolution.dimension, customBiome.getId())) + : chunk.customHolder(resolution.dimension, customBiome); if (holder != null) { return holder; } @@ -883,7 +898,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy } private BiomeResolution resolveStructureBiomeResolution(int x, int y, int z) { - return resolveBiomeResolution(x, y, z, null, false); + return resolveBiomeResolution(x, y, z, null, false, null, null); } private H resolvePhysicalBiomeHolder(String physicalKey) { @@ -898,65 +913,50 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy return holder; } - private BiomeResolution resolveBiomeResolution( - int x, - int y, - int z, - IrisDimensionCarvingResolver.State resolverState - ) { - return resolveBiomeResolution(x, y, z, resolverState, true); - } - - private BiomeResolution resolveBiomeResolution( - int x, - int y, - int z, - IrisDimensionCarvingResolver.State resolverState, - boolean includeDimensionStack - ) { - return resolveBiomeResolution( - x, y, z, resolverState, includeDimensionStack, null); - } - private BiomeResolution resolveBiomeResolution( int x, int y, int z, IrisDimensionCarvingResolver.State resolverState, boolean includeDimensionStack, - DimensionStackLayout stackLayout + DimensionStackLayout stackLayout, + ChunkResolution chunk ) { if (engine == null || engine.isClosed()) { return null; } - if (engine.getComplex() == null) { + if ((chunk == null ? engine.getComplex() : chunk.complex) == null) { return null; } int blockX = x << 2; int blockZ = z << 2; int blockY = y << 2; - int worldMinHeight = engine.getWorld().minHeight(); + int worldMinHeight = chunk == null ? engine.getWorld().minHeight() : chunk.worldMinHeight; int internalY = blockY - worldMinHeight; int caveSwitchInternalY = Math.max(-8 - worldMinHeight, 40); - IrisBiome featureBiome = engine.getSubterrainBiome(blockX, internalY, blockZ); + IrisBiome featureBiome = chunk != null && !chunk.subterrain + ? null : engine.getSubterrainBiome(blockX, internalY, blockZ); if (featureBiome != null) { - return createBiomeResolution(featureBiome, true, engine.getDimension(), blockX, blockY, blockZ); + return new BiomeResolution(featureBiome, true, chunk == null ? engine.getDimension() : chunk.dimension, + blockX, blockY, blockZ); } DimensionStackLayout.Layer stackLayer = includeDimensionStack ? stackLayout == null - ? resolveDimensionStackLayer(engine, blockX, internalY, blockZ) + ? resolveDimensionStackLayer( + chunk == null ? engine.getDimensionStackContext() : chunk.stackContext, + blockX, internalY, blockZ) : stackLayout.layerAt(internalY) : null; IrisDimension owningDimension = stackLayer == null - ? engine.getDimension() + ? chunk == null ? engine.getDimension() : chunk.dimension : stackLayer.terrainContext().getDimension(); if (stackLayer != null && !stackLayer.terrainContext().isSelfReferencing()) { IrisBiome stackedBiome = stackLayer.biome(); return stackedBiome == null ? null - : createBiomeResolution( + : new BiomeResolution( stackedBiome, false, owningDimension, blockX, blockY, blockZ); } boolean deepUnderground = internalY <= caveSwitchInternalY; @@ -976,41 +976,31 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy ) : stackLayer.biome(); } else if (deepUnderground) { - int surfaceInternalY = Engine.hostHeight(engine, blockX, blockZ, true); + int surfaceInternalY = chunk == null + ? Engine.hostHeight(engine, blockX, blockZ, true) + : chunk.hostHeight(blockX, blockZ); underground = internalY <= surfaceInternalY - 8; irisBiome = underground ? engine.getCaveBiome(blockX, internalY, blockZ, resolverState) - : engine.getHostSurfaceBiome(blockX, blockZ); + : hostSurfaceBiome(chunk, blockX, blockZ); } else { - irisBiome = engine.getHostSurfaceBiome(blockX, blockZ); + irisBiome = hostSurfaceBiome(chunk, blockX, blockZ); } if (irisBiome == null && underground) { irisBiome = stackLayer == null - ? engine.getHostSurfaceBiome(blockX, blockZ) + ? hostSurfaceBiome(chunk, blockX, blockZ) : stackLayer.biome(); } if (irisBiome == null) { return null; } - return createBiomeResolution( + return new BiomeResolution( irisBiome, underground, owningDimension, blockX, blockY, blockZ); } - private BiomeResolution createBiomeResolution( - IrisBiome irisBiome, - boolean underground, - IrisDimension dimension, - int blockX, - int blockY, - int blockZ - ) { - RNG noiseRng = new RNG(seed - ^ (((long) blockX) * 341873128712L) - ^ (((long) blockY) * 132897987541L) - ^ (((long) blockZ) * 42317861L)); - return new BiomeResolution( - irisBiome, underground, dimension, blockX, blockY, blockZ, noiseRng); + private IrisBiome hostSurfaceBiome(ChunkResolution chunk, int blockX, int blockZ) { + return chunk == null ? engine.getHostSurfaceBiome(blockX, blockZ) : chunk.hostSurfaceBiome(blockX, blockZ); } private H getFallbackBiome() { @@ -1094,12 +1084,11 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy } private static DimensionStackLayout.Layer resolveDimensionStackLayer( - Engine engine, + DimensionStackContext stackContext, int blockX, int internalY, int blockZ ) { - DimensionStackContext stackContext = engine.getDimensionStackContext(); return stackContext == null ? null : stackContext.getLayerAt(blockX, internalY, blockZ); } @@ -1136,7 +1125,6 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy private final int blockX; private final int blockY; private final int blockZ; - private final RNG rng; private BiomeResolution( IrisBiome irisBiome, @@ -1144,8 +1132,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy IrisDimension dimension, int blockX, int blockY, - int blockZ, - RNG rng + int blockZ ) { this.irisBiome = irisBiome; this.underground = underground; @@ -1153,7 +1140,100 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy this.blockX = blockX; this.blockY = blockY; this.blockZ = blockZ; - this.rng = rng; + } + } + + private final class ChunkVisibleResolver implements VisibleResolver { + private final IrisDimensionCarvingResolver.State resolverState = new IrisDimensionCarvingResolver.State(); + private final boolean flat = platformGenerator != null && platformGenerator.usesFlatStudioTerrain(); + private ChunkResolution chunk; + + @Override + public H biome(int quartX, int quartY, int quartZ) { + if (flat) { + return fallbackBiome; + } + int chunkX = quartX >> 2; + int chunkZ = quartZ >> 2; + ChunkResolution current = chunk; + if (current == null || current.chunkX != chunkX || current.chunkZ != chunkZ) { + current = new ChunkResolution(chunkX, chunkZ); + chunk = current; + } + return resolveVisibleBiomeHolder(quartX, quartY, quartZ, resolverState, null, current); + } + } + + private final class ChunkResolution { + private static final int COLUMNS = 16; + + private final int chunkX; + private final int chunkZ; + private final IrisComplex complex; + private final IrisDimension dimension; + private final DimensionStackContext stackContext; + private final int worldMinHeight; + private final boolean subterrain; + private final RuntimeBiomeState runtimeState; + private final int[] hostHeights = new int[COLUMNS]; + private final boolean[] hostHeightResolved = new boolean[COLUMNS]; + private final IrisBiome[] hostSurfaces = new IrisBiome[COLUMNS]; + private final boolean[] hostSurfaceResolved = new boolean[COLUMNS]; + private IrisDimension[] customDimensions = new IrisDimension[4]; + private IrisBiomeCustom[] customBiomes = new IrisBiomeCustom[4]; + private Object[] customHolders = new Object[4]; + private int customCount; + + private ChunkResolution(int chunkX, int chunkZ) { + this.chunkX = chunkX; + this.chunkZ = chunkZ; + this.complex = engine.getComplex(); + this.dimension = engine.getDimension(); + this.stackContext = engine.getDimensionStackContext(); + this.worldMinHeight = engine.getWorld().minHeight(); + this.subterrain = !engine.subterrainAbsentInChunk(chunkX, chunkZ); + this.runtimeState = runtimeBiomeState(); + } + + private int hostHeight(int blockX, int blockZ) { + int column = column(blockX, blockZ); + if (!hostHeightResolved[column]) { + hostHeights[column] = Engine.hostHeight(engine, blockX, blockZ, true); + hostHeightResolved[column] = true; + } + return hostHeights[column]; + } + + private IrisBiome hostSurfaceBiome(int blockX, int blockZ) { + int column = column(blockX, blockZ); + if (!hostSurfaceResolved[column]) { + hostSurfaces[column] = engine.getHostSurfaceBiome(blockX, blockZ); + hostSurfaceResolved[column] = true; + } + return hostSurfaces[column]; + } + + @SuppressWarnings("unchecked") + private H customHolder(IrisDimension owner, IrisBiomeCustom custom) { + for (int index = 0; index < customCount; index++) { + if (customBiomes[index] == custom && customDimensions[index] == owner) { + return (H) customHolders[index]; + } + } + H holder = runtimeState.customBiomes().get(customBiomeKey(owner, custom.getId())); + if (customCount == customBiomes.length) { + customDimensions = Arrays.copyOf(customDimensions, customCount * 2); + customBiomes = Arrays.copyOf(customBiomes, customCount * 2); + customHolders = Arrays.copyOf(customHolders, customCount * 2); + } + customDimensions[customCount] = owner; + customBiomes[customCount] = custom; + customHolders[customCount++] = holder; + return holder; + } + + private static int column(int blockX, int blockZ) { + return ((blockX & 15) >> 2) << 2 | ((blockZ & 15) >> 2); } } @@ -1181,8 +1261,7 @@ public final class BukkitBiomePolicy implements NativeBiomeSourcePolicy } @Override public VisibleResolver visibleResolver() { - IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); - return (x, y, z) -> getVisibleNoiseBiomeWithActiveGenerationLease(x, y, z, state); + return new ChunkVisibleResolver(); } } diff --git a/core/src/main/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndex.java b/core/src/main/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndex.java index 39da749e9..5a119efd4 100644 --- a/core/src/main/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndex.java +++ b/core/src/main/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndex.java @@ -12,8 +12,10 @@ import art.arcane.volmlib.util.cache.CacheKey; import art.arcane.volmlib.util.collection.KList; import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.WeakHashMap; @@ -34,12 +36,13 @@ public final class NativeStructureVolumeIndex { private static final int ORIGIN_REACH_CHUNKS = StructureOwnershipRecordView.MAX_REFERENCE_DISTANCE_CHUNKS; private static final int MAX_CACHED_ORIGIN_CHUNKS = 16_384; private static final int MAX_CACHED_QUERY_CHUNKS = 4_096; + private static final int CACHE_STRIPES = 32; private static final Map INDEXES = Collections.synchronizedMap(new WeakHashMap<>()); private final OriginResolver origins; - private final Map> originCache = lru(MAX_CACHED_ORIGIN_CHUNKS); - private final Map> queryCache = lru(MAX_CACHED_QUERY_CHUNKS); + private final VolumeCache originCache = new VolumeCache(MAX_CACHED_ORIGIN_CHUNKS); + private final VolumeCache queryCache = new VolumeCache(MAX_CACHED_QUERY_CHUNKS); private final ConcurrentHashMap>> originBuilds = new ConcurrentHashMap<>(); private final ConcurrentHashMap>> queryBuilds = @@ -180,12 +183,10 @@ public final class NativeStructureVolumeIndex { Engine engine, int chunkX, int chunkZ, Integer pinnedRuntimeId ) { if (pinnedRuntimeId != null) { - synchronized (originCache) { - KList cached = originCache.get( - new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ))); - if (cached != null) { - return cached; - } + KList cached = originCache.get( + new RuntimeChunkKey(pinnedRuntimeId, chunkKey(chunkX, chunkZ))); + if (cached != null) { + return cached; } } return originVolumes(engine, chunkX, chunkZ); @@ -210,22 +211,19 @@ public final class NativeStructureVolumeIndex { } void evictRuntime(int runtimeId) { - synchronized (originCache) { - originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); - originCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); - } - synchronized (queryCache) { - queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); - queryCache.keySet().removeIf(key -> key.runtimeId() == runtimeId); - } + originBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); + originCache.evictRuntime(runtimeId); + queryBuilds.keySet().removeIf(key -> key.runtimeId() == runtimeId); + queryCache.evictRuntime(runtimeId); } private KList cached( - Map> cache, + VolumeCache volumes, ConcurrentHashMap>> builds, RuntimeChunkKey key, Supplier> loader ) { + Map> cache = volumes.stripe(key); synchronized (cache) { KList hit = cache.get(key); if (hit != null) { @@ -314,6 +312,39 @@ public final class NativeStructureVolumeIndex { }; } + private static final class VolumeCache { + private final List>> stripes; + + private VolumeCache(int capacity) { + int stripeCapacity = Math.max(1, capacity / CACHE_STRIPES); + List>> created = new ArrayList<>(CACHE_STRIPES); + for (int stripe = 0; stripe < CACHE_STRIPES; stripe++) { + created.add(lru(stripeCapacity)); + } + stripes = List.copyOf(created); + } + + private Map> stripe(RuntimeChunkKey key) { + int hash = key.hashCode(); + return stripes.get((hash ^ (hash >>> 16)) & (CACHE_STRIPES - 1)); + } + + private KList get(RuntimeChunkKey key) { + Map> cache = stripe(key); + synchronized (cache) { + return cache.get(key); + } + } + + private void evictRuntime(int runtimeId) { + for (Map> cache : stripes) { + synchronized (cache) { + cache.keySet().removeIf(key -> key.runtimeId() == runtimeId); + } + } + } + } + static int runtimeChunkHash(int runtimeId, long chunkKey) { return Long.hashCode(CacheKey.mix(chunkKey)) * 31 + runtimeId; } diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java index 192f3e3dc..c98f11261 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticCapture.java @@ -155,7 +155,7 @@ public final class GenerationSemanticCapture { int minimumX = Math.multiplyExact(chunkX, CHUNK_SIZE); int minimumZ = Math.multiplyExact(chunkZ, CHUNK_SIZE); BitSet resolvedCaves = new BitSet(); - CaveBiomes caveBiomes = new CaveBiomes(engine, scoped); + CaveBiomes caveBiomes = new CaveBiomes(engine, scoped, chunkX, chunkZ); mantle.iterateChunk(chunkX, chunkZ, MatterCavern.class, (localX, y, localZ, cavern) -> { if (!caveSpace.test(localX, y, localZ)) { return; @@ -276,15 +276,17 @@ public final class GenerationSemanticCapture { private static final class CaveBiomes { private final Engine engine; private final boolean scoped; + private final boolean subterrain; private final CaveFallback fallback; private final IrisDimensionCarvingResolver.State fallbackState = new IrisDimensionCarvingResolver.State(); private IrisDimensionCarvingResolver.Snapshot snapshot; private int minimumY; private String lastKey; - private CaveBiomes(Engine engine, boolean scoped) { + private CaveBiomes(Engine engine, boolean scoped, int chunkX, int chunkZ) { this.engine = engine; this.scoped = scoped; + this.subterrain = scoped && !engine.subterrainAbsentInChunk(chunkX, chunkZ); this.fallback = scoped && engine instanceof IrisEngine irisEngine && irisEngine.hasGenerationRuntimeScope() && !engine.getPlatformHooks().isMainThread() ? new CaveFallback(engine.getComplex(), engine.getDimensionStackContext()) @@ -306,7 +308,7 @@ public final class GenerationSemanticCapture { snapshot = IrisDimensionCarvingResolver.snapshot(engine); minimumY = engine.getWorld().minHeight(); } - IrisBiome featureBiome = engine.getSubterrainBiome(x, y, z); + IrisBiome featureBiome = subterrain ? engine.getSubterrainBiome(x, y, z) : null; if (featureBiome != null) { return featureBiome; } diff --git a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java index 016ba9054..3b6b5b076 100644 --- a/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java +++ b/core/src/main/java/art/arcane/iris/world/history/GenerationSemanticIndex.java @@ -99,6 +99,7 @@ public final class GenerationSemanticIndex { private volatile IOException journalFailure; private long appendingRegionKey; private RegionShard appendingBase; + private boolean journalStorageReady; private GenerationSemanticIndex( Path dimensionRoot, @@ -264,12 +265,13 @@ public final class GenerationSemanticIndex { if (updates.isEmpty()) { return; } - ensureStorageDirectory(); + ensureJournalStorage(); + boolean existingJournal = journalEntries.containsKey(regionKey); appendingRegionKey = regionKey; appendingBase = baseRegion; publicationLock.unlock(); try { - SemanticJournal.append(directory, regionX, regionZ, updates, unpublished, durability); + SemanticJournal.append(directory, regionX, regionZ, updates, unpublished, durability, existingJournal); } catch (JournalAppendFailure failure) { journalFailure = failure; throw failure; @@ -1193,6 +1195,14 @@ public final class GenerationSemanticIndex { return true; } + private void ensureJournalStorage() throws IOException { + if (journalStorageReady) { + return; + } + ensureStorageDirectory(); + journalStorageReady = true; + } + private void ensureStorageDirectory() throws IOException { validateDimensionRoot(); Path irisDirectory = ensureChildDirectory(dimensionRoot, "iris"); @@ -1958,18 +1968,17 @@ public final class GenerationSemanticIndex { int regionZ, List claims, boolean unpublished, - DeferredDurability durability + DeferredDurability durability, + boolean existing ) throws IOException { Path file = directory.resolve(journalFileName(regionX, regionZ)); - boolean created = !Files.exists(file, LinkOption.NOFOLLOW_LINKS); - if (!created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) { + boolean created = !existing && !Files.exists(file, LinkOption.NOFOLLOW_LINKS); + if (!existing && !created && !Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)) { throw invalid(file, "path is not a regular file"); } - FileChannel opened = FileChannel.open( - file, - StandardOpenOption.CREATE, - StandardOpenOption.WRITE - ); + FileChannel opened = existing + ? FileChannel.open(file, StandardOpenOption.WRITE) + : FileChannel.open(file, StandardOpenOption.CREATE, StandardOpenOption.WRITE); boolean uncertain = created; boolean restored = false; try (FileChannel channel = opened) { diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java index 1964053f1..c71427cd8 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeCapture.java @@ -36,7 +36,8 @@ public final class SavedBiomeCapture { Math.addExact(startX, 15), Math.addExact(startZ, 15)).isEmpty(); Map> vertical = new HashMap<>(exactVolume ? 256 : 16); Cells cells = new Cells(activationId); - Column column = new Column(engine, minimumY, height, exactVolume ? 1 : 4); + Column column = new Column(engine, minimumY, height, exactVolume ? 1 : 4, + !engine.subterrainAbsentInChunk(stage.chunkX(), stage.chunkZ())); for (int localX = 0; localX < 16; localX++) { for (int localZ = 0; localZ < 16; localZ++) { int blockX = startX + localX; @@ -82,7 +83,8 @@ public final class SavedBiomeCapture { for (int index = 0; index < column.biomes().length; index++) { int worldY = minimumY + index * column.step(); SavedBiomeChunk.Cell current = cells.cell(column.biomes()[index], column.regions()[index]); - if (floating != null && !column.engine().getSubterrainCell(blockX, index * column.step(), blockZ).occupied()) { + if (floating != null && (!column.subterrain() + || !column.engine().getSubterrainCell(blockX, index * column.step(), blockZ).occupied())) { current = overlay(current, floating.volumeAt(blockX & 15, worldY - minimumY, blockZ & 15)); } if (transition != null) { @@ -139,9 +141,10 @@ public final class SavedBiomeCapture { Objects.requireNonNull(region, "region").getLoadKey()); } - private record Column(Engine engine, int minimumY, int height, int step, IrisBiome[] biomes, IrisRegion[] regions) { - private Column(Engine engine, int minimumY, int height, int step) { - this(engine, minimumY, height, step, new IrisBiome[(height + step - 1) / step], + private record Column(Engine engine, int minimumY, int height, int step, boolean subterrain, IrisBiome[] biomes, + IrisRegion[] regions) { + private Column(Engine engine, int minimumY, int height, int step, boolean subterrain) { + this(engine, minimumY, height, step, subterrain, new IrisBiome[(height + step - 1) / step], new IrisRegion[(height + step - 1) / step]); } } diff --git a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java index 713dc7905..78459d1dc 100644 --- a/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java +++ b/core/src/main/java/art/arcane/iris/world/history/SavedBiomeStore.java @@ -49,6 +49,7 @@ public final class SavedBiomeStore { private static final int MAXIMUM_CACHED_CHUNKS = 128; private static final long MAXIMUM_CACHED_BYTES = 64L * 1024L * 1024L; private static final int MAXIMUM_WRITE_BATCH = 64; + private static final ThreadLocal DEFLATERS = ThreadLocal.withInitial(() -> new Deflater(Deflater.BEST_SPEED)); private final Path dimensionRoot; private final Path directory; @@ -464,12 +465,16 @@ public final class SavedBiomeStore { } private void ensureRegionFile(RegionIndex region) throws IOException { + if (region.fileReady) { + return; + } requireSafeAncestors(); Files.createDirectories(directory); if (Files.exists(region.path, LinkOption.NOFOLLOW_LINKS)) { if (!Files.isRegularFile(region.path, LinkOption.NOFOLLOW_LINKS)) { throw new IOException("Saved biome region is not a regular file: " + region.path); } + region.fileReady = true; return; } ByteBuffer header = ByteBuffer.allocate(HEADER_BYTES); @@ -486,6 +491,7 @@ public final class SavedBiomeStore { } Files.move(temporary, region.path, StandardCopyOption.ATOMIC_MOVE); forceDirectory(directory); + region.fileReady = true; } finally { Files.deleteIfExists(temporary); } @@ -674,11 +680,10 @@ public final class SavedBiomeStore { output.writeInt(chunk.chunkX()); output.writeInt(chunk.chunkZ()); output.writeInt(raw.size()); - Deflater deflater = new Deflater(Deflater.BEST_SPEED); + Deflater deflater = DEFLATERS.get(); + deflater.reset(); try (DeflaterOutputStream compressed = new DeflaterOutputStream(output, deflater)) { raw.writeTo(compressed); - } finally { - deflater.end(); } if (encoded.size() > MAXIMUM_RECORD_BYTES) { throw new IOException("Saved biome chunk exceeds its encoded size limit"); @@ -847,6 +852,7 @@ public final class SavedBiomeStore { private final int regionZ; private final long[] offsets = new long[1024]; private final int[] lengths = new int[1024]; + private boolean fileReady; private RegionIndex(Path path, int regionX, int regionZ) { this.path = path; diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeVisibleResolverTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeVisibleResolverTest.java new file mode 100644 index 000000000..abf24ea01 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitBiomeVisibleResolverTest.java @@ -0,0 +1,111 @@ +package art.arcane.iris.platform.bukkit.nms; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.biome.IrisBiomeCustom; +import art.arcane.iris.generation.mantle.EngineMantle; +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.iris.generation.runtime.GenerationSessionLease; +import art.arcane.iris.generation.runtime.IrisComplex; +import art.arcane.iris.generation.runtime.IrisEngine; +import art.arcane.iris.generation.terrain.IrisDimension; +import art.arcane.iris.generation.terrain.IrisDimensionCarvingResolver; +import art.arcane.iris.pack.loading.IrisData; +import art.arcane.iris.world.IrisWorld; +import art.arcane.volmlib.nativelib.terrain.NativeBiomeRegistry; +import art.arcane.volmlib.nativelib.terrain.NativeBiomeSourcePolicy; +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.lang.reflect.Field; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Answers.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class BukkitBiomeVisibleResolverTest { + @Test + public void chunkResolverMatchesThePerQuartResolution() throws Exception { + IrisBiome surface = customBiome("surface", "meadow"); + IrisBiome cave = customBiome("cave", "grotto"); + IrisBiome custom = customBiome("custom", "mirage"); + IrisBiome feature = customBiome("feature", "bloom"); + IrisDimension dimension = mock(IrisDimension.class); + when(dimension.getReachableBiomes(any())).thenReturn(new KList<>(surface, cave, custom, feature)); + when(dimension.getCustomBiomeKey(anyString())).thenAnswer(invocation -> "pack:" + invocation.getArgument(0)); + IrisData data = mock(IrisData.class); + when(data.customBiomeResourceKey(any(IrisDimension.class), anyString())) + .thenAnswer(invocation -> "pack:" + invocation.getArgument(1)); + IrisComplex complex = mock(IrisComplex.class); + when(complex.historicalPhysicalBiomeKeyAt(anyInt(), anyInt(), anyInt())).thenReturn(Optional.empty()); + EngineMantle mantle = mock(EngineMantle.class); + when(mantle.trueHeight(anyInt(), anyInt())).thenAnswer(invocation -> + 120 + Math.floorMod(invocation.getArgument(0) * 7 + invocation.getArgument(1) * 3, 160)); + IrisEngine engine = mock(IrisEngine.class); + when(engine.getDimension()).thenReturn(dimension); + when(engine.getData()).thenReturn(data); + when(engine.getComplex()).thenReturn(complex); + when(engine.getMantle()).thenReturn(mantle); + when(engine.getWorld()).thenReturn(IrisWorld.builder().minHeight(-64).maxHeight(320).build()); + when(engine.getMinHeight()).thenReturn(-64); + when(engine.getCacheID()).thenReturn(1); + when(engine.acquireGenerationLease(anyString())).thenReturn(GenerationSessionLease.noop()); + when(engine.answersFromNaturalTerrain(anyInt(), anyInt())).thenReturn(false); + when(engine.getHostSurfaceBiome(anyInt(), anyInt())).thenAnswer(invocation -> + Math.floorMod(invocation.getArgument(0) + invocation.getArgument(1), 8) == 0 ? custom : surface); + when(engine.getCaveBiome(anyInt(), anyInt(), anyInt(), any(IrisDimensionCarvingResolver.State.class))) + .thenAnswer(invocation -> invocation.getArgument(1) % 3 == 0 ? custom : cave); + when(engine.getSubterrainBiome(anyInt(), anyInt(), anyInt())).thenAnswer(invocation -> + invocation.getArgument(0) == 4 && invocation.getArgument(1) == 40 ? feature : null); + when(engine.subterrainAbsentInChunk(1, 0)).thenReturn(true); + @SuppressWarnings("unchecked") + NativeBiomeRegistry registry = mock(NativeBiomeRegistry.class); + when(registry.lookupCustom(anyString())).thenAnswer(invocation -> invocation.getArgument(0)); + BukkitBiomePolicy policy = policy(engine, registry, "fallback"); + + for (int chunkX = 0; chunkX <= 1; chunkX++) { + NativeBiomeSourcePolicy.VisibleResolver resolver = policy.visibleResolver(); + for (int quartY = -16; quartY < 80; quartY++) { + for (int localX = 0; localX < 4; localX++) { + for (int localZ = 0; localZ < 4; localZ++) { + int quartX = (chunkX << 2) + localX; + String expected = policy.getVisibleNoiseBiomeWithActiveGenerationLease( + quartX, quartY, localZ, new IrisDimensionCarvingResolver.State()); + assertEquals("quart " + quartX + "," + quartY + "," + localZ, expected, + resolver.biome(quartX, quartY, localZ)); + } + } + } + } + } + + private static IrisBiome customBiome(String key, String customId) { + IrisBiome biome = new IrisBiome().setCustomDerivitives(new KList<>(new IrisBiomeCustom().setId(customId))); + biome.setLoadKey(key); + return biome; + } + + @SuppressWarnings("unchecked") + private static BukkitBiomePolicy policy(Engine engine, NativeBiomeRegistry registry, + String fallback) throws Exception { + BukkitBiomePolicy policy = mock(BukkitBiomePolicy.class, CALLS_REAL_METHODS); + set(policy, "engine", engine); + set(policy, "biomeCustomRegistry", registry); + set(policy, "biomeRegistry", registry); + set(policy, "fallbackBiome", fallback); + set(policy, "runtimeBiomeStates", new ConcurrentHashMap<>()); + set(policy, "noiseBiomeCache", new ConcurrentHashMap<>()); + return policy; + } + + private static void set(Object target, String name, Object value) throws Exception { + Field field = BukkitBiomePolicy.class.getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } +} diff --git a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitSubterrainBiomePrecedenceTest.java b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitSubterrainBiomePrecedenceTest.java index 3020de2d8..9730ac225 100644 --- a/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitSubterrainBiomePrecedenceTest.java +++ b/core/src/test/java/art/arcane/iris/platform/bukkit/nms/BukkitSubterrainBiomePrecedenceTest.java @@ -36,10 +36,12 @@ public class BukkitSubterrainBiomePrecedenceTest { owner.setAccessible(true); owner.set(policy, engine); DimensionStackLayout stack = mock(DimensionStackLayout.class); + Class chunkResolution = Class.forName(BukkitBiomePolicy.class.getName() + "$ChunkResolution"); Method resolve = BukkitBiomePolicy.class.getDeclaredMethod("resolveBiomeResolution", int.class, int.class, - int.class, IrisDimensionCarvingResolver.State.class, boolean.class, DimensionStackLayout.class); + int.class, IrisDimensionCarvingResolver.State.class, boolean.class, DimensionStackLayout.class, + chunkResolution); resolve.setAccessible(true); - Object resolved = resolve.invoke(policy, 3, 40, -5, null, true, stack); + Object resolved = resolve.invoke(policy, 3, 40, -5, null, true, stack, null); assertSame(feature, field(resolved, "irisBiome")); assertSame(host, field(resolved, "dimension")); assertEquals(true, field(resolved, "underground")); diff --git a/core/src/test/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndexCacheTest.java b/core/src/test/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndexCacheTest.java new file mode 100644 index 000000000..2796a23f3 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/structure/nativegen/NativeStructureVolumeIndexCacheTest.java @@ -0,0 +1,69 @@ +package art.arcane.iris.structure.nativegen; + +import art.arcane.iris.generation.runtime.Engine; +import art.arcane.volmlib.nativelib.terrain.structure.NativeStructureVolume; +import art.arcane.volmlib.util.collection.KList; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class NativeStructureVolumeIndexCacheTest { + @Test + public void concurrentQueriesBuildEachOriginOnceAndAnswerIdentically() throws Exception { + Map builds = new ConcurrentHashMap<>(); + NativeStructureVolumeIndex index = NativeStructureVolumeIndex.forTesting((engine, chunkX, chunkZ) -> { + builds.computeIfAbsent(((long) chunkX << 32) ^ (chunkZ & 0xffffffffL), key -> new AtomicInteger()).incrementAndGet(); + return volumesAt(chunkX, chunkZ); + }); + Engine engine = mock(Engine.class); + when(engine.getCacheID()).thenReturn(7); + KList expected = NativeStructureVolumeIndex.forTesting( + (ignored, chunkX, chunkZ) -> volumesAt(chunkX, chunkZ)).resolve(engine, 0, 0, 47, 47); + ExecutorService pool = Executors.newFixedThreadPool(8); + try { + List>> results = new ArrayList<>(); + for (int task = 0; task < 32; task++) { + results.add(pool.submit(() -> index.resolve(engine, 0, 0, 47, 47))); + } + for (Future> result : results) { + assertEquals(expected, result.get(30, TimeUnit.SECONDS)); + } + } finally { + pool.shutdown(); + assertTrue(pool.awaitTermination(30, TimeUnit.SECONDS)); + } + for (AtomicInteger count : builds.values()) { + assertEquals(1, count.get()); + } + int distinct = builds.size(); + index.evictRuntime(7); + assertEquals(expected, index.resolve(engine, 0, 0, 47, 47)); + assertEquals(distinct, builds.size()); + for (AtomicInteger count : builds.values()) { + assertEquals(2, count.get()); + } + } + + private static KList volumesAt(int chunkX, int chunkZ) { + if (Math.floorMod(chunkX * 31 + chunkZ * 17, 5) != 0) { + return NativeStructureVolume.NONE; + } + int minX = chunkX << 4; + int minZ = chunkZ << 4; + return new KList<>(new NativeStructureVolume("test:" + chunkX + "_" + chunkZ, + minX - 20, -10, minZ - 20, minX + 36, 40, minZ + 36, null)); + } +}