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 extends BiomeSource> 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)))
);