mapv10 Generator
This document covers the Rust generator process: how to invoke it, how
the pipeline orchestrator threads stages and products together, what
each numbered stage actually computes, and where the determinism and
precision boundaries live. The high-level system diagram, the workspace
layout, the scale-preset table, and the renderer-side contract live in
architecture.md. This file does not duplicate
that material; it focuses on generator internals. The 4-step wave
dispatch protocol that governs changes to this stack lives in
wave-protocol.md; the visual regression
fixtures and per-preset perf budgets live in
scenarios.md; the recipe for adding new
generator stages or wiring new products into the renderer lives in
extending.md.
Build and Run
The generator is a Rust 2021-edition crate at
examples/map/mapv10/generator/ with mapv10-generator as the binary
target. Build it directly with cargo:
cargo build --release --manifest-path examples/map/mapv10/generator/Cargo.toml
The CLI is parsed by generator/src/main.rs. All flags are
optional; running with no flags falls through to defaults.
| Flag | Type | Default | Purpose |
|---|---|---|---|
--output <dir> | path | (none) | Write the run directly to this directory |
--run-id <id> | string | derived from --output basename or generated | Override the run id baked into manifest.json |
--force | bool | false | Allow --output to replace an existing directory |
--scale-preset <id> | enum | province-slice (main.rs) | One of province-slice, regional-slice, realm-slice, continent |
--seed <u64> | u64 | 20260502 (config.rs) | Deterministic seed |
--world-width-km <f64> | f64 | preset value | Override preset width |
--world-height-km <f64> | f64 | preset value | Override preset height |
--raster-width <usize> | usize | preset value | Override heightfield width in cells |
--raster-height <usize> | usize | preset value | Override heightfield height in cells |
When --output <dir> is supplied, the run is written directly into
that directory. prepare_output at main.rs first checks
existence: if the directory exists and --force is not set the build
aborts; if --force is set the directory is removed and recreated.
When --output is omitted, the binary writes the run into a
working subdirectory of ../artifacts/mapv10/ named
.building-<unix-millis>-seed-<n>/ (main.rs), then atomically
renames it to run-<unix-millis>-seed-<n>-<scale-preset>/ after every
stage has written successfully (main.rs). This means an
interrupted run never appears at the canonical name; only fully
written runs are visible. --force has no effect in this default-
path mode because the destination name is unique per invocation.
The viewer ships a thin Node script at
viewer/scripts/bootstrap-continent-fixture.mjs that wraps the cargo
invocation and the post-generate Valenar validation. Running:
npm run fixture:continent --prefix examples/map/mapv10/viewer
invokes cargo run --release -- --scale-preset continent --output viewer/public/continent-lod6 --run-id continent-lod6 --force
(bootstrap-continent-fixture.mjs), then calls
validateValenarExport(runRoot) to confirm stage 15 emitted
schema-valid valenar/world-<seed>.json and
valenar/world-<seed>.mesh.json products.
architecture.md § Workspace Layout
documents how viewer/public/<run-id>/ is served at
/mapv10/runs/<run-id>/ by the dev (5443) and preview (4279) servers.
Pipeline Architecture
The orchestrator entry point is build_products(config, run_dir) at
pipeline.rs. It instantiates a single BuildRegistry
(pipeline.rs) that accumulates the run's outputs:
config: GeneratorConfig— the resolved scale preset and overrides.products: Vec<ProductRef>— every truth product written to disk.previews: Vec<PreviewRef>— every PNG preview from stage 14.stages: Vec<StageManifestRef>— per-stagestage-manifest.jsonreferences.validation_reports: Vec<(String, ValidationReport)>— keyed by<id>-<key>.
Stages run sequentially on the main thread. Each stage is wrapped in
the local StageWrite struct (pipeline.rs):
struct StageWrite<'a> {
id: u8,
key: &'a str,
name: &'a str,
description: &'a str,
dependencies: Vec<String>,
products: Vec<ProductRef>,
previews: Vec<PreviewRef>,
validation: ValidationReport,
contract: serde_json::Value,
}
add_stage (pipeline.rs) folds the wrapper into the
registry: it extends the running products and previews vectors,
appends the validation report keyed by <id>-<key>, then calls
write_stage_files which lays out the per-stage subdirectory
<NN-key>/ and emits a stage-index.json describing that stage's
declared products, previews, dependencies, validation status, and
contract value. Validation runs per stage but failures only append to
validation_reports; the orchestrator does not abort on a failed
stage report.
After the final stage has been added, write_run_envelope
(pipeline.rs, defined in artifacts.rs) writes the run-level
manifest.json with the mapv10-artifacts-v1 schema version
(lib.rs), the consolidated stage index, and the generation
metadata.
The pipeline itself is single-threaded. The only intra-pipeline parallelism is internal to stage 3 (erosion), which uses rayon double-buffered row-parallel updates inside the inner iteration loop. Stages are not run in parallel with each other.
Stage Reference
The pipeline emits 16 stages numbered 0..15. The full stage table
lives in architecture.md § Generator Pipeline;
this section describes what each stage actually computes.
Stage 0 — config
Validates the resolved GeneratorConfig and serializes it as the
canonical config.json truth product (stages/config.rs).
- Technique: parameter validation. No algorithm.
- Key parameters: scale preset, seed, world bounds, raster dims, elevation/slope ranges.
- Algorithm:
validate_configchecks the preset is in the known set, that bounds and raster dims are positive, and that derived province / Location count bands are well-formed. - Products:
config.json. - Depends on: nothing.
Stage 1 — continent (rewritten in M11c)
Generates the mainland coast polygon, the offshore islands (when the
recipe's coastPolicy.islandPolicy is not no-islands), the optional
isthmus extrusion, and the sea region shell with every land polygon as a
hole (stages/continent.rs).
- Technique: recursive midpoint subdivision (Fournier-Fussell-
Carpenter 1982) of a 16-vertex regular polygon at the continent
template centroid.
coastPolicy.jaggedness.maxdrives the per-round midpoint displacement σ;coastPolicy.fjordLikelihood.maxtriggers per-edge Z-fold fjord subdivisions;coastPolicy.islandPolicycontrols island count and size by token (no-islands,small-coastal,archipelago-fringe,scattered-major); islands are placed by Mitchell best-candidate sampling within an offshore band;coastPolicy.isthmusPolicy(mainland-only,one-isthmus,broken-isthmus) controls the isthmus extrusion. Every stochastic draw is keyed bySeedKey::rng_forso a fixed(seed, recipe, intents, vocab)four-tuple produces byte-identical polygons. - Products:
continentPolygons.json,coastlines.json,seaRegions.json. After M11c the polygon count is1 + N_islandsand the coastline count matches. - Depends on: stage 0; reads the recipe, vocabulary, and seed key.
Stage 2 — terrain genesis (rewritten in M11c)
The file path stays stages/geography_graph.rs and the stage key stays
02-geography-graph per the no-rename rule. Internally the stage is
called "terrain genesis" and emits eight truth products plus two derived
compatibility outputs.
- Technique: Lloyd-relaxed Voronoi tectonic provinces (Lloyd 1982),
built by half-plane intersection of perpendicular bisectors and
clipped to the (possibly concave) continent polygon via ear-clip
triangulation (
earcutr) + per-triangle Sutherland-Hodgman 1974 ("Reentrant Polygon Clipping", CACM 17(1)); anisotropic Gaussian uplift kernel (Cortial-Peytavie-Galin-Guérin 2019 § 5; Tucker- Slingerland 1994); per-province crust-age field with Gaussian boundary blur; Sklar-Dietrich 2001 bedrock-hardness empirical mapping; Smith-Barstad 2004 spatial-domain linear-theory orographic rainfall; Lorensen-Cline 1987 marching-squares basin polygons followed by Douglas-Peucker 1973 simplification at1.5 × cell_size_x_km; Sousbie 2011 DisPerSE-style persistence- filtered ridge tracing for drainage divides (saddle + steepest- ascent integral-curve tracing + Edelsbrunner-Letscher-Zomorodian 2002 elder-rule persistence pairing); Edelsbrunner-Harer- Zomorodian 2002 discrete-Morse-theory saddle detection for pass candidates, sampled from the FINAL erodedheight.f32.binraster after Stage 3 (the pre-erosion uplift+age field is not the actual terrain). - Truth products:
terrainGenesis.json,uplift.f32.bin,crustAge.f32.bin,bedrockHardness.f32.bin,reliefPotential.f32.bin,basinCandidateId.u32.bin,drainageDivide.u8.bin,orographicRainfall.f32.bin. - Derived compatibility outputs:
ridgeGraph.json(sampled fromOrogenAxispolylines at 80 km intervals; empty when the recipe declares no orogens — no placeholder synthesis per CLAUDE.md §57) andbasins.json(marching-squares polygons of the basin-candidate raster; one Basin per candidate, with no placeholder synthesis when the candidate set has fewer than two members — Wave G2's water.rs rewrite owns thelakes[1]-style index-access redesign). Both carryderivedFrom: "terrain-genesis-projection"in their metadata block and are scheduled for removal in Wave G2 whenstages/water.rsis redesigned. - Depends on: stage 1; reads the recipe, vocabulary, optional directives, and seed key.
Stage 3 — heightfield (input swap in M11c)
Computes the eroded elevation raster and its derivatives. This is the
most expensive stage, the source of every downstream raster, and the
only stage with internal parallelism (stages/heightfield/mod.rs,
stages/heightfield/erosion.rs).
-
M11c input swap:
generatenow takes&TerrainGenesisProductsinstead of&RidgeGraph, &Basins. Initial elevation per cell isuplift_m × age_factor × hardness_boost + relief_m × 0.4 + basin_depression_m + divide_boost_m + detail_noise × 90 mwith inputs sourced from the Stage 2 truth products. Noise is restricted to detail texture (±90 m on land, ±18 m offshore); macro shape is recipe-driven. -
M11c erosion swap: the Mei-2007 virtual-pipes + Šťava-2008 sediment-transport solver stays untouched; only the rainfall input swaps to per-cell
genesis.orographic_rainfall, and the dissolving rate gets per-cell hardness modulationlocal_ks = dissolving_ks × (2.0 − hardness).max(0.05). Soft rock (hardness = 0) dissolves at 2× the base rate, granite (hardness = 1) at 1× — a 40× soft-vs-hard ratio per Mei-2007 time-step, matching the Sklar-Dietrich 2001 stream-power exponent and the geologic age-vs-relief record (shield cratons retain higher relief than equally-aged active orogens). -
M11c pass / saddle ordering: pass / saddle candidates are extracted from the FINAL eroded
height.f32.binraster AFTER Stage 3 runs, not from the pre-erosion uplift+age field. The pipeline orchestrates Stage 2 → Stage 3 → post-erosion saddle pass before writing the Stage 2terrainGenesis.jsonartifact, sopassCandidates[*].elevationMetersfalls naturally insideelevation_range_meterswithout parameter scaling. -
Technique: rotated-gradient fBm detail noise on top of the terrain-genesis truth fields, followed by Mei-Decaudin-Hu virtual- pipe hydraulic erosion (Mei et al. 2007) with Šťava-Beneš sediment transport (Šťava et al. 2008) and isotropic talus thermal relaxation also from Šťava 2008.
-
Key parameters (from
erosion.rs):hydraulic_iterations = 4_000thermal_every = 10(every Nth hydraulic iteration runs one thermal pass)dt = 0.02secondsrainfall_rate = 0.012m/spipe_area_base = 20.0m^2 (linearly scaled by cell pitch)gravity = 9.81m/s^2capacity_kc = 0.10dissolving_ks = 0.5deposition_kd = 1.0evaporation_ke = 0.0151/smin_water_for_capacity = 0.01mtalus_angle_radians = PI * 33 / 180(33 degrees)thermal_max_move_per_step_meters = 1.0dx_reference_meters = 1.0
-
Algorithm: the noise pass at
mod.rswalks every cell at(col + 0.5, row + 0.5) * cell_sizeand combines: inland_uplift =35 + smoothstep(12, 280, coast_distance) * 390,continental_macro_relief(aTERRAIN_MACRO_NOISE_OCTAVESfBm scaled by interior smoothstep with directional tilts and lowland features), ridge influence (per-edge gaussian field jagged byRIDGE_JAGGED_NOISE_OCTAVESfBm plus per-node gaussian peaks), basin influence (subtracted), and detail relief (TERRAIN_DETAIL_NOISE_OCTAVESscaled by 58 m). Off-land cells get a shelf falloff of-160 - min(coast_distance * 11, 850)m. Every cell is clamped intoelevation_range_meters = [-1200, 3200]before erosion runs.Erosion runs in-place on the height raster (
erosion.rs). Each of the 4000 iterations: rain, flux update, water update, sediment transport, and (every 10th iteration) thermal relaxation. The flux update uses the canonical pipe-modelQ = max(prev + dt * A * g * dh / l, 0)with absorbing boundaries on the world edge, then volume-conservation rescales outflow so the total per-step transfer never exceeds the cell's available water. State is held in two read/write buffers per field (read_water/write_water,read_flux/write_flux,read_sediment/write_sediment) swapped viamem::swapat end-of-iteration. Inner loops use rayonpar_chunks_mut(width).enumerate()so each row computes against the immutableread_*buffers in parallel without races. After erosion the height field is re-clamped toelevation_range_meters(mod.rs) and slope and normals are computed bycompute_slope(Horn 1981 central-difference slope magnitude) andcompute_normals(centered-difference normals encoded as RG16). -
Products:
height.f32.bin,slope.f32.bin,normal.rg16.bin,sediment.f32.bin,flowAccumulation.f32.bin. -
Depends on: stages 1, 2.
Stage 4 — hydrology (rewritten in M11d / Wave G2)
Owns canonical hydrology truth (stages/water.rs). Stage key
04-hydrology. Replaces the prior fixed-six-node ridge / two-ellipse-
basin / seven-hardcoded-river-node / bent_centerline decoration with
the standard textbook hydrology algorithms. See
ad-mapv10-m11-world-genesis-source-of-truth.md § M11d for citations.
Algorithm chain
- Priority-flood DEM conditioning (Barnes-Lehman-Rajewski 2014).
Border + sea-level cells seed a min-heap; pop in elevation order,
raise neighbours by
PRIORITY_FLOOD_EPSILON_Mwhen at-or-below the current spill, propagate adepression_labelinto raised basins. ProducesconditionedDem.f32.bin. - D8 flow routing (O'Callaghan-Mark 1984). Lexicographic
neighbour order with lowest-index deterministic tie-break.
Sea-mouth cells encode
0xFE, interior sinks0xFF. ProducesflowDirection.u8.bin+downstreamIndex.u32.bin. - Topological flow accumulation. Per-cell source =
cell_area_km² × orographic_rainfall(from Stage 2). Sort by conditioned elevation descending, sum upstream into downstream. ProducesflowAccumulationD8.f32.bin— distinct from Stage 3'sflowAccumulation.f32.bin(Mei 2007 water column at sim end). - Catchments and sinks (Vincent-Soille 1991). Walk each cell
downstream until terminal; classify sinks as
coast-mouth,lake,endorheic, ordry-sink. Producescatchments.u32.binand embedssinks[]intohydrology.json. - Stream network (Strahler 1957 + Shreve 1966). Threshold cells
at
RIVER_FLOW_THRESHOLD_FRACTION × total_flow; tag nodes by inflow-count + downstream-is-sink; segment from each non-mouth node downstream. ProducesstreamOrder.u8.bin,streamMagnitude.u16.bin,riverSegmentId.u32.bin. - Lake polygons (Lorensen-Cline 1987 + Douglas-Peucker 1973).
Marching squares on
depression_label[idx] == this_label && raw_height[idx] <= spill + tolerance. Lakes belowMIN_LAKE_AREA_FRACTION × world_area_km²are demoted to dry-sink. Lakes are sorted by area descending so downstream area-sorted selectors (map_features.rs) get a stable order. - River centerlines (Chaikin 1974). D8 chain from
from_celltoto_cell, thenCHAIKIN_ITERATIONSrounds of corner-cutting. - River width (Leopold-Maddock 1953).
width_km = BASE_RIVER_WIDTH_KM × (1 + order × RIVER_WIDTH_ORDER_FACTOR) × max(0.5, sqrt(discharge / REFERENCE_DISCHARGE)). Navigability gate onorder >= NAVIGABLE_MIN_ORDER && width >= NAVIGABLE_MIN_WIDTH_KM. ProducesriverWidth.f32.bin. - Water masks + body ids + floodplain + wetland.
waterMask.u8.bin:0land,1sea,2lake,3river,4wetland,5floodplain.waterBodyId.u32.bin: sea=1, lakes 2..N, rivers N+1..floodplainMask.u8.bin: rank by distance band around high-order streams.wetlandMask.u8.bin: rank by accumulation × low-slope. Stage 5 reads the canonical mask rather than recomputing.
Products (truth):
hydrology.json, conditionedDem.f32.bin, flowDirection.u8.bin,
downstreamIndex.u32.bin, flowAccumulationD8.f32.bin,
catchments.u32.bin, streamOrder.u8.bin,
streamMagnitude.u16.bin, riverSegmentId.u32.bin,
riverWidth.f32.bin, waterBodyId.u32.bin, waterMask.u8.bin,
floodplainMask.u8.bin, wetlandMask.u8.bin.
Products (derived legacy projections — Wave T retires):
riverGraph.json, riverCenterlines.json, lakePolygons.json. Each
carries derivedFrom: "hydrology-truth" in its product metadata
(dimensions JSON), parallel to the Wave G1 sanction for derived
ridgeGraph.json / basins.json outputs.
Depends on: 02-geography-graph (orographic rainfall, ridge graph
basis for the derived RidgeGraph legacy projection consumed by
map_features.rs) and 03-heightfield (raw height for priority-flood
seeding and slope for wetland classification).
Constants pending vocabulary promotion (TODO(M11l)):
PRIORITY_FLOOD_EPSILON_M, RIVER_FLOW_THRESHOLD_FRACTION,
CHAIKIN_ITERATIONS, BASE_RIVER_WIDTH_KM,
RIVER_WIDTH_ORDER_FACTOR, RIVER_WIDTH_REFERENCE_DISCHARGE,
NAVIGABLE_MIN_ORDER, NAVIGABLE_MIN_WIDTH_KM,
FLOODPLAIN_BAND_RADIUS_CELLS, FLOODPLAIN_MIN_ORDER,
WETLAND_MIN_ACCUM_FRACTION, WETLAND_MAX_SLOPE,
LAKE_SIMPLIFY_FACTOR, MIN_LAKE_AREA_FRACTION.
Empty arrays at Stage 4:
crossingCandidates— depends on Wave T M11h route candidates.locationAssociations— depends on Stage 6 political polygons. Thehydrology.json:locationAssociations[]array stays authoritatively empty per the M11 ADR §744-757 + §773-782 (committed redesign — no dual write, no aliases, no fallback). The populated per-Location join lives only in the separatelocationHydrologyAssociations.jsonproduct written by the distinctstages/04-b-hydrology-locationssub-stage, which runs AFTER Stage 6 (06-political).
Forward references:
- Stage 4c (
stages/04-c-terrain-conditioning) is the M11x-W5 architectural seam between Stage 4 hydrology truth and Stage 5 biomes-materials. It borrowsWaterProductsimmutably and emits the post-incision conditioning raster family (conditionedHeight,riverbedElevation,channelDepth,waterSurfaceElevation, etc.). Stage 4 byte-stability is preserved; the conditioning lives in the new stage. Seead-mapv10-m11x-hydrology-conditioning-contract.md.
Stage 4b — hydrology-locations (M11h)
Folder stages/04-b-hydrology-locations. Runs AFTER Stage 6
political polygons land. Writes locationHydrologyAssociations.json
populated by stages::hydrology_locations::generate using a
majority-vote-over-enclosed-cells spatial join.
- Algorithm: for each
LocationPolygon, walk the cells inside its polygon bbox and filter bypolitical.location_id == numeric_id(the same cell-ownership oracle Stage 6'srasterize_location_idproduces). Accumulatetotal_cells,floodplain_cells,wetland_cells, abasin_candidate_idhistogram, and a sorted-unique set of non-zeroriver_segment_idcells. Per-Location derivation is then:floodplain_pct = floodplain_cells / total_cellswetland_pct = wetland_cells / total_cellsbasin_id= dominant basin histogram entry, tiebreak by lower numeric raster value (deterministic — earliest-emitted basin wins on ties).Noneif no enclosed cell has a basin.river_segment_ids= sorted-unique segment ids mapped throughhydrology.river_network.segments[](raster valuevis the 1-based index into the segments vector).freshwater_access= any river segment touches this Location OR any cell haswater_mask == 2(lake class).major_river_access= any touching segment hasstrahler_order >= MAJOR_RIVER_MIN_ORDER(default 3; same threshold asFLOODPLAIN_MIN_ORDERinwater.rsso the "major" semantic and the "floodplain-grower" semantic stay in sync). TODO(M11l) promotes the constant to vocabulary.navigable_river_access= any touching segment has.navigable == Some(true).valley_idisNoneuntil Wave I/M11i lands custom-valley directive resolution. Populating it earlier would silently fabricate geography truth, which the M11 no-fallback doctrine forbids; the validator enforces this invariant.
- Validator:
validation::validate_location_hydrology_associationshard-fails on coverage (one association per Location), unresolved Location / segment / basin ids, anySome(valley_id), and pcts outside[0, 1]. - Deferrals: every field declared in
hydrology.schema.json#/$defs/locationHydrologyAssociationis populated. Additional join-relevant fields (mountainPassIds,orogenIds,catchmentId,waterBodyId) are NOT added here — they require ahydrology-locations-v2schema bump scheduled for Wave M/M11m. Adding them in this wave would silently expand theadditionalProperties: falsecontract; forbidden.
Stage 4c — terrain-conditioning (M11x-W5 contract; M11x-W6 fills in)
Folder stages/04-c-terrain-conditioning. Stage key
04-c-terrain-conditioning. Owns the architectural seam between Stage 4
hydrology truth and the downstream stages that need post-incision
geometric truth (Stage 5 biomes-materials, Stage 11 tile-pyramid, Stage
12 meshes). See
docs/ad-mapv10-m11x-hydrology-conditioning-contract.md for the full
design rationale, the wave-label collision resolution, and the Wave 6
implementation hooks.
stages/terrain_conditioning.rs::generate borrows both
&HeightfieldProducts and &WaterProducts immutably; Stage 3 and Stage
4 byte-stability (M11d) is preserved.
Products (truth, all required):
conditionedHeight.f32.bin, conditionedSlope.f32.bin,
conditionedNormal.rg16.bin, riverbedElevation.f32.bin,
channelDepth.f32.bin, waterSurfaceElevation.f32.bin,
bankMask.u8.bin.
The seven channels are declared as required by
schema/terrain-conditioning.schema.json and validated by
validate_terrain_conditioning. Optional or Option<...> typing is
forbidden by the no-fallback doctrine: a missing channel would silently
degrade Stage 12 to ribbon-only rivers, which the M11x-W5 contract
explicitly retires.
Wave 5 status — contract + scaffold + stub:
Wave 5 ships the stage with a stub body. Six of the seven products are
zero-allocated buffers; conditioned_height is a byte-copy of
WaterProducts.conditioned_dem so downstream Wave 6 work can begin
reading the field. The validator asserts product presence and length-
correctness plus finite f32 values; Wave 6 extends with channel-depth-
positive / riverbed-below-surface / no-NaN-in-incised-fields rules once
the real algorithm lands.
Wave 6 algorithm chain (pending):
- Stream-power incision (Howard 1994). For each cell with
streamOrder > 0: integratedz/dt = -K * A^m * S^nto steady state.Kis a recipe-vocabulary calibration constant.AisflowAccumulationD8in km².Sis the local slope along the D8 downstream direction.m, nare Howard 1994 calibration constants. - Leopold-Maddock 1953 depth power-law. For each river segment,
compute
depth_km = c * Q^fwhereQis the per-segment rainfall- weighted discharge andc, fare the depth-equivalents of the existing width-formula constants. - Riverbed elevation:
conditioned_height - channel_depthfor channel cells; lake-bed depth for lake cells; equal toconditioned_heightfor dry cells. - Water surface elevation:
riverbed_elevationplus the Mei 2007 water-column thickness fromHeightfieldProducts.flow_accumulationfor river cells;lake.surfaceElevationMetersfor lake cells; equal toconditioned_heightfor dry cells. - Bank mask:
1for cells within 2 cells of anystreamOrder > 0cell;0elsewhere. Distinct fromWaterProducts.floodplain_mask(wider band, biome-classification scope). - Conditioned slope: Horn 1981 slope magnitude recomputed against
conditioned_height. - Conditioned normal: centred-difference normal recomputed against
conditioned_height, rg16-packed.
Depends on: 03-heightfield, 04-hydrology.
Stage 5 — biomes-materials
Classifies every land cell into a biome token and emits two RGBA8
material-weight bands plus binary forest and wetland masks
(stages/biomes_materials.rs).
-
Technique (Wave T1 / M11e): vocabulary-driven Whittaker classifier. Each cell's elevation × slope × moisture × latitude × province-kind × rainfall-pattern is resolved into an open-list biome token index via
classify_biome_token. The token list is declared invocabulary.biomeKindsand lands by editingdefault-world-vocabulary.json; the classifier reads the resolved vocabulary slice at stage entry, so adding a new biome token does not require touching this stage. The 22 starter tokens include the major Whittaker archetypes (tropical-rainforest, boreal-forest, temperate-deciduous-forest, mediterranean-shrubland, subtropical-desert, tundra, polar-ice) plus province-conditional variants (rift-valley-savanna, shield-exposed-rock, craton-arid-interior, mountain-alpine, mountain-meadow, coastal-strand). Water cells short-circuit to the firstisWater=truetoken (sea) or tofreshwaterwhen the Stage 4 water mask reports a lake/river. -
Key parameters:
ECOLOGICAL_NOISE_OCTAVES(4 octaves at 320/170/92/48 km wavelengths,biomes_materials.rs) drives the forest-noise gate. Moisture bands are sourced from Stage 2'sorographic_rainfall.f32.bin(the resolved Smith-Barstad output). Per-cell absolute latitude is projected from the recipe-declaredarchetype.latitudeBandDegrees = { min, max }and the per-cell world-y aslat = max - normalized_y * (max - min), wherenormalized_y = clamp(point.y / world_height_km, 0, 1). The sign convention is north-positive:y = 0is the world rectangle's north edge and projects tolatitudeBandDegrees.max;y = world_height_kmis the south edge and projects tolatitudeBandDegrees.min. The Köppen-Geiger zone breakpoints (Köppen-Geiger 1936; Peel, Finlayson & McMahon 2007, Hydrol. Earth Syst. Sci. 11: 1633-1644) are exposed as Rust constants inbiomes_materials.rs:LAT_DEG_TROPIC_OF_CANCER_ABS = 23.5,LAT_DEG_SUBTROPICAL_POLEWARD_LIMIT = 35.0,LAT_DEG_TEMPERATE_POLEWARD_LIMIT = 60.0,LAT_DEG_ARCTIC_CIRCLE_ABS = 66.5. These are physical-science constants, not authoring knobs. -
Algorithm: per-cell, the classifier walks a Whittaker decision tree (water → wetland → high-elevation → highland → lowland by latitude × moisture) and returns a token index into
vocabulary.biomeKinds. Province-kind hints (sampled by point-in-polygon againstgenesis.tectonicProvinces— Q1 deferred to a Wave G1 raster product in M11l) gate shield/craton/rift variants. The Mediterranean gate fires on(zone == Subtropical && moisture == Moderate) && rainfall_pattern == "zonal-temperate"inside the absolute-latitude band|lat| ∈ [LAT_DEG_MEDITERRANEAN_EQUATORWARD = 30°, LAT_DEG_MEDITERRANEAN_POLEWARD = 45°]. Wave M11f replaces the legacy world-y unit-band Csa/Csb gate with this recipe-driven absolute-latitude form; the proper wet-winter / dry-summer seasonality model lifts in M11k.After classification, the stage emits soft membership weights into bands A and B. Band A channels are grass/plains (R), rock/highland (G), forest canopy (B), and snow or wetland (A — the GPU NEAREST biome sample picks between the two palettes per fragment). Band B channels are sand/coast (R), bare earth (G), ice/glacier (B), riverbank mud (A). All eight membership channels are continuous smooth values so they survive mean-downsampling through the LOD pyramid without labyrinth artefacts.
-
Products:
biome.u8.bin(open-list token index;min=0, max=vocabulary.biomeKinds.len() - 1),materialWeights.rgba8.bin,materialWeightsB.rgba8.bin,forestMask.u8.bin. The biome product's manifest entry includes a per-tokentokensarray built from the resolved vocabulary (replaces the M11d hardcoded numeric → stringlegendblock). Since M11d / Wave G2 the canonicalwetlandMask.u8.binis emitted by Stage 4 hydrology truth (priority-flood × low-slope × high-accumulation); Stage 5 reads it directly fromWaterProducts.wetland_maskand no longer emits its own version. -
Depends on: stages 1, 2 (terrain genesis: tectonic provinces + orographic rainfall), 3, 4. The vocabulary instance (the same one validated by the M11c pre-flight) is read for biome tokens.
Stage 6 — political
Generates the realm, province, and Location polygon hierarchy plus
the per-cell ID rasters and the neighbor graph
(stages/political.rs).
The polygon stage emits every realm / province / location with a
deliberately-empty name field; the dedicated political_naming
stage (see "Naming", below) runs immediately after political
polygons are emitted and fills those names in from the per-biome
procedural namer. The pipeline order is biomes_materials →
political::generate (polygon shapes only, names empty) →
political_naming::run (samples biome raster, fills names), so the
namer always has both the polygons and the biome classification
available without the polygon stage needing to know anything about
biomes. The legacy 120-name location_name pool and the
"Province N" placeholder fallback have been deleted entirely; no
fallback path lives in the polygon stage.
-
Technique: Halton-sequence candidate seeding, Mitchell best-candidate selection (Mitchell 1991 with deterministic jitter) for blue-noise spacing, single-pass Lloyd relaxation, and half-plane Voronoi cell clipping over the bounding polygon.
-
Key parameters: candidate pool size `(count * 16).max(count
- 32)
with up totarget_candidates * 80Halton draws (political.rs). Lloyd relaxationiterations = 1(political.rsandpolitical.rs`).
- 32)
-
Algorithm:
relaxed_seed_points(political.rs) runsblue_noise_seed_pointsto draw Halton(2,3) candidates inside the bounding polygon, picks the best-candidate by squared distance to the existing selection (with a small splitmix64 jitter so ties break deterministically), then iterates Lloyd relaxation by computing the centroid of each Voronoi cell and replacing the seed if the centroid lies inside the container.voronoi_cell_polygons(political.rs) is naive O(N^2) half-plane clipping: for each seed, it starts with the open container ring and clips against every other seed using the perpendicular bisector. The clip equation is2 * (other.x - seed.x) * x + 2 * (other.y - seed.y) * y= other.x^2 + other.y^2 - seed.x^2 - seed.y^2(
political.rs); a point is "inside" (kept) whena*x + b*y - c <= 1e-9. This is performed for the realm, then per-realm provinces, then per-province Locations. Province and Location ID rasters are filled by point-in-polygon classification at(col + 0.5, row + 0.5) * cell_sizecell centres (political.rsraster ID fill helpers). The neighbor graph (political.rs) walks the ID rasters in scan order collecting raster-adjacency edges into aBTreeSetfor deterministic ordering. -
Products:
realmPolygons.json,provincePolygons.json,locationPolygons.json,provinceId.u32.bin,locationId.u32.bin,neighborGraph.json,provinceColorSeeds.json. -
Depends on: stages 0, 1, 3.
Stage 7 — routes
Generates the location-to-location connection graph, road network,
canonical route graph, route centerlines, and crossing anchors where
routes intersect rivers (stages/routes.rs).
- Technique: pairwise candidate construction over neighbor graph edges, deterministic cost ranking, and selected centerline emission with crossing detection against river centerlines.
- Algorithm:
build_route_nodesmaterializes one node per location plus province hubs. Candidate edges come from the stage 6 neighbor graph; each candidate carries a deterministic cost combining great-arc distance, terrain cost multiplier from the heightfield slope along the path, an additive Leopold-Maddock 1953 drainage-area-scaled engineering toll per crossed river segment, and a multiplicative pass-discount per Hill 1985 + Llobera-Sluckin 2007 archaeological least-cost-path GIS literature. The pass discount fires in BOTH the innerterrain_aware_centerlinesearch objective AND the finalbuild_route_candidatecost, so the search actually steers polylines toward declaredPassCandidatesaddles insidepass_proximity_km(config). The selected route graph is connected by construction. - Products:
locationConnections.json,roadNetwork.json,routeGraph.json,routeCenterlines.json,crossingAnchors.json. - Depends on: stages 2 (terrain-genesis pass candidates), 3, 4 (hydrology river segments), 6.
Stage 7b — route-crossings (M11g)
Sub-stage between Stage 7 and Stage 8. Emits a richer per-route
crossing payload that the legacy crossingAnchors.json (still emitted
by Stage 7) is too narrow to carry (stages/route_crossings.rs).
- Technique: per-route polyline walk over the
river_segment_idraster from Stage 4 hydrology with edge- triggered entry detection, plus a Leopold-Maddock 1953 drainage- area-scaled engineering toll classified by Strahler 1957 stream order. - Algorithm: each
RouteCenterlinepolyline is sampled cell-by- cell at a half-cell-diagonal step so no river cell is missed. A transition fromriver_segment_id = 0(or a different segment id) to a non-zero new segment id emits exactly oneRouteCrossingCandidateper (route_edge_id, river_segment_id) pair.crossingKindis"ford"for Strahler orders 1-2,"bridge"for orders 3-5, and"ferry-or-bridge-major"for orders 6+.costKmisbase * max(1, Q / Q_ref)^0.9wherebaseis the kind-specific anchor (ford 0.3, bridge 2.0, ferry 5.0 km-equivalent),Qis the segment's drainage area in km², andQ_ref = REFERENCE_DRAINAGE_AREA_KM2 = 1_000. - Products:
routeCrossingCandidates.json(schema:mapv10-route-crossing-candidates-v1). - Depends on: stages 4 (hydrology river segments), 7 (route centerlines).
- No fallback: the
hydrology.json::crossingCandidates[]array stays committed-empty per the M11 ADR §744-757 + the M11g section. Downstream consumers MUST readrouteCrossingCandidates.jsondirectly; there is no dual write and no alias.
Stage 8 — map-features
Emits map feature anchors, footprints, and label anchors
(stages/map_features.rs).
- Technique: deterministic anchor placement over the political hierarchy and routes plus zoom-band metadata for the renderer's label fade and collision system.
- Algorithm:
generate_labelswalks realms, provinces, Locations, water, and routes, emitting oneLabelAnchorper entity with a zoom-band rank derived from the entity kind. Feature footprints are closed polygon rings tied to declared anchor IDs. - Products:
mapFeatureAnchors.json,mapFeatureFootprints.json,labelAnchors.json. - Depends on: stages 4, 6, 7.
Stage 9 — influence
Builds generic influence truth products, with corruption as the first
registered preset (stages/influence.rs).
- Technique: typed influence registry + deterministic source anchors + per-type intensity masks. Influence never mutates the base biome/material truth products; it emits derived effective visual/material products for rendering and inspection.
- Algorithm: source anchors are selected from generated map
features (wetland, mountain pass, lake shore) with a deterministic
Location fallback. Corruption intensity combines smooth radial
falloff, organic edge noise, route drainage, slope gain, and water
damping into
influenceMask.corruption.u8.bin.influenceTypeMaskstores 0 for none and the registered type id for active influence. Effective material/forest/wetland masks are derived from the base biome, material weights, forest mask, and wetland mask. The effective-biome echo is byte-identical to the base biome raster (Wave T1 / M11e); the sparse override sidecar (effectiveOverride.u8.bin) carries the corruption palette swap via an(index + 1)reference intovocabulary.effectiveVisualBiomeKinds. - Products:
influence/influenceTypes.json,influence/influenceSources.json,influence/influenceRules.json,influence/influenceMask.corruption.u8.bin,influence/influenceTypeMask.u8.bin,effective/effectiveBiome.u8.bin(Wave T1 / M11e: base echo),effective/effectiveOverride.u8.bin(Wave T1 / M11e: sparse override intovocabulary.effectiveVisualBiomeKinds),effective/effectiveMaterialWeights.rgba8.bin,effective/effectiveMaterialWeightsB.rgba8.bin,effective/effectiveForestMask.u8.bin,effective/effectiveWetlandMask.u8.bin. - Depends on: stages 3, 4, 5, 6, 7, 8. Reads the
vocabulary.effectiveVisualBiomeKindsslice to resolve the corruption override token (corrupted-substrate).
Stage 10 — borders-sdf
Bakes a continent-wide signed-distance field over province IDs plus
a 24-bit nearest-province ID channel, encoded as RGBA8
(stages/borders_sdf.rs).
- Technique: 1+JFA — Jump Flooding Algorithm (Rong & Tan, 2006)
with one corrective
step = 1pass appended after the standard log2-step passes to restore full Voronoi accuracy. - Key parameters:
BORDER_SDF_RADIUS_CELLS = 127(borders_sdf.rs) — the saturation radius of the distance channel. - Algorithm:
seed_border_cellsmarks every cell whose province ID differs from at least one of its 4 von Neumann neighbours as a JFA seed at distance zero, carrying the OTHER side's province ID. The main loop computesstep = max(width, height).next_power_of_two() / 2and halves step each pass, runningjfa_passover the current/next buffers and swapping. After the final step-1 pass of the standard schedule, one additionaljfa_pass(width, height, 1, ...)corrective pass runs (1+JFA,borders_sdf.rs). The encoded RGBA8 channels are: R =clamp(d, -127, 127) + 128(signed distance in source cells, positive inside province, negative outside), G = low byte of nearest-province numericId, B = middle byte, A = high byte. The 24-bit ID encoding supports up to 16,777,215 unique provinces; nearest-id zero across all three bytes means "no nearest within search radius". - Products:
borderSdf.rgba8.bin. - Depends on: stage 6.
Stage 11 — tile-pyramid
Slices the source rasters into per-LOD per-tile assets with skirts
plus the per-tile vector / semantic / mesh manifests
(stages/tile_pyramid.rs).
- Technique: per-channel downsampling with a 1-cell skirt.
Continuous channels (height, slope, RGBA8 material weights,
forest coverage mask, SDF distance, M11i Wave-D1 genesis scalars
uplift/crustAge/bedrockHardness/reliefPotential/orographicRainfall, hydrology scalarsconditionedDem/flowAccumulationD8) use box-mean. Categorical channels (biome u8, province ID u32, location ID u32, SDF nearest-ID GBA bytes, M11i Wave-D1 hydrology id channelscatchments/riverSegmentId/waterBodyId/basinCandidateIdu32,streamMagnitudeu16,flowDirectionu8) use mode (plurality) downsampling. Rank/binary mask channels (wetlandMask,floodplainMask,drainageDivide,streamOrder) use max downsampling — the conservative-coverage rule preserves the mask edge across coarse LODs instead of eroding it with a mean. Slope is re-computed per LOD via Horn 1981 central-difference rather than mean-downsampled. The Toksvig closed-form roughness prefilter (Han 2007 / Olano-Baker 2010) is computed per LOD into a u8 sidecar. Stage 11 also emits the generated RGBA8closeDetailNormalsidecar; z6/z7 usecloseDetailScale = 8so close-detail raster SSE is backed by authored generated truth rather than a viewer-only noise shader. M11i Wave D1 re-sourcedwetlandMaskfrombiomes.wetland_maskto the canonical post-G2water.wetland_maskraster. - Key parameters:
BORDER_CELLS = 1(tile_pyramid.rs) — one row of skirt sampled from the SOURCE raster's neighbour cells, not from the next tile's already-downsampled bytes. The output tile extent is(W + 2N) x (H + 2N)per channel. - Algorithm: zoom levels come from the preset's LOD ladder
(
tile_pyramid.rs). For each zoom, the world is divided intotile_count_x x tile_count_yaxis-aligned tiles (tile_pyramid.rs). For each tile, aRasterWindowidentifies the source-cell range, then the per-channel writers sample with a half-output-cell offset into the source raster and emit the padded extent. Mode downsampling counts source cell values inside each output cell footprint and picks the plurality; it is the only sound choice for a u32 ID raster because mean produces meaningless intermediate IDs. Toksvig roughness encodesalpha = sqrt(2 * (1 - len(mean(N))) / len(mean(N)))asclamp(round(alpha * 255), 0, 255)per output texel (tile_pyramid.rs); the prefilter reads the next-finer LOD's unit normals because variances must add. The z7 normalRoughness sidecar is emitted all-zero because there is no finer LOD to prefilter from at source resolution. - Products:
tiles/tile-pyramid.json,tiles/tile-coordinate-index.json,tiles/raster-tiles/index.json,tiles/vector-tiles/index.json,tiles/semantic-tiles/index.json,tiles/mesh-tiles/index.json, required z-row family shards undertiles/<family>-tiles/z<z>/y<y>.json,tiles/semantic-display-policy.json, plus per-tile binary assets undertiles/<channel>/z<z>/<x>/<y>.bin. The coordinate index owns generated tile bounds/hierarchy/error truth; family manifest indexes own shard byte lengths so browser and Node proof paths stream row shards instead of reading a multi-GB monolithic tile manifest. BaseforestMaskandwetlandMaskare included as runtime raster tile sidecars for inspector payloads; the viewer does not fetch their full-resolution root products during default run load. M11i (Wave D1) added 16 additional channels sliced from Stage 2 (terrain genesis) and Stage 4 (hydrology) truth —uplift,crustAge,bedrockHardness,reliefPotential,basinCandidateId,drainageDivide,orographicRainfall,conditionedDem,flowDirection,flowAccumulationD8,catchments,streamOrder,streamMagnitude,riverSegmentId,waterBodyId, andfloodplainMask— for inspector overlays and downstream consumers.downstreamIndexis deliberately omitted from the tile pyramid: the per-cell pointer has no meaningful aggregation rule across a 2x2 source window, so it stays a Stage 4 base-resolution raster product and traversal consumers read the root product directly. - Depends on: stages 2 (terrain genesis), 3, 5, 6, 9, 10.
Stage 12 — meshes
Triangulates terrain, water, and route geometry into f32-LE position
- u32-LE index
.mesh.binfiles plus the family manifests (stages/meshes.rs).
- Technique: per-tile polygon-clip-and-triangulate for terrain (M11x).
The tile-local continent polygon (clipped from
continentPolygons.jsonagainst the tile rectangle) becomes theterrain-landmesh asset. The tile-local sea polygon (tile rectangle minus continent polygon minus intersecting lake polygons fromhydrology.json) becomes theterrain-seafloormesh asset. The coast and lake-edge segments touching the tile become theshoreline-transitionseam ribbon. Wave 3 selects the specific clipping/triangulation algorithms; the supported AAA techniques named indocs/ad-mapv10-m11x-coast-geometry-contract.mdare Sutherland-Hodgman (1974) and Greiner-Hormann (1998) / Vatti (1992) for clipping, constrained Delaunay (Shewchuk 1996) and the existingearcutrcrate for triangulation, and marching squares (Lorensen-Cline 1987, already used for lake polygons instages/water.rs) for any contour extraction. Earcutting viaearcutrcontinues to produce sea regions and lake polygons in the water family. Quad-strip ribbon construction continues to produce routes from polyline centerlines. - Algorithm: terrain tiles iterate
tile_coordinates(config). For each tile, the continent polygon is clipped against the tile rectangle to produce the land polygon (lake interiors removed viahydrology.jsoncomposition). When the land polygon is non-empty, aterrain-landasset is emitted whose triangles all lie inside the clipped land polygon. When the complementary sea polygon is non-empty, aterrain-seafloorasset is emitted whose triangles all lie inside the clipped sea polygon. When at least one coast or lake-edge BoundaryId touches the tile, ashoreline-transitionasset is emitted whose ribbons span the segments. No triangle interrain-landorterrain-seafloormay straddle a BoundaryId segment. Vertex elevations are read from the erodedheight.f32.binand converted from metres to kilometres via* 0.001at write time (meshes.rsmesh vertex write path). Each mesh asset records itsoriginKm = PointKm::new(tile.bounds_km.min_x, tile.bounds_km.min_y)(meshes.rs); vertex positions are stored tile-local so the f32 upload stays inside the precision budget. Coastline geometry is owned by the generator; viewer-side coastline extraction (runtime marching-squares, raster-to-polygon recovery, etc.) is forbidden by the contract. - Products:
meshes/mesh-manifest.json,meshes/terrain-land-meshes.json,meshes/terrain-seafloor-meshes.json,meshes/shoreline-transition-meshes.json,meshes/water-meshes.json,meshes/route-ribbons.json, plus per-asset.mesh.binblobs. - Depends on: stages 1, 3, 4, 7, 11.
Stage 13 — Readiness
Computes the readiness report covering memory, cache, artifact-size,
coordinate-precision, and streaming budgets, plus the continent
preset draft when running a non-continent preset
(stages/readiness.rs).
- Technique: aggregation over the registry of products, previews, tiles, and mesh assets emitted by stages 0..12.
- Key parameters:
F32_UNIT_ROUNDOFF_FACTOR = 1.0 / 8_388_608.0(readiness.rs),MAX_ALLOWED_F32_ROUNDOFF_KM = 0.005(readiness.rs). Per-preset budgets come from the preset table atconfig.rs. - Algorithm: artifact size aggregates byte length over all
products and previews; the streaming report counts overview vs
detail mesh tiles; the coordinate precision report folds
local_bounds_kmover every mesh asset to find the maximum tile-local extent and reportsmax_local_extent_km * F32_UNIT_ROUNDOFF_FACTORagainst the 0.005 km ceiling. - Products:
readiness/readiness-report.json,readiness/continent-preset-draft.config.json. - Depends on: stages 11, 12.
Stage 14 — previews
Renders human-readable PNGs derived from the typed truth products
(stages/previews.rs). Previews are derived artifacts only; they do
not feed any downstream stage.
- Technique: per-channel PNG encoder calls (
pngcrate). - Algorithm: each preview reads its source raster from the registry and emits an 8-bit PNG colourized for the channel kind (heat ramp for height/slope/sediment/flow accumulation; categorical palette for biome and political IDs; binary mask shading for forest/wetland; layered overlay for the political composite).
- Products:
previews/height-preview.png,previews/slope-preview.png,previews/normal-preview.png,previews/sediment-preview.png,previews/flow-accumulation-preview.png,previews/water-preview.png,previews/biome-preview.png,previews/splat-preview.png,previews/forest-preview.png,previews/wetland-preview.png,previews/province-id-preview.png,previews/location-id-preview.png,previews/political-preview.png,previews/influence-corruption-preview.png,previews/influence-type-preview.png,previews/effective-visual-biome-preview.png,previews/effective-splat-preview.png. - Depends on: stage 8 (declared dependency); reads outputs of stages 3, 4, 5, 6, 9.
Stage 15 — valenar-worlddata
Projects the generator's typed truth into the Valenar import JSON
plus a mesh manifest, and seals both with a shared SHA-256 content
hash (stages/valenar_worlddata.rs).
- Technique: deterministic projection over the political
polygons, neighbor graph, heightfield, water, biomes, routes, and
the Stage 4b
LocationHydrologyAssociationtruth product. - Algorithm: builds the snake_case
ValenarWorldDocumentwith regions, areas, provinces, locations, and anchors keyed by stable numeric IDs derived from stage 6, computes the SHA-256 of the canonical JSON encoding of the world payload, then writes the same hash into both the world and mesh documents so the viewer's Valenar export validator can confirm the pair was emitted from the same generator run. M11j (Wave D2) adds a per-Locationhydrologyprojection (ValenarWorldLocationHydrology) sourced from the Stage 4b association table; the projection drops the mapv10-internalvalley_idbecause Valenar consumes only the basin, and the helper hard-fails when a Stage 6 Location has no matching association (no silent default). World file path:valenar/world-<seed>.json; mesh manifest path:valenar/world-<seed>.mesh.json(valenar_worlddata.rs). M11j bumped the world schema id fromvalenar-world-v1tovalenar-world-v2; the mesh manifest shape is unchanged so its schema id stays atvalenar-world-mesh-v1. - Products:
valenar/world-<seed>.json,valenar/world-<seed>.mesh.json. The schemas live underexamples/map/mapv10/schema/valenar-world.schema.jsonandexamples/map/mapv10/schema/valenar-world-mesh.schema.json. - Depends on: stages 4b (hydrology-locations), 6, 7, 8, 12, 14.
For the field-level export schema, unit ranges, validator surface, and content-hash construction, see ./export-contract.md.
Naming
Political naming runs as a dedicated stage between biomes_materials
and water in the pipeline (pipeline.rs —
political_naming::run). It walks every realm, province, and
location in the polygon-stage Political product, samples the biome
raster at each polygon's label anchor, and emits a procedurally-
generated per-biome name into the polygon's name field. The
upstream political::generate deliberately leaves every name as
String::new() so the naming stage is the single source of truth for
political name strings.
Architecture
The namer follows a Stellaris-shaped per-region grammar
(stages/naming.rs): each biome owns a stem bank and a suffix bank,
and a deterministic splitmix64-mixed seed selects one of each plus
an optional rare connector. Reference points: Civilization VI per-civ
city name lists, CK3 per-culture authored token lists, EU4 / EU5
per-culture historical province names, Stellaris per-species and per-
region token banks with format grammars, Old World's "uniqueness
wins over naturalness" rule with ordinal disambiguators.
Composition grammar:
name = <stem> + [<connector>] + <suffix> [+ <disambiguator>]
Connectors (-of-, -on-, -by-) inject at ~30% probability per
slot seeded by the namer mix. Disambiguators
(" Lower", " Cross", " Hold", " Mark", " Watch",
" Reach", " Old", " High") layer on only after the namer has
exhausted re-rolls within the primary bank.
NamingContext
The namer is a pure function of NamingContext in
generator/src/stages/naming.rs (see stages::naming::NamingContext). The struct carries the world
seed, realm / province / location numeric IDs, the local
location_index_within_province, the biome tag for the slot, and a
reserved culture_tag: Option<&str> slot for future per-culture
variation (currently always None). No global state and no
non-determinism-prone hashing — every emission is reproducible from
the same context.
Capacity guarantee
Each biome carries ≈80 stems × ≈40 suffixes ≈ 3,200 base candidates;
across six biome tables that totals ≈19,200 base candidates. The
namer's splitmix64 re-roll loop (BASE_REROLL_ATTEMPTS = 4096 per
disambiguator layer) plus the connector multiplier (4 variants
including "no connector") and disambiguator layers (1 base + 8
disambiguators = 9 layers) lifts the effective realm-scoped capacity
to well over 160,000 distinct outputs. The
namer_capacity_exceeds_continent_max_locations unit test in
stages::naming::tests drives 160,000 synthetic contexts across
1,000 provinces × 6 biomes and asserts ≥ 160,000 unique strings.
Collision policy
On every emission the namer:
- Composes a candidate from the seed via
compose_name. - If the candidate collides with an already-emitted name in the
realm-scoped
BTreeSet, re-rolls the seed viasplitmix64up toBASE_REROLL_ATTEMPTStimes within the same bank. - If the primary bank cannot produce a unique candidate, the namer
walks the
DISAMBIGUATORSpool (8 entries), each layer running anotherBASE_REROLL_ATTEMPTSre-rolls of<base><disambiguator>. - If every layer collides, the namer
panic!s with the realm / province / location / biome context. There is no silent re-issue and no placeholder fallback.
This panic-on-exhaustion contract is the no-fallback semantic — the same one that governs every other required mapv10 artifact.
Pipeline split
The polygon stage (political::generate) and the naming stage
(political_naming::run) divide responsibility cleanly:
| Concern | Owner |
|---|---|
| Realm / province / location polygon shapes | political::generate |
numeric_id, province_numeric_id, realm_numeric_id assignment | political::generate |
province_id / location_id rasters | political::generate |
neighbor_graph adjacency | political::generate |
province_color_seeds LUT | political::generate |
Realm / province / location name strings | political_naming::run |
Per-biome routing of names via biome raster sample | political_naming::run |
| Realm-scoped collision detection and disambiguation | political_naming::run |
| Panic on disambiguator exhaustion | political_naming::run |
The order in pipeline.rs is biomes_materials::generate →
political::generate → political_naming::run →
validate_political_products. The polygon validator and every
downstream consumer (map_features, valenar_worlddata, the
viewer's LabelAnchors consumer) reads names that already reflect
the naming stage's output.
Noise Stack
All procedural noise in the generator routes through one primitive:
rotated_gradient_fbm at noise.rs. Each octave is summed at
its own per-octave seed seed ^ splitmix64((octave + 1).wrapping_mul(0x9e3779b97f4a7c15)) and the
fBm sum is amplitude-normalized to [-1, +1] by dividing by the
sum of per-octave amplitudes. The lattice gradient noise itself
samples one of 8 fixed unit gradients
(±1, 0), (0, ±1), (±sqrt(2)/2, ±sqrt(2)/2)
selected by lattice_hash(seed, ix, iy) and uses the quintic fade
6t^5 - 15t^4 + 10t^3 (noise.rs).
Four named octave tables drive the procedural surfaces:
TERRAIN_MACRO_NOISE_OCTAVES— 4 octaves at 940 / 520 / 280 / 150 km wavelengths with amplitudes 1.00 / 0.58 / 0.31 / 0.16 (heightfield/mod.rs). Used bycontinental_macro_reliefto shape the cross-continent uplift / lowland template.TERRAIN_DETAIL_NOISE_OCTAVES— 3 octaves at 140 / 74 / 38 km wavelengths with amplitudes 1.00 / 0.46 / 0.19 (heightfield/mod.rs). Used as the per-cell detail relief that gets multiplied by 58 m on land and as the per-node ridge-peak jitter modulator.RIDGE_JAGGED_NOISE_OCTAVES— 3 octaves at 360 / 190 / 96 km wavelengths with amplitudes 1.00 / 0.52 / 0.24 (heightfield/mod.rs). Used as the along-edge jagging factor for ridge influence.ECOLOGICAL_NOISE_OCTAVES— 4 octaves at 320 / 170 / 92 / 48 km wavelengths with amplitudes 1.00 / 0.55 / 0.30 / 0.16 (biomes_materials.rs). Used by the biome and material classifier as the ecological variation field.
The per-octave rotation (cos_theta, sin_theta) is what makes the
output non-axis-separable: a noise test
(noise.rs) explicitly verifies that horizontal-band and
vertical-band differences span more than 0.01 across a 24-sample
sweep.
What the noise stack does NOT do: there is no domain warping
anywhere in the stack — sample positions feed straight into
gradient_noise_2d per octave with no displacement. There is no
ridged-multifractal — no abs() on octave outputs, no 1 - abs(noise) ridge folding. The fBm is pure amplitude-normalized
summation.
Coordinate and Precision Boundary
The generator works in f64 km horizontally and f64 metres
vertically inside pipeline.rs::build_products and every stage. The
hand-off to the renderer is at stages/meshes.rs::push_vertex
(meshes.rs) where each MeshBuffer position is downcast
to f32 after rebasing to the mesh asset's originKm:
self.positions.push([
(vertex.world_x - self.tile.bounds_km.min_x) as f32,
vertex.elevation_km as f32,
(vertex.world_y - self.tile.bounds_km.min_y) as f32,
]);
Elevation is converted from metres to kilometres at the same write
site by reading self.height[idx] as f64 * 0.001 into
elevation_km (meshes.rs). Vertex positions are tile-local in
kilometres on all three axes; the renderer reconstructs world-space
by adding MeshAssetRef.originKm.
The f32 unit-roundoff budget at
readiness.rs is 1 / 8_388_608 per kilometre with a
max_allowed_roundoff_km = 0.005. The stage 13 readiness report
folds local_bounds_km over every mesh asset to compute the maximum
tile-local extent and verifies max_extent * (1 / 8_388_608) <= 0.005.
For a continent-preset tile with worst-case local extent of roughly
38 km (continent raster spans 2 400 km / 64 tiles in x at z5
plus skirts), the reported f32 roundoff sits around 0.0000045 km —
three orders of magnitude inside the budget.
For the high-level coordinate convention (top-left origin, Y-down
rows, kilometres horizontal, metres elevation), see
architecture.md § Coordinate System And Units.
Determinism Contract
The generator is byte-deterministic for any fixed
(scale_preset, seed, raster_width, raster_height, world_width_km, world_height_km) tuple within a single architecture and Rust
toolchain. The contract is enforced by a registered test.
Seed flow. The CLI --seed becomes config.seed: u64
(config.rs). Each stage XORs the base seed with a fixed salt
before drawing noise. Examples from the heightfield stage:
detail relief uses config.seed ^ 0xa6d0_84c5_31d2_4ef1
(heightfield/mod.rs); shelf relief uses config.seed ^ 0x54f8_d80f_f56d_1135 (heightfield/mod.rs); macro relief uses
config.seed ^ 0x25d3_8a91_501c_f4d7 (heightfield/mod.rs).
The relaxed_seed_points helper in stage 6 uses
0x7b58_7f5a_9915_2089 for provinces and a per-province
0xa28d_6c2f_3e15_9c01 ^ province.numeric_id as u64 for
Locations (political.rs seed-point relaxation calls).
RNG sources. Two: splitmix64 (a fixed-constant 64-bit hash
defined identically in noise.rs and political.rs)
for hashing seeds and lattice coordinates, and halton(index, base)
quasi-random sequences for blue-noise candidate placement
(political.rs). No OS RNG, no rand crate, no
floating-point clock reads.
Parallelism and ordering. The only intra-stage parallelism is
in stage 3 erosion. Each iteration uses
par_chunks_mut(width).enumerate() — every parallel task writes
to a disjoint output row from the immutable read-buffer view, so the
parallel reduction is associative-by-construction, not order-
dependent. Stage 6's neighbor graph collects edges into a
BTreeSet<(String, String, String)> (political.rs) so
the final Vec<NeighborEdge> ordering is the alphabetical ordering
of the relation/A/B triples regardless of how the scan-order walk
discovered them.
Why byte-identical. Every value written to a product is a
f32/f64/u8/u32 derived deterministically from the
(seed, config, position) triple. No timestamps appear in any
product (the generation timestamp lives only in manifest.json,
written by write_run_envelope outside build_products).
Determinism test. same_config_produces_same_core_product_bytes
at pipeline.rs builds two runs with identical
GeneratorConfig::default() into separate temp directories and
asserts that the on-disk byte length and content match exactly for
each of: config, height, slope, normal, sediment,
flowAccumulation, valenarWorld, valenarWorldMesh.
Potential break points. The determinism test asserts byte
identity within a single in-process build on a single machine; it
does not assert byte identity across CPU architectures or Rust
toolchain versions, so a binary built with a different LLVM
floating-point fast-math setting could in principle produce
different f32 cells. Inside add_raster_adjacency_edges and
add_adjacency_edge (political.rs) the lookup uses
HashMap<u32, String>; this is currently safe because the iterator
that consumes the graph reads BTreeSet edges (not the HashMap
itself) and the HashMap is only used for keyed lookup, but a future
code change that consumed ids.iter() for output ordering would
silently break determinism.
Limitations
The generator works end to end for the four scale presets and emits schema-valid products. The list below records substantive simplifying choices that limit what can be inferred from the outputs. Every item is technical debt.
- River network is hand-authored (synthetic topology).
Stage 4 emits a tiny explicit graph (currently 7 nodes / 5–6 edges) with fixed world-fraction positions and straight or bent centerlines. Lakes are perfect ellipses. This network is not derived from the eroded heightfield or fromflowAccumulation.f32.bin.
Stage 3 produces a real flow accumulation raster via 4,000 iterations of Mei-style hydraulic erosion, but Stage 4 ignores it for topology. The synthetic river graph is a temporary fixture until a gameplay river network (see "Water Layer – Gameplay River Network (Target State)" below) is implemented. This is the largest current mismatch between the quality of the terrain simulation and the water features placed on top of it. - Basin positions are hardcoded fractions. The two basins are
placed at
(W*0.29, H*0.63)and(W*0.68, H*0.72)(geography_graph.rs); the seed has no effect on basin placement, only on the noise that fills around them. A real basin discovery pass would scan the eroded heightfield for closed depressions. - Continent outline is parametric, not noise- or
tectonic-driven. Stage 1 uses a single ellipse plus three
sinusoidal lobe harmonics modulated by
sin(seed as f64)(continent.rs). The outline barely changes between seeds. No noise displacement, no plate-boundary simulation, no archipelago support beyond the single closed land polygon. - Voronoi clipping is O(N^2 * V) per stage 6 cell. The
half-plane clipper at
political.rswalks every other seed for every cell and re-clips the cell ring against each bisector. For the continent preset (~7 000 Locations across ~1 000 provinces) this is the dominant cost of stage 6 by a wide margin. A Fortune sweep-line pass (Fortune 1986) is the canonical replacement. - Lloyd relaxation runs once. Stages 6 calls
relaxed_seed_pointswithiterations = 1(political.rsseed-point relaxation calls). One Lloyd pass leaves visible anisotropy in cell shapes; production-quality Voronoi atlases typically run 3-8. - No domain warping in the noise stack. Sample positions feed
straight into
gradient_noise_2d(noise.rs). Nonoise(x + n(x,y), y + n(x,y))substitution that would break the octave-aligned grid look. - No ridged-multifractal.
rotated_gradient_fbmsums signed octaves; there is noabs()step that would create the sharp ridge-line look characteristic of ridged terrain. The "ridges" in mapv10 come from the explicit ridge graph in stage 2, not from a ridged-noise step. - No mesh simplification or LOD-adaptive geometry. Terrain
meshes are uniform grids per tile at the physical LOD's
sample_step(tile_pyramid.rs). There is no quadtree subdivision, no error-driven simplification (Quadric Error Metrics, Garland- Heckbert 1997), no chunked LOD with stitched borders. - No T-junction resolution between adjacent LOD tiles. The
stage 11
BORDER_CELLS = 1skirt resolves bilinear texture- sampling seams (tile_pyramid.rs) but it does not address geometry skirts at LOD boundaries. The terrain mesh at one z rung next to the next will show T-junctions; production engines emit explicit skirt geometry or stitch quads. - Map features and label anchors are deterministic but not placement-quality-aware. Stage 8 emits one anchor per entity with zoom-band metadata, but it does not resolve label collision, along-spline label placement, or anchor displacement around rendered geometry — that work lives in the renderer and the runtime label system, not the generator.
Water Layer – Gameplay River Network (Target State)
This section captures the intended future direction for rivers and lakes in mapv10, driven by Valenar’s strategic gameplay needs rather than hydrological realism.
Current Gap (Summary)
As of 2026-05, the generator has a high-quality terrain erosion simulation (Stage 3) that produces realistic flow accumulation, but the actual river and lake features (Stage 4) are a small hand-authored synthetic network. This creates two problems:
- Visual rivers and lakes do not follow the eroded terrain in a natural way.
- There is no gameplay-usable river data (flow volume, branching structure, major vs minor classification) that Valenar systems can query for settlement placement, logistics, defense, trade, or activities.
Gameplay Goals for Rivers (Valenar Priority)
Valenar needs rivers to create meaningful strategic differentiation across the map. The key desired behaviours are:
- Branching + flow accumulation: Small streams should merge into larger ones. Flow volume increases as tributaries join. This should be visible and queryable.
- Clear tiering of rivers:
- Major rivers (high flow, wide, difficult/expensive to cross without bridges or ferries, strong trade and irrigation value).
- Medium rivers (moderate crossing cost, useful for local logistics).
- Minor streams (low movement penalty, mainly local fresh water and small irrigation bonuses).
- Gameplay surfaces (examples of what Valenar systems should be able to ask):
- “Is this Location within irrigation range of a medium+ river?”
- “What is the crossing cost modifier for this river segment?”
- “Does founding a settlement here get a fresh-water or trade bonus from a major river?”
- “Is there a navigable river route between these two Territories?”
- “How much supply can be moved along this river?”
Visual fidelity is secondary to having a clean, queryable Gameplay River Network that the rest of the game can build mechanics on.
Recommended Target Architecture
We should produce two related but distinct layers:
| Layer | Owned By | Primary Consumers | Content |
|---|---|---|---|
| Gameplay River Network | Generator (new or extended Stage 4/16) | Valenar Host, Server, AI, Activities, Settlement founding | Directed graph of river segments with flow volume, Strahler order or tier, crossing difficulty, navigability flags, association to Locations/Territories |
| Visual River & Lake Meshes | Generator (current + improved) + Viewer | Renderer (water pass) | Lake polygons, river ribbon meshes, water mask raster. Should eventually be driven by (or at least consistent with) the gameplay network |
Proposed Gameplay River Network data shape (to be refined):
- River nodes (confluences, sources, mouths, major settlements on river)
- Directed river edges/segments with:
- From / To node
- Average flow volume (or binned tier: Minor / Medium / Major)
- Strahler order
- Estimated width category
- Crossing cost multiplier (or “requires bridge/ferry” flag)
- Navigability (can boats/trade move along this segment?)
- Association table: which Locations/Territories are adjacent to which river segments (for irrigation, fresh water, defense bonuses)
- Optional: major river “influence corridors” (buffer zones) for settlement scoring
The network should be derived from the existing flowAccumulation raster (plus height and slope) using a watershed / flow-routing pass, then simplified into a strategic-scale graph. Exact algorithm (D8, D-infinity, threshold tuning, pruning of tiny streams, etc.) is still open for exploration.
Phasing Recommendation
| Phase | Work | Output | Game Value |
|---|---|---|---|
| Phase 1 | Derive a real Gameplay River Network from flowAccumulation + height | New generator product (e.g. gameplayRiverNetwork.json) with nodes, directed edges, flow tiers, Strahler orders | Foundation. Valenar can start writing queries against real branching rivers |
| Phase 2 | Define and emit strategic gameplay attributes | Crossing cost, irrigation potential, navigability, major-river flags per segment | Direct gameplay mechanics become possible |
| Phase 3 | Improve visual rivers to follow the gameplay network | Better-aligned river centerlines and lake shapes (still not full realism) | Visual consistency with gameplay truth |
| Phase 4 | Valenar integration wave | Settlement founding, movement, supply, trade route, and activity systems consume the river data | Rivers become a real strategic factor in the game |
Until Phase 1 lands, any Valenar river-related mechanics should treat the current synthetic network as a temporary stand-in and avoid hard-coding assumptions about its structure.
Source / Symbol Map
| Topic | File | Symbol / section |
|---|---|---|
| CLI entry, output-path resolution | generator/src/main.rs | main, parse_args, prepare_output |
GeneratorConfig, scale-preset table | generator/src/config.rs | GeneratorConfig, ScalePreset, scale_preset_config |
| Pipeline orchestrator | generator/src/pipeline.rs | build_products |
| StageWrite + add_stage | generator/src/pipeline.rs | StageWrite, add_stage |
| Determinism test | generator/src/pipeline.rs | same_config_produces_same_core_product_bytes |
| Continent outline | generator/src/stages/continent.rs | generate_continent and helpers |
| Ridge graph + basins | generator/src/stages/geography_graph.rs | generate_geography_graph and basin helpers |
| Heightfield noise stack | generator/src/stages/heightfield/mod.rs | octave tables and generate_heightfield |
| Erosion physics | generator/src/stages/heightfield/erosion.rs | ErosionParams, erode_heightfield |
| Water | generator/src/stages/water.rs | generate_water |
| Biomes / materials | generator/src/stages/biomes_materials.rs | ecology octave table and generate_biomes_materials |
| Political (Voronoi + adjacency) | generator/src/stages/political.rs | seed relaxation, Voronoi clipping, adjacency builders |
| Procedural namer (per-biome banks, grammar, disambiguation) | generator/src/stages/naming.rs | NamingContext, BiomeNameTable, LocationNamer, ProvinceNamer, RealmNamer, compose_name |
| Political naming stage (biome sampling, name fill) | generator/src/stages/political_naming.rs | run, sample_biome_at |
| Routes | generator/src/stages/routes.rs | generate_routes |
| Map features and labels | generator/src/stages/map_features.rs | generate_map_features, generate_labels |
| Borders 1+JFA | generator/src/stages/borders_sdf.rs | generate_border_sdf, JFA passes |
| Tile pyramid + Toksvig | generator/src/stages/tile_pyramid.rs | tile constants, tile slicing, Toksvig prefilter |
| Meshes (originKm, m to km) | generator/src/stages/meshes.rs | mesh manifest structs, vertex write path, route ribbon build |
| Readiness budgets | generator/src/stages/readiness.rs | readiness budget constants and report builder |
| Previews | generator/src/stages/previews.rs | preview writer dispatch |
| Valenar export | generator/src/stages/valenar_worlddata.rs | file-name helpers and generate projection |
| Noise primitive | generator/src/noise.rs | rotated_gradient_fbm, splitmix64 |