You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fourth landform slice of the worldgen-relief roadmap (integration branch worldgen-relief, NOT
main), after tectonic W-SIM-4a (#396/#397, corrected #404), aeolian W-SIM-3a (#403/#406) and volcanic
W-SIM-5 (#410/#411). Base this work off worldgen-relief HEAD (c0d7950); the PR bases on and merges
into worldgen-relief.
Grounded in RnD sim/world/16-glacial-and-karst-landforms.md (this slice = the glacial gate only;
the karst gate and the glacial sub-forms listed under Out of scope are deferred). Glacial relief is a
NEW landform family: ice as a slow viscous flow that follows the existing drainage downhill, abrades and quarries the rock it flows over (subtractive), and drops the excavated debris as till where it melts (additive). Unlike volcanic (constructive, pre-erosion) it reshapes
ALREADY-eroded relief, so per RnD 16 §1 it is inserted POST-erosion, PRE-final-classify:
base height → volcanic → erosion → GLACIAL(this) → [karst, future] → aeolian → final classify
The forcing is a CLIMATE THRESHOLD — the ELA (Equilibrium Line Altitude / snowline): cells whose
terrain is colder than freezing (T ≤ T_freeze, read from the existing pure-integer lapse-rate
temperature field, centidegrees) are ice-covered. Ice forms on the cold/high belts only, so glaciation
is naturally rare and confined (like the toxicity belts), leaving the temperate world untouched.
Two determinism-mandated passes, in strict order (RnD 16 §6 — the load-bearing invariant):
Subtractive ice-incision (single-signed): ice routes downhill, erosion concentrates where ice
flux is high (deep troughs) and thin tributaries carve less (hanging valleys emerge). Ice-incision
only ever LOWERS height → single-signed, monotone.
Additive till deposition (separate pass, AFTER the incision pass settles): the rock-flour
excavated in pass 1 is spent as an integer ledger — moraine ridges built where ice melts (the ice
margin).
Mixing the two signs in one loop is forbidden (RnD 16 §6: a height-raise inside the routing loop
flip-flops the flow field and never settles). This split is a WHAT (a determinism requirement), not a
HOW — the implementer chooses the routing/iteration mechanism, subject to the acceptance invariants
below (pure-integer, order-independent, fixed non-convergence-terminated iteration in the house style of
the existing erosion macro-loop).
Opt-in, default-OFF (mirror enable_tectonics/enable_aeolian/enable_volcanic), threaded the
same orthogonal way through caps::classify_and_caps → ProcgenWorld::new; the OFF-path must be
byte-identical so the v2 acceptance corridors and existing goldens (which run with glacial OFF) are
untouched.
Feasibility (grounding, NOT prescription): the codebase already carries the primitives an
implementation would build on — a pure-integer lapse-rate temperature field, an integer downhill
drainage/flow-accumulation primitive with a total-order tie-break, and a fixed-count
recompute-drainage-each-iteration erosion macro-loop. HOW to compose them is the implementer's call;
the acceptance criteria constrain the OUTCOME, not the mechanism. See pm/reports/w-sim-glacial-grounding.md.
Goal
When enable_glacial = true, the post-erosion height field gains a deterministic glacial signature:
ice-covered cells (an ELA temperature gate) are carved by a monotone, single-signed ice-incision
pass that deepens/widens high-ice-flux valleys into U-shaped troughs and overdeepened basins, and the
excavated rock-flour is then redeposited as Till moraine ridges at the ice margin by a separate
additive pass, with total excavation and total deposition balanced by an integer ledger. Both the trough
(a subtractive form the erosion-only baseline cannot make — fluvial erosion incises V-notches, not
flat-floored overdeepened U-troughs) and the moraine (a positive till ridge) are features absent from
the current worldgen-relief world. enable_glacial = false reproduces that world byte-for-byte.
Acceptance criteria
New opt-in glacial stage gated by an enable_glacial flag, default-OFF, threaded to ProcgenWorld::new the same orthogonal way enable_tectonics/enable_aeolian/enable_volcanic
are (a new independent parameter; every production call site passes false). Inserted POST-erosion, PRE-final-classify — it consumes the fully eroded height field and hands its
reshaped height + new till material forward.
OFF-path byte-identical:enable_glacial = false reproduces the pre-slice classify_and_caps / ProcgenWorld output byte-for-byte — no new RNG/noise draw and no ELA/ice
computation of any kind when off (the gate SKIPS the whole pass, not merely discards it). A
determinism test asserts this against the existing baseline (mirror the
tectonics/aeolian/volcanic OFF-path tests in caps.rs).
ELA ice-mask is pure-integer and computed ONCE at stage entry (RnD 16 §6, §pitfalls): the ice
mask is an integer threshold on the lapse-rate temperature field (T ≤ T_freeze, centidegrees) —
no f32 snowline compare that could ULP-flip the ice edge, and the mask is derived ONCE from the
post-erosion height at the start of the stage, NOT re-derived inside the incision iterations
(which would couple mask→height→mask and risk non-convergence). A test asserts the mask is a pure
deterministic function of (seed, dim, height), byte-identical across repeated generation; a
warm world / an ice-free T_freeze yields an EMPTY mask ⇒ a byte-identical OFF-path result.
Ice-incision is subtractive-only, fixed-iteration, deterministic (RnD 16 §6; matches the
erosion macro-loop's house style — a FIXED iteration count, NOT ε/convergence-terminated, per the [erosion] R10 determinism rule): every height change in the pass is a non-increasing delta
(single-signed — no cell's height ever RISES during incision); the routing is downhill with a
total-order tie-break so the result is order-independent / byte-identical across runs. Tests
assert (a) no cell rises during the incision pass, and (b) the pass is byte-identical across two
standalone runs on the golden grid.
U-trough structural signature (anti-forcing-clean, ON-vs-OFF at fixed coordinates, no tuned
threshold): valley cells are identified on the glacial-OFF baseline (the highest-flux valley
cells at that state). At those SAME fixed cell coordinates, the glacial-ON field shows the
diagnostic glacial change RELATIVE to glacial-OFF: the valley floor is lowered (overdeepened)
AND the cross-valley section is flatter-floored and steeper-walled than glacial-OFF. The test
asserts the DIRECTION of change (ON vs OFF, same seed, same coordinates) — not an absolute
magnitude tuned to pass — on ≥3 independent valley segments; the erosion-only baseline yields the
V-form (no overdeepening) at those coordinates. The segments are selected DETERMINISTICALLY on
the OFF baseline (the highest-flux valley cells, pairwise separated by a fixed minimum cell
distance so they are geographically independent, not implementer-cherry-picked adjacent cells) —
the selection rule is part of the test, not a free choice. (Lesson W-SIM-4a/5: verify a sharp structural
feature the baseline can't produce, measured against the same-seed OFF baseline, not a bulk count
or a hand-tuned number.)
Till deposition is a separate additive-only pass writing Till (RnD 16 §6): after the
incision pass settles, the excavated volume is deposited as moraine — a positive height ridge at
the ice margin — tagged with a new MaterialId::Till. Till is APPENDED to gen::material::MaterialId (append-only: existing discriminants Air=0 … Tuff=6 unchanged; new
one = Till = 7); the OFF-path never emits it. A test asserts ≥1 moraine cell is a local height
MAXIMUM (a positive ridge) at the ice margin and carries Till.
Mass-ledger conservation (integer-exact, export = 0 for this slice): total height excavated by
the incision pass equals total height deposited by the till pass — Σ excavated ≡ Σ deposited as
an exact integer equality, with NO export term (all rock-flour is retained as till in this slice;
glaciofluvial outwash / loess export is explicitly deferred — see Out of scope). A conserved
golden pins the equality (same class as volcanic's v2_golden_conserved_*).
Post-glacial height stays valid: every cell height after the stage is an integer in the
terrain's existing valid range [0, hmax] (no underflow past 0 from over-incision, no overflow at
the ceiling from deposition); a test asserts the bound holds.
Determinism golden vector (two-pass, CI-sourced): pinned exact glacial-ON height + surface
material at fixed grid coordinates for the golden (seed, dim, hmax), read off the arm64 golden
job, re-derivable from the module's algorithm doc — mirrors the
tectonics/aeolian/volcanic golden-vector tests. Pass 1 = code + placeholder pin + push
(STATUS: blocked@N: awaiting CI (pass 2 of 2)); pass 2 = pin the CI-revealed value, finish the PR.
Orthogonality: enabling glacial alone leaves cells outside the ice mask byte-identical to the
glacial-OFF world (the pass is a no-op off the ice belts), and glacial composes with any subset of
tectonics/aeolian/volcanic without cross-contamination — a test mirrors the existing pairwise
orthogonality checks.
Pure integer / fixed-point throughout — no f32/f64 anywhere in any new gen/ file (the
recursive glob no-float guard world/tests/no_float_guard_gen.rs covers it automatically);
quantize-before-compare for every ELA / slope / ice-flux threshold.
scripts/compile-check.sh PASS quoted in the final report; pre-merge code-critic self-review
run against the branch diff + this ТЗ, verdict block appended as a PR comment.
Out of scope
Deferred to later glacial/karst sub-slices (do NOT build these here):
Karst gate entirely (dissolution mask, closed depressions/dolines/poljes, ponor→spring conduit,
caves) — RnD 16 §4–5; a separate landform gate, its 3D cave geometry belongs to the dive [18] slice.
Fjord — a U-trough eroded below sea_level then drowned (RnD 16 §3); needs the sea-level /
coastal interaction (the 5th roadmap slice) → defer to avoid coupling two unshipped landforms.
Glaciofluvial sub-forms + outwash export — drumlins, eskers, outwash/zandr plains, kames,
erratics/boulder-lag, and the non-zero mass-export term they introduce (RnD 16 §3) → later glacial
sub-slice; this slice ships the trough + terminal/lateral moraine core with export = 0.
Roche moutonnée / ice-direction surface texture — a surface material-pattern slot like columnar
jointing, not macrorelief (RnD 16 §3).
Aspect-shift ELA (±70–320 m folded-aspect integer-LUT, RnD 16 §1) — a refinement of the ELA
field; this slice uses the lat+lapse ELA without the aspect term (kept simple, still pure-integer).
Glacial→aeolian loess coupling (outwash/rock-flour as a loess source discharging the aeolian
[13 §5] reserve, RnD 16 §7) — a read on the aeolian side; aeolian already shipped without it, so the
coupling is its own follow-up, not this slice.
Live glaciation / retreat (Ф2+) — moving moraines, drifting ELA as a warm elevation-delta overlay
(RnD 16 §6, R29). This slice produces the COLD immutable base only.
Refactoring the enable_* flags into a config struct.enable_glacial is added as a 5th trailing
bool, matching the established house style (enable_patchiness/tectonics/aeolian/volcanic). The
proliferating-positional-bool mis-wire risk is real, but folding all five into a GenConfig is a
golden-neutral cross-cutting refactor that would balloon this feature's diff and touch every call
site — it belongs in its own infra slice. Guard here instead: the OFF-path byte-identity test + the
ON-path gate-changes-height test catch a mis-wired slot, and the new flag carries a call-site comment
marking its fixed position in the argument list.
Verification
Cloud CI — commit → git push → scripts/ci-report.sh (exit 0 = green). The v2 goldens
(v2_golden_conserved_*, arm64) plus the new glacial golden vector + conserved-ledger golden are the
gate; scripts/compile-check.sh PASS is quoted pre-push (rebased-from-worldgen-relief call sites).
Golden re-pin (pass 2) — read the glacial-ON pinned height/material and the conserved-ledger value
off .ci-report/failed.log (v2-golden-arm64 job), then pin.
No live-app / render assertion — this slice is numeric-verify only (UI/render out of roadmap scope).
Lane
sim
Passes
2
Depends-on
None (bases on worldgen-relief HEAD c0d7950; independent of the future karst/coastal slices).
Context
Fourth landform slice of the
worldgen-reliefroadmap (integration branchworldgen-relief, NOTmain), after tectonic W-SIM-4a (#396/#397, corrected #404), aeolian W-SIM-3a (#403/#406) and volcanic
W-SIM-5 (#410/#411). Base this work off
worldgen-reliefHEAD (c0d7950); the PR bases on and mergesinto
worldgen-relief.Grounded in RnD
sim/world/16-glacial-and-karst-landforms.md(this slice = the glacial gate only;the karst gate and the glacial sub-forms listed under Out of scope are deferred). Glacial relief is a
NEW landform family: ice as a slow viscous flow that follows the existing drainage downhill,
abrades and quarries the rock it flows over (subtractive), and drops the excavated debris as
till where it melts (additive). Unlike volcanic (constructive, pre-erosion) it reshapes
ALREADY-eroded relief, so per RnD 16 §1 it is inserted POST-erosion, PRE-final-classify:
The forcing is a CLIMATE THRESHOLD — the ELA (Equilibrium Line Altitude / snowline): cells whose
terrain is colder than freezing (
T ≤ T_freeze, read from the existing pure-integer lapse-ratetemperature field, centidegrees) are ice-covered. Ice forms on the cold/high belts only, so glaciation
is naturally rare and confined (like the toxicity belts), leaving the temperate world untouched.
Two determinism-mandated passes, in strict order (RnD 16 §6 — the load-bearing invariant):
flux is high (deep troughs) and thin tributaries carve less (hanging valleys emerge). Ice-incision
only ever LOWERS height → single-signed, monotone.
excavated in pass 1 is spent as an integer ledger — moraine ridges built where ice melts (the ice
margin).
Mixing the two signs in one loop is forbidden (RnD 16 §6: a height-raise inside the routing loop
flip-flops the flow field and never settles). This split is a WHAT (a determinism requirement), not a
HOW — the implementer chooses the routing/iteration mechanism, subject to the acceptance invariants
below (pure-integer, order-independent, fixed non-convergence-terminated iteration in the house style of
the existing erosion macro-loop).
Opt-in, default-OFF (mirror
enable_tectonics/enable_aeolian/enable_volcanic), threaded thesame orthogonal way through
caps::classify_and_caps→ProcgenWorld::new; the OFF-path must bebyte-identical so the v2 acceptance corridors and existing goldens (which run with glacial OFF) are
untouched.
Goal
When
enable_glacial = true, the post-erosion height field gains a deterministic glacial signature:ice-covered cells (an ELA temperature gate) are carved by a monotone, single-signed ice-incision
pass that deepens/widens high-ice-flux valleys into U-shaped troughs and overdeepened basins, and the
excavated rock-flour is then redeposited as
Tillmoraine ridges at the ice margin by a separateadditive pass, with total excavation and total deposition balanced by an integer ledger. Both the trough
(a subtractive form the erosion-only baseline cannot make — fluvial erosion incises V-notches, not
flat-floored overdeepened U-troughs) and the moraine (a positive till ridge) are features absent from
the current
worldgen-reliefworld.enable_glacial = falsereproduces that world byte-for-byte.Acceptance criteria
enable_glacialflag, default-OFF, threaded toProcgenWorld::newthe same orthogonal wayenable_tectonics/enable_aeolian/enable_volcanicare (a new independent parameter; every production call site passes
false). InsertedPOST-erosion, PRE-final-classify — it consumes the fully eroded height field and hands its
reshaped height + new till material forward.
enable_glacial = falsereproduces the pre-sliceclassify_and_caps/ProcgenWorldoutput byte-for-byte — no new RNG/noise draw and no ELA/icecomputation of any kind when off (the gate SKIPS the whole pass, not merely discards it). A
determinism test asserts this against the existing baseline (mirror the
tectonics/aeolian/volcanic OFF-path tests in
caps.rs).mask is an integer threshold on the lapse-rate temperature field (
T ≤ T_freeze, centidegrees) —no
f32snowline compare that could ULP-flip the ice edge, and the mask is derived ONCE from thepost-erosion height at the start of the stage, NOT re-derived inside the incision iterations
(which would couple mask→height→mask and risk non-convergence). A test asserts the mask is a pure
deterministic function of
(seed, dim, height), byte-identical across repeated generation; awarm world / an ice-free
T_freezeyields an EMPTY mask ⇒ a byte-identical OFF-path result.erosion macro-loop's house style — a FIXED iteration count, NOT ε/convergence-terminated, per the
[erosion] R10determinism rule): every height change in the pass is a non-increasing delta(single-signed — no cell's height ever RISES during incision); the routing is downhill with a
total-order tie-break so the result is order-independent / byte-identical across runs. Tests
assert (a) no cell rises during the incision pass, and (b) the pass is byte-identical across two
standalone runs on the golden grid.
threshold): valley cells are identified on the glacial-OFF baseline (the highest-flux valley
cells at that state). At those SAME fixed cell coordinates, the glacial-ON field shows the
diagnostic glacial change RELATIVE to glacial-OFF: the valley floor is lowered (overdeepened)
AND the cross-valley section is flatter-floored and steeper-walled than glacial-OFF. The test
asserts the DIRECTION of change (ON vs OFF, same seed, same coordinates) — not an absolute
magnitude tuned to pass — on ≥3 independent valley segments; the erosion-only baseline yields the
V-form (no overdeepening) at those coordinates. The segments are selected DETERMINISTICALLY on
the OFF baseline (the highest-flux valley cells, pairwise separated by a fixed minimum cell
distance so they are geographically independent, not implementer-cherry-picked adjacent cells) —
the selection rule is part of the test, not a free choice. (Lesson W-SIM-4a/5: verify a sharp structural
feature the baseline can't produce, measured against the same-seed OFF baseline, not a bulk count
or a hand-tuned number.)
Till(RnD 16 §6): after theincision pass settles, the excavated volume is deposited as moraine — a positive height ridge at
the ice margin — tagged with a new
MaterialId::Till.Tillis APPENDED togen::material::MaterialId(append-only: existing discriminantsAir=0 … Tuff=6unchanged; newone =
Till = 7); the OFF-path never emits it. A test asserts ≥1 moraine cell is a local heightMAXIMUM (a positive ridge) at the ice margin and carries
Till.the incision pass equals total height deposited by the till pass —
Σ excavated ≡ Σ depositedasan exact integer equality, with NO export term (all rock-flour is retained as till in this slice;
glaciofluvial outwash / loess export is explicitly deferred — see Out of scope). A conserved
golden pins the equality (same class as volcanic's
v2_golden_conserved_*).terrain's existing valid range
[0, hmax](no underflow past 0 from over-incision, no overflow atthe ceiling from deposition); a test asserts the bound holds.
material at fixed grid coordinates for the golden
(seed, dim, hmax), read off the arm64 goldenjob, re-derivable from the module's algorithm doc — mirrors the
tectonics/aeolian/volcanic golden-vector tests. Pass 1 = code + placeholder pin + push
(
STATUS: blocked@N: awaiting CI (pass 2 of 2)); pass 2 = pin the CI-revealed value, finish the PR.glacial-OFF world (the pass is a no-op off the ice belts), and glacial composes with any subset of
tectonics/aeolian/volcanic without cross-contamination — a test mirrors the existing pairwise
orthogonality checks.
f32/f64anywhere in any newgen/file (therecursive glob no-float guard
world/tests/no_float_guard_gen.rscovers it automatically);quantize-before-compare for every ELA / slope / ice-flux threshold.
scripts/compile-check.shPASS quoted in the final report; pre-mergecode-criticself-reviewrun against the branch diff + this ТЗ, verdict block appended as a PR comment.
Out of scope
Deferred to later glacial/karst sub-slices (do NOT build these here):
caves) — RnD 16 §4–5; a separate landform gate, its 3D cave geometry belongs to the dive [18] slice.
sea_levelthen drowned (RnD 16 §3); needs the sea-level /coastal interaction (the 5th roadmap slice) → defer to avoid coupling two unshipped landforms.
erratics/boulder-lag, and the non-zero mass-export term they introduce (RnD 16 §3) → later glacial
sub-slice; this slice ships the trough + terminal/lateral moraine core with export = 0.
jointing, not macrorelief (RnD 16 §3).
field; this slice uses the lat+lapse ELA without the aspect term (kept simple, still pure-integer).
[13 §5] reserve, RnD 16 §7) — a read on the aeolian side; aeolian already shipped without it, so the
coupling is its own follow-up, not this slice.
(RnD 16 §6, R29). This slice produces the COLD immutable base only.
enable_*flags into a config struct.enable_glacialis added as a 5th trailingbool, matching the established house style (
enable_patchiness/tectonics/aeolian/volcanic). Theproliferating-positional-bool mis-wire risk is real, but folding all five into a
GenConfigis agolden-neutral cross-cutting refactor that would balloon this feature's diff and touch every call
site — it belongs in its own infra slice. Guard here instead: the OFF-path byte-identity test + the
ON-path gate-changes-height test catch a mis-wired slot, and the new flag carries a call-site comment
marking its fixed position in the argument list.
Verification
commit → git push → scripts/ci-report.sh(exit 0 = green). The v2 goldens(
v2_golden_conserved_*, arm64) plus the new glacial golden vector + conserved-ledger golden are thegate;
scripts/compile-check.shPASS is quoted pre-push (rebased-from-worldgen-reliefcall sites).off
.ci-report/failed.log(v2-golden-arm64job), then pin.Lane
sim
Passes
2
Depends-on
None (bases on
worldgen-reliefHEADc0d7950; independent of the future karst/coastal slices).