Skip to main content

Generator Audit — mapv10 Visual Foundation (Wave 1 Agent C)

Scope: generator-side Rust only. Viewer not audited here.


config.rs (386 lines)

Public surface

  • pub struct GeneratorConfig — serializable world config
  • pub struct RangeU32 — min/max u32 pair
  • pub struct RangeF64 — min/max f64 pair
  • pub struct ScalePresetSpec — static preset definition (not serialized)
  • pub struct ScalePresetLodSpec — per-LOD spec entry
  • pub fn tile_lod_band_rank(lod_band: &str) -> Option<u8> — numeric rank for sort
  • pub fn label_zoom_band_rank(zoom_band: &str) -> Option<u8> — numeric rank
  • pub fn label_zoom_band_for_lod(lod_band: &str) -> Option<&'static str> — mapping
  • pub fn known_scale_presets() -> &'static [&'static str]
  • pub fn scale_preset_spec(id: &str) -> Option<ScalePresetSpec>

Function inventory

  • config.rs:61 tile_lod_band_rank — returns numeric 0–5 for continent…location-detail
  • config.rs:73 label_zoom_band_rank — returns numeric 0–3 for zoom strings
  • config.rs:83 label_zoom_band_for_lod — maps lod_band → label zoom-band string
  • config.rs:199 GeneratorConfig::from_preset — construct config from named preset
  • config.rs:223 GeneratorConfig::preset — look up ScalePresetSpec from stored id
  • config.rs:227 GeneratorConfig::cell_size_x_km — world width / raster_width
  • config.rs:231 GeneratorConfig::cell_size_y_km — world height / raster_height
  • config.rs:236 known_scale_presets — static list of 4 preset ids
  • config.rs:245 scale_preset_spec — returns static ScalePresetSpec for each of 4 presets

Visual-foundation findings

LOD ladder — continent preset (config.rs:147–190):

z=0 continent 1× 1 tiles sample_step=16
z=1 macro-region 4× 3 tiles sample_step=16 ← SAME step as z0
z=2 realm 8× 6 tiles sample_step=8
z=3 province-cluster 16×12 tiles sample_step=4
z=4 province 32×24 tiles sample_step=2
z=5 location-detail 64×48 tiles sample_step=1

Z5 (location-detail) has sample_step=1 and 64×48 tiles. With raster_width=2048, raster_height=1536, each z5 tile covers 2048/64=32 source columns and 1536/48=32 source rows. Cell size = 2400/2048 ≈ 1.172 km. Z5 tile spans 32 × 1.172 ≈ 37.5 km × 37.5 km. Pixels-per-km at z5: 1/1.172 ≈ 0.854 cells/km (i.e. 1 sample per ~1.17 km). This is strategic-map resolution, not fine terrain resolution.

Z1 "same step as Z0" anomaly (config.rs:155–163): z1 macro-region has sample_step=16, identical to z0 continent. The build_lod_pyramid code (tile_pyramid.rs:920–933) explicitly handles this as a "straight copy" case. This means the z1 raster is byte-identical to z0 — no additional detail is added. The z1 tiles are a re-cut of the same coarse raster into a 4×3 grid.

Rectangle slab risk: The continent preset's world bounds are hardcoded rectangles (config.rs:307–332). The sea region is a full-world rectangle with a continent polygon as a hole (continent.rs:83–101). If the viewer renders the sea mesh flat at elevation_km=0, the entire world bounding rectangle will render as a flat slab unless the continent polygon cutout is faithfully triangulated.

Tile sizing: no explicit pixel-dimension assertion per tile. nominal_tile_width_samples = source_width.div_ceil(sample_step) from tile_pyramid.rs:333–344. For z5 on continent: (2048/64).div_ceil(1) = 32 inner samples wide; padded to 32 + 2×1 = 34 by BORDER_CELLS=1.


continent.rs (101 lines)

Public surface

  • pub struct ContinentStageProducts
  • pub fn generate(config: &GeneratorConfig) -> ContinentStageProducts
  • pub fn contract() -> Value

Function inventory

  • continent.rs:16 generate — top-level entry: calls generate_continent, generate_sea_regions
  • continent.rs:43 generate_continent — produces ellipse polygon with sinusoidal lobing via config.seed
  • continent.rs:83 generate_sea_regions — produces outer-rectangle sea region with continent polygon as hole

Visual-foundation findings

Rectangle slab (continent.rs:83–101): The sea region outer ring is a full-world rectangle: (0,0) → (world_width,0) → (world_width,world_height) → (0,world_height) → (0,0). This is the outer boundary that the viewer renders as ocean. If the sea mesh is triangulated at flat elevation_km=0.0 (see meshes.rs:304) this will be a full-world rectangle behind the continent. That is architecturally correct with the continent polygon as a hole — but only if the hole is correctly triangulated. If triangulation fails or is skipped, the viewer renders a solid rectangular sea slab covering the entire world.

Continent polygon shape (continent.rs:43–81): The continent boundary uses sinusoidal lobe noise (amplitude up to 10%, 7%, 4% of the base ellipse). The clamp at world edges (continent.rs:58–59, config.world_width_km * 0.04 … 0.96) means continent boundary points can touch the 4% margin. The sea polygon hole is the continent points verbatim — no buffer. Any triangulation jitter at the margin will produce slivers.

Seed derivation (continent.rs:48): phase = (config.seed as f64).sin() — seed is cast to f64 first, then sin() applied. For seed=20260502, sin(20260502.0) is deterministic but has no guaranteed unique distribution for nearby seeds. This is a cosmetic concern, not a correctness problem.


heightfield/mod.rs (573 lines)

Public surface

  • pub mod erosion — submodule
  • pub struct HeightfieldProducts
  • pub fn generate(config, continent, ridge_graph, basins) -> HeightfieldProducts
  • pub fn contract(config) -> Value
  • pub fn horn_slope_from_height(...) — Horn 1981 slope operator
  • pub fn centered_normals_from_height(...) — centred-difference normals
  • pub fn encode_signed_normal(value: f32) -> i16

Function inventory

  • heightfield/mod.rs:105 generate — drives noise→erosion→slope/normals for full raster
  • heightfield/mod.rs:173 contract — JSON product contract
  • heightfield/mod.rs:200 ridge_influence — adds ridge spine elevation via Gaussian profile
  • heightfield/mod.rs:250 continental_macro_relief — continent-wide large-scale tilt/trough
  • heightfield/mod.rs:280 segment_field — signed distance from a point to a segment
  • heightfield/mod.rs:305 connected_ridge_width — average edge width at a node
  • heightfield/mod.rs:321 gaussian — Gaussian decay kernel
  • heightfield/mod.rs:326 smoothstep_f64 — canonical cubic smoothstep
  • heightfield/mod.rs:334 basin_influence — negative elevation depression for basins
  • heightfield/mod.rs:346 centroid / basin_radius — polygon centroid/radius
  • heightfield/mod.rs:363 deterministic_noise — thin wrapper on rotated_gradient_fbm
  • heightfield/mod.rs:367 compute_slope — calls horn_slope_from_height at full res
  • heightfield/mod.rs:397 horn_slope_from_height — public Horn 1981 operator
  • heightfield/mod.rs:439 centered_normals_from_height — public centred-difference normals
  • heightfield/mod.rs:484 compute_normals — calls centered_normals_from_height at full res
  • heightfield/mod.rs:498 encode_signed_normal — clamp+round to i16

Visual-foundation findings

Edge sharing between tiles (heightfield/mod.rs:388–392): The full-resolution raster uses clamp_to_edge (via .saturating_sub(1) and .min(height-1) stencil bounds in horn_slope_from_height and centered_normals_from_height). This means edge cells produce finite values but the raster does NOT share edge rows between tiles — each tile slices a window out of the same continent-wide LOD raster. The slice_lod_buffer function in tile_pyramid.rs adds BORDER_CELLS=1 padding by reading the LOD raster's neighbour cells. Adjacent tiles thus read overlapping 1-cell-wide strips from the same shared LOD raster, which guarantees continuity under bilinear sampling.

Slope operator mismatch at LODs: The compute_slope at z5 uses horn_slope_from_height with dx_m = cell_size_x_km * 1000. The lod_dx_dy_metres function in tile_pyramid.rs:793 correctly scales Δ by sample_step. The contract() comment (heightfield/mod.rs:185) confirms Horn is "harmonized across z5 and the LOD pyramid". No bug found here.

No shared-edge vertex duplication: the heightfield is a flat Vec<f32> indexed by row×width+col. The tile_pyramid slicer reads overlapping margins; there is no mesh-vertex deduplication concern at the raster level. At the mesh level, TerrainMeshBuilder uses a HashMap<TerrainVertexKey, u32> (meshes.rs:548) that deduplicates by (row, col) key — adjacent tiles do NOT share vertices; each tile's mesh is self-contained.


water.rs (376 lines)

Public surface

  • pub struct WaterProducts
  • pub fn generate(config, continent, ridge_graph, basins, heightfield) -> WaterProducts
  • pub fn contract(config) -> Value

Function inventory

  • water.rs:23 generate — drives lake, river, raster generation
  • water.rs:56 contract — JSON contract
  • water.rs:81 generate_lakes — ellipse-shaped lake polygons derived from basin centroids
  • water.rs:131 generate_rivers — hand-coded 7-node / 5-edge river graph with bent centerlines
  • water.rs:281 generate_water_rasters — per-cell waterMask (0=dry,1=sea,2=lake,3=river) and riverWidth
  • water.rs:333 river_node — builds RiverNode sampling height from heightfield
  • water.rs:348 bent_centerline — 9-point quadratic-like curve with sinusoidal bend
  • water.rs:368 sample_height — nearest-cell height lookup (floor, no bilinear interpolation)

Visual-foundation findings

Lake shape is a hardcoded ellipse (water.rs:92–116): lakes are closed_ellipse(center, radius_x, radius_y, 28) — 28-point ellipse, not a polygon extracted from the heightfield. The basin depression (basin_influence in heightfield) is a Gaussian well, but the lake polygon does not follow the actual terrain contour. This is a visual-foundation concern: at detailed zoom levels, lake shores will not align with the heightfield mesh.

River graph is hardcoded (water.rs:131–278): nodes are at fixed world-fraction positions (config.world_width_km * 0.105, etc.). The ridge_graph.nodes[1] and ridge_graph.nodes[4] positions are used as sources — these are hard-indexed and will panic if the ridge graph has fewer than 5 nodes.

Water is polylines + raster, not a triangulated mesh at this stage. The water_mask raster is consumed by meshes.rs:write_water_meshes which then produces actual mesh geometry: lake surfaces via earcut triangulation, rivers via ribbon meshes. The polyline is an intermediate product.

sample_height uses floor (nearest), not bilinear (water.rs:368–376): river node elevations and lake surface elevations are nearest-cell sampled. This is adequate for the strategic map but produces stair-step artifacts for water mesh elevation along river banks.

No edge welding between water meshes and terrain: river ribbons (meshes.rs:243–266) are independent meshes floating 0.000_22 km above the heightfield. Terrain mesh vertices are keyed by (row, col) and not shared with river ribbons. There is no gap between them — but the ribbon may not perfectly conform to the terrain if sample_height_km picks a different nearest cell than the terrain mesh vertex position formula.


political.rs (1112 lines)

Public surface

  • pub struct PoliticalProducts
  • pub fn generate(config, continent) -> PoliticalProducts
  • pub fn contract(config) -> Value
  • pub(crate) fn splitmix64(mut value: u64) -> u64

Function inventory

  • political.rs:43 generate — top-level: province Voronoi, location Voronoi, ID rasters, neighbor graph, color seeds
  • political.rs:115 build_province_color_seeds — low-discrepancy HSL palette per location, province-coherent
  • political.rs:157 province_palette_hue — golden-ratio hue assignment per province
  • political.rs:165 hsl_to_rgb8 / hue_to_rgb_channel / float_channel_to_u8 — HSL→RGB
  • political.rs:206 contract — JSON product contract
  • political.rs:242 generate_provinces — Voronoi subdivision of continent polygon
  • political.rs:297 generate_locations — per-province Voronoi subdivision
  • political.rs:377 generate_id_rasters — rasterize province and location polygon IDs
  • political.rs:410 rasterize_location_id / rasterize_polygon_id — bounding-box scan with PIP test
  • political.rs:472 raster_col / raster_row — world-km → raster cell
  • political.rs:484 outside_holes — true if point is outside all holes
  • political.rs:488 target_count — midpoint of a RangeU32
  • political.rs:495 allocate_counts_by_area — proportional allocation of locations to provinces
  • political.rs:529 relaxed_seed_points — Lloyd relaxation on Voronoi seeds
  • political.rs:562 blue_noise_seed_points — Halton+jitter blue noise seed placement
  • political.rs:620 update_nearest_distances — distance-based blue noise update
  • political.rs:635 voronoi_cell_polygons — Sutherland-Hodgman bisector clipping Voronoi
  • political.rs:662 clip_polygon_to_nearer_seed — Sutherland-Hodgman half-plane clip
  • political.rs:698 strip_closure / close_ring / dedupe_open_ring / points_equal — ring utilities
  • political.rs:754 halton — quasi-random low-discrepancy Halton sequence
  • political.rs:765 fract / unit_hash / splitmix64 — hash utilities
  • political.rs:781 generate_neighbor_graph — realm→province→location containment + raster adjacency
  • political.rs:864 add_raster_adjacency_edges / add_adjacency_edge — adjacency from raster scan

Visual-foundation findings

Names are intentionally empty at this stage (political.rs:37–42): a large block comment explicitly states "The name field on every emitted entity is intentionally the empty string here. Stage 6 (political_polygons) shapes the realm, provinces, and locations; the downstream political_naming stage fills in the procedurally-generated per-biome names." Old 120-name pool and "Province N" placeholder deleted.

No collision check in political.rs itself: the generate_provinces and generate_locations functions emit empty name: String::new(). Naming collisions are delegated entirely to political_naming.rs + naming.rs.

Unmatched land cells stay 0 (political.rs:381–408): rasterization only writes non-zero IDs where PIP test passes. A land cell not covered by any location polygon stays at locationId=0. The test unmatched_land_cell_without_location_polygon_owner_stays_zero validates this explicitly. This is the documented contract, not a bug, but viewers must handle locationId=0 as "no owner" rather than as an error.


political_naming.rs (402 lines)

Public surface

  • pub fn run(political: &mut PoliticalProducts, biomes: &BiomeMaterialProducts, config: &GeneratorConfig)

Function inventory

  • political_naming.rs:28 run — drives RealmNamer, ProvinceNamer, LocationNamer over all political entities; panics on empty name output
  • political_naming.rs:170 sample_biome_at — floor-clamp nearest-cell lookup into biome raster; returns 0 for out-of-bounds anchors
  • political_naming.rs:196 refresh_neighbor_graph_names — intentional no-op stub

Visual-foundation findings

Naming is scoped per-realm (political_naming.rs:121): location_namer = LocationNamer::new() is created once per run() call — it is realm-scoped. The emitted: BTreeSet<String> accumulates all previously assigned location names within the realm. Uniqueness is enforced at realm scope, not province scope.

"Sheafhaugh" duplication root-cause analysis: naming.rs:165–169 contains PLAIN_SUFFIXES which includes "haugh" at index 31. PLAIN_STEMS at naming.rs:155–162 includes "Sheaf" at index 46. compose_name at naming.rs:603–616 forms stem + suffix (or stem + connector + suffix). The string "Sheafhaugh" = stem "Sheaf" + suffix "haugh" is therefore a valid output from the plain biome table. Whether two entities receive this same name depends on whether the LocationNamer's emitted set successfully blocks the collision. The collision-prevention path is: LocationNamer::next_location_namedisambiguatereroll_within_bank — which checks self.emitted.contains(&candidate) before returning. If "Sheafhaugh" appears twice, it means one of: (a) two separate LocationNamer instances were used (one per province, not one per realm); (b) the namer was not called through run() for all entities; or (c) the run was split across separate generation calls. Looking at political_naming.rs:121: let mut location_namer = LocationNamer::new() is created ONCE and used for ALL locations in the realm — this is correct realm-scoping. However, if any path outside political_naming::run calls generate then serializes without calling run, the empty-string names survive and two different serialization runs could emit the same names. No path in the audited code shows this happening, but the concern would be at the orchestration level in main.rs (not audited here).

refresh_neighbor_graph_names is a no-op (political_naming.rs:196–198): the neighbor graph nodes do not carry a name field in the current schema. This means debug overlays reading the neighbor graph will see absent names.

sample_biome_at silent fallback at boundary (political_naming.rs:183–186): if label_anchor lands outside the raster (col < 0, row < 0, or beyond dimensions), returns 0 (sea biome id). This routes through biome_tag_for_id(0) which returns NoneDEFAULT_TABLE (plain stems/suffixes). This is a non-flagrant silent default but is documented as "only triggered by test fixtures".


routes.rs (1282 lines)

Public surface

  • pub struct RouteProducts
  • pub fn generate(config, locations, neighbor_graph, heightfield, water) -> RouteProducts
  • pub fn contract() -> Value

Function inventory

  • routes.rs:58 generate — full route pipeline: nodes, candidates, edge selection, centerlines, crossings
  • routes.rs:123 contract — JSON contract
  • routes.rs:140 crossing — construct a CrossingAnchor
  • routes.rs:159 generate_crossings — route×river intersection search
  • routes.rs:199 build_route_nodes — one node per location, identify province hubs
  • routes.rs:214 province_hub_location_ids — most-central location per province
  • routes.rs:237 province_centrality_cost — sum of squared distances to siblings
  • routes.rs:247 build_route_candidates — neighbor-graph adjacency + spatial fallback pairs
  • routes.rs:298 add_sparse_spatial_fallback_pairs — 2-nearest-neighbor fallback per node
  • routes.rs:320 insert_pair_source — deduplicated pair insertion
  • routes.rs:332 build_route_candidate — terrain-aware quadratic centerline + cost
  • routes.rs:379 assign_location_connection_ids — sequential connection-id assignment
  • routes.rs:385 connection_kind — "river-crossing" | "land"
  • routes.rs:393 terrain_aware_centerline — 5-candidate midpoint selection minimizing cost
  • routes.rs:451 quadratic_points — 9-point quadratic Bézier
  • routes.rs:471 average_route_penalty — per-segment slope/elevation/water penalty
  • routes.rs:497 terrain_penalty_at — slope + highland + lake penalty at a cell
  • routes.rs:524 raster_index — bounds-checked cell lookup
  • routes.rs:539 water_crossing_count — river intersection count per route
  • routes.rs:548 route_importance / route_width_km / route_kind — importance/width/kind from crossing flags
  • routes.rs:586 select_strategic_edges — Kruskal-MST + degree cap to ~1.3× node count
  • routes.rs:645 materialize_route_products — route edges + centerlines from selected plans
  • routes.rs:678 materialize_location_connection_graph — full candidate graph product
  • routes.rs:727 nearest_location_id — min-distance to label_anchor
  • routes.rs:739 squared_distance / ordered_pair — utilities
  • routes.rs:751 route_river_crossing_point — segment intersection + proximity fallback
  • routes.rs:780 segment_intersection — line segment intersection
  • routes.rs:797 cross / DisjointSet impl — utilities

Visual-foundation findings

No zoom-band field on route records themselves (routes.rs): RouteEdge carries route_kind, width_km, importance. The zoom-band visibility filtering is done in meshes.rs:route_detail_asset_visible_in_tile_lod and route_aggregate_min_importance. The route records do not carry explicit min_zoom_band / max_zoom_band fields — visibility is computed at mesh-generation time.

Aggregate meshes for z0–z3 only (meshes.rs:402): route aggregate meshes are written for z in 0..=3; at z>=4 (province and location-detail), only per-route individual meshes with route_detail_asset_visible_in_tile_lod filtering are written. Individual route meshes require lod_band to be "province" or "location-detail" to be included.

Route determinism: candidates are sorted (routes.rs:288–295) by cost then by location id strings before selection — fully deterministic given the same neighbor graph and seed. The neighbor graph itself is deterministic (raster adjacency scan in consistent order, BTreeSet for deduplication).


map_features.rs (321 lines)

Public surface

  • pub struct MapFeatureProducts
  • pub fn generate(config, ridge_graph, water, realm_polygons, province_polygons, locations, routes) -> MapFeatureProducts
  • pub fn contract() -> Value

Function inventory

  • map_features.rs:19 generate — constructs 6 hardcoded feature anchors + 3 footprints, then calls generate_labels
  • map_features.rs:141 contract — JSON product contract
  • map_features.rs:153 generate_labels — emits labels for realms, provinces, locations, lakes, crossings, map features
  • map_features.rs:247 feature — construct MapFeatureAnchor
  • map_features.rs:267 footprint — construct MapFeatureFootprint
  • map_features.rs:281 label — construct LabelAnchor with min/max zoom bands and category
  • map_features.rs:306 nearest_location_id — min-distance centroid fallback

Visual-foundation findings

All 6 feature anchors are hardcoded (map_features.rs:35–90): "Westwatch Harbor", "Crown Pass", "Crown Ford", "Mirrorfen", "Greenfold Woods", "Westmere Shore" are literal string names at fixed world-fraction positions. These are not generated from content — they will not vary with different seeds.

Label zoom bands are set (map_features.rs:163–244): every label record carries min_zoom_band, max_zoom_band, and category (see generate_labels). Realm labels: min="continent", max="realm". Province labels: min="realm", max="province". Location labels: min="province", max="location". Lake labels: min="realm", max="location". These answer Q4 affirmatively.

Feature name uniqueness: the 6 feature anchor names are hardcoded literals. There is no collision check. If the upstream wave runs multiple presets, the same 6 names appear regardless of seed. This is intentional for the current single-realm implementation.

Map-features label for "Crown Ford" indexes crossing_anchors.anchors[0] (map_features.rs:57): this will panic (index out of bounds) if generate_crossings in routes.rs produced zero crossings — possible if no route intersects any river. There is no guard.


tile_pyramid.rs (2774 lines)

Public surface

  • pub const BORDER_CELLS: usize = 1 — bilinear seam padding
  • pub struct TilePyramidProducts
  • pub fn generate(run_dir, config, heightfield, water, biomes, political, borders_sdf, routes, features) -> Result<TilePyramidProducts>
  • pub fn contract(config) -> Value
  • pub fn zoom_levels(config) -> Vec<TileZoomLevel>
  • pub fn tile_coordinates(config) -> Vec<TileCoordinate>
  • pub fn terrain_mesh_key(tile) -> String
  • pub(crate) struct LodLevel / pub(crate) struct LodPyramid — internal pyramid
  • pub(crate) struct BorderSdfLodLevel / pub(crate) struct BorderSdfLodPyramid
  • pub(crate) fn build_lod_pyramid(...) / pub(crate) fn build_border_sdf_pyramid(...)

Function inventory (selected — full module has ~60 functions)

  • tile_pyramid.rs:60 generate — top-level: builds LOD pyramid, then per-tile raster/vector/semantic/mesh fan-out
  • tile_pyramid.rs:277 contract — JSON contract with zoom-aware vector payload spec
  • tile_pyramid.rs:325 zoom_levels — derives TileZoomLevel from preset specs
  • tile_pyramid.rs:348 tile_coordinates — thin wrapper on tile_coordinates_with_lod_metadata
  • tile_pyramid.rs:352 tile_coordinates_with_lod_metadata — generates full tile list with bounds and source windows
  • tile_pyramid.rs:428 tile_id — "zZ.X.Y" string
  • tile_pyramid.rs:432 parent_tile_id / child_tile_ids — tile hierarchy navigation
  • tile_pyramid.rs:484 tile_height_bounds_and_error — per-tile height range + geometric error
  • tile_pyramid.rs:523 fallback_geometric_error_meters — formula-based error estimate
  • tile_pyramid.rs:541 tile_cost_metadata — estimated decode ms / upload bytes / triangles / texture bytes
  • tile_pyramid.rs:557 terrain_mesh_key — "terrain.zZ.X.Y"
  • tile_pyramid.rs:582 LodLevel struct definition with all channels
  • tile_pyramid.rs:672 build_border_sdf_pyramid — iterative-2:1 cascade for SDF channels
  • tile_pyramid.rs:793 lod_dx_dy_metres — LOD-scaled cell size in metres for Horn/normals
  • tile_pyramid.rs:863 build_lod_pyramid — two-pass iterative-2:1 mip pyramid with Toksvig roughness
  • tile_pyramid.rs:1033 sample_step_ratio — asserts power-of-2 ratios between LOD steps
  • tile_pyramid.rs:1054 cascade_box_mean_f32 / cascade_box_mean_rgba8 / cascade_mode_u8 / cascade_border_sdf_2to1
  • tile_pyramid.rs:1126 box_mean_2to1_f32 / box_mean_2to1_rgba8 / mode_2to1_u8
  • tile_pyramid.rs:1254 toksvig_roughness_from_finer_lod — Toksvig normal-roughness prefilter
  • tile_pyramid.rs:1360 slice_lod_buffer<T> — padded per-tile slice with clamp-to-edge
  • tile_pyramid.rs:1404 write_lod_f32_tile_asset / write_lod_rg16_tile_asset / write_lod_u8_tile_asset / write_lod_rgba8_tile_asset
  • tile_pyramid.rs:1510 write_raster_tile_assets — writes 9 raster channels per tile
  • tile_pyramid.rs:~1700 write_semantic_tile_assets — writes provinceId / locationId / waterMask u32/u8 rasters
  • tile_pyramid.rs:~1900 write_vector_tile — zoom-aware JSON vector tile with entity filtering
  • tile_pyramid.rs:~2100 write_tile_height_preview / write_tile_location_preview — PNG previews

Visual-foundation findings

Iterative-2:1 LOD pyramid is implemented (tile_pyramid.rs:863–1031): the earlier single-step "labyrinth bug" artifact has been addressed. The cascade uses cascade_box_mean_f32 / cascade_mode_u8 iteratively through power-of-2 ratios, not a single-step direct-from-source downsample. The Toksvig roughness sidecar is computed in a second pass.

Raster tiles are padded with BORDER_CELLS=1 (tile_pyramid.rs:47): each per-tile raster is (inner_W + 2) × (inner_H + 2). This is the canonical bilinear-seam fix. The slice_lod_buffer function reads the LOD raster's neighbour cells for the border ring — adjacent tiles share the same LOD raster source, guaranteeing matching values at seam texels.

Z1 straight-copy from Z0 (tile_pyramid.rs:920–933): when ratio == 1 (z0 and z1 both have sample_step=16 on continent), the height, water_mask, biome, and material channels are cloned byte-for-byte. The z1 tile rasters have identical content to z0 but are emitted as a separate 4×3 grid. The viewer must handle this without visible artifact.

No bbox-quad terrain mesh (meshes.rs:459–527): terrain_mesh_for_tile iterates over sampled grid cells and emits two triangles per cell via TerrainMeshBuilder. It is NOT a bounding-box quad — it is a full grid mesh conforming to the heightfield.

Sea surface mesh triangulates with earcut (meshes.rs:665–715): polygon_surface_mesh uses the earcutr crate with hole support. This is not a bbox quad; it correctly triangulates the sea outer boundary with the continent polygon as a hole.

Route aggregate mesh at z0–z3 only (meshes.rs:402): LODs z4 and z5 use per-route individual meshes with route_detail_asset_visible_in_tile_lod filter.


meshes.rs (1351 lines)

Public surface

  • pub struct MeshProducts
  • pub fn generate(run_dir, config, heightfield, water, continent, sea_regions, political, routes) -> Result<MeshProducts>
  • pub fn contract() -> Value
  • pub struct MeshBuffer

Function inventory

  • meshes.rs:29 generate — drives terrain/water/route mesh writing
  • meshes.rs:134 contract — JSON contract
  • meshes.rs:162 write_terrain_meshes — per-tile terrain grid meshes
  • meshes.rs:194 write_water_meshes — sea surface + lake surfaces + river ribbons
  • meshes.rs:270 write_sea_surface_meshes — earcut triangulation of sea regions
  • meshes.rs:324 write_route_meshes — aggregate + per-route ribbons per z
  • meshes.rs:392 write_route_aggregate_meshes — z0–z3 aggregate ribbons
  • meshes.rs:459 terrain_mesh_for_tile — grid mesh with all-sea fast-path
  • meshes.rs:529 TerrainMeshBuilder implementation
  • meshes.rs:656 lake_surface_mesh — earcut with holes
  • meshes.rs:665 polygon_surface_mesh — earcut with holes
  • meshes.rs:717 append_ring_vertices — flatten ring into earcutr coordinate array
  • meshes.rs:724 ribbon_mesh / append_ribbon_mesh — per-segment quad ribbon
  • meshes.rs:790 write_mesh_asset — write .mesh.bin + produce MeshAssetRef
  • meshes.rs:842 tile_membership_refs — TileMembershipRef conversion
  • meshes.rs:854 mesh_axis_indices — sampled axis index sequence with end guarantee
  • meshes.rs:869 strip_closure / sample_height_km / polyline_origin
  • meshes.rs:904 points_intersect_bounds / route_detail_asset_visible_in_tile_lod
  • meshes.rs:918 route_aggregate_min_importance — importance threshold per LOD band
  • meshes.rs:926 RouteMeshVisualBand struct + route_mesh_visual_band + route_visual_width_km
  • meshes.rs:999 local_bounds / write_bytes — utilities

Visual-foundation findings

No bbox-quad emitted anywhere: confirmed — terrain uses TerrainMeshBuilder grid, water uses earcut, routes use ribbon (strip quad per segment). CLAUDE.md tenet "Never bbox-quad a complex shape" is respected.

River ribbon uses per-segment quads (meshes.rs:757–788): each segment of the centerline gets a quad (2 triangles), with width_km defining the half-width. Adjacent segments do NOT share vertices — each segment independently pushes 4 positions. At tight curves this produces visible triangle-fan gaps at segment junctions. No mitigation (e.g. miter joints) is present.

Sea surface at elevation_km=0.0 (meshes.rs:304): polygon_surface_mesh(&sea.outer, &holes, origin, 0.0) — sea surface elevation is hardcoded to 0.0 km. If the terrain has negative elevation (ocean shelf, elevation_range_meters.min = -1200), the sea surface sits at 0 while the terrain mesh below it can have vertices at negative elevation. This produces a "sea floor visible through ocean surface" risk unless the renderer handles depth correctly.

write_sea_surface_meshes assigns all tiles as membership (meshes.rs:285): let membership = tiles.to_vec() — the sea surface mesh is listed as belonging to ALL tiles in the pyramid. This is correct for a continent-wide sea polygon but means the viewer must handle a single mesh artifact linked to thousands of tile entries.

Route ribbon elevation_offset_km = 0.000_8 (meshes.rs:383): route ribbons float 0.8 m above the terrain. The exact height is sampled with sample_height_km (nearest-cell floor lookup). Small-cell artifacts possible where the terrain has rapid height change between adjacent cells.


Specific Question Answers

Q1: LOD ladder and z5 raster sample size

Defined at config.rs:147–190 in CONTINENT_LODS. Z5 has sample_step=1, tile_count_x=64, tile_count_y=48. With raster_width=2048, each z5 tile covers 2048/64=32 source columns. Cell size = 2400/2048 ≈ 1.172 km. Z5 pixel-per-km estimate: 1/1.172 ≈ 0.854 cells/km (approximately 1 sample per 1.17 km).

Q2: Could the generator produce a rectangular slab artifact?

Yes, by design for the sea surface. continent.rs:83–101 generates a full-world rectangle as the sea outer ring. meshes.rs:304 triangulates this at elevation_km=0.0. The continent polygon is passed as a hole. If earcut succeeds (and tests confirm it does), the result is NOT a rectangle slab — it is a correctly-holed mesh. However, if earcut were to fail or the hole polygon were corrupted, the fallback would be an empty mesh (bail! on triangulation failure — meshes.rs:694), not a slab fallback. So the slab risk is during earcut failure, not during normal operation.

For raster tiles: the pyramid emits tiles covering the full world grid, including sea-only tiles. All-sea tiles produce no terrain mesh geometry (meshes.rs:492–509 fast path). The sea surface mesh covers the full world minus the continent hole. This is correct architecture — no spurious raster-behind-vector slab.

Q3: "Sheafhaugh" collision root cause

"Sheafhaugh" comes from PLAIN_STEMS[46]="Sheaf" + PLAIN_SUFFIXES[31]="haugh" in naming.rs:150–170. This is a valid composition from compose_name at naming.rs:603–616. The collision prevention is LocationNamer.emitted: BTreeSet<String> in naming.rs:316–411. The reroll_within_bank function at naming.rs:383–393 checks self.emitted.contains(&candidate) before returning a name. If "Sheafhaugh" was assigned twice, the bug would be: two separate LocationNamer instances (not realm-scoped), OR the location that received the name the second time was processed by a different political_naming::run call (e.g. split across generation runs that each start a fresh namer). In the audited political_naming.rs:121, a single LocationNamer is created and shared across all locations in the realm within one run() call. No duplication path is visible in the generator source. The root cause is likely at the orchestration level (main.rs not audited) or in a scenario where the artifacts from two separate runs were merged in the viewer.

Q4: Label zoom-band tagging

Confirmed present. Every LabelAnchor carries min_zoom_band, max_zoom_band, and category fields. These are written by map_features.rs:281–303 (label constructor function) and generate_labels at map_features.rs:153–244. Realm labels: min="continent", max="realm". Province labels: min="realm", max="province". Location labels: min="province", max="location".

Q5: Water polygons serialized as

Both polylines and triangulated meshes. Outputs:

  • riverCenterlines.json — polylines (list of PointKm), per river edge
  • lakePolygons.json — closed polygon rings, per lake
  • meshes/water/lake-*.mesh.bin — earcut-triangulated surfaces (meshes.rs:656–715)
  • meshes/water/river-*.mesh.bin — per-segment ribbon meshes (meshes.rs:724–788)
  • meshes/water/sea-*.mesh.bin — earcut-triangulated sea surface with land hole

Q6: Determinism and seed plumbing

The generation is fully deterministic given config.seed. The seed flows as:

  • GeneratorConfig.seed (u64, default 20260502) is the root
  • continent.rs:48: phase = (config.seed as f64).sin() — continent shape
  • heightfield/mod.rs:126: config.seed ^ 0xa6d0... — detail noise
  • political.rs:262: relaxed_seed_points(config.seed, 0x7b58..., ...) — province seeds
  • political.rs:334: config.seed ^ province_cell.polygon.numeric_id as u64 — location seeds per province
  • naming.rs:554–596: location_seed, province_seed, realm_seed mix config.seed with numeric IDs
  • All random choices use splitmix64 with XOR mixing of the seed and entity ids

Charter / Governance Lines

  • CLAUDE.md:29 — "Never bbox-quad a complex shape — if a feature has a non-rectangular outline (lakes, regions, areas), extract the actual polygon (marching squares) and triangulate (ear clipping or Delaunay)."
  • CLAUDE.md:28 — "Never use billboards or 2D primitives in a 3D scene where a real mesh is the correct primitive — this applies to: water (rivers, lakes, waterfalls)..."
  • CLAUDE.md:30 — "When you patch the same subsystem 3+ times, the original design is wrong — stop patching, redesign."
  • .claude/rules/orchestrator-mode.md:53–55 — "Not spawn further sub-agents. Forbidden tools: Agent, Task, spawn_task, ScheduleWakeup, CronCreate, and any other scheduling tool. Deferred work belongs in the sub-agent's response text."
  • .claude/rules/mapv10.md:30 — "Missing required generated artifacts must fail loudly."
  • .claude/rules/mapv10.md:34 — "The target is 3D terrain with real water/border/overlay geometry or stable generated data products, not a flat 2D political image."

Risks of Fabricated Source / Invented Syntax / Silent Fallback

  • map_features.rs:57 panic on zero crossings: routes.crossing_anchors.anchors[0] is an unconditional index-0 access. If no route crosses any river, generation panics. No .secs pressure, but a generator panic that could block test runs.
  • water.rs:137–138 ridge_graph.nodes[1] and ridge_graph.nodes[4] are hard-indexed. If a future preset produces a ridge graph with fewer nodes, this panics silently in the middle of water generation.
  • water.rs:104–108 lake names are hardcoded ("Westmere", "Mirrorfen Lake"). These are not procedurally generated and will not honor the no-fallback naming contract at continent scale where multiple lakes would be generated. Currently only 2 basins produce 2 lakes; if the basin count changes, index-based access still only names 2.
  • political_naming.rs:183–186 sample_biome_at returns 0 (sea) for out-of-bounds anchors. This silently routes to the DEFAULT_TABLE (plain stems) without any logged warning. An anchor placed off-continent would produce a plain-biome name for what might be a sea or highland area — silent wrong-palette, not a panic.
  • naming.rs:239–247 DEFAULT_TABLE = PLAIN_TABLE. Used when biome_tag is None. The comment says this "only catches the case where a synthetic NamingContext is built directly... without going through the political_naming stage's biome sampler." However, if biome_tag_for_id returns None (e.g. for biome id=5 freshwater or id=0 sea), the DEFAULT_TABLE silently produces plain-themed names for those entities without panicking or logging.

Open Questions for the Implement Step

  1. Z1=Z0 byte-identity: is the viewer intended to display z1 as a distinct visual LOD from z0, or is this a known placeholder until the continent preset adds a proper z1 intermediate band? The tile_pyramid.rs:920–933 "straight copy" path suggests this is known behavior, but the visual consequence (no additional detail at z1 vs z0) should be verified.
  2. Sea surface at elevation_km=0 vs terrain negative elevation: the ocean-shelf terrain runs as low as -1200 m. The sea surface mesh sits at 0. Is the viewer expected to render the terrain mesh for the seabed (which includes the coastal seabed via the "all_sea fast-path skip"), or rely on the sea-surface mesh at 0 to occlude the seabed? The current code emits a seabed terrain mesh for coastal (non-all-sea) tiles but not for fully-deep tiles.
  3. River ribbon miter joints: append_ribbon_mesh produces per-segment quads without miter joints at bends. Is visible triangle-fan gapping at river bends acceptable for the current visual target?
  4. "Sheafhaugh" duplication: if the duplication was seen in a specific viewer run, was it from two generation runs merged, or from a single run? The generator source shows no intra-run duplication path, but the orchestration (main.rs not audited) could split the naming stage.
  5. map_features.rs:57 hardcoded index-0 crossing access: should this be guarded with a conditional (no crossing → no Crown Ford feature), or is the crossing guaranteed by the hardcoded river/route layout?

Deferred Work

  • main.rs orchestration not audited — the seed plumbing, stage ordering, and potential multi-run artifact merging should be audited to confirm the Sheafhaugh duplication path.
  • stages/biomes_materials.rs not read — the biome classification logic (which biome IDs map to which biome tags) should be cross-checked against naming.rs:301–310 biome_tag_for_id to confirm no biome class is silently dropped to DEFAULT_TABLE in production runs.
  • stages/borders_sdf.rs not read — the border SDF stage that feeds into the tile pyramid's borderSdf channel is outside this audit's primary scope but was referenced.
  • stages/erosion.rs not read — the Mei 2007 pipe model + Štava 2008 sediment transport implementation details may have independent findings relevant to heightfield visual quality.