From 9a231b2bcfda11dcef90cc27aa54608fc463d885 Mon Sep 17 00:00:00 2001 From: Brian Neumann-Fopiano Date: Wed, 15 Apr 2026 09:45:02 -0400 Subject: [PATCH] Studio FIxes --- core/plugins/Iris/cache/instance | 2 +- .../iris/core/commands/CommandStudio.java | 13 +- .../arcane/iris/core/service/BoardSVC.java | 30 +- .../mantle/components/IrisCaveCarver3D.java | 395 ++++++++++++++-- .../components/MantleCarvingComponent.java | 130 ++--- .../iris/engine/object/IrisCaveProfile.java | 2 +- .../IrisCaveCarver3DNearParityTest.java | 444 ++++++++++++++++-- .../MantleCarvingComponentTop2BlendTest.java | 40 +- ...risDimensionCarvingResolverParityTest.java | 30 +- 9 files changed, 864 insertions(+), 222 deletions(-) diff --git a/core/plugins/Iris/cache/instance b/core/plugins/Iris/cache/instance index 339b5379d..a226f3f54 100644 --- a/core/plugins/Iris/cache/instance +++ b/core/plugins/Iris/cache/instance @@ -1 +1 @@ -466077434 \ No newline at end of file +149256635 \ No newline at end of file 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 953aef98b..1b33af13c 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 @@ -134,29 +134,30 @@ public class CommandStudio implements DirectorExecutor { @Director(description = "Close an open studio project", aliases = {"x", "c"}, sync = true) public void close() { + VolmitSender commandSender = sender(); if (!Iris.service(StudioSVC.class).isProjectOpen()) { - sender().sendMessage(C.RED + "No open studio projects."); + commandSender.sendMessage(C.RED + "No open studio projects."); return; } - sender().sendMessage(C.YELLOW + "Closing studio..."); + commandSender.sendMessage(C.YELLOW + "Closing studio..."); Iris.service(StudioSVC.class).close().whenComplete((result, throwable) -> J.s(() -> { if (throwable != null) { - sender().sendMessage(C.RED + "Studio close failed: " + throwable.getMessage()); + commandSender.sendMessage(C.RED + "Studio close failed: " + throwable.getMessage()); return; } if (result != null && result.failureCause() != null) { - sender().sendMessage(C.RED + "Studio close failed: " + result.failureCause().getMessage()); + commandSender.sendMessage(C.RED + "Studio close failed: " + result.failureCause().getMessage()); return; } if (result != null && result.startupCleanupQueued()) { - sender().sendMessage(C.YELLOW + "Studio closed. Remaining world-family cleanup was queued for startup fallback."); + commandSender.sendMessage(C.YELLOW + "Studio closed. Remaining world-family cleanup was queued for startup fallback."); return; } - sender().sendMessage(C.GREEN + "Studio closed."); + commandSender.sendMessage(C.GREEN + "Studio closed."); })); } diff --git a/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java index 19efee832..8ab03d677 100644 --- a/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java +++ b/core/src/main/java/art/arcane/iris/core/service/BoardSVC.java @@ -22,6 +22,7 @@ import art.arcane.iris.Iris; import art.arcane.iris.core.IrisSettings; import art.arcane.iris.core.loader.IrisData; import art.arcane.iris.core.tools.IrisToolbelt; +import art.arcane.iris.engine.platform.PlatformChunkGenerator; import art.arcane.volmlib.util.board.Board; import art.arcane.volmlib.util.board.BoardProvider; import art.arcane.volmlib.util.board.BoardSettings; @@ -97,9 +98,12 @@ public class BoardSVC implements IrisService, BoardProvider { return; } - if (IrisToolbelt.isIrisStudioWorld(p.getWorld())) { + if (isEligibleWorld(p)) { boards.computeIfAbsent(p, PlayerBoard::new); - } else remove(p); + return; + } + + remove(p); } private void remove(Player player) { @@ -132,6 +136,20 @@ public class BoardSVC implements IrisService, BoardProvider { return board.lines; } + private boolean isEligibleWorld(Player player) { + if (player == null) { + return false; + } + + World world = player.getWorld(); + if (!IrisToolbelt.isIrisWorld(world)) { + return false; + } + + PlatformChunkGenerator access = IrisToolbelt.access(world); + return access != null && access.getEngine() != null; + } + @Data public class PlayerBoard { private final Player player; @@ -159,18 +177,12 @@ public class BoardSVC implements IrisService, BoardProvider { return; } - if (!IrisToolbelt.isIrisStudioWorld(player.getWorld())) { + if (!isEligibleWorld(player)) { boards.remove(player); cancel(); return; } - if (!Iris.service(StudioSVC.class).isProjectOpen()) { - board.update(); - schedule(20); - return; - } - update(); board.update(); schedule(20); diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java index 054e31ac6..2622fbd07 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3D.java @@ -163,6 +163,7 @@ public class IrisCaveCarver3D { maxY = Math.min(maxY, rangeMaxY); } int sampleStep = Math.max(1, profile.getSampleStep()); + boolean exactSampling = sampleStep <= 2; int surfaceClearance = Math.max(0, profile.getSurfaceClearance()); int surfaceBreakDepth = Math.max(0, profile.getSurfaceBreakDepth()); double surfaceBreakNoiseThreshold = profile.getSurfaceBreakNoiseThreshold(); @@ -214,32 +215,53 @@ public class IrisCaveCarver3D { } } - int latticeStep = Math.max(2, sampleStep); - int carved = carvePassLattice( - chunk, - x0, - z0, - minY, - maxY, - latticeStep, - surfaceBreakThresholdBoost, - columnMaxY, - surfaceBreakFloorY, - surfaceBreakColumn, - columnThreshold, - clampedWeights, - verticalEdgeFade, - matterByY, - resolvedMinWeight, - resolvedThresholdPenalty, - 0D, - false - ); - int minCarveCells = Math.max(0, profile.getMinCarveCells()); double recoveryThresholdBoost = Math.max(0, profile.getRecoveryThresholdBoost()); - if (carved < minCarveCells && recoveryThresholdBoost > 0D) { - carved += carvePassLattice( + int carved; + if (exactSampling) { + carved = carvePassExact( + chunk, + x0, + z0, + minY, + maxY, + surfaceBreakThresholdBoost, + columnMaxY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + resolvedMinWeight, + resolvedThresholdPenalty, + 0D, + false + ); + if (carved < minCarveCells && recoveryThresholdBoost > 0D) { + carved += carvePassExact( + chunk, + x0, + z0, + minY, + maxY, + surfaceBreakThresholdBoost, + columnMaxY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + resolvedMinWeight, + resolvedThresholdPenalty, + recoveryThresholdBoost, + true + ); + } + } else { + int latticeStep = sampleStep; + carved = carvePassLattice( chunk, x0, z0, @@ -256,33 +278,32 @@ public class IrisCaveCarver3D { matterByY, resolvedMinWeight, resolvedThresholdPenalty, - recoveryThresholdBoost, - true - ); - } - - if (carved == 0 && hasFallbackCandidates(columnMaxY, clampedWeights, minY, resolvedMinWeight)) { - carved += carvePassFallback( - chunk, - x0, - z0, - minY, - maxY, - sampleStep, - surfaceBreakThresholdBoost, - columnMaxY, - surfaceBreakFloorY, - surfaceBreakColumn, - columnThreshold, - clampedWeights, - verticalEdgeFade, - matterByY, - resolvedMinWeight, - resolvedThresholdPenalty, 0D, false ); if (carved < minCarveCells && recoveryThresholdBoost > 0D) { + carved += carvePassLattice( + chunk, + x0, + z0, + minY, + maxY, + latticeStep, + surfaceBreakThresholdBoost, + columnMaxY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + resolvedMinWeight, + resolvedThresholdPenalty, + recoveryThresholdBoost, + true + ); + } + if (carved == 0 && hasFallbackCandidates(columnMaxY, clampedWeights, minY, resolvedMinWeight)) { carved += carvePassFallback( chunk, x0, @@ -300,9 +321,31 @@ public class IrisCaveCarver3D { matterByY, resolvedMinWeight, resolvedThresholdPenalty, - recoveryThresholdBoost, - true + 0D, + false ); + if (carved < minCarveCells && recoveryThresholdBoost > 0D) { + carved += carvePassFallback( + chunk, + x0, + z0, + minY, + maxY, + sampleStep, + surfaceBreakThresholdBoost, + columnMaxY, + surfaceBreakFloorY, + surfaceBreakColumn, + columnThreshold, + clampedWeights, + verticalEdgeFade, + matterByY, + resolvedMinWeight, + resolvedThresholdPenalty, + recoveryThresholdBoost, + true + ); + } } } @@ -312,6 +355,125 @@ public class IrisCaveCarver3D { } } + private int carvePassExact( + MantleChunk chunk, + int x0, + int z0, + int minY, + int maxY, + double surfaceBreakThresholdBoost, + int[] columnMaxY, + int[] surfaceBreakFloorY, + boolean[] surfaceBreakColumn, + double[] columnThreshold, + double[] clampedWeights, + double[] verticalEdgeFade, + MatterCavern[] matterByY, + double minWeight, + double thresholdPenalty, + double thresholdBoost, + boolean skipExistingCarved + ) { + int carved = 0; + Scratch scratch = SCRATCH.get(); + double[] passThreshold = scratch.passThreshold; + int[] activeColumnIndices = scratch.activeColumnIndices; + int[] activeColumnTopY = scratch.activeColumnTopY; + int activeColumnCount = 0; + + for (int index = 0; index < 256; index++) { + double columnWeight = clampedWeights[index]; + if (columnWeight <= minWeight || columnMaxY[index] < minY) { + passThreshold[index] = Double.NaN; + continue; + } + + passThreshold[index] = columnThreshold[index] + thresholdBoost - ((1D - columnWeight) * thresholdPenalty); + activeColumnIndices[activeColumnCount] = index; + activeColumnTopY[activeColumnCount] = columnMaxY[index]; + activeColumnCount++; + } + + if (activeColumnCount == 0) { + return 0; + } + + int[] planeColumnIndices = scratch.planeColumnIndices; + double[] planeDensity = scratch.planeDensity; + int minSection = minY >> 4; + int maxSection = maxY >> 4; + + for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) { + int sectionMinY = Math.max(minY, sectionIndex << 4); + int sectionMaxY = Math.min(maxY, (sectionIndex << 4) + 15); + MatterSlice cavernSlice = resolveCavernSlice(scratch, chunk, sectionIndex); + + for (int y = sectionMinY; y <= sectionMaxY; y++) { + int planeCount = 0; + for (int activeIndex = 0; activeIndex < activeColumnCount; activeIndex++) { + if (activeColumnTopY[activeIndex] < y) { + continue; + } + + planeColumnIndices[planeCount] = activeColumnIndices[activeIndex]; + planeCount++; + } + + if (planeCount == 0) { + continue; + } + + fillDensityPlane(x0, z0, y, planeColumnIndices, planeCount, planeDensity); + int fadeIndex = y - minY; + int localY = y & 15; + MatterCavern matter = matterByY[fadeIndex]; + + if (skipExistingCarved) { + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + double localThreshold = passThreshold[columnIndex]; + if (surfaceBreakColumn[columnIndex] && y >= surfaceBreakFloorY[columnIndex]) { + localThreshold += surfaceBreakThresholdBoost; + } + localThreshold -= verticalEdgeFade[fadeIndex]; + if (planeDensity[planeIndex] > localThreshold) { + continue; + } + + int localX = columnIndex >> 4; + int localZ = columnIndex & 15; + if (cavernSlice.get(localX, localY, localZ) != null) { + continue; + } + + cavernSlice.set(localX, localY, localZ, matter); + carved++; + } + continue; + } + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + double localThreshold = passThreshold[columnIndex]; + if (surfaceBreakColumn[columnIndex] && y >= surfaceBreakFloorY[columnIndex]) { + localThreshold += surfaceBreakThresholdBoost; + } + localThreshold -= verticalEdgeFade[fadeIndex]; + if (planeDensity[planeIndex] > localThreshold) { + continue; + } + + int localX = columnIndex >> 4; + int localZ = columnIndex & 15; + cavernSlice.set(localX, localY, localZ, matter); + carved++; + } + } + } + + return carved; + } + private int carvePassLattice( MantleChunk chunk, int x0, @@ -557,6 +719,137 @@ public class IrisCaveCarver3D { return sampleDensityWarpModules(x, y, z); } + private void fillDensityPlane(int x0, int z0, int y, int[] planeColumnIndices, int planeCount, double[] planeDensity) { + if (!hasWarp) { + if (!hasModules) { + fillDensityPlaneNoWarpNoModules(x0, z0, y, planeColumnIndices, planeCount, planeDensity); + return; + } + + fillDensityPlaneNoWarpModules(x0, z0, y, planeColumnIndices, planeCount, planeDensity); + return; + } + + if (!hasModules) { + fillDensityPlaneWarpOnly(x0, z0, y, planeColumnIndices, planeCount, planeDensity); + return; + } + + fillDensityPlaneWarpModules(x0, z0, y, planeColumnIndices, planeCount, planeDensity); + } + + private void fillDensityPlaneNoWarpNoModules(int x0, int z0, int y, int[] planeColumnIndices, int planeCount, double[] planeDensity) { + CNG localBaseDensity = baseDensity; + CNG localDetailDensity = detailDensity; + double localBaseWeight = baseWeight; + double localDetailWeight = detailWeight; + double normalization = inverseNormalization; + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int x = x0 + (columnIndex >> 4); + int z = z0 + (columnIndex & 15); + double density = signed(localBaseDensity.noiseFast3D(x, y, z)) * localBaseWeight; + density += signed(localDetailDensity.noiseFast3D(x, y, z)) * localDetailWeight; + planeDensity[planeIndex] = density * normalization; + } + } + + private void fillDensityPlaneNoWarpModules(int x0, int z0, int y, int[] planeColumnIndices, int planeCount, double[] planeDensity) { + CNG localBaseDensity = baseDensity; + CNG localDetailDensity = detailDensity; + ModuleState[] localModules = modules; + double localBaseWeight = baseWeight; + double localDetailWeight = detailWeight; + double normalization = inverseNormalization; + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + int x = x0 + (columnIndex >> 4); + int z = z0 + (columnIndex & 15); + double density = signed(localBaseDensity.noiseFast3D(x, y, z)) * localBaseWeight; + density += signed(localDetailDensity.noiseFast3D(x, y, z)) * localDetailWeight; + for (int moduleIndex = 0; moduleIndex < localModules.length; moduleIndex++) { + ModuleState module = localModules[moduleIndex]; + if (y < module.minY || y > module.maxY) { + continue; + } + + double moduleDensity = signed(module.density.noiseFast3D(x, y, z)) - module.threshold; + if (module.invert) { + moduleDensity = -moduleDensity; + } + + density += moduleDensity * module.weight; + } + + planeDensity[planeIndex] = density * normalization; + } + } + + private void fillDensityPlaneWarpOnly(int x0, int z0, int y, int[] planeColumnIndices, int planeCount, double[] planeDensity) { + CNG localBaseDensity = baseDensity; + CNG localDetailDensity = detailDensity; + CNG localWarpDensity = warpDensity; + double localBaseWeight = baseWeight; + double localDetailWeight = detailWeight; + double localWarpStrength = warpStrength; + double normalization = inverseNormalization; + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + double x = x0 + (columnIndex >> 4); + double z = z0 + (columnIndex & 15); + double warpA = signed(localWarpDensity.noiseFast3D(x, y, z)); + double warpB = signed(localWarpDensity.noiseFast3D(x + 31.37D, y - 17.21D, z + 23.91D)); + double warpedX = x + (warpA * localWarpStrength); + double warpedY = y + (warpB * localWarpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * localWarpStrength); + double density = signed(localBaseDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * localBaseWeight; + density += signed(localDetailDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * localDetailWeight; + planeDensity[planeIndex] = density * normalization; + } + } + + private void fillDensityPlaneWarpModules(int x0, int z0, int y, int[] planeColumnIndices, int planeCount, double[] planeDensity) { + CNG localBaseDensity = baseDensity; + CNG localDetailDensity = detailDensity; + CNG localWarpDensity = warpDensity; + ModuleState[] localModules = modules; + double localBaseWeight = baseWeight; + double localDetailWeight = detailWeight; + double localWarpStrength = warpStrength; + double normalization = inverseNormalization; + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) { + int columnIndex = planeColumnIndices[planeIndex]; + double x = x0 + (columnIndex >> 4); + double z = z0 + (columnIndex & 15); + double warpA = signed(localWarpDensity.noiseFast3D(x, y, z)); + double warpB = signed(localWarpDensity.noiseFast3D(x + 31.37D, y - 17.21D, z + 23.91D)); + double warpedX = x + (warpA * localWarpStrength); + double warpedY = y + (warpB * localWarpStrength); + double warpedZ = z + ((warpA - warpB) * 0.5D * localWarpStrength); + double density = signed(localBaseDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * localBaseWeight; + density += signed(localDetailDensity.noiseFast3D(warpedX, warpedY, warpedZ)) * localDetailWeight; + for (int moduleIndex = 0; moduleIndex < localModules.length; moduleIndex++) { + ModuleState module = localModules[moduleIndex]; + if (y < module.minY || y > module.maxY) { + continue; + } + + double moduleDensity = signed(module.density.noiseFast3D(warpedX, warpedY, warpedZ)) - module.threshold; + if (module.invert) { + moduleDensity = -moduleDensity; + } + + density += moduleDensity * module.weight; + } + + planeDensity[planeIndex] = density * normalization; + } + } + private double sampleDensityNoWarpNoModules(int x, int y, int z) { double density = signed(baseDensity.noiseFast3D(x, y, z)) * baseWeight; density += signed(detailDensity.noiseFast3D(x, y, z)) * detailWeight; @@ -765,6 +1058,10 @@ public class IrisCaveCarver3D { private final double[] passThreshold = new double[256]; private final double[] fullWeights = new double[256]; private final double[] clampedColumnWeights = new double[256]; + private final int[] activeColumnIndices = new int[256]; + private final int[] activeColumnTopY = new int[256]; + private final int[] planeColumnIndices = new int[256]; + private final double[] planeDensity = new double[256]; private final int[] tileIndices = new int[4]; private final int[] tileLocalX = new int[4]; private final int[] tileLocalZ = new int[4]; diff --git a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java index d29f6feb2..35e536ab8 100644 --- a/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java +++ b/core/src/main/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponent.java @@ -46,9 +46,6 @@ import java.util.Map; public class MantleCarvingComponent extends IrisMantleComponent { private static final int CHUNK_SIZE = 16; private static final int CHUNK_AREA = CHUNK_SIZE * CHUNK_SIZE; - private static final int TILE_SIZE = 2; - private static final int TILE_COUNT = CHUNK_SIZE / TILE_SIZE; - private static final int TILE_AREA = TILE_COUNT * TILE_COUNT; private static final int BLEND_RADIUS = 3; private static final int FIELD_SIZE = CHUNK_SIZE + (BLEND_RADIUS * 2); private static final double MIN_WEIGHT = 0.08D; @@ -104,20 +101,18 @@ public class MantleCarvingComponent extends IrisMantleComponent { private List resolveWeightedProfiles(int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { BlendScratch blendScratch = BLEND_SCRATCH.get(); IrisCaveProfile[] profileField = blendScratch.profileField; - Map tileProfileWeights = blendScratch.tileProfileWeights; + Map columnProfileWeights = blendScratch.columnProfileWeights; IdentityHashMap activeProfiles = blendScratch.activeProfiles; IrisCaveProfile[] kernelProfiles = blendScratch.kernelProfiles; double[] kernelProfileWeights = blendScratch.kernelProfileWeights; activeProfiles.clear(); fillProfileField(profileField, chunkX, chunkZ, complex, resolverState, caveBiomeCache); - for (int tileX = 0; tileX < TILE_COUNT; tileX++) { - for (int tileZ = 0; tileZ < TILE_COUNT; tileZ++) { + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { int profileCount = 0; - int sampleLocalX = (tileX * TILE_SIZE) + 1; - int sampleLocalZ = (tileZ * TILE_SIZE) + 1; - int centerX = sampleLocalX + BLEND_RADIUS; - int centerZ = sampleLocalZ + BLEND_RADIUS; + int centerX = localX + BLEND_RADIUS; + int centerZ = localZ + BLEND_RADIUS; double totalKernelWeight = 0D; for (int kernelIndex = 0; kernelIndex < KERNEL_SIZE; kernelIndex++) { @@ -164,30 +159,30 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } - int tileIndex = tileIndex(tileX, tileZ); + int columnIndex = (localX << 4) | localZ; double dominantWeight = clampWeight(dominantKernelWeight / totalKernelWeight); - double[] tileWeights = tileProfileWeights.get(dominantProfile); - if (tileWeights == null) { - tileWeights = new double[TILE_AREA]; - tileProfileWeights.put(dominantProfile, tileWeights); + double[] weights = columnProfileWeights.get(dominantProfile); + if (weights == null) { + weights = new double[CHUNK_AREA]; + columnProfileWeights.put(dominantProfile, weights); } else if (!activeProfiles.containsKey(dominantProfile)) { - Arrays.fill(tileWeights, 0D); + Arrays.fill(weights, 0D); } activeProfiles.put(dominantProfile, Boolean.TRUE); - tileWeights[tileIndex] = dominantWeight; + weights[columnIndex] = dominantWeight; } } - List tileWeightedProfiles = new ArrayList<>(); + List columnWeightedProfiles = new ArrayList<>(); for (IrisCaveProfile profile : activeProfiles.keySet()) { - double[] tileWeights = tileProfileWeights.get(profile); - if (tileWeights == null) { + double[] weights = columnProfileWeights.get(profile); + if (weights == null) { continue; } double totalWeight = 0D; double maxWeight = 0D; - for (double weight : tileWeights) { + for (double weight : weights) { totalWeight += weight; if (weight > maxWeight) { maxWeight = weight; @@ -198,12 +193,11 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } - double averageWeight = totalWeight / TILE_AREA; - tileWeightedProfiles.add(new WeightedProfile(profile, tileWeights, averageWeight, null)); + double averageWeight = totalWeight / CHUNK_AREA; + columnWeightedProfiles.add(new WeightedProfile(profile, weights, averageWeight, null)); } - List boundedTileProfiles = limitAndMergeBlendedProfiles(tileWeightedProfiles, MAX_BLENDED_PROFILE_PASSES, TILE_AREA); - List blendedProfiles = expandTileWeightedProfiles(boundedTileProfiles); + List blendedProfiles = limitAndMergeBlendedProfiles(columnWeightedProfiles, MAX_BLENDED_PROFILE_PASSES, CHUNK_AREA); List resolvedProfiles = resolveDimensionCarvingProfiles(chunkX, chunkZ, resolverState, blendScratch); resolvedProfiles.addAll(blendedProfiles); return resolvedProfiles; @@ -216,8 +210,8 @@ public class MantleCarvingComponent extends IrisMantleComponent { return weightedProfiles; } - Map dimensionTilePlans = blendScratch.dimensionTilePlans; - dimensionTilePlans.clear(); + Map dimensionColumnPlans = blendScratch.dimensionColumnPlans; + dimensionColumnPlans.clear(); for (IrisDimensionCarvingEntry entry : entries) { if (entry == null || !entry.isEnabled()) { @@ -229,13 +223,13 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } - IrisDimensionCarvingEntry[] tilePlan = dimensionTilePlans.computeIfAbsent(entry, key -> new IrisDimensionCarvingEntry[TILE_AREA]); - buildDimensionTilePlan(tilePlan, chunkX, chunkZ, entry, resolverState); + IrisDimensionCarvingEntry[] columnPlan = dimensionColumnPlans.computeIfAbsent(entry, key -> new IrisDimensionCarvingEntry[CHUNK_AREA]); + buildDimensionColumnPlan(columnPlan, chunkX, chunkZ, entry, resolverState); - Map rootProfileTileWeights = new IdentityHashMap<>(); + Map rootProfileColumnWeights = new IdentityHashMap<>(); IrisRange worldYRange = entry.getWorldYRange(); - for (int tileIndex = 0; tileIndex < TILE_AREA; tileIndex++) { - IrisDimensionCarvingEntry resolvedEntry = tilePlan[tileIndex]; + for (int columnIndex = 0; columnIndex < CHUNK_AREA; columnIndex++) { + IrisDimensionCarvingEntry resolvedEntry = columnPlan[columnIndex]; IrisBiome resolvedBiome = IrisDimensionCarvingResolver.resolveEntryBiome(getEngineMantle().getEngine(), resolvedEntry, resolverState); if (resolvedBiome == null) { continue; @@ -246,75 +240,33 @@ public class MantleCarvingComponent extends IrisMantleComponent { continue; } - double[] tileWeights = rootProfileTileWeights.computeIfAbsent(profile, key -> new double[TILE_AREA]); - tileWeights[tileIndex] = 1D; + double[] columnWeights = rootProfileColumnWeights.computeIfAbsent(profile, key -> new double[CHUNK_AREA]); + columnWeights[columnIndex] = 1D; } - List> profileEntries = new ArrayList<>(rootProfileTileWeights.entrySet()); + List> profileEntries = new ArrayList<>(rootProfileColumnWeights.entrySet()); profileEntries.sort((a, b) -> Integer.compare(a.getKey().hashCode(), b.getKey().hashCode())); for (Map.Entry profileEntry : profileEntries) { - double[] columnWeights = expandTileWeightsToColumns(profileEntry.getValue()); - weightedProfiles.add(new WeightedProfile(profileEntry.getKey(), columnWeights, -1D, worldYRange)); + weightedProfiles.add(new WeightedProfile(profileEntry.getKey(), profileEntry.getValue(), -1D, worldYRange)); } } return weightedProfiles; } - private void buildDimensionTilePlan(IrisDimensionCarvingEntry[] tilePlan, int chunkX, int chunkZ, IrisDimensionCarvingEntry entry, IrisDimensionCarvingResolver.State resolverState) { - for (int tileX = 0; tileX < TILE_COUNT; tileX++) { - int worldX = (chunkX << 4) + (tileX * TILE_SIZE); - for (int tileZ = 0; tileZ < TILE_COUNT; tileZ++) { - int worldZ = (chunkZ << 4) + (tileZ * TILE_SIZE); - int tileIndex = tileIndex(tileX, tileZ); - tilePlan[tileIndex] = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState); + private void buildDimensionColumnPlan(IrisDimensionCarvingEntry[] columnPlan, int chunkX, int chunkZ, IrisDimensionCarvingEntry entry, IrisDimensionCarvingResolver.State resolverState) { + int baseX = chunkX << 4; + int baseZ = chunkZ << 4; + for (int localX = 0; localX < CHUNK_SIZE; localX++) { + int worldX = baseX + localX; + for (int localZ = 0; localZ < CHUNK_SIZE; localZ++) { + int worldZ = baseZ + localZ; + int columnIndex = (localX << 4) | localZ; + columnPlan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(getEngineMantle().getEngine(), entry, worldX, worldZ, resolverState); } } } - private List expandTileWeightedProfiles(List tileWeightedProfiles) { - List expandedProfiles = new ArrayList<>(tileWeightedProfiles.size()); - for (WeightedProfile tileWeightedProfile : tileWeightedProfiles) { - double[] columnWeights = expandTileWeightsToColumns(tileWeightedProfile.columnWeights); - double averageWeight = computeAverageWeight(columnWeights, CHUNK_AREA); - expandedProfiles.add(new WeightedProfile(tileWeightedProfile.profile, columnWeights, averageWeight, tileWeightedProfile.worldYRange)); - } - expandedProfiles.sort(MantleCarvingComponent::compareByCarveOrder); - return expandedProfiles; - } - - private static double[] expandTileWeightsToColumns(double[] tileWeights) { - double[] columnWeights = new double[CHUNK_AREA]; - if (tileWeights == null || tileWeights.length == 0) { - return columnWeights; - } - - for (int tileX = 0; tileX < TILE_COUNT; tileX++) { - int columnX = tileX * TILE_SIZE; - int columnX2 = columnX + 1; - for (int tileZ = 0; tileZ < TILE_COUNT; tileZ++) { - int tileIndex = tileIndex(tileX, tileZ); - double weight = tileWeights[tileIndex]; - if (weight <= 0D) { - continue; - } - - int columnZ = tileZ * TILE_SIZE; - int columnZ2 = columnZ + 1; - columnWeights[(columnX << 4) | columnZ] = weight; - columnWeights[(columnX << 4) | columnZ2] = weight; - columnWeights[(columnX2 << 4) | columnZ] = weight; - columnWeights[(columnX2 << 4) | columnZ2] = weight; - } - } - - return columnWeights; - } - - private static int tileIndex(int tileX, int tileZ) { - return (tileX * TILE_COUNT) + tileZ; - } - private void fillProfileField(IrisCaveProfile[] profileField, int chunkX, int chunkZ, IrisComplex complex, IrisDimensionCarvingResolver.State resolverState, Long2ObjectOpenHashMap caveBiomeCache) { int startX = (chunkX << 4) - BLEND_RADIUS; int startZ = (chunkZ << 4) - BLEND_RADIUS; @@ -554,8 +506,8 @@ public class MantleCarvingComponent extends IrisMantleComponent { private final IrisCaveProfile[] profileField = new IrisCaveProfile[FIELD_SIZE * FIELD_SIZE]; private final IrisCaveProfile[] kernelProfiles = new IrisCaveProfile[KERNEL_SIZE]; private final double[] kernelProfileWeights = new double[KERNEL_SIZE]; - private final IdentityHashMap tileProfileWeights = new IdentityHashMap<>(); - private final IdentityHashMap dimensionTilePlans = new IdentityHashMap<>(); + private final IdentityHashMap columnProfileWeights = new IdentityHashMap<>(); + private final IdentityHashMap dimensionColumnPlans = new IdentityHashMap<>(); private final IdentityHashMap activeProfiles = new IdentityHashMap<>(); private final int[] chunkSurfaceHeights = new int[CHUNK_AREA]; } diff --git a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java index 815bddf74..06018195c 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java +++ b/core/src/main/java/art/arcane/iris/engine/object/IrisCaveProfile.java @@ -66,7 +66,7 @@ public class IrisCaveProfile { @MinNumber(1) @MaxNumber(8) @Desc("Vertical sample step used while evaluating cave density.") - private int sampleStep = 2; + private int sampleStep = 1; @MinNumber(0) @MaxNumber(4096) diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java index 1c43f179f..668334741 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/IrisCaveCarver3DNearParityTest.java @@ -5,6 +5,7 @@ import art.arcane.iris.engine.framework.Engine; import art.arcane.iris.engine.framework.EngineMetrics; import art.arcane.iris.engine.framework.SeedManager; import art.arcane.iris.engine.mantle.MantleWriter; +import art.arcane.iris.engine.object.IrisCaveFieldModule; import art.arcane.iris.engine.object.IrisCaveProfile; import art.arcane.iris.engine.object.IrisDimension; import art.arcane.iris.engine.object.IrisGeneratorStyle; @@ -12,8 +13,11 @@ import art.arcane.iris.engine.object.IrisRange; import art.arcane.iris.engine.object.IrisStyledRange; import art.arcane.iris.engine.object.IrisWorld; import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.iris.util.project.noise.CNG; +import art.arcane.volmlib.util.collection.KList; import art.arcane.volmlib.util.mantle.runtime.Mantle; import art.arcane.volmlib.util.mantle.runtime.MantleChunk; +import art.arcane.volmlib.util.math.RNG; import art.arcane.volmlib.util.matter.Matter; import art.arcane.volmlib.util.matter.MatterCavern; import art.arcane.volmlib.util.matter.MatterSlice; @@ -24,6 +28,8 @@ import org.bukkit.block.data.BlockData; import org.junit.BeforeClass; import org.junit.Test; +import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; @@ -41,49 +47,75 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; public class IrisCaveCarver3DNearParityTest { + private static Method sampleDensityMethod; + private static Field engineField; + private static Field dataField; + private static Field profileField; + private static Field surfaceBreakDensityField; + private static Field thresholdRngField; + private static Field carveAirField; + private static Field carveLavaField; + private static Field carveForcedAirField; + @BeforeClass - public static void setupBukkit() { - if (Bukkit.getServer() != null) { - return; + public static void setupBukkit() throws Exception { + if (Bukkit.getServer() == null) { + Server server = mock(Server.class); + BlockData emptyBlockData = mock(BlockData.class); + doReturn(Logger.getLogger("IrisTest")).when(server).getLogger(); + doReturn("IrisTestServer").when(server).getName(); + doReturn("1.0").when(server).getVersion(); + doReturn("1.0").when(server).getBukkitVersion(); + doReturn(emptyBlockData).when(server).createBlockData(any(Material.class)); + doReturn(emptyBlockData).when(server).createBlockData(anyString()); + Bukkit.setServer(server); } - Server server = mock(Server.class); - BlockData emptyBlockData = mock(BlockData.class); - doReturn(Logger.getLogger("IrisTest")).when(server).getLogger(); - doReturn("IrisTestServer").when(server).getName(); - doReturn("1.0").when(server).getVersion(); - doReturn("1.0").when(server).getBukkitVersion(); - doReturn(emptyBlockData).when(server).createBlockData(any(Material.class)); - doReturn(emptyBlockData).when(server).createBlockData(anyString()); - Bukkit.setServer(server); + sampleDensityMethod = IrisCaveCarver3D.class.getDeclaredMethod("sampleDensityOptimized", int.class, int.class, int.class); + sampleDensityMethod.setAccessible(true); + engineField = IrisCaveCarver3D.class.getDeclaredField("engine"); + engineField.setAccessible(true); + dataField = IrisCaveCarver3D.class.getDeclaredField("data"); + dataField.setAccessible(true); + profileField = IrisCaveCarver3D.class.getDeclaredField("profile"); + profileField.setAccessible(true); + surfaceBreakDensityField = IrisCaveCarver3D.class.getDeclaredField("surfaceBreakDensity"); + surfaceBreakDensityField.setAccessible(true); + thresholdRngField = IrisCaveCarver3D.class.getDeclaredField("thresholdRng"); + thresholdRngField.setAccessible(true); + carveAirField = IrisCaveCarver3D.class.getDeclaredField("carveAir"); + carveAirField.setAccessible(true); + carveLavaField = IrisCaveCarver3D.class.getDeclaredField("carveLava"); + carveLavaField.setAccessible(true); + carveForcedAirField = IrisCaveCarver3D.class.getDeclaredField("carveForcedAir"); + carveForcedAirField.setAccessible(true); } @Test public void carvedCellDistributionStableAcrossEquivalentCarvers() { Engine engine = createEngine(128, 92); - IrisCaveCarver3D firstCarver = new IrisCaveCarver3D(engine, createProfile()); + IrisCaveCarver3D firstCarver = new IrisCaveCarver3D(engine, createProfile(true, true)); WriterCapture firstCapture = createWriterCapture(128); int firstCarved = firstCarver.carve(firstCapture.writer, 7, -3); - IrisCaveCarver3D secondCarver = new IrisCaveCarver3D(engine, createProfile()); + IrisCaveCarver3D secondCarver = new IrisCaveCarver3D(engine, createProfile(true, true)); WriterCapture secondCapture = createWriterCapture(128); int secondCarved = secondCarver.carve(secondCapture.writer, 7, -3); assertTrue(firstCarved > 0); assertEquals(firstCarved, secondCarved); assertEquals(firstCapture.carvedCells, secondCapture.carvedCells); + assertEquals(firstCapture.carvedLiquids, secondCapture.carvedLiquids); } @Test - public void latticePathCarvesChunkEdgesAndRespectsWorldHeightClipping() { + public void exactPathCarvesChunkEdgesAndRespectsWorldHeightClipping() { Engine engine = createEngine(48, 46); - IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, createProfile()); + IrisCaveCarver3D carver = new IrisCaveCarver3D(engine, createProfile(true, true)); WriterCapture capture = createWriterCapture(48); - double[] columnWeights = new double[256]; - Arrays.fill(columnWeights, 1D); - int[] precomputedSurfaceHeights = new int[256]; - Arrays.fill(precomputedSurfaceHeights, 46); + double[] columnWeights = fullWeights(); + int[] precomputedSurfaceHeights = filledHeights(46); int carved = carver.carve(capture.writer, 0, 0, columnWeights, 0D, 0D, new IrisRange(0D, 80D), precomputedSurfaceHeights); @@ -96,6 +128,293 @@ public class IrisCaveCarver3DNearParityTest { assertTrue(minY(capture.carvedCells) >= 0); } + @Test + public void exactPathMatchesNaiveReferenceWithoutWarpOrModules() throws Exception { + assertExactParity(false, false); + } + + @Test + public void exactPathMatchesNaiveReferenceWithWarpAndModules() throws Exception { + assertExactParity(true, true); + } + + @Test + public void legacySampleStepTwoMatchesExactReference() throws Exception { + Engine engine = createEngine(96, 90); + double[] columnWeights = fullWeights(); + int[] precomputedSurfaceHeights = filledHeights(90); + IrisRange worldYRange = new IrisRange(0D, 88D); + + IrisCaveProfile exactProfile = createProfile(true, true).setSampleStep(1); + IrisCaveCarver3D exactCarver = new IrisCaveCarver3D(engine, exactProfile); + WriterCapture exactCapture = createWriterCapture(96); + int exactCarved = exactCarver.carve(exactCapture.writer, 5, -1, columnWeights, 0D, 0D, worldYRange, precomputedSurfaceHeights); + + IrisCaveProfile legacyProfile = createProfile(true, true).setSampleStep(2); + IrisCaveCarver3D legacyCarver = new IrisCaveCarver3D(engine, legacyProfile); + WriterCapture legacyCapture = createWriterCapture(96); + int legacyCarved = legacyCarver.carve(legacyCapture.writer, 5, -1, columnWeights, 0D, 0D, worldYRange, precomputedSurfaceHeights); + + assertEquals(exactCarved, legacyCarved); + assertEquals(exactCapture.carvedCells, legacyCapture.carvedCells); + assertEquals(exactCapture.carvedLiquids, legacyCapture.carvedLiquids); + } + + @Test + public void exactPathUsesExpectedLavaAndForcedAirBands() { + Engine engine = createEngine(48, 46); + double[] columnWeights = fullWeights(); + int[] precomputedSurfaceHeights = filledHeights(46); + + IrisCaveProfile lavaProfile = createProfile(false, false).setAllowLava(true).setAllowWater(false); + IrisCaveCarver3D lavaCarver = new IrisCaveCarver3D(engine, lavaProfile); + WriterCapture lavaCapture = createWriterCapture(48); + lavaCarver.carve(lavaCapture.writer, 0, 0, columnWeights, 0D, 0D, new IrisRange(0D, 80D), precomputedSurfaceHeights); + + IrisCaveProfile forcedAirProfile = createProfile(false, false).setAllowLava(false).setAllowWater(false); + IrisCaveCarver3D forcedAirCarver = new IrisCaveCarver3D(engine, forcedAirProfile); + WriterCapture forcedAirCapture = createWriterCapture(48); + forcedAirCarver.carve(forcedAirCapture.writer, 0, 0, columnWeights, 0D, 0D, new IrisRange(0D, 80D), precomputedSurfaceHeights); + + assertTrue(containsLiquidInRange(lavaCapture.carvedLiquids, 0, 18, (byte) 2)); + assertTrue(containsLiquidInRange(forcedAirCapture.carvedLiquids, 0, 18, (byte) 3)); + assertTrue(containsLiquidInRange(lavaCapture.carvedLiquids, 19, 47, (byte) 0)); + assertTrue(containsLiquidInRange(forcedAirCapture.carvedLiquids, 19, 47, (byte) 0)); + } + + @Test + public void optimizedExactPathOutperformsNaiveReference() throws Exception { + Engine engine = createEngine(128, 92); + IrisCaveCarver3D optimizedCarver = new IrisCaveCarver3D(engine, createProfile(true, true)); + IrisCaveCarver3D naiveCarver = new IrisCaveCarver3D(engine, createProfile(true, true)); + double[] columnWeights = fullWeights(); + int[] precomputedSurfaceHeights = filledHeights(92); + IrisRange worldYRange = new IrisRange(0D, 96D); + + for (int warmup = 0; warmup < 4; warmup++) { + runOptimizedOnce(optimizedCarver, 3, -2, columnWeights, worldYRange, precomputedSurfaceHeights, 128); + runNaiveOnce(naiveCarver, 3, -2, columnWeights, worldYRange, precomputedSurfaceHeights, 128); + } + + long optimizedTime = 0L; + long naiveTime = 0L; + for (int iteration = 0; iteration < 10; iteration++) { + if ((iteration & 1) == 0) { + optimizedTime += runOptimizedOnce(optimizedCarver, 3, -2, columnWeights, worldYRange, precomputedSurfaceHeights, 128); + naiveTime += runNaiveOnce(naiveCarver, 3, -2, columnWeights, worldYRange, precomputedSurfaceHeights, 128); + continue; + } + + naiveTime += runNaiveOnce(naiveCarver, 3, -2, columnWeights, worldYRange, precomputedSurfaceHeights, 128); + optimizedTime += runOptimizedOnce(optimizedCarver, 3, -2, columnWeights, worldYRange, precomputedSurfaceHeights, 128); + } + + double speedup = naiveTime / (double) optimizedTime; + assertTrue("expected at least 2.0x speedup but was " + speedup, speedup >= 2D); + } + + private void assertExactParity(boolean warp, boolean modules) throws Exception { + Engine engine = createEngine(96, 90); + double[] columnWeights = fullWeights(); + int[] precomputedSurfaceHeights = filledHeights(90); + IrisRange worldYRange = new IrisRange(0D, 88D); + + IrisCaveCarver3D optimizedCarver = new IrisCaveCarver3D(engine, createProfile(warp, modules)); + WriterCapture optimizedCapture = createWriterCapture(96); + int optimizedCarved = optimizedCarver.carve(optimizedCapture.writer, 5, -1, columnWeights, 0D, 0D, worldYRange, precomputedSurfaceHeights); + + IrisCaveCarver3D naiveCarver = new IrisCaveCarver3D(engine, createProfile(warp, modules)); + WriterCapture naiveCapture = createWriterCapture(96); + int naiveCarved = carveNaiveExact(naiveCarver, naiveCapture.writer, 5, -1, columnWeights, worldYRange, precomputedSurfaceHeights); + + assertEquals(optimizedCarved, naiveCarved); + assertEquals(optimizedCapture.carvedCells, naiveCapture.carvedCells); + assertEquals(optimizedCapture.carvedLiquids, naiveCapture.carvedLiquids); + } + + private long runOptimizedOnce(IrisCaveCarver3D carver, int chunkX, int chunkZ, double[] columnWeights, IrisRange worldYRange, int[] precomputedSurfaceHeights, int worldHeight) { + WriterCapture capture = createWriterCapture(worldHeight); + long start = System.nanoTime(); + carver.carve(capture.writer, chunkX, chunkZ, columnWeights, 0D, 0D, worldYRange, precomputedSurfaceHeights); + long elapsed = System.nanoTime() - start; + assertTrue(!capture.carvedCells.isEmpty()); + return elapsed; + } + + private long runNaiveOnce(IrisCaveCarver3D carver, int chunkX, int chunkZ, double[] columnWeights, IrisRange worldYRange, int[] precomputedSurfaceHeights, int worldHeight) throws Exception { + WriterCapture capture = createWriterCapture(worldHeight); + long start = System.nanoTime(); + int carved = carveNaiveExact(carver, capture.writer, chunkX, chunkZ, columnWeights, worldYRange, precomputedSurfaceHeights); + long elapsed = System.nanoTime() - start; + assertTrue(carved > 0); + return elapsed; + } + + private int carveNaiveExact(IrisCaveCarver3D carver, MantleWriter writer, int chunkX, int chunkZ, double[] columnWeights, IrisRange worldYRange, int[] precomputedSurfaceHeights) throws Exception { + Engine engine = (Engine) engineField.get(carver); + IrisData data = (IrisData) dataField.get(carver); + IrisCaveProfile profile = (IrisCaveProfile) profileField.get(carver); + CNG surfaceBreakDensity = (CNG) surfaceBreakDensityField.get(carver); + RNG thresholdRng = (RNG) thresholdRngField.get(carver); + MatterCavern carveAir = (MatterCavern) carveAirField.get(carver); + MatterCavern carveLava = (MatterCavern) carveLavaField.get(carver); + MatterCavern carveForcedAir = (MatterCavern) carveForcedAirField.get(carver); + + double[] resolvedWeights = columnWeights; + if (resolvedWeights == null || resolvedWeights.length < 256) { + resolvedWeights = fullWeights(); + } + + int worldHeight = writer.getMantle().getWorldHeight(); + int minY = Math.max(0, (int) Math.floor(profile.getVerticalRange().getMin())); + int maxY = Math.min(worldHeight - 1, (int) Math.ceil(profile.getVerticalRange().getMax())); + if (worldYRange != null) { + int worldMinHeight = engine.getWorld().minHeight(); + int rangeMinY = (int) Math.floor(worldYRange.getMin() - worldMinHeight); + int rangeMaxY = (int) Math.ceil(worldYRange.getMax() - worldMinHeight); + minY = Math.max(minY, rangeMinY); + maxY = Math.min(maxY, rangeMaxY); + } + if (maxY < minY) { + return 0; + } + + boolean allowSurfaceBreak = profile.isAllowSurfaceBreak(); + int surfaceClearance = Math.max(0, profile.getSurfaceClearance()); + int surfaceBreakDepth = Math.max(0, profile.getSurfaceBreakDepth()); + double surfaceBreakNoiseThreshold = profile.getSurfaceBreakNoiseThreshold(); + double surfaceBreakThresholdBoost = Math.max(0D, profile.getSurfaceBreakThresholdBoost()); + int[] columnTopY = new int[256]; + int[] surfaceBreakFloorY = new int[256]; + boolean[] surfaceBreakColumn = new boolean[256]; + double[] passThreshold = new double[256]; + double[] verticalEdgeFade = computeVerticalEdgeFade(profile, minY, maxY); + MatterCavern[] matterByY = computeMatterByY(engine, profile, carveAir, carveLava, carveForcedAir, minY, maxY); + + int x0 = chunkX << 4; + int z0 = chunkZ << 4; + for (int localX = 0; localX < 16; localX++) { + int x = x0 + localX; + for (int localZ = 0; localZ < 16; localZ++) { + int z = z0 + localZ; + int columnIndex = (localX << 4) | localZ; + int columnSurfaceY; + if (precomputedSurfaceHeights != null && precomputedSurfaceHeights.length > columnIndex) { + columnSurfaceY = precomputedSurfaceHeights[columnIndex]; + } else { + columnSurfaceY = engine.getHeight(x, z); + } + + int clearanceTopY = Math.min(maxY, Math.max(minY, columnSurfaceY - surfaceClearance)); + boolean breakColumn = allowSurfaceBreak && signed(surfaceBreakDensity.noiseFast2D(x, z)) >= surfaceBreakNoiseThreshold; + int resolvedTopY = breakColumn ? Math.min(maxY, Math.max(minY, columnSurfaceY)) : clearanceTopY; + columnTopY[columnIndex] = resolvedTopY; + surfaceBreakFloorY[columnIndex] = Math.max(minY, columnSurfaceY - surfaceBreakDepth); + surfaceBreakColumn[columnIndex] = breakColumn; + double columnWeight = clampColumnWeight(resolvedWeights[columnIndex]); + if (columnWeight <= 0D || resolvedTopY < minY) { + passThreshold[columnIndex] = Double.NaN; + continue; + } + + passThreshold[columnIndex] = profile.getDensityThreshold().get(thresholdRng, x, z, data) - profile.getThresholdBias(); + } + } + + @SuppressWarnings("unchecked") + MantleChunk chunk = writer.acquireChunk(chunkX, chunkZ); + if (chunk == null) { + return 0; + } + + int carved = 0; + for (int localX = 0; localX < 16; localX++) { + int x = x0 + localX; + for (int localZ = 0; localZ < 16; localZ++) { + int z = z0 + localZ; + int columnIndex = (localX << 4) | localZ; + if (Double.isNaN(passThreshold[columnIndex])) { + continue; + } + + int topY = columnTopY[columnIndex]; + for (int y = minY; y <= topY; y++) { + double localThreshold = passThreshold[columnIndex]; + if (surfaceBreakColumn[columnIndex] && y >= surfaceBreakFloorY[columnIndex]) { + localThreshold += surfaceBreakThresholdBoost; + } + localThreshold -= verticalEdgeFade[y - minY]; + + double density = (double) sampleDensityMethod.invoke(carver, x, y, z); + if (density > localThreshold) { + continue; + } + + Matter sectionMatter = chunk.getOrCreate(y >> 4); + MatterSlice cavernSlice = sectionMatter.slice(MatterCavern.class); + cavernSlice.set(localX, y & 15, localZ, matterByY[y - minY]); + carved++; + } + } + } + + return carved; + } + + private double[] computeVerticalEdgeFade(IrisCaveProfile profile, int minY, int maxY) { + int size = Math.max(0, maxY - minY + 1); + double[] verticalEdgeFade = new double[size]; + int fadeRange = Math.max(0, profile.getVerticalEdgeFade()); + double fadeStrength = Math.max(0D, profile.getVerticalEdgeFadeStrength()); + if (size == 0 || fadeRange <= 0 || maxY <= minY || fadeStrength <= 0D) { + return verticalEdgeFade; + } + + for (int y = minY; y <= maxY; y++) { + int floorDistance = y - minY; + int ceilingDistance = maxY - y; + int edgeDistance = Math.min(floorDistance, ceilingDistance); + int offsetIndex = y - minY; + if (edgeDistance >= fadeRange) { + continue; + } + + double t = Math.max(0D, Math.min(1D, edgeDistance / (double) fadeRange)); + double smooth = t * t * (3D - (2D * t)); + verticalEdgeFade[offsetIndex] = (1D - smooth) * fadeStrength; + } + + return verticalEdgeFade; + } + + private MatterCavern[] computeMatterByY(Engine engine, IrisCaveProfile profile, MatterCavern carveAir, MatterCavern carveLava, MatterCavern carveForcedAir, int minY, int maxY) { + MatterCavern[] matterByY = new MatterCavern[Math.max(0, maxY - minY + 1)]; + boolean allowLava = profile.isAllowLava(); + boolean allowWater = profile.isAllowWater(); + int lavaHeight = engine.getDimension().getCaveLavaHeight(); + int fluidHeight = engine.getDimension().getFluidHeight(); + + for (int y = minY; y <= maxY; y++) { + int offset = y - minY; + if (allowLava && y <= lavaHeight) { + matterByY[offset] = carveLava; + continue; + } + if (allowWater && y <= fluidHeight) { + matterByY[offset] = carveAir; + continue; + } + if (!allowLava && y <= lavaHeight) { + matterByY[offset] = carveForcedAir; + continue; + } + + matterByY[offset] = carveAir; + } + + return matterByY; + } + private Engine createEngine(int worldHeight, int sampledHeight) { Engine engine = mock(Engine.class); IrisData data = mock(IrisData.class); @@ -117,7 +436,7 @@ public class IrisCaveCarver3DNearParityTest { return engine; } - private IrisCaveProfile createProfile() { + private IrisCaveProfile createProfile(boolean warp, boolean modules) { IrisCaveProfile profile = new IrisCaveProfile(); profile.setEnabled(true); profile.setVerticalRange(new IrisRange(0D, 120D)); @@ -129,10 +448,10 @@ public class IrisCaveCarver3DNearParityTest { profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.09D)); profile.setBaseWeight(1D); profile.setDetailWeight(0.48D); - profile.setWarpStrength(0.37D); + profile.setWarpStrength(warp ? 0.37D : 0D); profile.setDensityThreshold(new IrisStyledRange(1D, 1D, new IrisGeneratorStyle(NoiseStyle.FLAT))); profile.setThresholdBias(0D); - profile.setSampleStep(2); + profile.setSampleStep(1); profile.setMinCarveCells(0); profile.setRecoveryThresholdBoost(0D); profile.setSurfaceClearance(5); @@ -144,6 +463,26 @@ public class IrisCaveCarver3DNearParityTest { profile.setWaterMinDepthBelowSurface(8); profile.setWaterRequiresFloor(false); profile.setAllowLava(true); + if (modules) { + KList caveModules = new KList<>(); + caveModules.add(new IrisCaveFieldModule( + new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.11D), + 0.23D, + 0.04D, + new IrisRange(0D, 72D), + false + )); + caveModules.add(new IrisCaveFieldModule( + new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.19D), + 0.17D, + -0.06D, + new IrisRange(24D, 120D), + true + )); + profile.setModules(caveModules); + } else { + profile.setModules(new KList<>()); + } return profile; } @@ -156,6 +495,7 @@ public class IrisCaveCarver3DNearParityTest { Map sections = new HashMap<>(); Map> sectionCells = new HashMap<>(); Set carvedCells = new HashSet<>(); + Map carvedLiquids = new HashMap<>(); doReturn(mantle).when(writer).getMantle(); doReturn(worldHeight).when(mantle).getWorldHeight(); @@ -167,15 +507,15 @@ public class IrisCaveCarver3DNearParityTest { return section; } - Matter created = createSection(sectionIndex, sectionCells, carvedCells); + Matter created = createSection(sectionIndex, sectionCells, carvedCells, carvedLiquids); sections.put(sectionIndex, created); return created; }).when(chunk).getOrCreate(anyInt()); - return new WriterCapture(writer, carvedCells); + return new WriterCapture(writer, carvedCells, carvedLiquids); } - private Matter createSection(int sectionIndex, Map> sectionCells, Set carvedCells) { + private Matter createSection(int sectionIndex, Map> sectionCells, Set carvedCells, Map carvedLiquids) { Matter matter = mock(Matter.class); @SuppressWarnings("unchecked") MatterSlice slice = mock(MatterSlice.class); @@ -195,13 +535,44 @@ public class IrisCaveCarver3DNearParityTest { MatterCavern value = invocation.getArgument(3); localCells.put(packLocal(localX, localY, localZ), value); int worldY = (sectionIndex << 4) + localY; - carvedCells.add(cellKey(localX, worldY, localZ)); + String cellKey = cellKey(localX, worldY, localZ); + carvedCells.add(cellKey); + carvedLiquids.put(cellKey, value.getLiquid()); return null; }).when(slice).set(anyInt(), anyInt(), anyInt(), any(MatterCavern.class)); return matter; } + private double[] fullWeights() { + double[] columnWeights = new double[256]; + Arrays.fill(columnWeights, 1D); + return columnWeights; + } + + private int[] filledHeights(int height) { + int[] heights = new int[256]; + Arrays.fill(heights, height); + return heights; + } + + private double clampColumnWeight(double weight) { + if (Double.isNaN(weight) || Double.isInfinite(weight)) { + return 0D; + } + if (weight <= 0D) { + return 0D; + } + if (weight >= 1D) { + return 1D; + } + return weight; + } + + private double signed(double value) { + return (value * 2D) - 1D; + } + private int packLocal(int x, int y, int z) { return (x << 8) | (y << 4) | z; } @@ -210,6 +581,21 @@ public class IrisCaveCarver3DNearParityTest { return x + ":" + y + ":" + z; } + private boolean containsLiquidInRange(Map carvedLiquids, int minY, int maxY, byte liquid) { + for (Map.Entry entry : carvedLiquids.entrySet()) { + if (entry.getValue() != liquid) { + continue; + } + + String[] split = entry.getKey().split(":"); + int y = Integer.parseInt(split[1]); + if (y >= minY && y <= maxY) { + return true; + } + } + return false; + } + private boolean hasX(Set carvedCells, int x) { for (String cell : carvedCells) { String[] split = cell.split(":"); @@ -259,10 +645,12 @@ public class IrisCaveCarver3DNearParityTest { private static final class WriterCapture { private final MantleWriter writer; private final Set carvedCells; + private final Map carvedLiquids; - private WriterCapture(MantleWriter writer, Set carvedCells) { + private WriterCapture(MantleWriter writer, Set carvedCells, Map carvedLiquids) { this.writer = writer; this.carvedCells = carvedCells; + this.carvedLiquids = carvedLiquids; } } } diff --git a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java index 74c5a4791..c25edac81 100644 --- a/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java +++ b/core/src/test/java/art/arcane/iris/engine/mantle/components/MantleCarvingComponentTop2BlendTest.java @@ -17,7 +17,6 @@ import static org.junit.Assert.assertEquals; public class MantleCarvingComponentTop2BlendTest { private static Constructor weightedProfileConstructor; private static Method limitMethod; - private static Method expandTileMethod; private static Field profileField; private static Field columnWeightsField; @@ -28,8 +27,6 @@ public class MantleCarvingComponentTop2BlendTest { weightedProfileConstructor.setAccessible(true); limitMethod = MantleCarvingComponent.class.getDeclaredMethod("limitAndMergeBlendedProfiles", List.class, int.class, int.class); limitMethod.setAccessible(true); - expandTileMethod = MantleCarvingComponent.class.getDeclaredMethod("expandTileWeightsToColumns", double[].class); - expandTileMethod.setAccessible(true); profileField = weightedProfileClass.getDeclaredField("profile"); profileField.setAccessible(true); columnWeightsField = weightedProfileClass.getDeclaredField("columnWeights"); @@ -67,21 +64,16 @@ public class MantleCarvingComponentTop2BlendTest { } @Test - public void tileWeightsExpandIntoFourColumnsPerTile() throws Exception { - double[] tileWeights = new double[64]; - tileWeights[0] = 0.42D; - tileWeights[9] = 0.73D; - double[] expanded = invokeExpand(tileWeights); + public void topTwoMergePreservesIndependentWeightsAtHighColumnIndexes() throws Exception { + WeightedInput input = createWeightedProfiles(); + List limited = invokeLimit(input.weightedProfiles(), 2); - assertEquals(0.42D, expanded[(0 << 4) | 0], 0D); - assertEquals(0.42D, expanded[(0 << 4) | 1], 0D); - assertEquals(0.42D, expanded[(1 << 4) | 0], 0D); - assertEquals(0.42D, expanded[(1 << 4) | 1], 0D); + Map byProfile = extractWeightsByProfile(limited); + IrisCaveProfile first = input.profiles().first(); + IrisCaveProfile second = input.profiles().second(); - assertEquals(0.73D, expanded[(2 << 4) | 2], 0D); - assertEquals(0.73D, expanded[(2 << 4) | 3], 0D); - assertEquals(0.73D, expanded[(3 << 4) | 2], 0D); - assertEquals(0.73D, expanded[(3 << 4) | 3], 0D); + assertEquals(1.0D, byProfile.get(first)[255], 0D); + assertEquals(1.0D, byProfile.get(second)[254], 0D); } private WeightedInput createWeightedProfiles() throws Exception { @@ -90,17 +82,21 @@ public class MantleCarvingComponentTop2BlendTest { IrisCaveProfile third = new IrisCaveProfile().setEnabled(true).setBaseWeight(0.93D); Profiles profiles = new Profiles(first, second, third); - double[] firstWeights = new double[64]; + double[] firstWeights = new double[256]; firstWeights[0] = 0.2D; firstWeights[1] = 0.8D; + firstWeights[255] = 0.6D; - double[] secondWeights = new double[64]; + double[] secondWeights = new double[256]; secondWeights[0] = 0.7D; secondWeights[1] = 0.1D; + secondWeights[254] = 0.7D; - double[] thirdWeights = new double[64]; + double[] thirdWeights = new double[256]; thirdWeights[0] = 0.3D; thirdWeights[1] = 0.4D; + thirdWeights[254] = 0.3D; + thirdWeights[255] = 0.4D; List weighted = new ArrayList<>(); weighted.add(weightedProfileConstructor.newInstance(first, firstWeights, average(firstWeights), null)); @@ -110,11 +106,7 @@ public class MantleCarvingComponentTop2BlendTest { } private List invokeLimit(List weightedProfiles, int limit) throws Exception { - return (List) limitMethod.invoke(null, weightedProfiles, limit, 64); - } - - private double[] invokeExpand(double[] tileWeights) throws Exception { - return (double[]) expandTileMethod.invoke(null, (Object) tileWeights); + return (List) limitMethod.invoke(null, weightedProfiles, limit, 256); } private Map extractWeightsByProfile(List weightedProfiles) throws Exception { diff --git a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java index f55092a9a..67cf1974c 100644 --- a/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java +++ b/core/src/test/java/art/arcane/iris/engine/object/IrisDimensionCarvingResolverParityTest.java @@ -104,20 +104,20 @@ public class IrisDimensionCarvingResolverParityTest { } @Test - public void tileAnchoredChunkPlanResolutionIsStableAcrossRepeatedBuilds() { + public void columnAnchoredChunkPlanResolutionIsStableAcrossRepeatedBuilds() { Fixture fixture = createMixedDepthFixture(); IrisDimensionCarvingResolver.State state = new IrisDimensionCarvingResolver.State(); IrisDimensionCarvingEntry root = legacyResolveRootEntry(fixture.engine, 80); for (int chunkX = -24; chunkX <= 24; chunkX += 6) { for (int chunkZ = -24; chunkZ <= 24; chunkZ += 6) { - IrisDimensionCarvingEntry[] firstPlan = buildTilePlan(fixture.engine, root, chunkX, chunkZ, state); - IrisDimensionCarvingEntry[] secondPlan = buildTilePlan(fixture.engine, root, chunkX, chunkZ, state); - for (int tileIndex = 0; tileIndex < firstPlan.length; tileIndex++) { + IrisDimensionCarvingEntry[] firstPlan = buildColumnPlan(fixture.engine, root, chunkX, chunkZ, state); + IrisDimensionCarvingEntry[] secondPlan = buildColumnPlan(fixture.engine, root, chunkX, chunkZ, state); + for (int columnIndex = 0; columnIndex < firstPlan.length; columnIndex++) { assertSame( - "tile plan mismatch at chunkX=" + chunkX + " chunkZ=" + chunkZ + " tileIndex=" + tileIndex, - firstPlan[tileIndex], - secondPlan[tileIndex] + "column plan mismatch at chunkX=" + chunkX + " chunkZ=" + chunkZ + " columnIndex=" + columnIndex, + firstPlan[columnIndex], + secondPlan[columnIndex] ); } } @@ -272,14 +272,14 @@ public class IrisDimensionCarvingResolverParityTest { return new Fixture(engine); } - private IrisDimensionCarvingEntry[] buildTilePlan(Engine engine, IrisDimensionCarvingEntry rootEntry, int chunkX, int chunkZ, IrisDimensionCarvingResolver.State state) { - IrisDimensionCarvingEntry[] plan = new IrisDimensionCarvingEntry[64]; - for (int tileX = 0; tileX < 8; tileX++) { - int worldX = (chunkX << 4) + (tileX << 1); - for (int tileZ = 0; tileZ < 8; tileZ++) { - int worldZ = (chunkZ << 4) + (tileZ << 1); - int tileIndex = (tileX * 8) + tileZ; - plan[tileIndex] = IrisDimensionCarvingResolver.resolveFromRoot(engine, rootEntry, worldX, worldZ, state); + private IrisDimensionCarvingEntry[] buildColumnPlan(Engine engine, IrisDimensionCarvingEntry rootEntry, int chunkX, int chunkZ, IrisDimensionCarvingResolver.State state) { + IrisDimensionCarvingEntry[] plan = new IrisDimensionCarvingEntry[256]; + for (int localX = 0; localX < 16; localX++) { + int worldX = (chunkX << 4) + localX; + for (int localZ = 0; localZ < 16; localZ++) { + int worldZ = (chunkZ << 4) + localZ; + int columnIndex = (localX << 4) | localZ; + plan[columnIndex] = IrisDimensionCarvingResolver.resolveFromRoot(engine, rootEntry, worldX, worldZ, state); } } return plan;