Files
2026-09-25 17:02:24 +03:00

69 KiB
Raw Permalink Blame History

Terrain: next steps

A working brief for the next session on the heightmap generator. It exists so a cleared session can pick the work up cold. Terrain.md is still the specification and the decision record; this is only "where it stands, what looks wrong, and what to do about it". Fold anything settled back into Terrain.md and delete the corresponding section here. Do not let the two drift.

Everything below is off the ladder (D-47). No gameplay code may reach into the generator, and nothing here blocks a step in Steps.md.


1. Read this first

The generator is a Go CLI in Tools/Terrain/. It turns a seed and RawContent/World/World.json into a heightmap, by building an uplift rate field and letting a stream-power erosion solve produce the terrain from it. The noise is not the terrain; the noise is the tectonics.

cd Tools/Terrain && go build -o bin/terrain.exe ./cmd/terrain && go test ./...
# a preview run: ~4 minutes at 1400², the iteration loop
Tools/Terrain/bin/terrain.exe generate --size 1400 --out RawContent/World/Try1 --quiet
# another continent
Tools/Terrain/bin/terrain.exe generate --size 1400 --seed 9342 --out RawContent/World/Seed_9342 --quiet

--size keeps the map's physical extent and samples it more coarsely, so metres, uplift rates and the stream-power constants all still mean what they mean. Do not tune on --size 512: the geology cell there is 28 m against 8–10 m at full size, and steady-state slope goes as U/(K*A^m) with A = cell² at every divide, so a 512 preview shows gentler ground than the real run for reasons that have nothing to do with the change being judged.

Every bs.*-style knob in the manifest has a --flag override so an experiment does not need a file edit: --intraplate, --intraplate-swell, --convergent, --k, --diffusion, --talus, --critical-m2, --critical-slope, --slope-cap, --hillslope-substeps, --lithology-types, --fault-scale, --steps, and for the coast --no-coast, --outline-octaves, --outline-gain, --shelf-km, --surf-reach, --cut-fraction, --deposit-reach, --drift, --river-sediment.

The planet, which is the other half of the tool now

A painted template is a different command and a different manifest. RawContent/World/Planet.json points at an image and a legend; RawContent/World/Templates/README.md is how to paint one.

# four seconds: read the painting, cut the planet into regions, solve nothing.
# map_class.png and map_regions.png are the two pictures that decide whether a bake is worth starting.
Tools/Terrain/bin/terrain.exe plan

# about two hours at 100 km round. Run it detached, never under a tool timeout.
Tools/Terrain/bin/terrain.exe bake --out RawContent/World/Bake --jobs 4

# one landmass, short, for tuning the legend's numbers
Tools/Terrain/bin/terrain.exe bake --only 11 --steps 200 --out /tmp/try

# the detail passes over a bake, a batch of tiles at a time. About twelve seconds a 5 km tile.
Tools/Terrain/bin/terrain.exe tiles --bake RawContent/World/Bake --only 11,7,13,8

Every number in this section is pre-D-53 and has not been re-measured. The router jitter moved from a hash of the grid index to a hash of the world position, which changes the square canvas's output everywhere; the summary block below was measured before it. Re-baseline before comparing anything against it.

What a run writes

File What it is for
preview.png Hypsometric tint, hillshade, rivers. "Does this look like a landscape"
preview_detail.png A crop at 2× vertical exaggeration. The whole continent at 1600 px cannot show whether lowlands read as hill country or as small mountains; this can. Move it with --crop-x/-y/-size. It cannot be rendered finer than the grid: a crop of 0.14 at --size 1400 is 196 cells, so that is the image, whatever Size asks for
geology_height.png The 16-bit heightmap itself, encoded to the manifest's elevation range
map_uplift.png The most useful diagnostic. Fault traces are stroked over it in cyan - the line rather than the rate it contributes, because even after D-62 a fault is a few kilometres wide and the map is a hundred kilometres across
map_slope.png Degrees, 0–45
map_relief.png Local relief over 500 m. Separates a 5 m hummock from a 500 m mountainside — both stand at 30° and the slope map cannot tell them apart
map_erodibility.png The lithology multiplier on K. Where texture inside a range comes from - on a painted planet that is each class's k_mult times the planet's rock field, which the seed re-rolls (D-58)
map_exposure.png How open the water is in front of each stretch of shore, 0 sheltered to 1 open. Drawn only within a kilometre of the waterline, because past that it is a map of the continent's medial axis. The one to read when a beach turns up on a headland
map_coast.png Everything the coastal pass moved, in metres: cool where the surf cut, warm where the sediment landed. The sea floor is excluded, or its few hundred metres would swamp the few the processes move
map_flow.png, geology_flow.png Log drainage area: the rivers
map_basins.png One colour per drainage basin, hashed from the basin root. The direct test of whether the solve made a network rather than scratches: basins must tile the land, sizes must span orders of magnitude, and divides must sit on the ridge crests. Confetti means the router is re-deciding where water goes every few cells
map_overlay.png The annotation layer over a dimmed class map, when the planet has one. A mark means nothing on its own and everything against the coastline it was drawn along
overlay.json Every mark's area and piece count, and every feature in world metres: a centre, area, radius and extent per painted blob, an ordered polyline per path. What the engine places things from; nothing in the generator reads it back
meta.json The full manifest as resolved, plus every statistic

How a run is judged

The class table prints two angles now and the second one is the one to read (D-57). divide is U/(K·A^m) at a single cell, which is exact and is the steepest ground a rate can make; typical is the median over the class, measured at a third of it in tangent and flat across a factor of twenty in rate. Almost none of a map is divide. The old single column is why a legend could be set two or three times too hot and still look reasonable on paper.

A planet bake prints this block now (D-59). Until then it printed its elevation range and nothing else, which is why two questions this session - "are the lowlands hilly" and "do faults leave scarps" - had to be answered by hand in Python off a PNG. A partial run (bake --only) marks itself PARTIAL: its extent is the whole cylinder and its ground statistics are only the landmasses that were solved.

The printed summary is the verdict, and the block that matters most is the per-uplift-class breakdown — map-wide medians cannot answer "are the plains plains", which is precisely how the last problem stayed invisible. Current state, seed 7 at 1400²:

  slopes: 54% under 15 deg, 77% under 30, 0.3% over 50, median 11.2 deg
  slope-area: exponent -0.384 (expect -0.500), R2 0.225 over 6 bins
  hypsometric integral 0.107 (concave: over-eroded); drainage density 0.48 /km
  by uplift class:
    plain     0.00..0.10 mm/yr    42% of land  slope  0.8 deg median,  2.8 P90  relief     9 m/490 m  at talus  1%
    rolling   0.10..0.50 mm/yr     7% of land  slope  7.5 deg median, 23.1 P90  relief    85 m/490 m  at talus  1%
    mountain  0.50..  up mm/yr    51% of land  slope 28.5 deg median, 36.0 P90  relief   195 m/490 m  at talus 33%

The coast prints its own block after it, and the numbers to read first are the sediment budget — it is the one part of the pass not derived from something already measured — and the exposure percentiles, which say whether the shoreline has any bays for the shelter to work with:

coast: 95 km of shoreline, 37% sea, shelf 86% of it; surf planed 2.8 km2 and cut 21.23 Mm3,
       16 river mouths delivered 3.87 Mm3, 25.04 Mm3 laid (0% unplaced) as 2.92 km2 of new beach;
       mean cliff 3 m, 0.00 km2 drowned; shore exposure 0.00 / 0.90 / 1.00 (p10/p50/p90)

An unplaced fraction above a few per cent means the sediment has nowhere to go and the deposition gates are wrong; a median exposure of 1.00 means the outline has no bays at all and neither the surf reach nor the shelter is doing any work.

The slope–area fit cannot judge one change on one seed, and it was nearly used to reject a good one. It is the number this document calls the proof that closes the work, and at five or six bins on a 1400 grid its seed-to-seed spread on identical code is larger than most changes: seed 7 gives −0.480 at R² 0.317, seed 9342 gives −0.698 at R² 0.704, and seed 67914 gives −0.575 at R² 0.944. Use it paired — the same seed before and after — and across at least two seeds, or raise the bin count before leaning on it.


2. What was just built, in one paragraph

Painted planets (D-53). The source stops being a seed: an author paints a flat cylindrical world map, a JSON legend beside it says what each colour means in uplift and erodibility, and the simulation makes the terrain. X wraps and Y does not, so a landmass may straddle the seam and comes out whole. The geology is solved one landmass at a time, which is exact rather than approximate because ocean cells are fixed at sea level for the whole run and nothing in the solve can move them, so no flow path crosses open water; the coastal pass and everything else run once over the whole cylinder, because the coast costs 26 ns a cell against 80 ns a cell per step for the solve and cutting it up would truncate the fetch across every strait and split the sediment budget. terrain plan reads the painting and cuts the planet into regions in four seconds, without eroding anything; terrain bake solves it. The router jitter moved to a hash of the world position at the same time, which is rule 1 of the tiling plan and re-baselines §1.

The detail passes came with it: 8 to 12 and 14 are built and tiled, so there is a full-resolution output for the first time — 5 km tiles of 2500 samples at 2 m, twelve seconds each, with a margin measured at three droplet lifetimes rather than reasoned at rounds × lifetime. The droplets had to become a pure function of world position for that to close, which is rule 1 arriving where it was always headed.

Full detail, including the eight things the shape of the work revealed, is in Terrain.md under What was built, and where it differs.

3. Where this is going

Composition is parked. The mountain fraction, the range grain and the fault traces are all real and all still listed below, but they are tuning, and on a painted world two of the three are the author's job now. Do not spend the next session on them. One item that looked like composition was not and is closed: a class was one rate and therefore one landscape, which is §4.A0 and D-55.

The goal is: get generation working end to end at player scale. Three things, in order.

3.1 Finish the planet's own passes

Two things the planet needed that the square canvas did not, and neither was optional for a bake to be judged. Both are closed:

  • The coastal pass has to wrap. Closed (D-60). Four primitives wrap now, the abyss is a field so the derived slope meets the painted ocean depth rather than stepping to it, and Geometry.Ref holds a waterline slot so supply is a few hundred thousand entries instead of 608 MB. Measure holds one distance transform at a time. Measured: 7.9 s over the whole 76 M cell cylinder, and the seam step in the sea floor went from a mean of 9.1 m to 0.32 m, which is what an ordinary interior column is. The pass is now the memory peak of a bake, about 8.3 GB against the solve's 3.6.
  • internal/stats does not survive 28 M land cells. Closed (D-59). Fixed-bin histograms replaced every sort and field.LocalRelief replaced the O(radius²) window, but the change that mattered was not either of those: a histogram adds, so a planet's statistics are now pooled from its regions rather than computed on a grid that never exists. Each region accumulates while its own grid is alive and they merge in region order; the extent is measured once on the composited cylinder, because regions carry overlapping ocean margins and pooling their cell counts would double-count the water between them. Measured: 1.09 s for a 9 M cell region, about 120 ns a cell, so a whole planet is a few seconds at the end of a two-hour bake. A bake prints the full block now, and a partial one says so rather than letting the whole world's extent be compared with three islands' worth of ground.

And a third, smaller, which is both a cost and a correctness wart:

  • DiffuseNonlinear bounds its sub-step count with the steepest slope on the whole grid (hillslope.go:62). That is where the five-fold cost of mountains comes from and most of it is honest work — the sub-steps buy stability, and truncating them checkerboards the surface a few hundred steps later. But the bound is a global statistic of the grid it is given, so the steepest cell anywhere in a region sets the diffusion for every plain in it, and two different decompositions of the same world would differ slightly. It is the one place where the per-landmass split leaks into the answer, which is why the margin and the minimum landmass size are in the manifest and in meta.json. A per-band bound would close it and would be cheaper; whether it changes anything visible has not been measured.

3.1a The painting has a tool now

terrain studio (D-56) is the loop for everything in this section that is authoring rather than physics: brushes that carry the legend's numbers, a seam-aware canvas, and plan as a button. It has two sheets now (D-57): classes is the geology and overlay is the annotation layer, whose brushes are its marks and whose coast_jitter marks are the only thing on it any pass reads. The two measurements below - the 9.4 % seam disagreement and the JPEG halo - are both things it exists to stop happening again, and the first is a thing it can fix by painting.

Plan and Bake apply the panel before they run. They always pushed both paintings first and never the numbers beside them, and since both read the legend and the manifest off disk, an edit still sitting in the rail was an edit the prepare never saw. A re-rolled seed was the case that showed it: the plan key did not change, the cached prepare came back, and the uplift and erodibility maps were identical - which looks exactly like a generator that ignores its seed. Both buttons now flush the planet block, the class legend and the overlay legend, in that order, and the report says what it applied; a unsaved: note under the Plan bar says what is pending before you press anything. Bake also pushes the paintings, which it never did: it solves the server's copy, so a stroke made since the last plan was two hours of answering the wrong question.

The region map is numbered. Region hues came from a hash of the index, and independent hues collide - the closest pair of the first twenty was 8.5 apart in RGB, which nobody can distinguish on a map whose whole job is "is that one landmass or two". They walk by the golden angle now, with saturation and value on a 3 and 2 cycle: 44.0 at twenty regions, 41.9 at twenty-six, 37.7 at forty. Colour alone still cannot carry forty regions, so planet.RegionLabels returns the centroid of each region's land and the studio writes the id over the map in screen-space text - crisp at any zoom, and tiled across the seam like everything else. The mean across is circular, because a landmass at x = 0 and x = W-1 has an arithmetic mean on the far side of the planet. These are the ids bake --only takes.

A finished bake lets go of the screen. Its status and stamp outlive the run, so the first poll of every page load re-opened the preview of a bake that had ended hours ago - over the painting, blocking the brush, and reloading the page put it straight back. A load that finds nothing running now adopts the stamp instead of drawing it; a bake that finishes while somebody watches still lands its final preview. Every map view also has a visible way out now rather than only Escape, and the bake block has a Preview button to bring the last one back.

3.1b The first template does not wrap, and that is the input rather than the tool

Measured by terrain plan on Map3.jpg: the left and right edges, which are the same meridian, disagree on 9.4 % of rows, 261 of them land against water. The crater island crosses the seam perfectly — heights run continuously from the last geology column into the first — but islets drawn touching x = 0 have nothing to meet them at x = W-1, so the world has a 400 m cliff down the seam wherever that happens. There is also a two-pixel JPEG halo on the outermost columns which classifies as shelf, putting a 400 m ledge the height of the map down the same line.

The tool is right and the painting is not, so nothing here is a defect to fix in code. What was added is the measurement, because it is the one defect an author cannot see by looking at their own picture: the two edges are as far apart on screen as they can be. The fix is to paint round the edge and export PNG.

3.2 The coastal detail, which is the last pass with nothing built

Passes 8 to 12 and 14 are built (see Terrain.md), so there is a full-resolution output: 5 km tiles of 2500 samples at 2 m, about twelve seconds each. What is missing at player scale is now only the shore, and it is no longer blocked: internal/coast wraps (D-60), so there is a shelf, a shore platform and a beach for a detail pass to refine. Section 4.E3 is still the shape of it - the surf reach is 110 m, which is 55 detail cells, enough for a real berm, a wave-cut notch and a scree apron below a cliff - and the tile bake is where it goes.

Two smaller gaps in pass 14: the weightmaps are not derived (nothing imports them yet), and pass 13, the spawn pad, is deliberately skipped because a planet has no single centre.

3.3 What is left of tiling

Both halves are built. The geology solve is decomposed per landmass and everything else runs whole (D-53); the detail passes tile, with a margin measured rather than reasoned - three droplet lifetimes plus the brush, which is 122 detail cells at the defaults, about five per cent of a 5 km tile on each side. Rule 1 is done for the router, for the painted path and for every detail pass; the one place still on map-relative coordinates is the square canvas's own uplift.Build, and that is deliberate, because there the noise is the continent.

How big can a world be, now. The binding number is no longer the planet but its largest landmass, because the solve is per landmass. Measured on the 100 km template: 18 regions, 49 M cells of 76 M, the largest 14 M at under a gigabyte, and the detail another forty minutes for all 200 tiles, fully batchable. A 200 km world with landmasses of the same shape is four times that; the case to watch is one landmass four times as wide, because that single region is the peak.

The wall time is set by the largest single region, and that region runs at about one core. Measured on the second bake of the 100 km template: seventeen regions finished in 11 586 s of solve with four in flight, and the eighteenth - the 14 M cell central lowland - then ran alone for over 90 minutes at 1.0 to 1.1 cores. That is not a defect, it is the shape of the solver: Terrain.md records that most of the runtime is the stack walk and the priority-flood's cursor, and neither parallelises within one grid. Running regions concurrently hides it while there are several left and hides nothing at the end.

Two consequences. The --jobs throughput number is not the wall time: a template whose land is one big landmass gets almost no benefit from it. And parallelising the stack update by basin - which Terrain.md already lists as option 2 for the time budget, and which is where the cores would actually go - has moved from "a real gain, bounded" to the only thing that would shorten a bake of this shape. Disjoint basins are independent; only the walk within one is sequential.

And the cost per cell depends on the uplift rate, by a factor of eighteen. Measured on the same bake at 1000 steps with four regions in flight:

class rate cells time per million cells
lowland 0.08 mm/yr 14.0 M 1014 s 72 s
highland 0.90 mm/yr 4.0 M 1394 s 350 s
crater 1.60 mm/yr 1.4 M 1829 s 1278 s

It is not the stream power; it is the hillslope. DiffuseNonlinear sub-steps to stay stable, the count rises with the steepest slope on the grid, and it saturates at max_hillslope_substeps — 24 by default. Steep ground pays all 24 every step and a plain pays one. Three consequences: terrain plan's estimate is calibrated on the plains and is a floor; raising an uplift_mm_yr changes the bake time as well as the terrain; and the wall time is set by the single slowest region, not the total, so one small steep landmass can be the whole tail.

4. What looks wrong now

Ordered by how much it matters to the direction above, which is not the order of how visible it is on a preview image.

C. Detail — built, and what it left behind

Nothing exists at player scale — closed. Passes 8 to 12 and 14 are built and tiled; see Terrain.md for what the port cost and the four things that were wrong on the way. What is left of this entry is three narrower items, none of them a blocker:

  • The shore is still a step. Closed (D-60): the coastal pass wraps, so a planet has a shelf, a shore platform and a beach for the detail passes to refine. §4.E3 is now unblocked and is the next thing. D-56's coast mask is a different thing and does not close this: it decides where the waterline is, at the paint's own resolution, before anything is solved. internal/coast is what puts a shelf and a shore platform under it, and that still has to wrap.
  • The weightmaps of pass 14 are not derived. The rules are in the numpy and they are ported unchanged when something imports them; flow, wear and deposit already come out per tile, which is what those rules read.
  • Nothing has been judged on the ground. The tiles look right in a hillshade and the seam is measured, but the question the whole pipeline exists to answer — does this read as ground to somebody standing on it — has not been asked, because nothing imports a tile yet.

A0b. What the lowlands are actually doing — measured, and mostly the author's numbers

Raised again as "the lowlands still by default become super hilly". Measured at last, on a 600² grid of 8 m cells with the manifest's own constants at 1000 steps and a uniform rate:

U mm/yr divide median P90 over 3° max elevation
0.012 1.7° 0.58° 1.00° 4 % 32 m
0.045 6.4° 2.12° 2.85° 8 % 38 m
0.080 11.3° 3.72° 4.85° 75 % 54 m
0.250 32.0° 11.13° 14.03° 99 % 143 m

Three things follow and the first is the answer to the complaint.

The massif floor already is a plain, and the real continent confirms it. Region 12 baked whole — 45.9 × 19.8 km, 9.0 M land cells, 1000 steps, 27 minutes — comes out 0..47 m with a slope distribution of p50 0.61°, p90 1.22°, 4.4 % over three degrees and nothing at all over eight. The controlled run at a uniform 0.012 mm/yr gives 0.58° and 4 %, so the two agree. There is no missing process here and no fine dissection to remove; D-55 did what it said.

What made it look hilly was preview.png. The hypsometric ramp's top is a percentile of the world being drawn, so the whole ramp — green, tan, rock, snow — was stretched over this continent's 32 m, and its 40 m hills came out with the white caps a 2800 m range would get. Redrawn against a fixed 400 m ceiling the same heightmap is a flat green plain with four pale massifs on it. Closed: palette.land_top_m is an absolute ceiling, and every run now prints which ceiling its preview was drawn against. The percentile stays the default, because an absolute ramp over a world with no mountains is a green shape with nothing legible on it.

What makes a painted lowland read as hill country is its massif share and the rate the massifs reach. At fraction 0.16 the ramp opens at the 76th percentile of the planet, so about a quarter of the class is off the floor, and the class rate it climbs to — 0.08 mm/yr — is a 3.7° median, which is continuous rolling ground. Both numbers are the author's. The lever for "more flat ground" is a smaller fraction; the lever for "gentler hills where they are" is a lower uplift_mm_yr.

And the number they were steering by was wrong by a factor of three, which is D-57 and is closed: the table printed the divide angle, which is the steepest place in a catchment, as if it were the landscape.

What is not closed, and is the real version of "lowlands should not consider mountainous erosion": every cell of the world runs one process with one diffusivity, one critical slope and one channel threshold, and the only per-class levers are U and k_mult. At a fixed cell those two set relief and steepness together (§4.B0), so "flat but with real relief" is not expressible. The principled fix is the pairing in §4.D.3 — a critical area with a hillslope diffusivity to match — and the thing that makes it newly plausible is that it could be per class: §6 rejects it because D large enough to shed the uplift "smooths away every landform", which is a fatal objection on a mountain and a description of a plain. Cost is the obstacle, not principle: DiffuseNonlinear sub-steps on D·dt/dx², so D 0.3 on a lowland region is 36 sub-steps against 3, and the 14 M cell region that already takes an hour and a half would take most of a day. Implicit diffusion, or a per-band sub-step bound, is what would make it affordable. Not started, and not to be started without measuring the sub-step cost first.

D. Scale-independence — the one that becomes load-bearing

D.3 Relief is resolution-dependent, and multi-scale makes that a blocker rather than a wart. Measured earlier: 1020 m relief at 512² against 2605 m at 1786² on one seed. The cause is that with critical_area_m2 at 0, stream power is applied down to a single cell, so the divide slope is U/(K·cell^2m) — halve the cell and every divide steepens, for ever. As long as there was one canvas this was a wart. The moment the same painted map is meant to produce worlds at different sizes or resolutions, it means the same input gives a different landscape depending on grid size, which is fatal to the whole idea.

The textbook fix is a critical area, and it was re-measured this session and still fails (§6) because the hillslope it creates has no transport law strong enough to shed its uplift. So this is genuinely open, and the two candidate directions are:

  • Fix the pairing: critical area plus a hillslope diffusivity that scales with cell size (effective D grows roughly as cell², which is the standard sub-grid argument). Principled, and it makes the critical area work rather than fail.
  • Sidestep it: always solve the geology at one fixed physical cell size and never vary it, per §3.3. Free, correct by construction, and it costs the ability to trade resolution for runtime on a big world.

The second is the recommendation for now because it is free and unblocks everything; the first is what to build if the bake times in §3.3 turn out to be unacceptable.

D.4 Two open questions that are design, not defects. The hypsometric integral is 0.10 against 0.4–0.6 for a mature landscape — no longer a bimodal uplift field, now simply that 42 % of the land is a near-sea-level plain, which is a question for the continent block and will move when composition is tuned. And drainage density is 0.41–0.48 /km, right on the 0.5 floor set as do-not-cross; it is measured at a 1 km² channel threshold so it is not directly comparable to a field value, but if it drops further while tuning, raise the intraplate rate to 0.05 before touching anything else.

E. The coast — what it built, and the four things it cannot do yet

E1. There are no sea cliffs — withdrawn, and the metric that said so is replaced. This entry read "mean cliff height is 2 to 3 m, and the surf has nothing to cut". Both halves were wrong, and the way they were wrong is the part worth keeping: the statistic measured the drop from a cell to its seaward neighbour, which is a gradient. One cell of a 10 m grid at the angle of repose is 7 m, so the number could never have exceeded 7 whatever the coast did — it read 2 m on a plain coast and 3 m on a cliffed one because it could not tell them apart. It is now backshore height: the land's elevation one to two surf reaches inland, median and P90. On that metric the coast has always had cliffs (seed 7 P90 88 m, seed 9342 108 m, seed 67914 120 m); the median, 3 to 9 m, says the ordinary coast is a plain, which it should.

What the mask taper was actually doing is narrower and is now fixed anyway (D-52): it flattened the ~100 m strip the surf works in, so the cliff began a hundred metres inland instead of at the water. Removing it is worth 35 to 67 % more surf cut and a visibly steeper shore, and the principle stands — where the land ends does not decide how fast it is rising — but it was a sharpening, not the transformation this entry predicted.

E1b. The map margin draws one coastline in seven, and it draws it straight — on the square canvas only. Measured on three seeds: 14.1 %, 15.0 % and 14.0 % of the waterline sits inside the 4 % margin band that continentMask imposes to keep land off the map border. The margin tapers by distance-to-edge, and a contour of distance-to-edge is a line parallel to that edge, so wherever the continent would have run past the boundary it is cut off square. This is pre-existing and it is not the frozen-rim failure the margin exists to prevent — TestBorderIsAlwaysOcean confirms every border cell is still ocean on all three seeds, so nothing is frozen. It is cosmetic, and D-52 made it conspicuous: land inside the band now takes the full 2.0 mm/yr instead of a tapered rate, so the straight-cut coast can be a mountain range rather than a low plain, which is exactly what seed 67914's southern coast is.

On a painted planet this is closed rather than deferred (D-53): there is no continentMask and no map border, because the outline is the paint and a region's edges are open ocean by construction. The cheapest fix for the square canvas is still the same one — perturb the margin distance with a low-amplitude noise field so the cut follows a crenellated line instead of a ruled one — and it now has one fewer consumer.

E2. The shelter contrast is real but thin. Exposure comes out 0.00 / 0.90 / 1.00 at p10 / p50 / p90, so the distribution is one long tail: a handful of genuine embayments and a lot of open coast. A floor of 0.15 on shelter carries most of the deposition, which is deliberate — measured with no floor, 73 % of the sediment budget came back unplaced, because real exposed coasts do have beaches, they just have less sand than the bay next door. Re-measure this once the outline is painted rather than noised; it is the same question as E1 from the other end.

First painted reading (D-60), and it is not yet the answer: a partial planet bake came out 0.96 / 1.00 / 1.00, which is not a thinner contrast than the square canvas had but a flatter world - the run solved one landmass, so most of the painted coast was still unsolved ground sitting at sea level with nothing behind it to shelter anything. Exposure is measured on the waterline and the shape of the land behind the shore is half of what sets it. Read this off a whole bake before touching the floor.

E3. The beach is a beach at 8 m, which is to say it is not one. The surf reach is 110 m, or 14 cells on the geology grid, and the berm, the wave-cut notch, the scree below a cliff and the sand itself are all finer than that. They belong in the detail passes (§4.C) — and note that the surf reach is one of the few lengths in the generator that is set by physics rather than by the canvas, so it does not scale with the map: at the 2 m detail grid it is 55 cells, which is enough for a real profile. The coastal detail pass is a natural addition to the list in §4.C rather than a separate piece of work.

E4. Deltas are placed, not tuned. river_m3_per_km2 is 1.2e5 with an exponent of 0.6, and both are guesses; on seed 7 they put 3.9 Mm³ through 16 mouths against 21 Mm³ from the cliffs. The mechanism is right — the supply concentrates at the mouth and the drift kernel spreads it into the shallows — but nothing has yet asked whether what comes out reads as a delta. map_coast.png at a crop is where that is judged.

E5. Unverified: speckle at the finer canvas. The outline gain went from 0.50 to 0.62 on measurements taken at --size 1400 (10.2 m cells). A finer grid resolves more of the threshold's wander, so the D-48 canvas at 8 m, and any painted map after it, could turn the same setting into a scatter of one-cell islands. Check the shoreline length per unit land area and look at preview.png before assuming it carries over.

E6. The shelf break is fixed, and the whole-planet measurement is owed (D-64). The near-shore sea was 30 m deep everywhere because BreakM came from pipeline.continent.sea_floor_m, a square-canvas default; it is pipeline.coast.break_m now and a planet gets 130 m. The mechanism is measured at unit scale — a painted 512 m sea goes from 33 % to 8 % shallower than 50 m — but the re-bake that would give the planet numbers was killed by memory pressure before it wrote anything. What to run, and the numbers to put beside Bake_020's:

Tools/Terrain/bin/terrain.exe bake --out RawContent/World/Bake_D64 --jobs 4   # detached; ~13 min, peaks near 8.3 GB

Read back the depth histogram of planet_height_low.png against Bake_020's, which was -520 m 11.7 %, -20 m 26.8 %, 0 m 23.6 %. What should have changed: the −20 m spike disperses across 0…−130 m, the share deeper than 400 m rises towards the 55.9 % the legend paints, and preview.png gains a bathymetric gradient where it had one flat mid-blue halo — the preview ramps sea_shallow→sea_deep linearly over the deepest sea, so 20 m out of 520 was the first colour and nothing else. --jobs 2 if memory is tight.

E7. elevation_m is three times wider than any world that has been baked, and it is the author's key. −1024…2048 against data of −521…+340: the planet uses 28 % of its 16-bit ramp and its land 7 %, which is most of why the exported heightmap reads as a flat grey picture with no coastline in it. A bake prints this now. It is not a defect — headroom is a legitimate choice and D-64 deliberately did not touch Planet.json — but about −576…320 is 3.4× the contrast and 3.4× the vertical resolution for the compositions built so far. The trap if it is changed: tiles decodes planet_height.png through the current manifest, so a bake made under one range and tiled under another is silently wrong by the difference. Re-bake, or do not change it.

B. Texture — wrong at mid scale

B0. The clamp ceiling has a number now, and a painted legend walks straight into it. Steady state is S = U/(K·A^m) applied down to a single cell, so at a divide A is one cell squared and A^m is the cell size. Setting that equal to the angle of repose gives the rate above which the clamp does all the shaping:

U_max = tan(talus) · K · cell — at 35°, K 5e-5 and an 8 m cell, 0.280 mm/yr.

The first painted legend put highland at 0.9 mm/yr, which is 66° at a divide, 3.2 times over. Baked, the geology comes out as flat polygonal faces with hard 45- and 90-degree edges - the D8 clamp, visible at a glance once the detail passes are stripped off with terrain tiles --no-detail. Not 33 % of the class shaped by the clamp: all of it.

terrain plan now prints the implied divide angle for every land class and says which are clamped, which is four seconds against an hour and a half. The deeper point is the one §4.D.3 is about: at a fixed cell, relief and steepness are the same knob. U/K sets both, so there is no setting that gives a 700 m range with hillslopes below repose - 0.28 mm/yr on a 20 km island is about 350 m. Getting more relief than that out of erosion-shaped ground needs the channelization threshold to work, which is exactly the open problem in §4.D.3 and §6.

B1. 33 % of the mountain class still sits within 2° of the repose angle, so a third of the mountains are shaped by the clamp rather than by erosion. Down from 44 %, and the nonlinear diffusion pass after the clamp keeps it from showing as hard facets. Levers, most principled first: raise max_hillslope_substeps and slope_cap (costs runtime, buys real stiffening); raise diffusion_m2_yr (cheap, but past ~0.05 it smooths away the landforms — measured before as "melted wax"); or accept it, since a belt rising at 2 mm/yr genuinely is landslide-dominated in the real world and the clamp is the right model there.

B2. Multiple-flow-direction accumulation is unbuilt - built (D-65), and it turned out to be B3's cause rather than a refinement of the plains. This entry read "some basin boundaries on the plains in map_basins.png are visibly straight" and treated MFD as tidying. It is not: on a planar hillslope the correct specific catchment area is the same at every point along a contour, and D8 cannot say so - every cell picks the same steepest neighbour, the flow lines run exactly parallel and never converge, and a cell either sits on a line and carries the whole tube or sits off one and carries a single cell for ever. Measured on a ramp at an aspect of 22.5 degrees with no erosion at all, one fill and one accumulate (internal/fluvial/flow_test.go): the most-drained cell in a contour band carried 769 times the median and 29.5 % of the grid drained nothing. At an MFD exponent of one the same numbers are 1.34 and 0.4 %.

Built as planned - Freeman/Quinn/Holmgren partition for Accumulate only, D8 receivers kept for the implicit update, because Braun-Willett walks one receiver chain and has no unconditionally stable multi-receiver form. Two things this entry had wrong. The processing order is not descending elevation: Kahn's algorithm over the flow graph is exact, O(n), and needs no elevation comparison at all - count each cell's strictly higher neighbours, release on zero. A bucket sort by elevation would have been worse than useless, because the queue quantises to a centimetre while the flood's epsilon ladder across a filled flat is a millimetre a cell, so ten cells of one descending chain share a bucket and every lake bed would leak its area. And float32 is enough: a cell's accumulator takes at most eight contributions, each already an aggregate, so the drift is a random walk over the flow path and measures 2.4e-9 relative over a closed basin.

The bake-scale verification is owed. Everything above is measured on the router in isolation and on the square canvas. The comparison that matters - region 8 of Planet.json, the streaked left continent, the same painting and seed - has a D8 baseline in RawContent/World/R8_d8 (15m40s, 922 s of solve, land relief 195 m, slope-area exponent -0.509 at R² 0.966, drainage density 0.43 /km, and 31 sources to 13 confluences in the 12.8 km window at 1391,2625) and no MFD twin: that run was killed by memory pressure about 70 % through and wrote nothing. Until it exists, what is established is the mechanism, not the cure - and one result argues for caution rather than optimism, which is that after three hundred steps of solving a planar ramp the leaf fraction converges (D8 8.2 %, MFD 8.9 %), because a dissected landscape's own divides dominate that count. Run:

Tools/Terrain/bin/terrain.exe bake --only 8 --steps 1000 --jobs 1 --out RawContent/World/R8_all   # detached, ~20 min

and read the hillshade of the same window against R8_d8's first, the sources-per-confluence second, and the slope-area exponent third - it should move towards -0.5, which is the number that says the fix is physics rather than a smoother.

The cost is real and it is the argument against, if there is one: 101 ns a cell against D8's 17, measured on a 1024² ramp, and the walk is serial where ComputeReceivers and the hillslope law are not, so it lands on wall clock rather than on cores. pipeline.fluvial.mfd_exponent is 1 by default and 0 is the old behaviour, so the A/B is one flag. secondsPerCellStep in internal/planet/planet.go is still the D8 number, so terrain plan's estimate now reads low; recalibrate it from the first full MFD bake rather than guessing, and note that its comment already says it is a floor.

B3. A ribbed, combed texture on the range flanks - the diagnosis in this entry was wrong, and the cause is B2 (D-65). It read "the ridged-noise initial relief showing through where the solve has not had time to overwrite it", and the reasoning that ruled out the alternative does not hold: it dismissed grid locking because "the ribs are oblique, not axis-aligned", but D8's parallel-flow grooves run in whatever direction the slope faces, so obliqueness is the expected appearance and not a counter-indication.

What the grooves are, measured on Bake_x4 and Bake_020 - the same painting and seed at 32 m and at 8 m, both 12500 x 6076, so the same window in cells compares directly:

  • they are in map_flow.png as parallel high-accumulation lines, so they are channels, not surface texture, and they are absent from map_uplift.png;
  • the network re-derived from planet_height.png is pinnate - ruler-straight parallel trunks with short barbs joining at a near-constant angle - not dendritic. In a 12.8 km window at a 1 km² channel threshold: 25 sources and 0 confluences at x4, 30 and 11 at 100 km, against about one source per confluence for a dendritic network; the largest catchment in a 164 km² window is 4.07 km²;
  • the D8 receiver histogram over that window is anisotropic: 17.3 % on one diagonal, 14.3 % on its opposite, the other six 10.7-12.4 %, against 12.5 % uniform;
  • and the pitch is the same 18 cells in both bakes. That is the decisive one. Every physical candidate - the ridged fBm this entry blamed (250-300 m), the massif fabric, the fault grain - is fixed in metres and would change its pitch in cells by four. Only a grid-scale mechanism survives it.

The spacing is set by the ±0.05 % tie-break jitter at fluvial.go, which is a static field - the same hash at step 1 and at step 1000 - so the rare merges it allows are re-carved a thousand times instead of averaged out. The ridged fBm may still contribute; it cannot make grooves that are strictly downslope, strictly parallel, and visible in the flow map.

B4. ClampToRepose left grid-aligned facets, and bucketPQ broke ties in raster order - fixed (D-65), and the fix is worth less than it looks.

The entry as written, which is still the right description of the mechanism: Checked while chasing B3 and ruled out as its cause - B3's ribs are oblique - but real and worth not re-deriving. internal/fluvial/repose.go pushes every cell in flat-index order and lowers neighbours in place, so which neighbour gets cut is decided by pop order; bucketpq.go pops LIFO within a 1 cm bucket, so ground flat to within a centimetre propagates consistently along −X within a row. hillslope.go's own comment admits the signature: "pyramids with faces aligned to the grid - the blocky, ruler-cut facets". The designed mitigation is DiffuseNonlinear running after it, which bails entirely at diffusion_m2_yr 0. Visible today only as a fine chevron texture inside B3's ribs. The related hazard - bucketPQ collapsing everything above SetElevationRange's ceiling into one bucket processed in strict reverse row-major - is not firing: ClipFrac is 0 in every region of every bake measured.

What the fix did, and what it did not. The clamp now jitters both its pop order and its allowance with the same world-keyed hash ComputeReceivers uses, through a pushJittered that scatters a cell over sixteen buckets rather than one. Half a bucket was tried first and is not enough - it splits a tie across two buckets and halves the correlation instead of removing it. Sixteen is safe for a reason worth keeping: the clamp's order can only matter between two cells whose heights differ by about the talus allowance, which is metres, so reordering cells that are centimetres apart cannot break a constraint that only bites metres apart. The bound is talus*cell/2, 2.8 m at 35 degrees on an 8 m cell. Measured: the pop order's rank correlation with the flat index went from -1.000 to -0.025.

But the isotropy test built for it (TestClampToReposeIsIsotropic) reads identical with the jitter, without it, and with either half alone - 0.97 % four-fold and 2.39 % eight-fold on a clamped cone. On a cone no two cells share a bucket, because the surface falls twenty metres a cell against a one-centimetre bucket, so the ordering bias has nothing to bite on. The residual octagon is geometry, not order: a path to a point at 22.5 degrees is built of cardinal and diagonal steps and the octile distance it accumulates exceeds the straight line by up to 8 %, so an eight-connected clamp cuts an octagon out of a cone whatever order it works in. That is irreducible without a wider neighbourhood. And the clamp is a small actor anyway - near_talus_fraction is 0.32 % of mountain cells - so this was housekeeping, not the fix.

B5. The hillslope smoother transported across four faces while the clamp cut across eight - fixed (D-65). Run's design is that the clamp cuts and DiffuseNonlinear rounds off what it cut before the next step sees it, and a five-point stencil cannot transport across a diagonal face at all, so a diagonally-cut facet was left standing by construction. That was a hole in the stated design rather than a refinement of it. The stencil is nine-point now, weights 4/6 cardinal and 1/6 diagonal - the isotropic nine-point Laplacian, which on h = (a/2)(x²+y²) gives (1/6)(8ad² + 4ad²) = 2ad², exactly what the five-point gave, so coeff is unchanged. A diagonal face carries its own critical height difference, sc*dx*sqrt(2), or every diagonal would read as 1.41 times its true S/Sc. Stability improves and pays for the extra faces: the checkerboard amplification goes from 1 - 8*coeff to 1 - 5.333*coeff, so the limit moves 0.25 -> 0.375 and the sub-step target moves 0.2 -> 0.3 at the same 1.25x margin. Making either change without the other is a scheme that checkerboards a few hundred steps in, which is why they are one commit and why the stability test now runs 2000 steps rather than 500.

B6. There is an edge-preserving smooth now, and it is off. field.SmoothEdgePreserving, ported from the World Orogen browser generator, which has one for exactly this reason - to blend its own routing artefacts without rounding the landforms off with them. w = 1/(1 + |dh|/(d*slopeRef)) over eight neighbours, land only, waterline locked, run once after the solve and never inside the step loop: it conserves nothing and has no time in it, so per-step it would act as an uncontrolled extra diffusivity, and that moves the steady-state slope, which is U/K, which is the one knob the generator's relief hangs on. The deviation from the reference is units: a sensitivity in 1/m is a height threshold and means something four times as aggressive on an 8 m cell as on a 32 m one, which is exactly what section 4.D says the generator lives or dies by, so it is a slope. Measured on a synthetic: 77 % of a 4 m ripple removed, 98 % of a 300 m cliff kept. pipeline.smooth.passes is 0 by default - turning it on is a decision to hide something rather than fix it, so it is a decision somebody makes in a file - and a run with it on has to match a run with it off on the slope-area exponent, the drainage density and the per-class median slopes, or it is shaping terrain rather than polishing it.

And measured against those gates it fails, which is the point of having them. On the square canvas at --size 500, 300 steps: two passes at slope_ref 0.3 take the slope-area exponent from -1.02 to +0.36 with the fit collapsing from R² 0.92 to 0.36, the channel cells from 772 to 566, and the mountain class's median slope from 15.3° to 11.6°. Backing off does not rescue it - one pass at 0.02, which is a one-degree reference, still lands at +0.16 and 14.4°. Drainage density is the one thing that does not move (0.43 /km throughout). So this is not a free polish at any setting: it is a filter, it changes the slope-area relation the solve exists to produce, and what it is for is somebody deciding in a file that they want the look more than they want the statistic. It is not a substitute for B2, and it was not turned on to get B2's result.

A. Composition — parked, but recorded

A0. One class is one landscape — closed (D-55), and it was not a tuning item at all. This entry used to be absent and the defect it names is the one a person spotted by looking at the bake: every landmass came out uniformly dissected, coast to summit, with no flat ground on any of them. n is 1, a class was one rate, and D-49 says the rate alone fixes the hillslope angle — so one painted colour was one landscape, at whatever angle its rate named, over every cell of it. A class now carries massif: {floor_mm_yr, fraction} and cuts one planet-wide upland fabric, so a painted lowland is a plain with hill masses standing out of it. See Terrain.md for the threshold problem, which is the part with a wrong answer available: a percentile of the region would have made two regions disagree along every boundary.

Two numbers from it worth keeping here. The fabric wavelength has to sit well below a landmass — 12.5 km against islands of 20–45 km put one island entirely above the cut, which is this same defect one size down; 7 km is what the current template uses. And internal/stats' "plain below 0.1 mm/yr" is a reporting bucket, not a description of terrain: 0.1 mm/yr is a fourteen-degree hillslope, and reading that line as guidance is how the legend's plains were set ten times too hot. The buckets are unchanged — they are an axis with a run of measured numbers behind them — but terrain plan now prints what a class reads as from its angle, which is the number an author is really choosing.

The three below are one-or-two-constant changes on the procedural path. They are listed so they are not rediscovered, not because they are next.

A1. Half the continent is mountain — 51 % of land on seed 7, 44 % on seed 9342, against nothing like that in reality. The cause is arithmetic:

// internal/uplift/uplift.go
rangeMask := percentileMask(band, cfg.Plates.LowUpliftFraction.Hi()*100, 86)  // ramps 40th → 86th percentile
r := base + (convergent-base)*float64(rangeMask.Data[i])

With base ≈ 0.055 and convergent 2.0 a cell clears the 0.5 mm/yr mountain threshold at rangeMask 0.229, which the smoothstep reaches at the 54th percentile — so 46 % of the map is mountain-class by construction, matching the 44–51 % measured on land. Computed options:

percentile ramp exponent on the mask mountain class, % of map
40 → 86 1 — today 46 %
40 → 86 2 38 %
40 → 86 3 34 %
60 → 92 2 24 %
70 → 92 2 19 %
75 → 95 2 15 %

Squaring the mask is not the same as narrowing the ramp: the foreland stays continuous, which is the property the percentile ramp exists to provide. Note also that the manifest key low_uplift_fraction encodes the spec's "20–40 % of the map at low uplift" constraint, which is satisfied trivially and always has been; the constraint that actually binds is what fraction is high uplift, and nothing names it.

A2a. A fault is a welt with a cliff down the middle - closed (D-62). The cross-fault profile put the whole throw either side of the trace one cell apart (89 degrees) inside a 600 m flank - narrower than the 1.1 km hillslope the drainage density implies, so nothing could dissect it and the uplift profile printed onto the surface as a smooth ruled ridge. Continuous, kilometres wide and bell-tapered along strike now; see Terrain.md. The general lesson is in section 6.

A2. Fault traces are drawn curves with stamped ends - closed on the painted path (D-58), still open on the procedural one. The painted implementation is a separate file written against this list rather than a port of the code below: a walked heading-perturbed trace, a throw tapered over the last sixth at each tip, en-echelon segments past twelve kilometres, and no clamp to a fraction of a global rate. The procedural buildFaults is untouched and still has all four. What follows is that list, kept because it is what the new one was written against.

The original entry, against the procedural buildFaults: visible in map_uplift.png as straight-edged polygonal facets and an abrupt cut across a summit. Four causes, all in buildFaults: the trace is a single 8-point parabola (const segs = 8, one wander bow); signedDistance over 8 straight segments gives a piecewise-linear distance field, hence polygonal contours; beyond the last segment inside is false and the influence stops dead; and if r > convergent*1.6 flattens the strongest throws into plateaus. Fix: an fBm-perturbed heading, ThrowM tapered to zero over the last ~15 % of length instead of cut at the tip, and long faults broken into 2–3 overlapping en-echelon segments.

A3. Range grain runs as straight parallel bands - closed on the painted path (D-58). The painted fault set takes its strike from a grain field sampled as a vector through atan2, so traces are sub-parallel within a province and the set swings across the world. Sampled as an angle it would have been worse than one global angle: a value lattice runs 0..1 and jumps a whole turn along its own wrap. The procedural path below is unchanged.

The original entry, against the procedural path: chains run NW–SE like corduroy on seed 9342. bv.Data[i] = float32(0.5 + across*2.2 + float64(wy.Data[i]-0.5)*0.32) stretches the band 2.2× along one angle with a single mild warp octave. Raise the warp, or warp with two octaves at different scales so chains bend and bifurcate.


5. Suggested order

  1. Make internal/coast wrap. Done (D-60). The mass-balance tests still pass unchanged on a flat grid and have cylinder twins.
  2. Fix internal/stats for planet scale. Done (D-59). A bake prints the whole block, pooled from its regions.
  3. Then the coastal detail (§3.2, §4.E3), which by then has a shelf and a shore platform to refine and is where a player will stand first.
  4. Then composition (§4.A), which by then can be judged against a real painted world rather than against noise — and on a painted world two thirds of it is the author's job, not the generator's.

Build-order steps 1 and 2 in Terrain.md — the editor viewport and the Generated edit layer — remain open, remain first in that list, and are worth doing whatever happens here: a generator whose output cannot be seen in the editor cannot be iterated on, and sculpting that does not survive a rerun makes the whole tool one-shot. They matter more under this direction, not less, because an authored world is one somebody will want to touch up by hand.

6. Do not redo these

Each was measured, not guessed.

  • critical_area_m2 above 0, on its own. Re-measured after the uplift fix and it still fails: 1e4 sends the plains back to 7.0°, pins 49 % of the rolling class and 79 % of the mountains against the clamp, and collapses the slope-area fit to R² 0.001. The hillslope it creates has to shed its uplift by diffusion and at D 0.02 it cannot. It is not a tuning knob — it needs a transport law to pair with. See §4.D.3, where it comes back as a real candidate for a real reason.
  • Running the repose clamp only once at the end. A thousand steps of growth arrive together, it cuts deeply, and nothing runs afterwards to soften it. The grid facets came back in the summits.
  • fill_every above 1. The 50 was written to protect the time budget and it silently destroys the solve.
  • Raising the intraplate uplift rate to give the plains relief. That is the mistake this round undid. For n = 1 the uplift rate alone fixes the hillslope angle; U sets how high summits get, not how steep ground is. If plains need texture it comes from the detail passes or from lithology, never from U.
  • An elevation-gated talus ("below 150 m, don't dissect"). It would work immediately because the clamp binds everywhere, but elevation is the output of the solve, so gating a process on it is circular, it produces a visible shelf at the threshold, and it hides the cause.
  • A percentile stretch for shore exposure, and casting fetch in every direction. Both measured, both wrong, and both recorded in Terrain.md: a percentile collapses on a coast that does not vary and is a global statistic two tiles would disagree about, and an all-directions fetch counts the land behind the shore as shelter, which made a straight open coast score as more sheltered than a bay.
  • One blur kernel for both the sediment and the carried per-shore values. The sediment balance needs a symmetric kernel, which means zero padding; a carried value needs edge clamping, or every shelf near the map border shrinks to nothing. There are two, and they share their arithmetic on purpose.
  • Giving each region enough margin to run the coastal pass inside it. It needs shelf_km plus slope_km, 4.6 km, which nearly doubles every region; and it buys nothing while costing the fetch across every strait, the sediment budget's conservation proof, an ocean-ownership rule where two margins overlap, and pooled coastal statistics. The pass is 26 ns a cell. Run it once on the cylinder.
  • Clustering landmasses by overlapping dilated bounding boxes. Box overlap is transitively closed and one long thin landmass has an enormous box; on the first real template — where one landmass is 70 km of a 100 km circumference — it collapses the planet into a single region. Dilate the mask itself with the distance transform and connected-component the result, which groups exactly those landmasses within a margin of each other and is the same code the stroke fill already needs.
  • Hanging a world origin on field.Field. A Field is used for masks, coordinate pairs and scratch, field.New has no origin to give them, and every one of them would quietly claim to sit at (0, 0). A wrong-by-default origin cannot be seen; a missing argument is a compile error.
  • Taking the massif threshold as a percentile of the region. It is the obvious implementation and it is the one thing D-53's decomposition forbids: uplift.Build's percentile band is a global operation over the grid it is given, and two regions taking quantiles of their own extents put the same physical hillside on different sides of the cut, so the planet disagrees with itself along every region boundary. The threshold is a quantile of the planet, from a fixed probe, identical in every region by construction. TestTwoFramesAgreeAboutTheSameGround is the negative control.
  • A massif fabric per class. One field for the whole planet, cut at a different level by each class. A fabric per class makes a highland belt and the hills in the lowland beside it two unrelated noises meeting at a painted edge, when what they should be is the high and low parts of one structure.
  • A massif wavelength near the size of a landmass. Measured: 12.5 km against islands of 20–45 km put one whole island above the cut, which is the defect the fabric exists to fix, one size down. Several blocks per continent, so well under a landmass.
  • Reading a hypsometric preview as a statement about scale. The ramp is normalised to the world in front of it, and it has to be - absolute over a world with no mountains is a green shape with nothing legible on it. palette.land_top_m is there for when absolute is what you want, and the run summary names the ceiling either way. The judgement of steepness belongs to map_slope.png and the plan's typical column.
  • Porting uplift.Build's lithology or faults as they stand. The first ends in f.Percentile() of the grid it is handed and the second places trace centres at Float() pairs read as fractions of it, so on a decomposed planet both give a different answer in every region. D-58 rewrote both against world coordinates and a planet quantile; TestTwoFramesAgreeAboutTheSameRock and TestTwoFramesAgreeAboutTheSameFaults are the negative controls.
  • Blurring a rock boundary to soften it. A blur is a neighbourhood operation and one near a region's edge reads cells another decomposition would not have given it. The softening is pointwise, in rank space, which needs only the cell's own value and grades the boundary by the fabric's own gradient.
  • Cutting a fault's influence off where the exponential is still worth something. At three gentle lengths it is 5 % of peak, which on a 400 m throw is a fifth of a lowland's whole uplift rate - a step at a line six kilometres out that the solve carves into a straight scarp. Superseded by D-62, which drops the exponential for an envelope that reaches zero with zero gradient at its own width - the cut-off is then the support of the function rather than a truncation of it, and there is nothing to renormalise. The lesson is the general one and it is why the old profile's near end was worse than its far end: any step left in the uplift field, anywhere, is a straight scarp the solve cannot undo.
  • Reading the divide angle as the landscape. It is exact and it is the steepest ground a rate can make, because A is smallest at the top of a catchment; the median is a third of it in tangent and almost none of a map is divide. Both print now (D-57). This is the same class of mistake as reading internal/stats' "plain below 0.1 mm/yr" as terrain, and it cost the same thing: every rate in the first legend set two or three times too hot.
  • A reserved background colour for the overlay's blank. Alpha already says it, every editor produces it, and a colour would be spent on nothing and lost the moment somebody exported with a white matte.
  • Snapping an unmatched overlay pixel to its nearest mark. That is right on a class template, where every pixel must become something, and wrong here, where most of the sheet is nothing: it turns a JPEG halo round a road into road. Drop it and count it.
  • One mask image per overlay mark. Marks cannot overlap - one painting, one colour a pixel - so an 8-bit index raster holds 254 of them in the space one boolean mask would take.
  • Treating an unmarked cell as coast_jitter 1. A mark lands on whichever side of the waterline the author's hand was on, so an uninstructed cell has to take its instruction from the far side or a stroke on the land is overruled by the water beside it.
  • Importing a painted heightmap as the terrain. See §3.2. The solve will erode it into something else and the drainage network — the reason the generator exists — is thrown away. Paint the uplift.

7. Traps

  • map_flow.png at planet scale is aliased, and it looks exactly like broken drainage. It point-samples every fourth cell (3000 px on a 12500 grid), so a one-cell channel survives about a quarter of the time and the network reads as disconnected yellow stubs, while the divides - which are broad - come through whole. Bake_001 shows the identical pattern, so it is the diagnostic and not the terrain. Judge the network from the preview's drawn rivers, or crop the map at 1:1.
  • Max elevation cannot see a change to the distribution of uplift. D-55 dropped the lowland continent from 71 m to 41 m while leaving the rate its massifs reach untouched, because relief is the integral of slope along the whole flow path: a trunk crossing a plain climbs where the massifs are and nowhere else. The statistic to read is the slope distribution, which internal/stats still cannot produce at planet scale - it had to be taken off the heightmap by hand for that measurement.
  • Rendering a diagnostic map smaller does not make plan quicker. Measured: prepare is flat at about 6.5 s from a 400 px map to a 2400 px one, because the cost is region.Build at 2.68 s over the 76 M cell planet grid - classify 0.09, dissolve strokes 0.93, despeckle 1.52, the coast mask 1.62, project 0.05. Neither the size of the painting nor the size of the maps touches the big one. What does work is caching: the studio reuses the whole prepare when only the legend's numbers changed, 7.0 s to 0.37 s.
  • terrain plan's divide column is not the ground. See §6. Read typical.
  • preview.png's tint is relative and says nothing about scale. The ramp's top is a percentile of the world being drawn, so a 47 m lowland continent whose median slope is 0.61° comes out with the bare rock and white caps of an alpine massif. That is where "the lowlands are hilly" came from, twice. Every run prints the ceiling it used; read that line, or read map_slope.png, or set palette.land_top_m.
  • A manifest key with a default is not a feature. coast_jitter_px was declared, documented, defaulted and never read by anything from D-53 until D-56, and the only reason it was found is that somebody asked why the coastlines looked drawn. A grep for a key's own name is three seconds and it is worth doing before tuning one.
  • A planet bake is not a rerun-while-you-judge loop. It is about two hours at 100 km round. terrain plan is four seconds and settles the two decisions that can waste those two hours — how the legend read the painting, and how the planet was cut up. --only <id> --steps 200 is the loop for tuning the legend's numbers; the elevation range is confirmed from that measurement, not guessed before it.
  • The seed alone no longer names a painted world. The ocean margin and the minimum landmass size decide how the planet is cut into regions, and the priority-flood's epsilon ladder across a flat depends on the box it is flooding. All three are in Planet.json and all three are recorded in meta.json; a change to any of them is a change to the world.
  • A noise period that does not divide the circumference breaks every noise field at the seam. noise.Lattice.Sample wraps modulo its cell count and WorldUV divides world metres by the period, so u returns to the same lattice point at x = W only when the circumference is a whole number of periods. world.Planet.Validate refuses anything else, and TestNoiseBreaksWhenThePeriodDoesNotDivide is the negative control that keeps the positive test honest.
  • RawContent/World/World.json is still pre-D-48: 4081 vertices at 350 cm, elevation −460…2800, and it still carries the legacy erosion block the tool warns about on every run. The Go defaults implement D-48 (7141 at 200 cm, −512…1536) and the manifest overrides them straight back. Migrating it is build-order step 8 and it changes every measured number in this doc, so either do it deliberately and re-baseline, or leave it alone.
  • Never run an authoring script while the editor has L_World open, and run create_world.py detached, never under a tool timeout — a timeout killed one mid-import before.
  • The elevation ceiling is a hard clip in the 16-bit encoding, so a run reporting a clip fraction above 0.1 % is a failed run, not a rounded one. U/K is the one relief knob. Note this gets harder with painted uplift, where an author can ask for more relief than the range holds.
  • Determinism from the seed is cross-cutting rule 12 and it applies here completely. Anything random must be a hash of (seed, position), never a stateful source: the value for a cell must not depend on how many cells were visited first, which goroutine ran, or how many steps have passed. TestDeterministicAcrossGOMAXPROCS hashes the output at five values of GOMAXPROCS and requires one hash. Under §3.3's rule 1 that becomes a hash of (seed, world position).
  • A test on a grid whose edge is an outlet must say which cells it is asking about. Two of the four new hillslope tests passed while measuring nothing: one read the fixed border cells back and called them the result, the other wrote its initial condition across the fixed border, which then re-injected it into the interior for ever.