From 604c76532df93899d5885fc78145d54a1ce1d01c Mon Sep 17 00:00:00 2001 From: marceld23 Date: Tue, 4 Aug 2026 01:04:48 +0200 Subject: [PATCH] =?UTF-8?q?perf(worldgen):=20per-world=20wonder=20profile?= =?UTF-8?q?=20=E2=80=94=20undo=20the=20#698-#709=20hot-path=20cost=20(#712?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure refactor, bit-identical terrain (verified via an FNV height/band/tunnel hash sweep over every planet type x seeds x continents on/off): - WonderProfile cached per world (CalibFor pattern + lock-free instance fast path): feature gates, PlanetSeed, terrain grain, hybrid thresholds, continent profile and the lowered style resolve once per world instead of per column — no more per-column ToLowerInvariant allocations or Noise.Hash world rolls. - Tunnel worms are built once per cell (static cache + instance slot) instead of per column; AddSegmentSpan gains an XZ bounding reject. - Sea stacks / hoodoos / cenotes check distance BEFORE computing their RawSurfaceHeight anchor (and the stack swell probe) — every column of a feature-bearing cell used to pay the full anchor. Measured: harness sweep 2174 -> 1472 ms (-32%); local fast tier 7m05 -> 5m31. Co-Authored-By: Claude Fable 5 --- TODO.md | 18 + .../WorldGenerator.cs | 436 ++++++++++++------ 2 files changed, 322 insertions(+), 132 deletions(-) diff --git a/TODO.md b/TODO.md index 961993aa..ec6072ee 100644 --- a/TODO.md +++ b/TODO.md @@ -7085,6 +7085,24 @@ is **pre-approved** (keys in `tools/ai-assets/.env`, run via `uv`). --- +## ✅ Done (2026-08-04): worldgen perf pass — per-world wonder profile (#712) + +The #698–#709 wave made `SurfaceHeight` ~2.6× costlier (PR CI test job 6.5 → 17 min; same cost in +live chunk gen). Pure refactor, **bit-identical terrain** (verified by an FNV height/band/tunnel hash +sweep over every planet type × seeds × continents on/off): + +- **`WonderProfile`** cached per world (CalibFor pattern + lock-free instance fast path): all feature + gates, `PlanetSeed`, terrain grain, hybrid thresholds, continent profile, lowered style — the + per-column string allocations (`ToLowerInvariant`) and `Noise.Hash` world rolls are gone from the + hot path. +- **Tunnel worm cache**: the polyline was rebuilt (dozens of xorshift rolls) for every column of + every tunnel-bearing cell (half the map) — now built once per cell; per column only capsule + distance checks remain, plus an XZ bounding reject before the 8-point sampling. +- **Anchor-order fixes**: sea stacks/hoodoos/cenotes computed a full `RawSurfaceHeight` anchor (and + the stack's 4-octave swell probe) before checking the distance — every column of a feature-bearing + cell paid it. Distance rejects now go first (behaviour-preserving, conjunctive gates). +- Measured: harness sweep 2174 → 1472 ms (−32 %); local fast tier 7m05 → **5m31** (−22 %). + ## ✅ Done (2026-08-03): terrain wonders — signature scars, continents, overhangs & the tunnel carver (#698–#709) Three worldgen waves in one branch (⚠️ Waves 1+3 are a one-time reshape of existing worlds, accepted diff --git a/src/BlocksBeyondTheStars.WorldGeneration/WorldGenerator.cs b/src/BlocksBeyondTheStars.WorldGeneration/WorldGenerator.cs index 844a24bc..56456629 100644 --- a/src/BlocksBeyondTheStars.WorldGeneration/WorldGenerator.cs +++ b/src/BlocksBeyondTheStars.WorldGeneration/WorldGenerator.cs @@ -231,7 +231,7 @@ private double ValueT(long seed, double worldX, double worldY, double worldZ, do /// (amplitude, ridged) parameter pairs because the #576 additions — quantised decks, asymmetric /// gorges — cannot be expressed as parameters of one shared formula; the regional blend therefore /// lerps computed OFFSETS, not parameters. - private double ArchetypeOffset(int archetype, PlanetType planet, long seed, double h, int worldX, int worldZ) + private double ArchetypeOffset(int archetype, PlanetType planet, WonderProfile w, long seed, double h, int worldX, int worldZ) { double amp = planet.Amplitude; double Ridge(double v) => (1.0 - System.Math.Abs(v)) * 2.0 - 1.0; // smooth swell → sharp ridge/valley @@ -243,7 +243,7 @@ private double ArchetypeOffset(int archetype, PlanetType planet, long seed, doub case 2: return h * amp * 1.00; // hills case 3: // mountains (lightly ridged; #700 — a directional share so ranges chain along the grain) return (h * 0.58 + Ridge(h) * 0.12 - + OrientedRidge(seed, GrainFor(seed), worldX, worldZ, planet.TerrainScale * 0.9) * 0.30) + + OrientedRidge(seed, w.Grain, worldX, worldZ, planet.TerrainScale * 0.9) * 0.30) * amp * 1.9; case 4: // canyons (strongly ridged) return (h * 0.35 + Ridge(h) * 0.65) * amp * 1.3; @@ -259,7 +259,7 @@ private double ArchetypeOffset(int archetype, PlanetType planet, long seed, doub case 6: // extreme peaks (#576): the far tail of relief, well above the mountains archetype { // #700: the extreme crests follow the world's grain too — the tallest ranges chain. - double or6 = OrientedRidge(seed, GrainFor(seed), worldX, worldZ, planet.TerrainScale * 0.9); + double or6 = OrientedRidge(seed, w.Grain, worldX, worldZ, planet.TerrainScale * 0.9); double r = h * 0.25 + Ridge(h) * 0.45 + or6 * 0.30; if (r > 0) { @@ -298,6 +298,95 @@ private static double DramaFor(long seed) /// it; the final clamp here is the safety net for freak archetype × drama × landmark stacks. private const int MaxNaturalSurfaceY = 288; + // --- Per-world wonder profile (#712): every feature GATE, world roll and lowered string that the + // #698–#709 wave added is constant for a given world, yet was re-derived per COLUMN — string + // allocations (ToLowerInvariant), Noise.Hash world rolls and repeated planet-key hashing in the + // hottest function in the codebase (~2.6× slower chunk gen / calibration / CI). Resolved ONCE per + // world here, cached like the calibration; per-column code reads booleans and doubles only. --- + private sealed class WonderProfile + { + public long Seed; // PlanetSeed, resolved once (string hash) + public TerrainGrain Grain; // #700 per-world direction roll + public ContinentProfile Continent; // #704 + public string Style = string.Empty; // TerrainStyle lowered ONCE + public bool Cratered; + public bool Volcanoes, Calderas, Massifs, TableMountains, Rifts, MegaRift, Escarpment; + public bool Arches, SeaStacks, Hoodoos, OverhangLandmarks; + public bool Travertine, Penitentes, Cenotes, Crevasses; + public bool SaltPolygons, BasaltFields, Tunnels, Caverns, AnyBands; + public bool HybridEligible; // #703 + public double HybridA, HybridB; // #703 rolled fade thresholds + public int SkyTiers; // #707 (0 on non-floating worlds) + } + + // Static cross-instance cache (client bakes fresh generators per preview; tests spin up hundreds) + // PLUS a lock-free instance fast path: a generator works one world at a time, so per-column lookups + // almost always hit the instance slot and never touch the lock. + private static readonly System.Collections.Generic.Dictionary<(long, string, int, bool, long, bool), WonderProfile> _wonders = new(); + private static readonly object _wonderLock = new object(); + private WonderProfile? _wonderCached; + private (long, string, int, bool, long, bool) _wonderCachedKey; + + private WonderProfile WonderFor(PlanetType planet) + { + var key = (_worldSeed, planet.Key, _circumference, _crateredWorld, _locationSalt, _continentsEnabled); + if (_wonderCached is { } fast && _wonderCachedKey == key) + { + return fast; + } + + lock (_wonderLock) + { + if (!_wonders.TryGetValue(key, out var w)) + { + long seed = PlanetSeed(planet); + w = new WonderProfile + { + Seed = seed, + Grain = GrainFor(seed), + Continent = ContinentProfileFor(planet, seed), + Style = planet.TerrainStyle?.ToLowerInvariant() ?? string.Empty, + Cratered = planet.Cratered || _crateredWorld, + Volcanoes = HasVolcanoes(planet), + Calderas = HasCalderas(planet), + Massifs = HasMassifs(planet), + TableMountains = HasTableMountains(planet), + Rifts = HasRifts(planet), + MegaRift = HasMegaRift(planet, seed), + Escarpment = HasEscarpment(planet, seed), + Arches = HasArches(planet), + SeaStacks = HasSeaStacks(planet), + Hoodoos = HasHoodoos(planet), + Travertine = HasTravertine(planet), + Penitentes = HasPenitentes(planet), + Cenotes = HasCenotes(planet), + Crevasses = HasCrevasses(planet), + SaltPolygons = HasSaltPolygons(planet), + BasaltFields = HasBasaltFields(planet), + Tunnels = HasTunnels(planet), + Caverns = HasCaverns(planet), + SkyTiers = planet.FloatingIslands ? SkyTiersFor(seed) : 0, + }; + w.OverhangLandmarks = w.Arches || w.SeaStacks || w.Hoodoos; + w.AnyBands = planet.FloatingIslands || w.Arches || w.SeaStacks || w.Hoodoos || w.Cenotes; + w.HybridEligible = StyleHybridEligible(w.Style); + ulong uh = Noise.Hash(seed ^ 0x57FADE, 2, 4, 8); + w.HybridA = 0.34 + (uh & 0xFF) / 255.0 * 0.08; + w.HybridB = w.HybridA + 0.08; + if (_wonders.Count >= 256) + { + _wonders.Clear(); // soft cap — profiles are a few dozen bytes each + } + + _wonders[key] = w; + } + + _wonderCached = w; + _wonderCachedKey = key; + return w; + } + } + /// Computes the surface height (world Y) of a column for a planet — the raw terrain plus at /// most ONE landmark overlay: volcano cones (#477), massifs, table mountains or rift chasms /// (#577/#578). Precedence volcano > massif > butte > rift, one landmark per column, so a @@ -306,56 +395,57 @@ private static double DramaFor(long seed) /// every system sees the same mountain. public int SurfaceHeight(PlanetType planet, int worldX, int worldZ) { - int h = RawSurfaceHeight(planet, worldX, worldZ); - long seed = PlanetSeed(planet); - double overlay = HasVolcanoes(planet) ? VolcanoOffset(planet, seed, worldX, worldZ) : 0.0; - if (overlay == 0.0 && HasCalderas(planet)) + var w = WonderFor(planet); // #712: every gate below is a cached boolean, not a re-derivation + int h = RawSurfaceHeight(planet, w, worldX, worldZ); + long seed = w.Seed; + double overlay = w.Volcanoes ? VolcanoOffset(planet, seed, worldX, worldZ) : 0.0; + if (overlay == 0.0 && w.Calderas) { overlay = CalderaOffset(seed, worldX, worldZ); } - if (overlay == 0.0 && HasMassifs(planet)) + if (overlay == 0.0 && w.Massifs) { overlay = MassifOffset(planet, seed, worldX, worldZ); } - if (overlay == 0.0 && HasTableMountains(planet)) + if (overlay == 0.0 && w.TableMountains) { overlay = TableMountainOffset(seed, worldX, worldZ); } - if (overlay == 0.0 && HasOverhangLandmarks(planet)) + if (overlay == 0.0 && w.OverhangLandmarks) { - overlay = OverhangGroundOffset(planet, seed, worldX, worldZ); + overlay = OverhangGroundOffset(planet, w, worldX, worldZ); } - if (overlay == 0.0 && HasTravertine(planet) + if (overlay == 0.0 && w.Travertine && TryGetTravertine(seed, worldX, worldZ, out double deckRise, out _)) { overlay = deckRise; } - if (overlay == 0.0 && HasPenitentes(planet)) + if (overlay == 0.0 && w.Penitentes) { overlay = PenitenteRise(planet, seed, worldX, worldZ); } - if (overlay == 0.0 && HasCenotes(planet)) + if (overlay == 0.0 && w.Cenotes) { overlay = CenoteOffset(planet, seed, worldX, worldZ); } - if (overlay == 0.0 && HasCrevasses(planet)) + if (overlay == 0.0 && w.Crevasses) { overlay = CrevasseOffset(seed, worldX, worldZ); } - if (overlay == 0.0 && HasRifts(planet)) + if (overlay == 0.0 && w.Rifts) { overlay = RiftOffset(seed, worldX, worldZ); } - if (overlay == 0.0 && HasMegaRift(planet, seed)) + if (overlay == 0.0 && w.MegaRift) { overlay = MegaRiftOffset(seed, worldX, worldZ); } @@ -371,8 +461,11 @@ public int SurfaceHeight(PlanetType planet, int worldX, int worldZ) /// The terrain height WITHOUT the volcano overlay — the base field volcano geometry itself is /// anchored to (the crater's lava level derives from the pre-cone ground under the cone's centre). private int RawSurfaceHeight(PlanetType planet, int worldX, int worldZ) + => RawSurfaceHeight(planet, WonderFor(planet), worldX, worldZ); + + private int RawSurfaceHeight(PlanetType planet, WonderProfile w, int worldX, int worldZ) { - long seed = PlanetSeed(planet); + long seed = w.Seed; double n = FbmT(seed, worldX, worldZ, planet.TerrainScale, octaves: 4); double h = (n - 0.5) * 2.0; // [-1, 1] base rolling terrain @@ -380,7 +473,7 @@ private int RawSurfaceHeight(PlanetType planet, int worldX, int worldZ) // hills/mountains/canyons) pocked with round impact craters carved on top. How rolling that regolith // is, and how dense/deep/sharp the craters are, is this BODY's own character (#518). Crater CHAINS // (#699) — aligned secondary-impact strings — carve on top of the primary field. - if (planet.Cratered || _crateredWorld) + if (w.Cratered) { double flat = h * CraterProfileFor(seed).Flatness * planet.Amplitude; return planet.BaseHeight + (int)System.Math.Round( @@ -391,22 +484,22 @@ private int RawSurfaceHeight(PlanetType planet, int worldX, int worldZ) // Continents (#704, new worlds only): a bimodal platform/basin offset UNDER everything else, so // styles, archetypes and landmarks simply ride on the continent or drown in the ocean basin. - double baseline = ContinentOffset(planet, seed, worldX, worldZ); + double baseline = w.Continent.Active ? ContinentOffset(w.Continent, seed, worldX, worldZ) : 0.0; // Whole-planet escarpment (#702): a rare two-storey world — the step is part of the baseline too. - if (HasEscarpment(planet, seed)) + if (w.Escarpment) { baseline += EscarpmentOffset(seed, worldX, worldZ); } // Salt polygons (#701): the cracked-plate ridge network of salt pans. - if (HasSaltPolygons(planet)) + if (w.SaltPolygons) { baseline += SaltPolygonRidge(seed, worldX, worldZ); } // Basalt column fields (#701): stepped hex prisms on volcanic-reading worlds. - if (HasBasaltFields(planet) && TryGetBasaltColumns(seed, worldX, worldZ, out double hexRise)) + if (w.BasaltFields && TryGetBasaltColumns(seed, worldX, worldZ, out double hexRise)) { baseline += hexRise; } @@ -415,25 +508,22 @@ private int RawSurfaceHeight(PlanetType planet, int worldX, int worldZ) // mesas, dunes, spires, etc. — instead of every world using the same mixed blend. Since #703 a broad // fade field hands 20–40 % of most styled worlds to the archetype blend, so a dunes world has gravel // plains between its dune seas instead of being dunes from pole to pole. - if (!string.IsNullOrEmpty(planet.TerrainStyle)) + if (w.Style.Length != 0) { - double styled = StyledHeightOffset(planet, planet.TerrainStyle, seed, h, worldX, worldZ); - if (StyleHybridEligible(planet.TerrainStyle)) + double styled = StyledHeightOffset(planet, w, seed, h, worldX, worldZ); + if (w.HybridEligible) { - ulong uh = Noise.Hash(seed ^ 0x57FADE, 2, 4, 8); - double a = 0.34 + (uh & 0xFF) / 255.0 * 0.08; // archetype share threshold, rolled per world - double b = a + 0.08; double fade = FbmT(seed + 0x57FAD1, worldX, worldZ, planet.TerrainScale * 6.0, octaves: 2); - if (fade < b) + if (fade < w.HybridB) { - double arch = BlendedArchetypeOffset(planet, seed, h, worldX, worldZ); - if (fade <= a) + double arch = BlendedArchetypeOffset(planet, w, seed, h, worldX, worldZ); + if (fade <= w.HybridA) { styled = arch; } else { - double f = (fade - a) / (b - a); + double f = (fade - w.HybridA) / (w.HybridB - w.HybridA); styled = arch + (styled - arch) * (f * f * (3.0 - 2.0 * f)); } } @@ -445,13 +535,13 @@ private int RawSurfaceHeight(PlanetType planet, int worldX, int worldZ) // Regional terrain character: a large-scale field selects how rugged this area is (a blend across // the world's archetype subset), so the surface varies between flat plains, hills, mountains — and, // where the subset drew the #576 archetypes, terraced decks, extreme crests or rift gorges. - return planet.BaseHeight + (int)System.Math.Round(baseline + BlendedArchetypeOffset(planet, seed, h, worldX, worldZ) * drama); + return planet.BaseHeight + (int)System.Math.Round(baseline + BlendedArchetypeOffset(planet, w, seed, h, worldX, worldZ) * drama); } /// Styles that hand a rolled 20–40 % of their surface to the archetype blend (#703). The /// identity styles stay pure: flats IS the world (ocean floor, salt pan, sky-world ground) and spires - /// is the crystal identity. - private static bool StyleHybridEligible(string style) => style.ToLowerInvariant() switch + /// is the crystal identity. Expects an ALREADY-LOWERED style (#712 — no per-column allocations). + private static bool StyleHybridEligible(string loweredStyle) => loweredStyle switch { "mountains" or "canyons" or "mesa" or "dunes" or "hills" or "tablelands" or "badlands" or "karst" => true, _ => false, @@ -471,7 +561,7 @@ public bool FloatingIslandBand(PlanetType planet, int worldX, int worldZ, out in // Tier 0 of the (#707) multi-tier sky: same seeds and shaping as the classic single band, so // existing sky worlds keep their islands; upper tiers are GetExtraBands' business. - return FloatingIslandTier(planet, PlanetSeed(planet), 0, worldX, worldZ, out top, out bottom, out _); + return FloatingIslandTier(planet, WonderFor(planet).Seed, 0, worldX, worldZ, out top, out bottom, out _); } /// The TOP world-Y of a floating sky island at this column, or if none. @@ -1241,17 +1331,11 @@ private ContinentProfile ContinentProfileFor(PlanetType planet, long seed) return new ContinentProfile(true, _circumference / k, threshold, shelf, basin, lift, landFrac); } - /// The continental platform/basin offset at a column (#704), 0 on non-continental worlds. - /// Domain-warped continentalness → smoothstep between −BasinDepth and +Lift across the shelf band. - /// Where the field hovers near the threshold the shelf yields shallow seas and island arcs. - private double ContinentOffset(PlanetType planet, long seed, int worldX, int worldZ) + /// The continental platform/basin offset at a column (#704). Domain-warped continentalness → + /// smoothstep between −BasinDepth and +Lift across the shelf band. Where the field hovers near the + /// threshold the shelf yields shallow seas and island arcs. Callers gate on p.Active (#712). + private double ContinentOffset(in ContinentProfile p, long seed, int worldX, int worldZ) { - var p = ContinentProfileFor(planet, seed); - if (!p.Active) - { - return 0.0; - } - double warpAmp = p.Wavelength * 0.1; double wx = worldX + (FbmT(seed + 0x0C047AA0, worldX, worldZ, p.Wavelength / 3.0, octaves: 2) - 0.5) * 2.0 * warpAmp; double wz = worldZ + (FbmT(seed + 0x0C047AA1, worldX, worldZ, p.Wavelength / 3.0, octaves: 2) - 0.5) * 2.0 * warpAmp; @@ -1295,10 +1379,11 @@ public struct ColumnBand public int GetExtraBands(PlanetType planet, int worldX, int worldZ, System.Span bands) { int n = 0; - long seed = PlanetSeed(planet); + var w = WonderFor(planet); // #712: gates + seed resolved once per world + long seed = w.Seed; if (planet.FloatingIslands) { - int tiers = SkyTiersFor(seed); + int tiers = w.SkyTiers; for (int t = 0; t < tiers && n < bands.Length; t++) { if (FloatingIslandTier(planet, seed, t, worldX, worldZ, out int top, out int bottom, out double it)) @@ -1326,22 +1411,22 @@ public int GetExtraBands(PlanetType planet, int worldX, int worldZ, System.Span< } } - if (n < bands.Length && HasArches(planet) && TryGetArchBar(planet, seed, worldX, worldZ, out int abLo, out int abHi)) + if (n < bands.Length && w.Arches && TryGetArchBar(planet, seed, worldX, worldZ, out int abLo, out int abHi)) { bands[n++] = new ColumnBand { Bottom = abLo, Top = abHi, Kind = BandKind.Cap }; } - if (n < bands.Length && HasSeaStacks(planet) && TryGetSeaStackCap(planet, seed, worldX, worldZ, out int scLo, out int scHi)) + if (n < bands.Length && w.SeaStacks && TryGetSeaStackCap(planet, seed, worldX, worldZ, out int scLo, out int scHi)) { bands[n++] = new ColumnBand { Bottom = scLo, Top = scHi, Kind = BandKind.Cap }; } - if (n < bands.Length && HasHoodoos(planet) && TryGetHoodooCap(planet, seed, worldX, worldZ, out int hcLo, out int hcHi)) + if (n < bands.Length && w.Hoodoos && TryGetHoodooCap(planet, seed, worldX, worldZ, out int hcLo, out int hcHi)) { bands[n++] = new ColumnBand { Bottom = hcLo, Top = hcHi, Kind = BandKind.Cap }; } - if (n < bands.Length && HasCenotes(planet) && TryGetCenoteLip(planet, seed, worldX, worldZ, out int clLo, out int clHi)) + if (n < bands.Length && w.Cenotes && TryGetCenoteLip(planet, seed, worldX, worldZ, out int clLo, out int clHi)) { bands[n++] = new ColumnBand { Bottom = clLo, Top = clHi, Kind = BandKind.Cap }; } @@ -1351,8 +1436,7 @@ public int GetExtraBands(PlanetType planet, int worldX, int worldZ, System.Span< /// True when any band-producing feature can exist on this world at all — Generate's cheap /// whole-chunk gate so classic worlds pay nothing for #705. - public bool HasExtraBands(PlanetType planet) - => planet.FloatingIslands || HasArches(planet) || HasSeaStacks(planet) || HasHoodoos(planet) || HasCenotes(planet); + public bool HasExtraBands(PlanetType planet) => WonderFor(planet).AnyBands; // --- Multi-tier skylands (#707): floating worlds roll 1–3 island tiers; tier 0 is the classic band // (same seeds and shaping, so existing sky worlds keep their islands), upper tiers stack ~36 blocks @@ -1495,6 +1579,16 @@ private bool TryGetSeaStack(PlanetType planet, long seed, int worldX, int worldZ return false; } + // #712 perf: reject by DISTANCE before the expensive probes — every column of a stack-bearing + // cell used to pay a 4-octave FBM plus a full RawSurfaceHeight anchor even far from the stack. + // Callers only ever act within stemR + 2 (the cap), so this early-out is behaviour-preserving. + dist = System.Math.Sqrt(dx * dx + dz * dz); + stemR = 2.0 + ((hash >> 16) & 0x3); // 2..5 + if (dist > stemR + 2.0) + { + return false; + } + // Stacks only rise from LOW ground (coasts + shallows): probe the base swell at the stack centre. int cx = worldX - (int)System.Math.Round(dx); int cz = worldZ - (int)System.Math.Round(dz); @@ -1504,8 +1598,6 @@ private bool TryGetSeaStack(PlanetType planet, long seed, int worldX, int worldZ return false; } - dist = System.Math.Sqrt(dx * dx + dz * dz); - stemR = 2.0 + ((hash >> 16) & 0x3); // 2..5 rise = 14.0 + ((hash >> 20) & 0x7); // 14..21 anchor = RawSurfaceHeight(planet, cx, cz); return true; @@ -1562,14 +1654,21 @@ private bool TryGetHoodoo(PlanetType planet, long seed, int worldX, int worldZ, dist = rise = 0.0; anchor = 0; hash = 0; - if (!HoodooRegionAt(planet, seed, worldX, worldZ) - || !TryGetHotspot(seed ^ 0x400D01, HoodooCellSize, HoodooChance, 4.0, + // #712 perf: hotspot + distance first — in a hoodoo REGION every column used to pay the region + // FBM plus a full RawSurfaceHeight anchor; callers only act within the cap radius 2.6, so the + // cheap rejects go first and the anchor is computed last. Behaviour-preserving (conjunctive gates). + if (!TryGetHotspot(seed ^ 0x400D01, HoodooCellSize, HoodooChance, 4.0, worldX, worldZ, out hash, out double dx, out double dz)) { return false; } dist = System.Math.Sqrt(dx * dx + dz * dz); + if (dist > 2.6 || !HoodooRegionAt(planet, seed, worldX, worldZ)) + { + return false; + } + rise = 6.0 + ((hash >> 16) & 0x7); // 6..13 anchor = RawSurfaceHeight(planet, worldX - (int)System.Math.Round(dx), worldZ - (int)System.Math.Round(dz)); return true; @@ -1609,17 +1708,17 @@ private bool HasOverhangLandmarks(PlanetType planet) /// The overhang landmarks' ground rise at a column (#706): arch abutments, then sea-stack /// stems, then hoodoo stems — first hit wins (they share the one-landmark-per-column rule). - private double OverhangGroundOffset(PlanetType planet, long seed, int worldX, int worldZ) + private double OverhangGroundOffset(PlanetType planet, WonderProfile w, int worldX, int worldZ) { - double o = HasArches(planet) ? ArchGroundOffset(seed, worldX, worldZ) : 0.0; - if (o == 0.0 && HasSeaStacks(planet)) + double o = w.Arches ? ArchGroundOffset(w.Seed, worldX, worldZ) : 0.0; + if (o == 0.0 && w.SeaStacks) { - o = SeaStackGroundOffset(planet, seed, worldX, worldZ); + o = SeaStackGroundOffset(planet, w.Seed, worldX, worldZ); } - if (o == 0.0 && HasHoodoos(planet)) + if (o == 0.0 && w.Hoodoos) { - o = HoodooGroundOffset(planet, seed, worldX, worldZ); + o = HoodooGroundOffset(planet, w.Seed, worldX, worldZ); } return o; @@ -1658,6 +1757,13 @@ private bool TryGetCenote(PlanetType planet, long seed, int worldX, int worldZ, dist = System.Math.Sqrt(dx * dx + dz * dz); radius = 9.0 + ((hash >> 16) & 0x7) * 1.5; // 9..19.5 depth = 30.0 + ((hash >> 20) & 0x3F) * 0.8; // 30..80 + if (dist > radius) + { + // #712 perf: no caller acts outside the shaft radius — skip the RawSurfaceHeight anchor for + // the vast majority of columns in a cenote-bearing cell. Behaviour-preserving. + return false; + } + anchor = RawSurfaceHeight(planet, worldX - (int)System.Math.Round(dx), worldZ - (int)System.Math.Round(dz)); return true; } @@ -1699,12 +1805,13 @@ private bool TryGetCenoteLip(PlanetType planet, long seed, int worldX, int world public bool TryGetCenotePool(PlanetType planet, int worldX, int worldZ, out int poolTopY) { poolTopY = 0; - if (!HasCenotes(planet)) + var w = WonderFor(planet); // #712 + if (!w.Cenotes) { return false; } - long seed = PlanetSeed(planet); + long seed = w.Seed; if (!TryGetCenote(planet, seed, worldX, worldZ, out double dist, out double radius, out double depth, out int anchor, out ulong h) || dist >= radius || ((h >> 40) & 0xFF) >= 154) @@ -1785,12 +1892,13 @@ public bool TryGetCavernSpan(PlanetType planet, int worldX, int worldZ, { yLo = yHi = 0; lakeY = int.MinValue; - if (!HasCaverns(planet)) + var w = WonderFor(planet); // #712 + if (!w.Caverns) { return false; } - long seed = PlanetSeed(planet); + long seed = w.Seed; if (!TryGetHotspot(seed ^ 0x0CAFE27A, CavernCellSize, CavernChance, CavernMaxRx + 10.0, worldX, worldZ, out ulong h, out double dx, out double dz)) { @@ -1831,71 +1939,126 @@ public bool TryGetCavernSpan(PlanetType planet, int worldX, int worldZ, private bool HasTunnels(PlanetType planet) => !planet.Void && planet.CaveThreshold > 0.0; + /// One capsule segment of a tunnel worm (#708), in hotspot-cell-local X/Z (Y absolute). + private readonly struct TunnelSeg + { + public readonly double X0, Y0, Z0, X1, Y1, Z1, R; + + public TunnelSeg(double x0, double y0, double z0, double x1, double y1, double z1, double r) + { + X0 = x0; Y0 = y0; Z0 = z0; X1 = x1; Y1 = y1; Z1 = z1; R = r; + } + } + + // #712 perf: the worm polyline is a pure function of the CELL hash, yet it was rebuilt (dozens of + // xorshift rolls + clamps) for EVERY COLUMN of every tunnel-bearing cell — half the map. Cached per + // cell (static dict + a lock-free single-slot instance fast path; a chunk's 256 columns share one + // cell), so per column only the cheap distance checks remain. Output is bit-identical: the segment + // stream never depended on the queried column. + private static readonly System.Collections.Generic.Dictionary<(long, ulong), TunnelSeg[]> _tunnelSegCache = new(); + private static readonly object _tunnelSegLock = new object(); + private (long, ulong) _tunnelSegKey; + private TunnelSeg[]? _tunnelSegs; + + private TunnelSeg[] TunnelSegmentsFor(PlanetType planet, WonderProfile w, ulong h) + { + var key = (w.Seed, h); + if (_tunnelSegs is { } fast && _tunnelSegKey == key) + { + return fast; + } + + lock (_tunnelSegLock) + { + if (!_tunnelSegCache.TryGetValue(key, out var segsArr)) + { + // Build the worm from its cell hash — an xorshift stream keeps the rolls cheap + portable. + ulong s = h | 1UL; + double Next() + { + s ^= s << 13; + s ^= s >> 7; + s ^= s << 17; + return (s & 0xFFFFF) / 1048576.0; + } + + bool tube = w.Volcanoes && Next() < 0.3; // lava tubes: wider, smoother, shallower (#709) + int segs = tube ? 5 + (int)(Next() * 4) : 6 + (int)(Next() * 7); + double px = 0.0, pz = 0.0; + double py = planet.BaseHeight - (tube ? 4.0 + Next() * 10.0 : 8.0 + Next() * 26.0); + double vx = Next() * 2.0 - 1.0, vz = Next() * 2.0 - 1.0; + double vlen = System.Math.Sqrt(vx * vx + vz * vz); + if (vlen < 0.2) { vx = 1.0; vz = 0.0; vlen = 1.0; } + vx /= vlen; + vz /= vlen; + + var list = new System.Collections.Generic.List(segs * 2); + for (int i = 0; i < segs; i++) + { + double len = 24.0 + Next() * 20.0; + double drift = tube ? 0.15 : 0.35; + double vy = (Next() - 0.55) * drift; // worms trend gently downward + double qx = px + vx * len, qz = pz + vz * len, qy = py + vy * len; + double lim = TunnelMargin - 24.0; + qx = System.Math.Clamp(qx, -lim, lim); + qz = System.Math.Clamp(qz, -lim, lim); + double radius = tube ? 4.5 + Next() * 2.0 : 2.5 + Next() * 2.0; + list.Add(new TunnelSeg(px, py, pz, qx, qy, qz, radius)); + + // Skylight shaft (#709): sometimes the roof opens to the sky — a thin vertical worm + // from the segment end up past the surface, so tunnels get real MOUTHS. (0.22 since + // the 2026-08-03 visibility tuning: mouths are how players FIND the tunnels.) + if (Next() < 0.22) + { + list.Add(new TunnelSeg(qx, qy, qz, qx, planet.BaseHeight + 60.0, qz, 1.7)); + } + + px = qx; py = qy; pz = qz; + double turn = (Next() - 0.5) * 0.9; + double nvx = vx + turn * -vz, nvz = vz + turn * vx; // cheap heading drift, no trig + double nl = System.Math.Sqrt(nvx * nvx + nvz * nvz); + vx = nvx / nl; + vz = nvz / nl; + } + + segsArr = list.ToArray(); + if (_tunnelSegCache.Count >= 512) + { + _tunnelSegCache.Clear(); // soft cap — a few hundred bytes per cell + } + + _tunnelSegCache[key] = segsArr; + } + + _tunnelSegs = segsArr; + _tunnelSegKey = key; + return segsArr; + } + } + /// Computes this column's tunnel-carve y-spans (#708) into and - /// returns the count. Deterministic per (seed, column); the polyline is rebuilt from the cell hash - /// (cheap arithmetic), so no state is shared between server, client and tests. + /// returns the count. Deterministic per (seed, column); the cell's worm polyline comes from the + /// per-cell cache (#712), so per column only capsule distance checks run. public int TunnelSpans(PlanetType planet, int worldX, int worldZ, System.Span<(int Lo, int Hi)> spans) { - if (!HasTunnels(planet)) + var w = WonderFor(planet); // #712 + if (!w.Tunnels) { return 0; } - long seed = PlanetSeed(planet); - if (!TryGetHotspot(seed ^ 0x7A22E1, TunnelCellSize, TunnelChance, TunnelMargin, + if (!TryGetHotspot(w.Seed ^ 0x7A22E1, TunnelCellSize, TunnelChance, TunnelMargin, worldX, worldZ, out ulong h, out double dx, out double dz)) { return 0; } - // Rebuild the worm from its cell hash — an xorshift stream keeps the rolls cheap + portable. - ulong s = h | 1UL; - double Next() - { - s ^= s << 13; - s ^= s >> 7; - s ^= s << 17; - return (s & 0xFFFFF) / 1048576.0; - } - - bool tube = HasVolcanoes(planet) && Next() < 0.3; // lava tubes: wider, smoother, shallower (#709) - int segs = tube ? 5 + (int)(Next() * 4) : 6 + (int)(Next() * 7); - double px = 0.0, pz = 0.0; - double py = planet.BaseHeight - (tube ? 4.0 + Next() * 10.0 : 8.0 + Next() * 26.0); - double vx = Next() * 2.0 - 1.0, vz = Next() * 2.0 - 1.0; - double vlen = System.Math.Sqrt(vx * vx + vz * vz); - if (vlen < 0.2) { vx = 1.0; vz = 0.0; vlen = 1.0; } - vx /= vlen; - vz /= vlen; - + var segs = TunnelSegmentsFor(planet, w, h); int n = 0; - for (int i = 0; i < segs && n < spans.Length; i++) - { - double len = 24.0 + Next() * 20.0; - double drift = tube ? 0.15 : 0.35; - double vy = (Next() - 0.55) * drift; // worms trend gently downward - double qx = px + vx * len, qz = pz + vz * len, qy = py + vy * len; - double lim = TunnelMargin - 24.0; - qx = System.Math.Clamp(qx, -lim, lim); - qz = System.Math.Clamp(qz, -lim, lim); - double radius = (tube ? 4.5 + Next() * 2.0 : 2.5 + Next() * 2.0); - - AddSegmentSpan(px, py, pz, qx, qy, qz, radius, dx, dz, spans, ref n); - - // Skylight shaft (#709): sometimes the roof opens to the sky — a thin vertical worm from the - // segment end up past the surface, so tunnels get real MOUTHS you can climb into. - // (0.12 → 0.22, visibility tuning 2026-08-03: mouths are how players FIND the tunnels.) - if (Next() < 0.22 && n < spans.Length) - { - AddSegmentSpan(qx, qy, qz, qx, planet.BaseHeight + 60.0, qz, 1.7, dx, dz, spans, ref n); - } - - px = qx; py = qy; pz = qz; - double turn = (Next() - 0.5) * 0.9; - double nvx = vx + turn * -vz, nvz = vz + turn * vx; // cheap heading drift, no trig - double nl = System.Math.Sqrt(nvx * nvx + nvz * nvz); - vx = nvx / nl; - vz = nvz / nl; + for (int i = 0; i < segs.Length && n < spans.Length; i++) + { + ref readonly var sg = ref segs[i]; + AddSegmentSpan(sg.X0, sg.Y0, sg.Z0, sg.X1, sg.Y1, sg.Z1, sg.R, dx, dz, spans, ref n); } return n; @@ -1906,6 +2069,14 @@ double Next() private static void AddSegmentSpan(double pxx, double pyy, double pzz, double qx, double qy, double qz, double r, double dx, double dz, System.Span<(int Lo, int Hi)> spans, ref int n) { + // #712 perf: XZ bounding reject before the 8-point sampling — most columns of a tunnel cell are + // nowhere near any given segment. Pure shortcut, identical output. + if (dx < System.Math.Min(pxx, qx) - r || dx > System.Math.Max(pxx, qx) + r + || dz < System.Math.Min(pzz, qz) - r || dz > System.Math.Max(pzz, qz) + r) + { + return; + } + double lo = double.MaxValue, hi = double.MinValue; for (int k = 0; k <= 8; k++) { @@ -1934,13 +2105,14 @@ private static void AddSegmentSpan(double pxx, double pyy, double pzz, double qx /// Height offset (blocks, added to BaseHeight) for a planet with an explicit /// (item 21 V2). is the base FBM swell in [-1,1]. Each style reshapes it into a distinct - /// landform so worlds look structurally different. Deterministic + seam-safe (all noise wraps on X). - private double StyledHeightOffset(PlanetType planet, string style, long seed, double h, int worldX, int worldZ) + /// landform so worlds look structurally different. Deterministic + seam-safe (all noise wraps on X). + /// Dispatches on the profile's pre-lowered style and pre-rolled grain (#712 — no per-column strings). + private double StyledHeightOffset(PlanetType planet, WonderProfile w, long seed, double h, int worldX, int worldZ) { double amp = planet.Amplitude; double Ridge(double v) => (1.0 - System.Math.Abs(v)) * 2.0 - 1.0; // smooth swell → sharp ridge/valley - switch (style.ToLowerInvariant()) + switch (w.Style) { case "flats": return h * amp * 0.22; // near-flat plains (salt flats, ocean floor, low islands) @@ -1952,7 +2124,7 @@ private double StyledHeightOffset(PlanetType planet, string style, long seed, do { // #700: part of the ridging comes from an oriented field, so ranges form CHAINS along // the world's grain instead of isotropic knots. - double or = OrientedRidge(seed, GrainFor(seed), worldX, worldZ, planet.TerrainScale * 0.9); + double or = OrientedRidge(seed, w.Grain, worldX, worldZ, planet.TerrainScale * 0.9); double r = h * 0.25 + Ridge(h) * 0.30 + or * 0.45; // sharp, rugged, directional if (r > 0) { @@ -1989,8 +2161,7 @@ private double StyledHeightOffset(PlanetType planet, string style, long seed, do // Parallel wind-blown ridges: a ridged mid-frequency field laid over a gentle base. // Since #700 the field is sampled in the world's GRAIN — every desert rolls a wind // direction and its dune crests march that way instead of blobbing isotropically. - var g = GrainFor(seed); - double d = GrainFbm(seed + 0x0D0E, g, worldX, worldZ, planet.TerrainScale * 0.45, octaves: 2); + double d = GrainFbm(seed + 0x0D0E, w.Grain, worldX, worldZ, planet.TerrainScale * 0.45, octaves: 2); double ridged = 1.0 - System.Math.Abs(d * 2.0 - 1.0); // 0..1 dune crests return h * amp * 0.25 + ridged * amp * 0.85; } @@ -2224,12 +2395,13 @@ private double CraterChainCarve(long seed, int worldX, int worldZ) /// around 5–8 rolled integer directions, reaching 1.3–4.5 primary radii out. public bool CraterRayAt(PlanetType planet, int worldX, int worldZ) { - if (!(planet.Cratered || _crateredWorld)) + var w = WonderFor(planet); // #712 + if (!w.Cratered) { return false; } - long seed = PlanetSeed(planet); + long seed = w.Seed; if (!TryGetHotspot(seed ^ 0x0C4A17, ChainCellSize, ChainChance, ChainMargin, worldX, worldZ, out ulong h, out double dx, out double dz)) { @@ -2324,7 +2496,7 @@ public bool CraterRayAt(PlanetType planet, int worldX, int worldZ) /// the world's seed-chosen subset of archetypes (deterministic, seam-free across the X wrap) and /// smoothstep-blends the two neighbours' computed OFFSETS (#576 — shapes like quantised decks or /// asymmetric gorges cannot be blended as parameters). - private double BlendedArchetypeOffset(PlanetType planet, long seed, double h, int worldX, int worldZ) + private double BlendedArchetypeOffset(PlanetType planet, WonderProfile w, long seed, double h, int worldX, int worldZ) { int pool = TerrainArchetypeCount; long s = seed ^ 0x7E44A1; @@ -2342,13 +2514,13 @@ private double BlendedArchetypeOffset(PlanetType planet, long seed, double h, in int a0 = (rot + i0) % pool; int a1 = (rot + i1) % pool; - double o0 = ArchetypeOffset(a0, planet, seed, h, worldX, worldZ); + double o0 = ArchetypeOffset(a0, planet, w, seed, h, worldX, worldZ); if (a1 == a0 || f <= 0.0) { return o0; } - return o0 + (ArchetypeOffset(a1, planet, seed, h, worldX, worldZ) - o0) * f; + return o0 + (ArchetypeOffset(a1, planet, w, seed, h, worldX, worldZ) - o0) * f; } /// The world's surface sea level (world Y) — the height water/lava fills basins to, or