From 75031e235d0369641b1917ac83d51b06bb5c6ce0 Mon Sep 17 00:00:00 2001 From: Peter Johnson Date: Mon, 27 Apr 2026 22:55:26 -0500 Subject: [PATCH] Add BiomeProvider structure search fast path to CustomWorldChunkManager Adds getStructurePlacementBiome(WorldInfo, int, int) to the Bukkit BiomeProvider API as a default method returning Optional.empty(). Generators may override this to return a cheap 2D biome approximation for structure placement eligibility checks (e.g. land vs ocean for stronghold ring position computation), bypassing expensive 3D pipeline evaluation for standard terrain generation. CustomWorldChunkManager overrides findBiomeHorizontal, setting a ThreadLocal flag while the search runs so getNoiseBiome can call getStructurePlacementBiome() instead of the full getBiome() path. This avoids the extensive pipeline calculation cost for biome placement in more complex terrain generators like Terra. Falls back silently to getBiome() if the provider returns empty, CraftBiome.bukkitToMinecraftHolder returns null, or the generator does not implement getStructurePlacementBiome. Includes one-shot diagnostic logging to confirm the fast path is active and being used during stronghold searches. --- .../org/bukkit/generator/BiomeProvider.java | 24 +++++++ .../generator/CustomWorldChunkManager.java | 70 +++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/paper-api/src/main/java/org/bukkit/generator/BiomeProvider.java b/paper-api/src/main/java/org/bukkit/generator/BiomeProvider.java index 65fd14b7b3fe..c761033a2675 100644 --- a/paper-api/src/main/java/org/bukkit/generator/BiomeProvider.java +++ b/paper-api/src/main/java/org/bukkit/generator/BiomeProvider.java @@ -1,6 +1,7 @@ package org.bukkit.generator; import java.util.List; +import java.util.Optional; import org.bukkit.block.Biome; import org.jetbrains.annotations.NotNull; @@ -61,6 +62,29 @@ public Biome getBiome(@NotNull WorldInfo worldInfo, int x, int y, int z, @NotNul return getBiome(worldInfo, x, y, z); } + /** + * Return a biome for structure placement searching (e.g. stronghold ring + * position computation), bypassing expensive biome pipeline evaluation. + *

+ * Implementations may return a coarse approximation sufficient to determine + * whether a position is eligible for a structure (e.g. land vs ocean). + * Return {@link Optional#empty()} to fall back to the full + * {@link #getBiome(WorldInfo, int, int, int)} path. + *

+ * This is called from {@code findBiomeHorizontal} which runs on background + * executor threads. Implementations must be thread-safe. + * + * @param worldInfo The world info of the world being searched + * @param x The X block coordinate + * @param z The Z block coordinate + * @return An optional biome for fast structure placement eligibility + * checking, or empty to use the full pipeline + */ + @NotNull + public Optional getStructurePlacementBiome(@NotNull WorldInfo worldInfo, int x, int z) { + return Optional.empty(); + } + /** * Returns a list with every biome the {@link BiomeProvider} will use for * the given world. diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomWorldChunkManager.java b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomWorldChunkManager.java index f1b3286a83c7..79a483f077e3 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomWorldChunkManager.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/generator/CustomWorldChunkManager.java @@ -1,18 +1,42 @@ package org.bukkit.craftbukkit.generator; import com.google.common.base.Preconditions; +import com.mojang.datafixers.util.Pair; import com.mojang.serialization.MapCodec; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; import java.util.stream.Stream; +import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.QuartPos; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.biome.Biome; import net.minecraft.world.level.biome.BiomeSource; import net.minecraft.world.level.biome.Climate; import org.bukkit.craftbukkit.block.CraftBiome; import org.bukkit.generator.BiomeProvider; import org.bukkit.generator.WorldInfo; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class CustomWorldChunkManager extends BiomeSource { + private static final Logger LOGGER = LoggerFactory.getLogger(CustomWorldChunkManager.class); + // One-shot guards: prevent flooding debug/warn logs across 400k+ biome queries per search. + private static final AtomicBoolean LOGGED_SEARCH_INTERCEPT = new AtomicBoolean(false); + private static final AtomicBoolean LOGGED_FAST_PATH = new AtomicBoolean(false); + private static final AtomicBoolean LOGGED_FAST_PATH_EMPTY = new AtomicBoolean(false); + + // Set true on the calling thread while inside findBiomeHorizontal so that + // getNoiseBiome can use the fast path for structure placement searches. + // findBiomeHorizontal is the only caller of getNoiseBiome for stronghold + // ring position computation; normal chunk generation calls getNoiseBiome + // directly without going through findBiomeHorizontal. + private static final ThreadLocal IN_STRUCTURE_SEARCH = + ThreadLocal.withInitial(() -> false); + private final WorldInfo worldInfo; private final BiomeProvider biomeProvider; public final BiomeSource vanillaBiomeSource; @@ -28,8 +52,54 @@ protected MapCodec codec() { throw new UnsupportedOperationException("Cannot serialize CustomWorldChunkManager"); } + // Paper - structure search fast path: intercept the horizontal biome search used + // by ConcentricRingsStructurePlacement (strongholds). Sets a thread-local so + // getNoiseBiome can call BiomeProvider.getStructurePlacementBiome() instead of + // the full pipeline, avoiding 100-2000x amplification from pipeline chunk cache misses. + @Override + public @Nullable Pair> findBiomeHorizontal( + int x, int y, int z, int searchRadius, int skipSteps, + Predicate> allowed, RandomSource random, + boolean findClosest, Climate.Sampler sampler) { + if (LOGGED_SEARCH_INTERCEPT.compareAndSet(false, true)) { + LOGGER.debug("Structure search fast path active for world '{}'", this.worldInfo.getName()); + } + IN_STRUCTURE_SEARCH.set(true); + try { + return super.findBiomeHorizontal(x, y, z, searchRadius, skipSteps, + allowed, random, findClosest, sampler); + } finally { + IN_STRUCTURE_SEARCH.set(false); + } + } + @Override public Holder getNoiseBiome(int x, int y, int z, Climate.Sampler noise) { + if (IN_STRUCTURE_SEARCH.get()) { + Optional fast = + this.biomeProvider.getStructurePlacementBiome(this.worldInfo, QuartPos.toBlock(x), QuartPos.toBlock(z)); + if (fast.isPresent()) { + Holder biome = CraftBiome.bukkitToMinecraftHolder(fast.get()); + if (biome != null) { + if (LOGGED_FAST_PATH.compareAndSet(false, true)) { + LOGGER.debug("Structure search fast path hit for world '{}' — provider: {}, biome: {}", + this.worldInfo.getName(), this.biomeProvider.getClass().getSimpleName(), fast.get()); + } + return biome; + } else { + if (LOGGED_FAST_PATH_EMPTY.compareAndSet(false, true)) { + LOGGER.warn("Structure search fast path for world '{}': biome '{}' returned by {} is not in the biome registry; falling back to full pipeline", + this.worldInfo.getName(), fast.get(), this.biomeProvider.getClass().getSimpleName()); + } + } + } else { + if (LOGGED_FAST_PATH_EMPTY.compareAndSet(false, true)) { + LOGGER.debug("Structure search fast path not available for world '{}' — {} returned empty; using full pipeline", + this.worldInfo.getName(), this.biomeProvider.getClass().getSimpleName()); + } + } + } + Holder biome = CraftBiome.bukkitToMinecraftHolder( this.biomeProvider.getBiome(this.worldInfo, QuartPos.toBlock(x), QuartPos.toBlock(y), QuartPos.toBlock(z), CraftBiomeParameterPoint.createBiomeParameterPoint(noise, noise.sample(x, y, z))) );