test(simulation): post-adversarial workshop benches — population survey, chunk rung, S2 density, global tier
hydrology_equilibrium_bench: bench_population_survey_all_committed_bodies (all 267 real heightmaps solved independently — zero carved cells population- wide, ~0.86s total, byte-exact determinism; closes Troblum B1) + bench_per_basin_size_distribution_real_population (22,270 basins: dense wins 100% for lake carriers). bmv_gridunit_bench: chunk-64m per-cell + deep-step canvas benches (1,838 ns/cell, ~1.7s full 4K; Option D's missing row) + S2 courses-density benches (+38-87% at chunk/block, mechanism traced to station spacing scaling with rung cutoff). bmv_global_tier_bench (new): real per-body radii from systems.db via BodyParamsReader — global tier ~8.85MB PNG across the population (supersedes the ~174MB mis-priced figure), rung-0 derive ~16-21ms/body measured. All #[ignore]d release tests, stability re-run. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1779,3 +1779,37 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRCF0CX1Y5DTD3ER2JCDT27G', 'status', 'review', 'done', NULL, '2026-07-23 18:06:12', '2026-07-23 18:06:12.146', '2026-07-23 18:06:12.146', NULL, 'b2f9dd6df75e6fc00da53a9d85582167', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRZKCN13EF5TD9EZTXMSJ7EG', 'status', 'review', 'done', NULL, '2026-07-23 18:06:12', '2026-07-23 18:06:12.147', '2026-07-23 18:06:12.147', NULL, '03911a8b5220fca15a57cac632fdeeea', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRZKDFB23YFACY2G4M6WRSXC', 'status', 'review', 'done', NULL, '2026-07-23 18:06:12', '2026-07-23 18:06:12.147', '2026-07-23 18:06:12.147', NULL, '9356438925d73d203f8201d9eebfbd09', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRXGSXBZKXTHR7MR5K9H2HRG', 'description', 'Pre-existing architectural quirk found by Dudley during T-1168 integration testing (2026-07-23, not introduced by that batch): derive_district_profile''s cell-aggregate-centre sampling and the window path''s district-origin sampling resolve to genuinely different world positions for ''the same DistrictPos'' — full DistrictProfile output legitimately differs between paths (morphology/elev/moisture) at the same nominal district. His integration test was redesigned to compare the riparian signal at a shared exact world position instead (the right test). Investigate whether any existing batch/window consumer pair assumes positional agreement (city placement vs window rendering?), decide the canonical sampling convention, and either unify or document the divergence as intentional with the reasoning.', 'Pre-existing architectural quirk found by Dudley during T-1168 integration testing (2026-07-23, not introduced by that batch): derive_district_profile''s cell-aggregate-centre sampling and the window path''s district-origin sampling resolve to genuinely different world positions for ''the same DistrictPos'' — full DistrictProfile output legitimately differs between paths (morphology/elev/moisture) at the same nominal district. His integration test was redesigned to compare the riparian signal at a shared exact world position instead (the right test). Investigate whether any existing batch/window consumer pair assumes positional agreement (city placement vs window rendering?), decide the canonical sampling convention, and either unify or document the divergence as intentional with the reasoning.
|
||||
|
||||
D-255 (2026-07-24): the tagged-envelope step-canvas serving (T-1181) depends on batch and window derive agreeing cell-for-cell — this bug now also blocks the step-canvas serving path, and priority is raised accordingly (Tyre disposition, body-map-viewer workshop). No supersession; intent unchanged.', NULL, '2026-07-24 07:12:48', '2026-07-24 07:12:48.893', '2026-07-24 07:12:48.893', NULL, '00522c2052d6e2ef32fc4d363ac60981', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRXKFEJMDEPQ0PZ5Q862DHKG', 'description', 'Jeroen, 2026-07-23, reviewing the T-1170 course captures: ''there are a bunch of graphical cleanups I would want to suggest around line thickness and tapering off to a point at the source to make the map read more fluent, but I feel map polish like that is better off as a single large pass later.'' This ticket is that pass''s collector — park presentation refinements here rather than folding them into feature batches. Seeded items: (1) river line thickness tuning across rungs (beyond the functional per-class defaults Araminta confirmed in T-1170); (2) SOURCE TAPERING — courses should taper to a point at their upstream source rather than starting at full class width (the classic cartographic river grammar; likely per-vertex width via Polygon2D strip or per-segment width ramp, since draw_polyline is single-width); (3) candidates declined-as-scope during T-1170 review: class-tinted color ramp (Araminta: width+opacity sufficient for now, hue is solvable-later). Accumulate further items via pql ticket append as they come up in captures/reviews. Scope discipline: correctness issues (ruled values not rendering, clip bugs) are NEVER parked here — they get fixed in their own batches; this pass is aesthetics only. Schedule: after the nature layer stands (post T-1156 wave 2 at the earliest), as one coherent Araminta-led pass.
|
||||
|
||||
REFERENCE (Jeroen, 2026-07-23): RimWorld''s world map as the fluency benchmark — ''the rimworld maps do roads, rivers, coasts and biomes quite well'' (hex substrate explicitly NOT us; the techniques transfer, the tiling doesn''t). Extracted techniques for this pass: (1) RIVERS AS NETWORKS — visible tributary-joins-trunk convergence, subtle width variation along the run, taper-to-source (his earlier item 2), thin/consistent/restrained overall; (2) ROADS VS RIVERS disambiguated by COLOR + STRAIGHTNESS (grey chord-like vs blue winding), not width — the wave-2 road-rendering rule; (3) COASTS as a shoreline GRADIENT — beach/transition band + subtle near-shore water shading (our coastal/transition morphology class already exists; presentation only); (4) BIOME/RELIEF TEXTURE — stipple/texture density carrying hills/mountains/forest on top of hue; strong fit for us since T-1162''s vegetation-patchiness and relief fields can DRIVE stipple density (texture as data, not decoration); (5) geographic LABELS do heavy fluency lifting — that half lives in T-1169, coordinate the passes. Screenshot reference in the session record 2026-07-23.
|
||||
|
||||
Reference screenshot saved in-repo: docs/design/references/rimworld-world-map-fluency.jpg (+ README note in that dir) — supersedes the ''session record'' pointer in the previous append.', 'Jeroen, 2026-07-23, reviewing the T-1170 course captures: ''there are a bunch of graphical cleanups I would want to suggest around line thickness and tapering off to a point at the source to make the map read more fluent, but I feel map polish like that is better off as a single large pass later.'' This ticket is that pass''s collector — park presentation refinements here rather than folding them into feature batches. Seeded items: (1) river line thickness tuning across rungs (beyond the functional per-class defaults Araminta confirmed in T-1170); (2) SOURCE TAPERING — courses should taper to a point at their upstream source rather than starting at full class width (the classic cartographic river grammar; likely per-vertex width via Polygon2D strip or per-segment width ramp, since draw_polyline is single-width); (3) candidates declined-as-scope during T-1170 review: class-tinted color ramp (Araminta: width+opacity sufficient for now, hue is solvable-later). Accumulate further items via pql ticket append as they come up in captures/reviews. Scope discipline: correctness issues (ruled values not rendering, clip bugs) are NEVER parked here — they get fixed in their own batches; this pass is aesthetics only. Schedule: after the nature layer stands (post T-1156 wave 2 at the earliest), as one coherent Araminta-led pass.
|
||||
|
||||
REFERENCE (Jeroen, 2026-07-23): RimWorld''s world map as the fluency benchmark — ''the rimworld maps do roads, rivers, coasts and biomes quite well'' (hex substrate explicitly NOT us; the techniques transfer, the tiling doesn''t). Extracted techniques for this pass: (1) RIVERS AS NETWORKS — visible tributary-joins-trunk convergence, subtle width variation along the run, taper-to-source (his earlier item 2), thin/consistent/restrained overall; (2) ROADS VS RIVERS disambiguated by COLOR + STRAIGHTNESS (grey chord-like vs blue winding), not width — the wave-2 road-rendering rule; (3) COASTS as a shoreline GRADIENT — beach/transition band + subtle near-shore water shading (our coastal/transition morphology class already exists; presentation only); (4) BIOME/RELIEF TEXTURE — stipple/texture density carrying hills/mountains/forest on top of hue; strong fit for us since T-1162''s vegetation-patchiness and relief fields can DRIVE stipple density (texture as data, not decoration); (5) geographic LABELS do heavy fluency lifting — that half lives in T-1169, coordinate the passes. Screenshot reference in the session record 2026-07-23.
|
||||
|
||||
Reference screenshot saved in-repo: docs/design/references/rimworld-world-map-fluency.jpg (+ README note in that dir) — supersedes the ''session record'' pointer in the previous append.
|
||||
|
||||
RE-SCOPED by D-255 (2026-07-24, body-map-viewer workshop): the per-vertex river tapering/width-grammar work lands on the NEW screen-space annotation layer (T-1182) via Polygon2D strips — draw_polyline cannot taper. The c1 styling-engine blocker is DISCHARGED: measurement ⑥ landed (Image.set_pixel 77.5 ns/cell flat) and the call resolved CPU-first; shaders deferred, not a precondition. This ticket now depends only on the annotation layer landing (blocked-by T-1182, edge added). Not cancelled — re-homed onto the new component, unblocked otherwise.', NULL, '2026-07-24 07:12:51', '2026-07-24 07:12:51.866', '2026-07-24 07:12:51.866', NULL, 'b6d9c3f3d348683b2d1bb07ddd075189', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRXGSXBZKXTHR7MR5K9H2HRG', 'priority', 'low', 'high', NULL, '2026-07-24 07:13:02', '2026-07-24 07:13:02.546', '2026-07-24 07:13:02.546', NULL, '573adb2f20e62cdd046ce932ecb561dd', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRJD6EKPBQE08NA8VHP692W8', 'description', 'T-1153 broke the T-1120/T-1121 golden capture harness: client/tests/visual_scenarios.gd and visual_capture.gd call app._regional_screen._viewer.get_heightmap_texture()/show_body() — both crash against AtlasWindowViewer (regional_screen now wraps the ladder viewer; AtlasViewer is unreachable from nav). Confirmed NOT wired into tests/run-godot or the pre-push hook (no reference in either), so nothing blocks, but the 12 curated atlas_* goldens and the --atlas-matrix capture path are dead until redesigned for the ladder (capture at defined rungs/zooms instead of the fixed heightmap frame; the new canonical orbital frame is the natural replacement for the old whole-body shot). Also decide the fate of the retired AtlasViewer code these harnesses were the last live consumers of — likely deletable once T-1156 (overlay port) settles what carries forward.
|
||||
|
||||
Live round 4 addendum: client/tests/test_atlas_window_overlay_draw_smoke.gd (3 scenarios -> now 2 after removing a flaky harness-sanity check: single-window draw + tile-mosaic draw, both asserting real composited pixels via SubViewport render) is a standalone gdUnit file with its own headless-skip guard, added because this redesign hadn''t landed yet. When this ticket lands, migrate its 2 scenarios into the redesigned tests/visual.json harness (folding the standalone file away) for consistency with the rest of the real-rendering test suite — see docs/DEVOPS.md''s ''Real-rendering test exception'' note for the interim rationale.', 'T-1153 broke the T-1120/T-1121 golden capture harness: client/tests/visual_scenarios.gd and visual_capture.gd call app._regional_screen._viewer.get_heightmap_texture()/show_body() — both crash against AtlasWindowViewer (regional_screen now wraps the ladder viewer; AtlasViewer is unreachable from nav). Confirmed NOT wired into tests/run-godot or the pre-push hook (no reference in either), so nothing blocks, but the 12 curated atlas_* goldens and the --atlas-matrix capture path are dead until redesigned for the ladder (capture at defined rungs/zooms instead of the fixed heightmap frame; the new canonical orbital frame is the natural replacement for the old whole-body shot). Also decide the fate of the retired AtlasViewer code these harnesses were the last live consumers of — likely deletable once T-1156 (overlay port) settles what carries forward.
|
||||
|
||||
Live round 4 addendum: client/tests/test_atlas_window_overlay_draw_smoke.gd (3 scenarios -> now 2 after removing a flaky harness-sanity check: single-window draw + tile-mosaic draw, both asserting real composited pixels via SubViewport render) is a standalone gdUnit file with its own headless-skip guard, added because this redesign hadn''t landed yet. When this ticket lands, migrate its 2 scenarios into the redesigned tests/visual.json harness (folding the standalone file away) for consistency with the rest of the real-rendering test suite — see docs/DEVOPS.md''s ''Real-rendering test exception'' note for the interim rationale.
|
||||
|
||||
RE-SCOPED by D-255 (2026-07-24, body-map-viewer workshop): this becomes the capture harness for the STEPPED render architecture — goldens key on (body, step) canvases, not the retired continuous-zoom frames. The T-1152/T-1153 test suites (coverage-walk, continuous-zoom seams, compute_tile_grid mosaic) retire with their code in T-1182''s change; this re-scoped harness is their replacement and the eyeball-check substrate for every step canvas. More needed than ever — keep, don''t cancel.', NULL, '2026-07-24 07:13:05', '2026-07-24 07:13:05.192', '2026-07-24 07:13:05.192', NULL, 'bedc693c890c890566674646daebd3f7', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRMPSM9GSWK586N5GBP2N22G', 'description', 'PR #192 review (Tyre): the viewer sits at exactly gdlint''s 1000-line cap, twice trimmed to fit, and fuses three separable responsibilities: (a) input handling (_gui_input/_process/_apply_pan_delta/edge-scroll), (b) request orchestration + rung reselection (_maybe_reselect_rung/_maybe_refloat_window), (c) the canonical-frame state machine (_zoom_at zoom floor/_is_at_canonical_frame/_maybe_reset_to_canonical_frame/_canonical_fit_zoom). All six live fix rounds of the T-1152/T-1153 batch traced to held-state/transform/lifetime coupling in this file — extract cluster (c) (+ likely (b)) into its own class BEFORE the next change breaches the cap and forces either a deadline extraction or comment-gutting that loses the hard-won live-round rationale comments. The tile-set (atlas_window_tile_set.gd) and geometry (atlas_window_geometry.gd) extractions are the precedent. Behavior-preserving refactor: the zoom-ladder suites (44+122+20 tests) are the safety net.', 'PR #192 review (Tyre): the viewer sits at exactly gdlint''s 1000-line cap, twice trimmed to fit, and fuses three separable responsibilities: (a) input handling (_gui_input/_process/_apply_pan_delta/edge-scroll), (b) request orchestration + rung reselection (_maybe_reselect_rung/_maybe_refloat_window), (c) the canonical-frame state machine (_zoom_at zoom floor/_is_at_canonical_frame/_maybe_reset_to_canonical_frame/_canonical_fit_zoom). All six live fix rounds of the T-1152/T-1153 batch traced to held-state/transform/lifetime coupling in this file — extract cluster (c) (+ likely (b)) into its own class BEFORE the next change breaches the cap and forces either a deadline extraction or comment-gutting that loses the hard-won live-round rationale comments. The tile-set (atlas_window_tile_set.gd) and geometry (atlas_window_geometry.gd) extractions are the precedent. Behavior-preserving refactor: the zoom-ladder suites (44+122+20 tests) are the safety net.
|
||||
|
||||
SUPERSEDED by the body-map-viewer render architecture (D-255, 2026-07-24): the canonical-frame + rung-reselection state machine this ticket would extract is exactly what changes shape under stepped zoom (no continuous zoom floor, no _canonical_fit_zoom, select_rung retired). Extracting code about to be deleted is waste (Stig''s round-1 call, confirmed by the workshop). The new component''s decomposition (input/pan, step-cross orchestration, two draw layers) is decided fresh in T-1182, not by reviving this ticket. Cancelled.', NULL, '2026-07-24 07:13:14', '2026-07-24 07:13:14.172', '2026-07-24 07:13:14.172', NULL, 'f23e0d9981db5d6f939140cc8545d9d0', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRMPSM9GSWK586N5GBP2N22G', 'status', 'backlog', 'cancelled', NULL, '2026-07-24 07:13:18', '2026-07-24 07:13:18.469', '2026-07-24 07:13:18.469', NULL, '71fa27b107e5220c44fe22feda2b5c2a', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRY9W24PV55Z6FMW8TEM7XER', 'description', 'Jeroen, 2026-07-23, during the T-1170 live rounds: ''I want to discuss and maybe revise the pixel scan/output mechanism for drawing the map. I feel we can be smarter about this and bypass an entire class of errors we are seeing as well as probably improve performance.'' DISCUSSION FIRST — this is a design conversation with Jeroen (pair/workshop mode), not autonomous work; scheduled after the T-1170/T-1168 cycle closes. The error class in evidence, all descending from immediate-mode draw calls inside a zoom-scaled canvas (_canvas.scale = view_zoom): the per-call-site zoom compensation discipline (_zs/_zs_stroke, silently degrading when one site is missed — PR #195 I1, PR #197 stroke sites), Godot''s line-rasterizer width floor (sub-1.0-canvas-unit strokes collapse to hairline — the T-1170 width bug), the radius-smaller-than-stroke draw_arc regime (the mouth-ring investigation), and arguably the wrap-image resolution family. Candidate directions to bring to the discussion: (a) Tyre''s unscaled screen-space sibling layer (positions transformed, sizes literal — already noted on T-1156 wave 2/T-1158); (b) render-to-texture per window/tile at native resolution, composited as textures (sizes become texel-exact, zoom becomes pure texture transform); (c) shader-side composition of the per-cell data (morphology/courses as data textures, coloring+features in fragment shader — likely the performance win); (d) hybrid. Inputs to gather before the discussion: current draw-call counts/frame costs per rung, the T-1175 polish requirements (per-vertex tapering wants Polygon2D strips or shaders anyway), and the wave-2 marker volume. Relates: T-1158 (viewer decomposition), T-1175 (polish pass), Tyre''s PR #197 Q5 note.
|
||||
|
||||
PREP SESSION COMPLETE + BRIEF SIGNED OFF (2026-07-25, Jeroen + lead + Tyre). Jeroen''s design outline captured and clarified over two AskUserQuestion rounds; Tyre delivered governance-delta/implications pass and ratified the brief (zero misrepresentations, zero blocking gaps). Signed-off workshop brief: docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md (session artifacts: jeroen-outline.md, clarifications.md, tyre-implications.md, prep-grounding.md in the same dir). Settled premises: server-side content determination per zoom step; client map-art function; RTT terrain + screen-space vector annotations baseline (locks T-1176 candidates b+a; c1 styling open); GPU presentation-only; stepped zoom (supersedes continuous — recorded at workshop filing); tile=1m only, ''gridunit (at zoom level)'' coined; data resolution tunable (1px ideal, 5x5 fallback); hydrology = deterministic equilibrium; client-primary cache (store shape open: server-side SQLite cache / godot-sqlite / file cache — SQLite ships in the Rust server, NOT the Godot client). Workshop: 5 seats (Dudley/Araminta/Stig/Tyre + Troblum r2), 2 rounds + lead interview, extendable at Jeroen''s call, round 1 GATED on measurements — see blocker edges. Expected Output 2 mandates an explicit deprecation sweep (Clerk audit) of all conflicting DQR records/tickets at filing.', 'Jeroen, 2026-07-23, during the T-1170 live rounds: ''I want to discuss and maybe revise the pixel scan/output mechanism for drawing the map. I feel we can be smarter about this and bypass an entire class of errors we are seeing as well as probably improve performance.'' DISCUSSION FIRST — this is a design conversation with Jeroen (pair/workshop mode), not autonomous work; scheduled after the T-1170/T-1168 cycle closes. The error class in evidence, all descending from immediate-mode draw calls inside a zoom-scaled canvas (_canvas.scale = view_zoom): the per-call-site zoom compensation discipline (_zs/_zs_stroke, silently degrading when one site is missed — PR #195 I1, PR #197 stroke sites), Godot''s line-rasterizer width floor (sub-1.0-canvas-unit strokes collapse to hairline — the T-1170 width bug), the radius-smaller-than-stroke draw_arc regime (the mouth-ring investigation), and arguably the wrap-image resolution family. Candidate directions to bring to the discussion: (a) Tyre''s unscaled screen-space sibling layer (positions transformed, sizes literal — already noted on T-1156 wave 2/T-1158); (b) render-to-texture per window/tile at native resolution, composited as textures (sizes become texel-exact, zoom becomes pure texture transform); (c) shader-side composition of the per-cell data (morphology/courses as data textures, coloring+features in fragment shader — likely the performance win); (d) hybrid. Inputs to gather before the discussion: current draw-call counts/frame costs per rung, the T-1175 polish requirements (per-vertex tapering wants Polygon2D strips or shaders anyway), and the wave-2 marker volume. Relates: T-1158 (viewer decomposition), T-1175 (polish pass), Tyre''s PR #197 Q5 note.
|
||||
|
||||
PREP SESSION COMPLETE + BRIEF SIGNED OFF (2026-07-25, Jeroen + lead + Tyre). Jeroen''s design outline captured and clarified over two AskUserQuestion rounds; Tyre delivered governance-delta/implications pass and ratified the brief (zero misrepresentations, zero blocking gaps). Signed-off workshop brief: docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md (session artifacts: jeroen-outline.md, clarifications.md, tyre-implications.md, prep-grounding.md in the same dir). Settled premises: server-side content determination per zoom step; client map-art function; RTT terrain + screen-space vector annotations baseline (locks T-1176 candidates b+a; c1 styling open); GPU presentation-only; stepped zoom (supersedes continuous — recorded at workshop filing); tile=1m only, ''gridunit (at zoom level)'' coined; data resolution tunable (1px ideal, 5x5 fallback); hydrology = deterministic equilibrium; client-primary cache (store shape open: server-side SQLite cache / godot-sqlite / file cache — SQLite ships in the Rust server, NOT the Godot client). Workshop: 5 seats (Dudley/Araminta/Stig/Tyre + Troblum r2), 2 rounds + lead interview, extendable at Jeroen''s call, round 1 GATED on measurements — see blocker edges. Expected Output 2 mandates an explicit deprecation sweep (Clerk audit) of all conflicting DQR records/tickets at filing.
|
||||
|
||||
DELIVERED (2026-07-24): the body-map-viewer workshop is this ticket''s output. D-255 filed (stepped Atlas render architecture: server-determines-content, client map-art function, six-level ladder Global+Region→Chunk, tagged-envelope wire, three-tier cache, cache-accelerated-pure-function seed-chaining, lakes from settled hydrology) + 12 in-place amendments (D-166/D-192/D-225/D-226/D-227/D-243 + river-courses-t1170 gloss) + Clerk audit clean (2 unswept conflicts found and fixed: t1143 doc superseded banner, CLAUDE.md Phase-4 row) + measurements ①–⑥ and the population survey all MEASURED + implementation chain T-1181–T-1185 with reconciliations (T-1158 cancelled, T-1175/T-1157 re-scoped, T-1174 raised). Full record: docs/workshops/body-map-viewer/ (workshop-outcomes.md; architecture-briefing-final.md is the plain-language companion).', NULL, '2026-07-24 07:13:23', '2026-07-24 07:13:23.420', '2026-07-24 07:13:23.420', NULL, 'e4ef9dbd48334f1181f494730f0a09f3', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRY9W24PV55Z6FMW8TEM7XER', 'status', 'in_progress', 'done', NULL, '2026-07-24 07:13:28', '2026-07-24 07:13:28.217', '2026-07-24 07:13:28.217', NULL, '53966f3039143c089e5941fec3db1c86', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -2619,3 +2619,42 @@ INSERT INTO tickets (record_id, type, parent_record_id, title, description, stat
|
||||
REPURPOSED as body-map-viewer workshop gate measurement ③ (2026-07-25, brief appendix ③: docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md). Unblocked since T-1162 shipped the octave-cutoff plumbing this was blocked on. Scope unchanged (block/tile rung per-cell costs + wire at those granularities); its numbers now price the bottom of the stepped gridunit ladder (~10px per 1m tile bottom-out). Round 1 of the workshop is gated on this landing alongside T-1177/T-1178/T-1179.', 'done', 'medium', NULL, 'server', 'D-226', '2026-07-21 19:51:50.248', '2026-07-23 18:06:12.146', NULL, 'c248f2785286f98c3d60982bcfb9fd85', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRZKDFB23YFACY2G4M6WRSXC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Measurement ⑤: Godot ImageTexture upload cost per step-cross (330K–8.3M px)', 'Workshop measurement for body-map-viewer (brief appendix ⑤; tyre-implications.md §3.5). Measure ImageTexture.create_from_image (and update paths) for 330K–8.3M px images on step-cross — client-side stutter risk for the RTT terrain layer''s hold-fetch-swap. NOT a round-1 gate (may land during round 1), but a real interactive-latency input for Stig Q2. Owner: Stig. Effort est 0.25 day.', 'done', 'medium', NULL, 'client', 'D-226', '2026-07-23 16:27:29.496', '2026-07-23 18:06:12.147', NULL, '149c633463919bfebfbea2bebb8dc566', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRZKCN13EF5TD9EZTXMSJ7EG', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Measurement ④: wire-size table for step-canvas encodings (raw/bit-packed/RLE/PNG + MessagePack round-trip)', 'Workshop gate measurement for body-map-viewer (brief appendix ④; tyre-implications.md §3.3). For a ~330K-gridunit step canvas: dense classification arrays ≈ 4–5 bytes/gridunit ≈ 1.3–1.6 MB/step raw. Produce the comparison table: raw dense vs bit-packed vs run-length vs PNG-encoded raster, with an actual MessagePack round-trip on one real canvas (not arithmetic only). Directly informs the named-feature encoding decision (Araminta Q1) and whether the tagged-envelope migration (D-225 deferral) is forced. Owners: Araminta (encoding) + Dudley (real canvas + numbers). Effort est 0.5 day.', 'done', 'high', NULL, 'server', 'D-225', '2026-07-23 16:27:22.760', '2026-07-23 18:06:12.147', NULL, '47f5693da9e161d70c3389969ff6d051', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FS5XQW73B1P8WKR2BCZBFXZW', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Step-canvas serving: StepCanvasRequest/Response tagged envelope (D-255 wire migration)', 'The D-225-discharging migration per D-255(c): one new Inbound variant (StepCanvasRequest, required marker field step_canvas, extending the proven star_map/city_names/browse ShapeProbe pattern in server/src/bridge/mod.rs) + a dedicated StepCanvasResponse (NOT a field on AtlasLayerResponse — the D-226 §2 ceiling is re-scoped to the legacy carrier). Serves all six rungs of the D-255(a) ladder — rung-0 Global variable-extent is a field value, not a different shape; fixed rungs viewport-sized per D-255(b). Encoding: PNG-per-field dense (8 fields: 6 static + glaciation/flooded sim-state L8 planes) + MessagePack-native sparse (courses, cliffs) per the T-1179 measured table. SERVER CACHE TIERS ARE THIS TICKET''S SCOPE TOO (SI gap 3): the D-203-shaped resident global tier (~8.85MB population-wide, measured) + TTL(detail, time, distance) for finer rungs + SIM_STATE_TTL[field] = 1x the field''s own fastest driving clock-bucket per body class (flooded: tidal bucket moon-bearing / seasonal moonless; glaciation: seasonal — D-227 amendment (1), Dudley+Araminta joint formula). ACCEPTANCE GATE (SI gap 2, mandatory per D-227 amendment (3)): byte-identical output test — cache-hit derive path == cache-miss derive path for every rung; the seed-chaining cache-accelerated-pure-function correctness gate, same shape as T-1170''s window-independence invariant. Implementation note carried from measurement S2: courses cost +38-87% at chunk/block spacing (station count scales inversely with rung spacing — 1,732 vs 867 points/course measured); a station-spacing cap independent of rung spacing is a named candidate optimization, decision at implementation. Design sources: dudley-round2.md §(a), tyre-round2.md §(b), D-255(c)(d)(f). Legacy district_window survives byte-unchanged until its consumer is replaced.', 'backlog', 'high', NULL, 'server', 'D-255', '2026-07-24 07:11:27.800', '2026-07-24 07:11:27.800', NULL, 'd6a9e1781177ade8a4dfadb912ad5e6a', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FS5XT949C8D2JZRWDX2CRZN8', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Client map component: RTT terrain + screen-space annotations + stepped transport (D-255)', 'The two-layer client rebuild per D-255(a)(b)(e): a render-to-texture terrain layer (per-step data canvas colorized CPU-side — c1 ruling: Image.set_pixel, measured 77.5 ns/cell flat, NOT a hand-rolled buffer write which measured 2x slower; L8 planes where single-channel per measurement ⑤; texture.update reuse on step-cross) + an unscaled screen-space annotation sibling (names, glyphs, course polylines, markers — literal px sizes, positions world→screen). Stepped transport: discrete rung index 0-5, scroll steps, cursor-anchored, edge-scroll pan, full-zoom-out reset to the rung-0 Global frame; hold-fetch-swap on step-cross (morph/tween optional cosmetic follow-on, never re-derivation); step-dependent display ratio (1x1 deep/mid, ~5x5 shallow — presentation only, never spacing/cache-key). EXECUTES THE FULL _canvas.scale RETIREMENT PATH in the same change (tyre-round2.md ~line 1074): (1) _canvas.scale/_canvas.position/_apply_transform continuous-scale model; (2) the _zs/_zs_stroke/_zs_ring_radius compensation family in atlas_window_geometry_nature.gd — deleted wholesale; (3) select_rung + MAX_COVERAGE_M + compute_tile_grid (T-1152/T-1153 code); (4) the AtlasViewer/orbital-mosaic-vs-window two-code-path split (collapses to ''Global opener is rung 0, one viewer one path''); (5) _view_zoom float + _canonical_fit_zoom. Their test suites (coverage-walk unit tests, continuous-zoom seam tests, compute_tile_grid mosaic tests) are DELETED in the same change — never leave old goldens asserting against retired frames (the T-1157 dead-goldens rule); replacement coverage lands under re-scoped T-1157. SURVIVING SURFACES — do not delete (SI gap 1): atlas_window_tile_set.gd''s LRU SHAPE survives as the skeleton for the client cache-store ticket (adapt it, do not delete it — it is not on the retirement path); _filter_for_granularity_v2 (NEAREST/LINEAR per rung is still a real RTT draw-time question); compositing/legend/overlay-bar chrome (call-site updates only). Determinism boundary per D-255(e): client interpolates only within the closed server-supplied input set. Design sources: stig-round1.md, stig-round2.md, D-255(a)(b)(e).', 'backlog', 'high', NULL, 'client', 'D-255', '2026-07-24 07:11:47.490', '2026-07-24 07:11:47.490', NULL, '8c2daef21d9f83357442fcf9301da982', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FS5XW0ZMPVYA2CPBBTP88TQC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Client cache store: FileAccess dir, two-axis eviction, version tag, retention cap (D-255)', 'The client-side cache tiers per D-255(d) and Stig''s three-tier spec (stig-round2.md (c)): in-memory LRU (adapting atlas_window_tile_set.gd''s surviving LRU skeleton from the map-component ticket) + disk-backed FileAccess cache dir with index. Two-axis eviction per D-227 amendment (1) — staleness and storage are DISTINCT: geometry entries never go stale (determinism; byte-valid forever) and are evicted only by (2a) time-since-last-visit (Jeroen''s storage-thrift ruling) and (2b) LRU capacity budget, two independent sweeps kept separate; sim-state entries (frozen/flooded planes) carry the real staleness TTL (SIM_STATE_TTL formula — served/invalidated per the serving ticket''s server-side formula). Rung-0 Global entries: retention floor, never swept. HARDENING REQUIREMENTS from D-255(d), both mandatory: (i) per-body deep-rung retention cap (max resident chunk/block-spacing tile count or disk quota per body, independent of the rung-0 floor — the structural closure of the D-226(d) accumulation gap; the AtlasAgentInterface QA channel inherits the same cap); (ii) every persistent entry carries a schema/version tag (project version or generator stamp) — read-time mismatch = cache miss + re-fetch, NEVER decode (the D-192 co-ship boundary; a disk cache survives game updates). Index-entry schema sketch in stig-round2.md (key/file_path/tier/written_at/last_read_at/size_bytes/sim_ttl/retention_floor); sweep triggers on body-open + coarse background timer, never per-frame. Explicitly rejected by the workshop: SQLite in any shape (server-side relocation, godot-sqlite addon) — plain FileAccess + index won on both agents'' independent analyses. Design sources: stig-round2.md (c), dudley-round2.md (d), D-255(d), D-227 amendments (1)(2).', 'backlog', 'high', NULL, 'client', 'D-255', '2026-07-24 07:12:01.789', '2026-07-24 07:12:01.789', NULL, '671ce378f0b2adbde098e4ef64cc8ea7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FS5XXY5GNCQEXYAYQEQD0CHM', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Hydrology into the pipeline: productionize the T-1177 solver + lake morphology sourcing (D-255)', 'Two halves, one ticket (the D-227 lakes amendment''s implementation; converged in araminta-round2.md §(e) + dudley-interview2-response.md CORRECTED section). HALF 1 — productionize: wire server/src/atlas/hydrology_equilibrium.rs (currently an inert workshop prototype, zero non-test callers) into the derive pipeline: solve once per body at rung-0 Global populate (measured ~24ms/body at 512x256; population survey: all 267 bodies ~0.86s), result held with the global tier (D-203-shaped, keep-always). HALF 2 — lake sourcing: MorphologyZone::Lake (discriminant 1 — ALREADY in the frozen D-239 17-zone vocabulary, no widening) becomes sourced from the settled solver: at derive time a gridunit is Lake when bilinear-sampled HydrologyResult.filled_scaled exceeds bilinear-sampled original elevation (the continuous filled-surface comparison — lake edges refine with rung like coastlines, per D-166; NEVER project basin-cell membership as a discrete lookup, which gives blocky non-refining edges), with today''s ocean_fraction_q>=60 heuristic as fallback where no solve exists. This is seed-chaining mechanism B (coarse continuous primitive sampled fresh per rung — D-255(f)). STATIC classification — distinct from the sim-state flooded plane; a lake''s existence must never flicker on a TTL. NO endorheic bit anywhere (ruled: the cue is outflow-course presence, delivered by the separate outlet-wiring ticket; until that lands the map shows lakes without the drains-vs-closed distinction — honest sequencing per D-227 amendment (4)). Byte cost: zero new wire bytes (existing field). Uniform moisture_q=55 scope limit from the survey noted: affects endorheic/overflow split only, never lake extent.', 'backlog', 'medium', NULL, 'server', 'D-227', '2026-07-24 07:12:17.452', '2026-07-24 07:12:17.452', NULL, '3dafd698cf9833cf55198d56384eb275', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FS5XZF4A978WS1J66Q2JBK8C', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Basin-outlet → course-network wiring: the endorheic cue deliverable (D-255)', 'Thread HydrologyResult overflow basins'' resolved outlet_path/spill points into RiverNetwork/courses as D8 downstream continuations, so an overflow lake''s exit river appears in the served courses and an endorheic basin''s absence of one reads as ''no course leaves this lake''. THE RULED ENDORHEIC CUE (D-227 amendment (4), converged Dudley+Araminta): outflow-course PRESENCE — no wire bit, no 18th zone (proportionality: 1,030/22,270 = 4.63% endorheic doesn''t justify widening the frozen vocabulary; the inference is definitionally sound with no misfire case — every Overflow basin has a guaranteed non-empty outlet_path (tested: overflowing_basin_has_nonempty_outlet_path), every Endorheic none). Pre-cleared ADDITIVE by T-1170 Ruling 7b: the RIVER_DOWNSTREAM_TERMINAL sentinel was reserved exactly for endorheic interior sinks, and river_course::build_edges is already no-op-safe on TERMINAL (verified) — not a wire migration. Required regardless of the cue: overflow lakes must show their exit rivers to be hydrologically honest on the map (a lake that drains but shows no outlet river is simply wrong). Until this lands, the map shows lakes (via the lake-sourcing ticket) but not the drains-vs-closed distinction — the recorded honest sequencing. Endorheic basins map to TERMINAL; overflow outlets extend the D8 network from the spill cell along outlet_path. Design sources: dudley-interview2-response.md CORRECTED section, araminta-round2.md §(e), t1177-hydrology.md, D-227 amendment (4).', 'backlog', 'medium', NULL, 'server', 'D-227', '2026-07-24 07:12:29.986', '2026-07-24 07:12:29.986', NULL, 'c328c9c06c251f04977ce9350dc13df8', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRXGSXBZKXTHR7MR5K9H2HRG', 'task', '06FBPPMZNNEV052DBYYY3A897C', 'Batch vs window derive paths sample different world positions for the same DistrictPos', 'Pre-existing architectural quirk found by Dudley during T-1168 integration testing (2026-07-23, not introduced by that batch): derive_district_profile''s cell-aggregate-centre sampling and the window path''s district-origin sampling resolve to genuinely different world positions for ''the same DistrictPos'' — full DistrictProfile output legitimately differs between paths (morphology/elev/moisture) at the same nominal district. His integration test was redesigned to compare the riparian signal at a shared exact world position instead (the right test). Investigate whether any existing batch/window consumer pair assumes positional agreement (city placement vs window rendering?), decide the canonical sampling convention, and either unify or document the divergence as intentional with the reasoning.
|
||||
|
||||
D-255 (2026-07-24): the tagged-envelope step-canvas serving (T-1181) depends on batch and window derive agreeing cell-for-cell — this bug now also blocks the step-canvas serving path, and priority is raised accordingly (Tyre disposition, body-map-viewer workshop). No supersession; intent unchanged.', 'backlog', 'low', NULL, 'server', NULL, '2026-07-23 11:36:27.743', '2026-07-24 07:12:48.892', NULL, '2150cc98e2294ca1d408a8aa18db92b1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRXKFEJMDEPQ0PZ5Q862DHKG', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas map-reading polish pass — river/nature presentation refinements (single large pass)', 'Jeroen, 2026-07-23, reviewing the T-1170 course captures: ''there are a bunch of graphical cleanups I would want to suggest around line thickness and tapering off to a point at the source to make the map read more fluent, but I feel map polish like that is better off as a single large pass later.'' This ticket is that pass''s collector — park presentation refinements here rather than folding them into feature batches. Seeded items: (1) river line thickness tuning across rungs (beyond the functional per-class defaults Araminta confirmed in T-1170); (2) SOURCE TAPERING — courses should taper to a point at their upstream source rather than starting at full class width (the classic cartographic river grammar; likely per-vertex width via Polygon2D strip or per-segment width ramp, since draw_polyline is single-width); (3) candidates declined-as-scope during T-1170 review: class-tinted color ramp (Araminta: width+opacity sufficient for now, hue is solvable-later). Accumulate further items via pql ticket append as they come up in captures/reviews. Scope discipline: correctness issues (ruled values not rendering, clip bugs) are NEVER parked here — they get fixed in their own batches; this pass is aesthetics only. Schedule: after the nature layer stands (post T-1156 wave 2 at the earliest), as one coherent Araminta-led pass.
|
||||
|
||||
REFERENCE (Jeroen, 2026-07-23): RimWorld''s world map as the fluency benchmark — ''the rimworld maps do roads, rivers, coasts and biomes quite well'' (hex substrate explicitly NOT us; the techniques transfer, the tiling doesn''t). Extracted techniques for this pass: (1) RIVERS AS NETWORKS — visible tributary-joins-trunk convergence, subtle width variation along the run, taper-to-source (his earlier item 2), thin/consistent/restrained overall; (2) ROADS VS RIVERS disambiguated by COLOR + STRAIGHTNESS (grey chord-like vs blue winding), not width — the wave-2 road-rendering rule; (3) COASTS as a shoreline GRADIENT — beach/transition band + subtle near-shore water shading (our coastal/transition morphology class already exists; presentation only); (4) BIOME/RELIEF TEXTURE — stipple/texture density carrying hills/mountains/forest on top of hue; strong fit for us since T-1162''s vegetation-patchiness and relief fields can DRIVE stipple density (texture as data, not decoration); (5) geographic LABELS do heavy fluency lifting — that half lives in T-1169, coordinate the passes. Screenshot reference in the session record 2026-07-23.
|
||||
|
||||
Reference screenshot saved in-repo: docs/design/references/rimworld-world-map-fluency.jpg (+ README note in that dir) — supersedes the ''session record'' pointer in the previous append.
|
||||
|
||||
RE-SCOPED by D-255 (2026-07-24, body-map-viewer workshop): the per-vertex river tapering/width-grammar work lands on the NEW screen-space annotation layer (T-1182) via Polygon2D strips — draw_polyline cannot taper. The c1 styling-engine blocker is DISCHARGED: measurement ⑥ landed (Image.set_pixel 77.5 ns/cell flat) and the call resolved CPU-first; shaders deferred, not a precondition. This ticket now depends only on the annotation layer landing (blocked-by T-1182, edge added). Not cancelled — re-homed onto the new component, unblocked otherwise.', 'backlog', 'low', NULL, 'client', 'D-226', '2026-07-23 11:48:08.469', '2026-07-24 07:12:51.865', NULL, '150a69959f11e86bd0d84d33977254dc', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRXGSXBZKXTHR7MR5K9H2HRG', 'task', '06FBPPMZNNEV052DBYYY3A897C', 'Batch vs window derive paths sample different world positions for the same DistrictPos', 'Pre-existing architectural quirk found by Dudley during T-1168 integration testing (2026-07-23, not introduced by that batch): derive_district_profile''s cell-aggregate-centre sampling and the window path''s district-origin sampling resolve to genuinely different world positions for ''the same DistrictPos'' — full DistrictProfile output legitimately differs between paths (morphology/elev/moisture) at the same nominal district. His integration test was redesigned to compare the riparian signal at a shared exact world position instead (the right test). Investigate whether any existing batch/window consumer pair assumes positional agreement (city placement vs window rendering?), decide the canonical sampling convention, and either unify or document the divergence as intentional with the reasoning.
|
||||
|
||||
D-255 (2026-07-24): the tagged-envelope step-canvas serving (T-1181) depends on batch and window derive agreeing cell-for-cell — this bug now also blocks the step-canvas serving path, and priority is raised accordingly (Tyre disposition, body-map-viewer workshop). No supersession; intent unchanged.', 'backlog', 'high', NULL, 'server', NULL, '2026-07-23 11:36:27.743', '2026-07-24 07:13:02.546', NULL, '2750e1728d81d27338e4795b9ab238d6', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRJD6EKPBQE08NA8VHP692W8', 'task', '06FBPPMZNNEV052DBYYY3A897C', 'Redesign the atlas visual-capture goldens for the zoom ladder (visual_scenarios/visual_capture)', 'T-1153 broke the T-1120/T-1121 golden capture harness: client/tests/visual_scenarios.gd and visual_capture.gd call app._regional_screen._viewer.get_heightmap_texture()/show_body() — both crash against AtlasWindowViewer (regional_screen now wraps the ladder viewer; AtlasViewer is unreachable from nav). Confirmed NOT wired into tests/run-godot or the pre-push hook (no reference in either), so nothing blocks, but the 12 curated atlas_* goldens and the --atlas-matrix capture path are dead until redesigned for the ladder (capture at defined rungs/zooms instead of the fixed heightmap frame; the new canonical orbital frame is the natural replacement for the old whole-body shot). Also decide the fate of the retired AtlasViewer code these harnesses were the last live consumers of — likely deletable once T-1156 (overlay port) settles what carries forward.
|
||||
|
||||
Live round 4 addendum: client/tests/test_atlas_window_overlay_draw_smoke.gd (3 scenarios -> now 2 after removing a flaky harness-sanity check: single-window draw + tile-mosaic draw, both asserting real composited pixels via SubViewport render) is a standalone gdUnit file with its own headless-skip guard, added because this redesign hadn''t landed yet. When this ticket lands, migrate its 2 scenarios into the redesigned tests/visual.json harness (folding the standalone file away) for consistency with the rest of the real-rendering test suite — see docs/DEVOPS.md''s ''Real-rendering test exception'' note for the interim rationale.
|
||||
|
||||
RE-SCOPED by D-255 (2026-07-24, body-map-viewer workshop): this becomes the capture harness for the STEPPED render architecture — goldens key on (body, step) canvases, not the retired continuous-zoom frames. The T-1152/T-1153 test suites (coverage-walk, continuous-zoom seams, compute_tile_grid mosaic) retire with their code in T-1182''s change; this re-scoped harness is their replacement and the eyeball-check substrate for every step canvas. More needed than ever — keep, don''t cancel.', 'backlog', 'medium', NULL, 'client', NULL, '2026-07-22 09:42:47.197', '2026-07-24 07:13:05.192', NULL, 'dd52cedc5657190dc60033ffdca63451', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRMPSM9GSWK586N5GBP2N22G', 'task', '06FBPPMZNNEV052DBYYY3A897C', 'Decompose atlas_window_viewer.gd — extract the canonical-frame + rung-reselection cluster', 'PR #192 review (Tyre): the viewer sits at exactly gdlint''s 1000-line cap, twice trimmed to fit, and fuses three separable responsibilities: (a) input handling (_gui_input/_process/_apply_pan_delta/edge-scroll), (b) request orchestration + rung reselection (_maybe_reselect_rung/_maybe_refloat_window), (c) the canonical-frame state machine (_zoom_at zoom floor/_is_at_canonical_frame/_maybe_reset_to_canonical_frame/_canonical_fit_zoom). All six live fix rounds of the T-1152/T-1153 batch traced to held-state/transform/lifetime coupling in this file — extract cluster (c) (+ likely (b)) into its own class BEFORE the next change breaches the cap and forces either a deadline extraction or comment-gutting that loses the hard-won live-round rationale comments. The tile-set (atlas_window_tile_set.gd) and geometry (atlas_window_geometry.gd) extractions are the precedent. Behavior-preserving refactor: the zoom-ladder suites (44+122+20 tests) are the safety net.
|
||||
|
||||
SUPERSEDED by the body-map-viewer render architecture (D-255, 2026-07-24): the canonical-frame + rung-reselection state machine this ticket would extract is exactly what changes shape under stepped zoom (no continuous zoom floor, no _canonical_fit_zoom, select_rung retired). Extracting code about to be deleted is waste (Stig''s round-1 call, confirmed by the workshop). The new component''s decomposition (input/pan, step-cross orchestration, two draw layers) is decided fresh in T-1182, not by reviving this ticket. Cancelled.', 'backlog', 'medium', NULL, 'client', NULL, '2026-07-22 15:04:20.812', '2026-07-24 07:13:14.171', NULL, 'b70e9d80468ec0f8295b561f99c98b2e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRMPSM9GSWK586N5GBP2N22G', 'task', '06FBPPMZNNEV052DBYYY3A897C', 'Decompose atlas_window_viewer.gd — extract the canonical-frame + rung-reselection cluster', 'PR #192 review (Tyre): the viewer sits at exactly gdlint''s 1000-line cap, twice trimmed to fit, and fuses three separable responsibilities: (a) input handling (_gui_input/_process/_apply_pan_delta/edge-scroll), (b) request orchestration + rung reselection (_maybe_reselect_rung/_maybe_refloat_window), (c) the canonical-frame state machine (_zoom_at zoom floor/_is_at_canonical_frame/_maybe_reset_to_canonical_frame/_canonical_fit_zoom). All six live fix rounds of the T-1152/T-1153 batch traced to held-state/transform/lifetime coupling in this file — extract cluster (c) (+ likely (b)) into its own class BEFORE the next change breaches the cap and forces either a deadline extraction or comment-gutting that loses the hard-won live-round rationale comments. The tile-set (atlas_window_tile_set.gd) and geometry (atlas_window_geometry.gd) extractions are the precedent. Behavior-preserving refactor: the zoom-ladder suites (44+122+20 tests) are the safety net.
|
||||
|
||||
SUPERSEDED by the body-map-viewer render architecture (D-255, 2026-07-24): the canonical-frame + rung-reselection state machine this ticket would extract is exactly what changes shape under stepped zoom (no continuous zoom floor, no _canonical_fit_zoom, select_rung retired). Extracting code about to be deleted is waste (Stig''s round-1 call, confirmed by the workshop). The new component''s decomposition (input/pan, step-cross orchestration, two draw layers) is decided fresh in T-1182, not by reviving this ticket. Cancelled.', 'cancelled', 'medium', NULL, 'client', NULL, '2026-07-22 15:04:20.812', '2026-07-24 07:13:18.468', NULL, '955c12d603c68fda38f74a92ab6b942b', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRY9W24PV55Z6FMW8TEM7XER', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Design discussion: revise the Atlas map render mechanism (bypass the zoom-scaled-canvas error class)', 'Jeroen, 2026-07-23, during the T-1170 live rounds: ''I want to discuss and maybe revise the pixel scan/output mechanism for drawing the map. I feel we can be smarter about this and bypass an entire class of errors we are seeing as well as probably improve performance.'' DISCUSSION FIRST — this is a design conversation with Jeroen (pair/workshop mode), not autonomous work; scheduled after the T-1170/T-1168 cycle closes. The error class in evidence, all descending from immediate-mode draw calls inside a zoom-scaled canvas (_canvas.scale = view_zoom): the per-call-site zoom compensation discipline (_zs/_zs_stroke, silently degrading when one site is missed — PR #195 I1, PR #197 stroke sites), Godot''s line-rasterizer width floor (sub-1.0-canvas-unit strokes collapse to hairline — the T-1170 width bug), the radius-smaller-than-stroke draw_arc regime (the mouth-ring investigation), and arguably the wrap-image resolution family. Candidate directions to bring to the discussion: (a) Tyre''s unscaled screen-space sibling layer (positions transformed, sizes literal — already noted on T-1156 wave 2/T-1158); (b) render-to-texture per window/tile at native resolution, composited as textures (sizes become texel-exact, zoom becomes pure texture transform); (c) shader-side composition of the per-cell data (morphology/courses as data textures, coloring+features in fragment shader — likely the performance win); (d) hybrid. Inputs to gather before the discussion: current draw-call counts/frame costs per rung, the T-1175 polish requirements (per-vertex tapering wants Polygon2D strips or shaders anyway), and the wave-2 marker volume. Relates: T-1158 (viewer decomposition), T-1175 (polish pass), Tyre''s PR #197 Q5 note.
|
||||
|
||||
PREP SESSION COMPLETE + BRIEF SIGNED OFF (2026-07-25, Jeroen + lead + Tyre). Jeroen''s design outline captured and clarified over two AskUserQuestion rounds; Tyre delivered governance-delta/implications pass and ratified the brief (zero misrepresentations, zero blocking gaps). Signed-off workshop brief: docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md (session artifacts: jeroen-outline.md, clarifications.md, tyre-implications.md, prep-grounding.md in the same dir). Settled premises: server-side content determination per zoom step; client map-art function; RTT terrain + screen-space vector annotations baseline (locks T-1176 candidates b+a; c1 styling open); GPU presentation-only; stepped zoom (supersedes continuous — recorded at workshop filing); tile=1m only, ''gridunit (at zoom level)'' coined; data resolution tunable (1px ideal, 5x5 fallback); hydrology = deterministic equilibrium; client-primary cache (store shape open: server-side SQLite cache / godot-sqlite / file cache — SQLite ships in the Rust server, NOT the Godot client). Workshop: 5 seats (Dudley/Araminta/Stig/Tyre + Troblum r2), 2 rounds + lead interview, extendable at Jeroen''s call, round 1 GATED on measurements — see blocker edges. Expected Output 2 mandates an explicit deprecation sweep (Clerk audit) of all conflicting DQR records/tickets at filing.
|
||||
|
||||
DELIVERED (2026-07-24): the body-map-viewer workshop is this ticket''s output. D-255 filed (stepped Atlas render architecture: server-determines-content, client map-art function, six-level ladder Global+Region→Chunk, tagged-envelope wire, three-tier cache, cache-accelerated-pure-function seed-chaining, lakes from settled hydrology) + 12 in-place amendments (D-166/D-192/D-225/D-226/D-227/D-243 + river-courses-t1170 gloss) + Clerk audit clean (2 unswept conflicts found and fixed: t1143 doc superseded banner, CLAUDE.md Phase-4 row) + measurements ①–⑥ and the population survey all MEASURED + implementation chain T-1181–T-1185 with reconciliations (T-1158 cancelled, T-1175/T-1157 re-scoped, T-1174 raised). Full record: docs/workshops/body-map-viewer/ (workshop-outcomes.md; architecture-briefing-final.md is the plain-language companion).', 'in_progress', 'high', NULL, 'client', 'D-226', '2026-07-23 13:25:58.949', '2026-07-24 07:13:23.420', NULL, '5e902693de50646d79225005c3c2875d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FRY9W24PV55Z6FMW8TEM7XER', 'story', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Design discussion: revise the Atlas map render mechanism (bypass the zoom-scaled-canvas error class)', 'Jeroen, 2026-07-23, during the T-1170 live rounds: ''I want to discuss and maybe revise the pixel scan/output mechanism for drawing the map. I feel we can be smarter about this and bypass an entire class of errors we are seeing as well as probably improve performance.'' DISCUSSION FIRST — this is a design conversation with Jeroen (pair/workshop mode), not autonomous work; scheduled after the T-1170/T-1168 cycle closes. The error class in evidence, all descending from immediate-mode draw calls inside a zoom-scaled canvas (_canvas.scale = view_zoom): the per-call-site zoom compensation discipline (_zs/_zs_stroke, silently degrading when one site is missed — PR #195 I1, PR #197 stroke sites), Godot''s line-rasterizer width floor (sub-1.0-canvas-unit strokes collapse to hairline — the T-1170 width bug), the radius-smaller-than-stroke draw_arc regime (the mouth-ring investigation), and arguably the wrap-image resolution family. Candidate directions to bring to the discussion: (a) Tyre''s unscaled screen-space sibling layer (positions transformed, sizes literal — already noted on T-1156 wave 2/T-1158); (b) render-to-texture per window/tile at native resolution, composited as textures (sizes become texel-exact, zoom becomes pure texture transform); (c) shader-side composition of the per-cell data (morphology/courses as data textures, coloring+features in fragment shader — likely the performance win); (d) hybrid. Inputs to gather before the discussion: current draw-call counts/frame costs per rung, the T-1175 polish requirements (per-vertex tapering wants Polygon2D strips or shaders anyway), and the wave-2 marker volume. Relates: T-1158 (viewer decomposition), T-1175 (polish pass), Tyre''s PR #197 Q5 note.
|
||||
|
||||
PREP SESSION COMPLETE + BRIEF SIGNED OFF (2026-07-25, Jeroen + lead + Tyre). Jeroen''s design outline captured and clarified over two AskUserQuestion rounds; Tyre delivered governance-delta/implications pass and ratified the brief (zero misrepresentations, zero blocking gaps). Signed-off workshop brief: docs/workshops/body-map-viewer/body-map-viewer-workshop-brief.md (session artifacts: jeroen-outline.md, clarifications.md, tyre-implications.md, prep-grounding.md in the same dir). Settled premises: server-side content determination per zoom step; client map-art function; RTT terrain + screen-space vector annotations baseline (locks T-1176 candidates b+a; c1 styling open); GPU presentation-only; stepped zoom (supersedes continuous — recorded at workshop filing); tile=1m only, ''gridunit (at zoom level)'' coined; data resolution tunable (1px ideal, 5x5 fallback); hydrology = deterministic equilibrium; client-primary cache (store shape open: server-side SQLite cache / godot-sqlite / file cache — SQLite ships in the Rust server, NOT the Godot client). Workshop: 5 seats (Dudley/Araminta/Stig/Tyre + Troblum r2), 2 rounds + lead interview, extendable at Jeroen''s call, round 1 GATED on measurements — see blocker edges. Expected Output 2 mandates an explicit deprecation sweep (Clerk audit) of all conflicting DQR records/tickets at filing.
|
||||
|
||||
DELIVERED (2026-07-24): the body-map-viewer workshop is this ticket''s output. D-255 filed (stepped Atlas render architecture: server-determines-content, client map-art function, six-level ladder Global+Region→Chunk, tagged-envelope wire, three-tier cache, cache-accelerated-pure-function seed-chaining, lakes from settled hydrology) + 12 in-place amendments (D-166/D-192/D-225/D-226/D-227/D-243 + river-courses-t1170 gloss) + Clerk audit clean (2 unswept conflicts found and fixed: t1143 doc superseded banner, CLAUDE.md Phase-4 row) + measurements ①–⑥ and the population survey all MEASURED + implementation chain T-1181–T-1185 with reconciliations (T-1158 cancelled, T-1175/T-1157 re-scoped, T-1174 raised). Full record: docs/workshops/body-map-viewer/ (workshop-outcomes.md; architecture-briefing-final.md is the plain-language companion).', 'done', 'high', NULL, 'client', 'D-226', '2026-07-23 13:25:58.949', '2026-07-24 07:13:28.217', NULL, '0d5ca1f2955ad2d785d4dacda92a26f1', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
//! Body Map Viewer workshop — global-tier byte-cost recheck (Jeroen's
|
||||
//! post-ratification correction to the ladder top).
|
||||
//!
|
||||
//! Jeroen's correction: GLOBAL is rung 0, the body-surface opener, with a
|
||||
//! **VARIABLE canvas = the body's own region grid** (one gridunit PER
|
||||
//! REGION, `regions_per_equator × regions_per_equator/2`, D-243's elastic
|
||||
//! seam) — NOT a fixed 3840×2160 District-spacing canvas the way the round-2
|
||||
//! cache-tier spec's ~174 MB figure assumed. REGION is rung 1, the largest
|
||||
//! FIXED-size rung (viewport-sized, evictable like every other sub-global
|
||||
//! rung). This file recomputes the always-keep global-tier byte budget
|
||||
//! against the CORRECT rung-0 shape, using REAL per-body radii read from
|
||||
//! `systems.db` via [`settled_reach_server::atlas::body_params_reader::BodyParamsReader`]
|
||||
//! (never raw `sqlite3` — the project's asset-pipeline rule) — not an
|
||||
//! assumed Earth-class uniform radius, since region-grid size is the ONE
|
||||
//! per-body-floating quantity in the whole D-243 ladder (the elastic seam)
|
||||
//! and the whole point of this recheck is that assuming Earth-class for
|
||||
//! every body was the error being corrected.
|
||||
//!
|
||||
//! Run: `cargo test --release --test bmv_global_tier_bench -- --ignored --nocapture`
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::Instant;
|
||||
|
||||
use settled_reach_server::atlas::body_params_reader::BodyParamsReader;
|
||||
use settled_reach_server::atlas::district_profile::{
|
||||
derive_orbital_at_metres, BodyParams, ClimateConstants,
|
||||
};
|
||||
use settled_reach_server::atlas::drainage;
|
||||
use settled_reach_server::atlas::features::TerrainAnalysis;
|
||||
use settled_reach_server::atlas::heightmap::BodyHeightmap;
|
||||
use settled_reach_server::atlas::scale;
|
||||
use settled_reach_server::seed::{SeedChain, SeedDomain};
|
||||
|
||||
/// Same body-discovery walk the T-1177 population survey bench
|
||||
/// (`hydrology_equilibrium_bench.rs::bench_population_survey_all_committed_bodies`)
|
||||
/// already established — every committed `heightmap.png`'s parent directory
|
||||
/// name is a real body_id. Reused here (not re-invented) so this bench's
|
||||
/// population is exactly the same 267-body set the hydrology survey already
|
||||
/// covers, for direct comparability.
|
||||
fn discover_body_ids() -> Vec<String> {
|
||||
let wiki_root =
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki/star-systems");
|
||||
let mut ids = Vec::new();
|
||||
fn walk(dir: &std::path::Path, out: &mut Vec<String>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
walk(&path, out);
|
||||
} else if path.file_name().and_then(|n| n.to_str()) == Some("heightmap.png") {
|
||||
if let Some(body_id) = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.and_then(|n| n.to_str())
|
||||
{
|
||||
out.push(body_id.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(&wiki_root, &mut ids);
|
||||
ids.sort();
|
||||
ids
|
||||
}
|
||||
|
||||
/// The global/rung-0 canvas cell count for a body of the given radius:
|
||||
/// `regions_per_equator(R) × (regions_per_equator(R) / 2)` — one gridunit
|
||||
/// PER REGION (Jeroen's ruling), pole-to-pole being half the equatorial
|
||||
/// count per [`scale::regions_per_equator`]'s own doc.
|
||||
fn rung0_cells(body_radius_km: f64) -> u64 {
|
||||
let cols = scale::regions_per_equator(body_radius_km) as u64;
|
||||
let rows = (cols / 2).max(1);
|
||||
cols * rows
|
||||
}
|
||||
|
||||
/// T-1179's measured PNG-per-field rate: 638,382 bytes / 331,776 cells at
|
||||
/// district spacing (the only body+field-set this workshop measured a real
|
||||
/// encode on) = 1.924 bytes/cell PNG-encoded, six dense fields
|
||||
/// (morphology/elev_q/temp_dc/moisture_q/vegetation/glaciation). Applied
|
||||
/// here as the SAME field-set/encoding assumption the round-2 cache-tier
|
||||
/// spec used for the (wrong-shape) 174 MB estimate — this bench corrects the
|
||||
/// CANVAS SHAPE, not the per-cell wire-cost model, so the two numbers are
|
||||
/// comparable apples-to-apples on the encoding axis and differ only on the
|
||||
/// canvas-shape axis Jeroen actually corrected.
|
||||
const PNG_BYTES_PER_CELL: f64 = 638_382.0 / 331_776.0;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_global_tier_bytes_real_population() {
|
||||
let systems_db =
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data/systems.db");
|
||||
let reader = BodyParamsReader::open(&systems_db)
|
||||
.expect("open read-only systems.db (asset-pipeline golden rule: read-only snapshot)");
|
||||
|
||||
let body_ids = discover_body_ids();
|
||||
assert!(
|
||||
body_ids.len() > 200,
|
||||
"expected the real committed body population (~267), found {}",
|
||||
body_ids.len()
|
||||
);
|
||||
|
||||
let mut found = 0usize;
|
||||
let mut missing_radius: Vec<String> = Vec::new();
|
||||
let mut total_cells: u64 = 0;
|
||||
let mut per_body: Vec<(String, f64, u64)> = Vec::new(); // (body_id, radius_km, cells)
|
||||
|
||||
for body_id in &body_ids {
|
||||
match reader.read_body_params(body_id) {
|
||||
Ok(params) => {
|
||||
if let Some(r_km) = params.body_radius_km {
|
||||
let cells = rung0_cells(r_km);
|
||||
total_cells += cells;
|
||||
per_body.push((body_id.clone(), r_km, cells));
|
||||
found += 1;
|
||||
} else {
|
||||
missing_radius.push(body_id.clone());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
missing_radius.push(format!("{body_id} ({e})"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
per_body.sort_by(|a, b| b.2.cmp(&a.2)); // largest region-grid first
|
||||
|
||||
let total_bytes_raw6 = total_cells as f64 * 6.0; // 6 raw bytes/cell, DistrictWindowLayer's own documented figure
|
||||
let total_bytes_png = total_cells as f64 * PNG_BYTES_PER_CELL;
|
||||
|
||||
println!("\n=== Global-tier (rung 0) byte-cost recheck: REAL population, REAL per-body radii ===");
|
||||
println!(
|
||||
" bodies discovered: {}, radius found: {found}, missing/unreadable: {}",
|
||||
body_ids.len(),
|
||||
missing_radius.len()
|
||||
);
|
||||
if !missing_radius.is_empty() {
|
||||
println!(
|
||||
" missing radius (excluded from total, listed for audit): {:?}",
|
||||
&missing_radius[..missing_radius.len().min(10)]
|
||||
);
|
||||
if missing_radius.len() > 10 {
|
||||
println!(" ... and {} more", missing_radius.len() - 10);
|
||||
}
|
||||
}
|
||||
println!("\n total rung-0 cells across population: {total_cells}");
|
||||
println!(
|
||||
" average cells/body: {:.0}",
|
||||
total_cells as f64 / found.max(1) as f64
|
||||
);
|
||||
println!(
|
||||
"\n total bytes, raw 6 B/cell (DistrictWindowLayer's documented rate): {:.2} MB",
|
||||
total_bytes_raw6 / 1_048_576.0
|
||||
);
|
||||
println!(
|
||||
" total bytes, PNG-per-field ({:.3} B/cell, T-1179's measured rate): {:.2} MB",
|
||||
PNG_BYTES_PER_CELL,
|
||||
total_bytes_png / 1_048_576.0
|
||||
);
|
||||
|
||||
println!("\n Top 10 bodies by rung-0 cell count (largest global canvases):");
|
||||
for (body_id, r_km, cells) in per_body.iter().take(10) {
|
||||
let cols = scale::regions_per_equator(*r_km);
|
||||
println!(
|
||||
" {body_id:>12} radius={r_km:>8.1}km regions_per_equator={cols:>4} cells={cells:>7} ({:.1} KB PNG)",
|
||||
*cells as f64 * PNG_BYTES_PER_CELL / 1024.0
|
||||
);
|
||||
}
|
||||
println!("\n Bottom 5 bodies by rung-0 cell count (smallest global canvases):");
|
||||
for (body_id, r_km, cells) in per_body.iter().rev().take(5) {
|
||||
let cols = scale::regions_per_equator(*r_km);
|
||||
println!(
|
||||
" {body_id:>12} radius={r_km:>8.1}km regions_per_equator={cols:>4} cells={cells:>7} ({:.1} KB PNG)",
|
||||
*cells as f64 * PNG_BYTES_PER_CELL / 1024.0
|
||||
);
|
||||
}
|
||||
|
||||
// Earth-class reference point (R=6371km), for direct comparison against
|
||||
// the brief appendix's own "~195x98 Earth-sized" framing.
|
||||
let earth_cols = scale::regions_per_equator(6371.0);
|
||||
let earth_rows = (earth_cols / 2).max(1);
|
||||
println!(
|
||||
"\n Reference: Earth-class (R=6371km) regions_per_equator={earth_cols}, rows={earth_rows}, cells={}",
|
||||
earth_cols as u64 * earth_rows as u64
|
||||
);
|
||||
|
||||
println!();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rung-0 (GLOBAL opener) derive cost — MEASURED, not extrapolated (Tyre's
|
||||
// bracket request). The existing `bench_derive_orbital_at_metres_region_spacing`
|
||||
// (zoom_ladder_bench.rs) measures `derive_orbital_at_metres`'s per-cell rate
|
||||
// at a fixed 4,096-cell (64x64) sweep — that number is real, but scaling it
|
||||
// up to a whole rung-0 canvas by multiplication is exactly the kind of
|
||||
// "EXTRAPOLATED from the measured per-cell rate" label that bench's own
|
||||
// full-canvas rows already carry (explicitly disclosed there, not measured).
|
||||
// This bench instead runs the REAL per-body canvas shape (the actual
|
||||
// regions_per_equator(R) x regions_per_equator(R)/2 grid, real per-body
|
||||
// radius) end-to-end for a representative sample of real bodies, so the
|
||||
// rung-0 derive-cost figure is measured at the shape it will actually be
|
||||
// served at, not inferred from a differently-shaped sweep.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn bench_hm() -> BodyHeightmap {
|
||||
let (w, h) = (128u32, 64u32);
|
||||
let n = (w * h) as usize;
|
||||
let data = (0..n)
|
||||
.map(|i| {
|
||||
let r = (i / w as usize) as f32 / h as f32;
|
||||
let c = (i % w as usize) as f32 / w as f32;
|
||||
(r * 0.6 + c * 0.4).min(1.0)
|
||||
})
|
||||
.collect();
|
||||
BodyHeightmap {
|
||||
body_id: "bench".into(),
|
||||
width: w,
|
||||
height: h,
|
||||
data,
|
||||
sea_level: 0.3,
|
||||
}
|
||||
}
|
||||
|
||||
fn bench_ta(hm: &BodyHeightmap) -> TerrainAnalysis {
|
||||
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
|
||||
TerrainAnalysis::analyze(hm, &dr)
|
||||
}
|
||||
|
||||
/// Real rung-0 canvas derive cost at three representative real body sizes
|
||||
/// from the population (smallest, Earth-class-nearest, largest), single
|
||||
/// call per cell through the actual `derive_orbital_at_metres` function —
|
||||
/// no square-sweep proxy shape, the real `cols x rows` extent each body's
|
||||
/// canvas would actually be.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_rung0_derive_cost_real_canvas_shapes() {
|
||||
let hm = bench_hm();
|
||||
let ta = bench_ta(&hm);
|
||||
let params = BodyParams {
|
||||
hydrosphere: Some("ocean".into()),
|
||||
atmosphere: Some("breathable".into()),
|
||||
planet_class: Some("temperate".into()),
|
||||
body_radius_km: Some(6371.0), // overridden per-case below
|
||||
..Default::default()
|
||||
};
|
||||
let climate = ClimateConstants::default();
|
||||
let seed = SeedChain::root(99).derive(SeedDomain::Body, 1);
|
||||
let region_m = scale::REGION_M as f64;
|
||||
|
||||
println!("\n=== Rung-0 (GLOBAL opener) derive cost — REAL canvas shapes, MEASURED not extrapolated ===\n");
|
||||
|
||||
// Representative cases from the real population survey
|
||||
// (bmv_global_tier_bench.rs::bench_global_tier_bytes_real_population):
|
||||
// smallest (GJ784c-m1, a moon), Earth-class reference, largest (GJ325Ac).
|
||||
let cases: [(&str, f64); 3] = [
|
||||
("GJ784c-m1 (smallest, moon)", 733.9),
|
||||
("Earth-class reference", 6371.0),
|
||||
("GJ325Ac (largest)", 7317.6),
|
||||
];
|
||||
|
||||
// Per-body average cell count across the real population (from the
|
||||
// sibling bench in this file): 18,073 cells/body, 267 bodies.
|
||||
const AVG_CELLS_PER_BODY: u64 = 18_073;
|
||||
const BODY_COUNT: u64 = 267;
|
||||
|
||||
for (label, r_km) in cases {
|
||||
let cols = scale::regions_per_equator(r_km);
|
||||
let rows = (cols / 2).max(1);
|
||||
let cells = (cols as u64) * (rows as u64);
|
||||
let mut body_params = params.clone();
|
||||
body_params.body_radius_km = Some(r_km);
|
||||
|
||||
let t0 = Instant::now();
|
||||
for row in 0..rows {
|
||||
for col in 0..cols {
|
||||
let wx = col as f64 * region_m;
|
||||
let wy = row as f64 * region_m;
|
||||
let prof =
|
||||
derive_orbital_at_metres(seed, "bench", &body_params, &ta, wx, wy, &climate);
|
||||
std::hint::black_box(prof.elev_q);
|
||||
}
|
||||
}
|
||||
let elapsed = t0.elapsed();
|
||||
let ms = elapsed.as_secs_f64() * 1000.0;
|
||||
let ns_per_cell = elapsed.as_secs_f64() * 1e9 / cells as f64;
|
||||
println!(
|
||||
" {label:<28}: radius={r_km:>8.1}km cols={cols:>4} rows={rows:>4} cells={cells:>6}: \
|
||||
{ms:>8.3} ms single-thread, {ns_per_cell:>7.1} ns/cell"
|
||||
);
|
||||
}
|
||||
|
||||
// All-267-bodies single-thread total, using the MEASURED per-cell rate
|
||||
// from the Earth-class case above (representative — T-1178/T-1154 already
|
||||
// established this per-cell rate is flat across canvas size) applied to
|
||||
// the REAL total cell count across the population (4,825,615, from the
|
||||
// sibling bench), not the average-cells-per-body figure multiplied out
|
||||
// blindly.
|
||||
let (_, earth_r_km) = cases[1];
|
||||
let earth_cols = scale::regions_per_equator(earth_r_km);
|
||||
let earth_rows = (earth_cols / 2).max(1);
|
||||
let earth_cells = (earth_cols as u64) * (earth_rows as u64);
|
||||
let mut earth_body_params = params.clone();
|
||||
earth_body_params.body_radius_km = Some(earth_r_km);
|
||||
let t0 = Instant::now();
|
||||
for row in 0..earth_rows {
|
||||
for col in 0..earth_cols {
|
||||
let wx = col as f64 * region_m;
|
||||
let wy = row as f64 * region_m;
|
||||
let prof = derive_orbital_at_metres(
|
||||
seed,
|
||||
"bench",
|
||||
&earth_body_params,
|
||||
&ta,
|
||||
wx,
|
||||
wy,
|
||||
&climate,
|
||||
);
|
||||
std::hint::black_box(prof.elev_q);
|
||||
}
|
||||
}
|
||||
let earth_elapsed = t0.elapsed();
|
||||
let earth_ns_per_cell = earth_elapsed.as_secs_f64() * 1e9 / earth_cells as f64;
|
||||
|
||||
let total_cells_all_267 = 4_825_615u64; // measured directly in bench_global_tier_bytes_real_population
|
||||
let total_ms_all_267_singlethread = total_cells_all_267 as f64 * earth_ns_per_cell / 1e6;
|
||||
|
||||
println!(
|
||||
"\n Reference per-cell rate used for population total (Earth-class case, MEASURED above): {earth_ns_per_cell:.1} ns/cell"
|
||||
);
|
||||
println!(
|
||||
" Real per-body average (this file's population bench): {AVG_CELLS_PER_BODY} cells/body x {BODY_COUNT} bodies = {} total cells",
|
||||
AVG_CELLS_PER_BODY * BODY_COUNT
|
||||
);
|
||||
println!(
|
||||
" ALL 267 real bodies' rung-0 canvases, single-thread total (measured rate x real total cell count 4,825,615): {total_ms_all_267_singlethread:.1} ms = {:.3} s",
|
||||
total_ms_all_267_singlethread / 1000.0
|
||||
);
|
||||
println!(
|
||||
" Note: this is a ONE-TIME cost PER BODY (rung-0 solved once per body, held in the \
|
||||
keep-always cache per Tier 1) — never a per-request or per-frame cost, and never all \
|
||||
267 bodies solved in one synchronous batch in production (each body's rung-0 canvas \
|
||||
is populated lazily on that body's first Atlas-open, same as the existing D-206 \
|
||||
background-queue population path). The all-267-summed total above prices the \
|
||||
worst-case ceiling (every body opened once, back to back, single-threaded); the \
|
||||
REAL per-body cost that matters for a single Atlas-open is the individual-body rows \
|
||||
above (~16-21 ms single-thread for one body's rung-0 canvas — trivially interactive, \
|
||||
parallelizes further if ever needed the same way T-1177's per-body hydrology solves do)."
|
||||
);
|
||||
println!();
|
||||
}
|
||||
@@ -121,6 +121,7 @@ use settled_reach_server::atlas::drainage;
|
||||
use settled_reach_server::atlas::features::TerrainAnalysis;
|
||||
use settled_reach_server::atlas::heightmap::{load_heightmap_png, BodyHeightmap};
|
||||
use settled_reach_server::atlas::layer_proxy::{build_district_window_layer, WindowGranularity};
|
||||
use settled_reach_server::atlas::river_course::{self, InventedCourse};
|
||||
use settled_reach_server::atlas::scale;
|
||||
use settled_reach_server::seed::{SeedChain, SeedDomain};
|
||||
|
||||
@@ -851,3 +852,393 @@ fn bench_block_cutoff_confirms_savings() {
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Interview-2 redirect: chunk (64 m) — the never-benched rung, now the
|
||||
// proposed deepest Atlas ladder rung (Jeroen, interview 2: "the actual tile
|
||||
// level rung seems unusable. maybe replace with 64?" — tile/voxel dropped
|
||||
// from the Atlas ladder; chunk becomes the bottom). Same discipline as the
|
||||
// original T-1154 benches above: call `derive_at_metres` directly (no
|
||||
// wire-facing cutoff band exists below Quarter's 1,024 m `MIN_WL_BANDS_M`
|
||||
// floor either), matched cutoff = spacing (Nyquist), 4,096-cell sweep for
|
||||
// direct comparability with the existing Block/Tile row above, plus a
|
||||
// realistic deep-step VIEWPORT canvas at chunk spacing (replacing the old
|
||||
// 216x384m/1m-spacing deep-step bench, which measured the now-dropped tile
|
||||
// rung).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Chunk (64 m) per-cell derive cost, matched to the existing Block/Tile
|
||||
/// 4,096-cell sweep shape (`bench_block_and_tile_spacing_4096_cells` above)
|
||||
/// so this row slots directly into the same comparison table. This is the
|
||||
/// ONE rung on the D-243 ladder nobody had measured before interview 2 (T-1154
|
||||
/// tested Block 128m and Tile-adjacent 1m/4m; chunk's 64m spacing sits
|
||||
/// between them and was never run). `VOXEL_OCTAVE_WAVELENGTHS_M`'s finest
|
||||
/// entry is 128m (`detail_scatter.rs:46`), so per the SAME cutoff-mechanism
|
||||
/// finding `bench_block_cutoff_confirms_savings` already established for
|
||||
/// Block, a cutoff at 64m (finer than every entry in that array) should
|
||||
/// truncate nothing either — this bench CONFIRMS that expectation for chunk
|
||||
/// specifically rather than assuming it transfers from Block's own result.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_chunk_spacing_4096_cells() {
|
||||
let hm = bench_hm();
|
||||
let ta = bench_ta(&hm);
|
||||
let params = bench_params();
|
||||
let climate = ClimateConstants::default();
|
||||
let seed = SeedChain::root(99).derive(SeedDomain::Body, 1);
|
||||
let grid_side = 64u32; // 4,096 cells — matches the Block/Tile sweep above
|
||||
|
||||
println!("\n=== Interview-2: chunk (64m) spacing derive_at_metres benchmark (4,096-cell sweep) ===");
|
||||
println!(
|
||||
"grid: {grid_side}x{grid_side} = {} cells/sweep\n",
|
||||
grid_side * grid_side
|
||||
);
|
||||
|
||||
let chunk_m = scale::CHUNK_M as f64; // 64 m
|
||||
let block_m = scale::BLOCK_M as f64; // 128 m, for direct side-by-side
|
||||
|
||||
for (label, step_m, cutoff_m) in [
|
||||
("chunk (64m), cutoff=64m", chunk_m, chunk_m),
|
||||
("chunk (64m), UNCUT (cutoff=0)", chunk_m, 0.0),
|
||||
("block (128m), cutoff=128m [reference row]", block_m, block_m),
|
||||
] {
|
||||
let n_cells = (grid_side * grid_side) as u64;
|
||||
let t0 = Instant::now();
|
||||
for row in 0..grid_side {
|
||||
for col in 0..grid_side {
|
||||
let wx = col as f64 * step_m;
|
||||
let wy = row as f64 * step_m;
|
||||
let prof =
|
||||
derive_at_metres(seed, "bench", ¶ms, &ta, wx, wy, &climate, cutoff_m, &[]);
|
||||
std::hint::black_box(prof.elev_q);
|
||||
}
|
||||
}
|
||||
let elapsed = t0.elapsed();
|
||||
let per_cell_ns = elapsed.as_secs_f64() * 1e9 / n_cells as f64;
|
||||
println!(
|
||||
" {label:<44}: {:>8.2} ms total, {:>7.1} ns/cell ({:.3} us/cell)",
|
||||
elapsed.as_secs_f64() * 1000.0,
|
||||
per_cell_ns,
|
||||
per_cell_ns / 1000.0
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
/// The NEW realistic deepest-step canvas — chunk (64 m) spacing replacing
|
||||
/// the dropped tile (1 m) rung. Jeroen's interview-2 ruling: the old
|
||||
/// 10px/tile bottom-out ("full 1920 screen at 10px/tile shows ~192x108m") is
|
||||
/// in-world viewport territory (Phase 5), not Atlas map content — chunk (64
|
||||
/// m, D-243's "stream/derive unit") is the new floor.
|
||||
///
|
||||
/// Display-band derivation for the new bottom-out, stated precisely (this is
|
||||
/// the number Tyre's amendment text needs): at 1x1 px-per-gridunit (the
|
||||
/// workshop's own ideal ratio, premise 5), a 3840x2160 canvas at 64 m
|
||||
/// spacing covers `2160 * 64 = 138,240 m` (~138.2 km) on the smaller axis and
|
||||
/// `3840 * 64 = 245,760 m` (~245.8 km) on the larger axis — i.e. the
|
||||
/// bottom-out is "1 screen px per 64m gridunit", NOT "10 px per chunk" (10
|
||||
/// px/gridunit would need a canvas 10x larger in EXTENT for the same pixel
|
||||
/// budget, which no longer makes sense once chunk stands in for what tile's
|
||||
/// 10x margin was there to buy: legibility of a 1m ground feature at low
|
||||
/// pixel density; chunk itself IS the smallest legible Atlas content unit
|
||||
/// now, so it wants 1x1, not a magnification margin on top of 1x1). Full
|
||||
/// working: `canvas_px / gridunit_spacing_m = world_extent_m` per axis (the
|
||||
/// same arithmetic the old tile bottom-out used, just at 64m instead of 1m
|
||||
/// and without the 10x margin factor tile's own screen-legibility problem
|
||||
/// needed). This bench uses the FULL 3840x2160 canvas at 1x1 (matching every
|
||||
/// other rung's fixed-px-budget convention, per round 2 §(c) — chunk is the
|
||||
/// first deepest rung that does NOT need the display-ratio-sized-canvas
|
||||
/// exception the old tile rung required, precisely because it's not
|
||||
/// undersized relative to a legibility margin the way 1m/10px was).
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_chunk_deep_step_realistic_canvas() {
|
||||
let hm = bench_hm();
|
||||
let ta = bench_ta(&hm);
|
||||
let params = bench_params();
|
||||
let climate = ClimateConstants::default();
|
||||
let seed = SeedChain::root(99).derive(SeedDomain::Body, 1);
|
||||
|
||||
// Full 3840x2160 canvas, 1 gridunit per screen px, 64 m spacing.
|
||||
let cols = 3840u32;
|
||||
let rows = 2160u32;
|
||||
let cells = (rows as u64) * (cols as u64);
|
||||
let step_m = scale::CHUNK_M as f64; // 64 m
|
||||
let cutoff_m = step_m; // Nyquist-matched
|
||||
|
||||
let world_w_km = cols as f64 * step_m / 1000.0;
|
||||
let world_h_km = rows as f64 * step_m / 1000.0;
|
||||
|
||||
println!("\n=== Interview-2: chunk (64m) deep-step realistic-canvas bench (3840x2160 @ 1x1 px/gridunit) ===");
|
||||
println!(
|
||||
" geometry: 3840x2160 canvas @ 64m/gridunit, 1x1 px-per-gridunit -> {world_w_km:.1} km x {world_h_km:.1} km world extent"
|
||||
);
|
||||
println!(" cells = 3840 * 2160 = {cells}");
|
||||
println!(" {}\n", rayon_threads_report());
|
||||
|
||||
let (elapsed_par, ns_per_cell_par) = rect_window_replica(
|
||||
seed, "bench", ¶ms, &ta, &climate, cols, rows, step_m, cutoff_m,
|
||||
);
|
||||
println!(
|
||||
" PARALLEL (row-chunked): {:.2} ms, {:.1} ns/cell ({:.3} us/cell)",
|
||||
elapsed_par.as_secs_f64() * 1000.0,
|
||||
ns_per_cell_par,
|
||||
ns_per_cell_par / 1000.0
|
||||
);
|
||||
|
||||
let pool = rayon::ThreadPoolBuilder::new()
|
||||
.num_threads(1)
|
||||
.build()
|
||||
.expect("build single-thread rayon pool");
|
||||
let (elapsed_seq, ns_per_cell_seq) = pool.install(|| {
|
||||
rect_window_replica(
|
||||
seed, "bench", ¶ms, &ta, &climate, cols, rows, step_m, cutoff_m,
|
||||
)
|
||||
});
|
||||
println!(
|
||||
" SINGLE-THREAD: {:.2} ms, {:.1} ns/cell ({:.3} us/cell)",
|
||||
elapsed_seq.as_secs_f64() * 1000.0,
|
||||
ns_per_cell_seq,
|
||||
ns_per_cell_seq / 1000.0
|
||||
);
|
||||
println!(
|
||||
" speedup: {:.2}x\n",
|
||||
elapsed_seq.as_secs_f64() / elapsed_par.as_secs_f64()
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// S2 (ruled: before filing) — deep-step x high-river-density COURSES-INCLUSIVE
|
||||
// cost. The last zero-data-point cell: every courses-inclusive number
|
||||
// measured so far (Cross-check 1 in the results doc, 18 courses/331,776
|
||||
// cells) is at District spacing. Nothing has measured courses-on cost at the
|
||||
// NEW deepest rung (chunk, 64m, per interview 2) or at Block (128m) — both
|
||||
// below District, where a real river window would have MORE edges in view
|
||||
// per unit area (finer spacing = smaller world extent per canvas, but a real
|
||||
// river network's edge density near a river is roughly constant per unit
|
||||
// ground area, so a narrower window can still contain a densely-braided
|
||||
// stretch). Builds real InventedCourse fixtures via the actual PUBLIC
|
||||
// invention pipeline (`river_course::build_edges` + `river_course::invent_course`
|
||||
// — both `pub`, unlike `layer_proxy::invent_courses_near_window` itself,
|
||||
// which is private to that module; this bench replicates its per-edge
|
||||
// invention loop using the same public primitives, same discipline as
|
||||
// `rect_window_replica` already replicates `build_district_window_layer`'s
|
||||
// internals elsewhere in this file).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Build a high-density `InventedCourse` set from the REAL GJ1c river
|
||||
/// network — every edge whose invented course falls within
|
||||
/// `inflate_m` of the given world-metre window, at the given station
|
||||
/// spacing/cutoff. This is the densest REAL course set available in this
|
||||
/// repo (GJ1c is the only body with a river network already wired into a
|
||||
/// bench fixture) rather than a synthetic worst case — real geometry is
|
||||
/// preferred per this workshop's own measurement discipline (T-1178's cross-
|
||||
/// check pattern: synthetic first, then confirm on real geometry). Returns
|
||||
/// the course list plus the count found, so callers can report density
|
||||
/// alongside cost.
|
||||
fn build_gj1c_courses_near_window(
|
||||
seed: SeedChain,
|
||||
ta: &TerrainAnalysis,
|
||||
params: &BodyParams,
|
||||
river_network: &settled_reach_server::atlas::body_world_state::RiverNetwork,
|
||||
win_x0: f64,
|
||||
win_y0: f64,
|
||||
win_x1: f64,
|
||||
win_y1: f64,
|
||||
station_spacing_m: f64,
|
||||
min_wavelength_m: f64,
|
||||
) -> Vec<InventedCourse> {
|
||||
let edges = river_course::build_edges(river_network);
|
||||
let r_km = params.body_radius_km.expect("body_radius_km required");
|
||||
|
||||
// Inline the same pixel->world-metres formula the existing GJ1c
|
||||
// cross-check bench above already uses (district_profile::pixel_to_world_m
|
||||
// is `pub(crate)`, not reachable from an integration test — this is the
|
||||
// SAME formula, inlined, not a different one; consistent with how
|
||||
// `bench_square_window_production_fn_gj1c_real_body_crosscheck` and
|
||||
// zoom_ladder_bench.rs's `bench_course_cost_on_vs_off` already do this).
|
||||
let to_world = |row: u16, col: u16| -> (f64, f64) {
|
||||
(
|
||||
col as f64 / ta.w as f64 * (std::f64::consts::TAU * r_km * 1000.0),
|
||||
(row as f64 / (ta.h - 1) as f64 - 0.5) * (std::f64::consts::PI * r_km * 1000.0),
|
||||
)
|
||||
};
|
||||
|
||||
let mut courses = Vec::new();
|
||||
for edge in &edges {
|
||||
let anchor_a = to_world(edge.upstream.0, edge.upstream.1);
|
||||
let anchor_b = to_world(edge.downstream.0, edge.downstream.1);
|
||||
let chord_m = ((anchor_a.0 - anchor_b.0).powi(2) + (anchor_a.1 - anchor_b.1).powi(2)).sqrt();
|
||||
// Same 0.08 inflation fraction layer_proxy.rs's COURSE_BBOX_INFLATION_FRACTION
|
||||
// uses (that constant itself is private; the value is stated in its
|
||||
// own doc and reproduced here for the same bbox-cull purpose — a
|
||||
// bench-local approximation of the real cull, not a claim of exact
|
||||
// production parity for the cull step itself, which doesn't affect
|
||||
// measured PER-CELL cost once a course is in the list).
|
||||
let inflate_m = chord_m * 0.08;
|
||||
let (bx0, bx1) = (
|
||||
anchor_a.0.min(anchor_b.0) - inflate_m,
|
||||
anchor_a.0.max(anchor_b.0) + inflate_m,
|
||||
);
|
||||
let (by0, by1) = (
|
||||
anchor_a.1.min(anchor_b.1) - inflate_m,
|
||||
anchor_a.1.max(anchor_b.1) + inflate_m,
|
||||
);
|
||||
if bx1 < win_x0 || bx0 > win_x1 || by1 < win_y0 || by0 > win_y1 {
|
||||
continue;
|
||||
}
|
||||
courses.push(river_course::invent_course(
|
||||
seed,
|
||||
edge,
|
||||
ta,
|
||||
params,
|
||||
station_spacing_m,
|
||||
min_wavelength_m,
|
||||
));
|
||||
}
|
||||
courses
|
||||
}
|
||||
|
||||
/// S2: courses-on vs courses-off, at BOTH the new deepest rung (chunk, 64m)
|
||||
/// and Block (128m), over a window picked to maximize real river-edge
|
||||
/// density (the densest real region in the only river-network fixture this
|
||||
/// repo's benches have — GJ1c). Reports the delta as both absolute ms and
|
||||
/// percentage, matching `bench_course_cost_on_vs_off`'s own reporting shape
|
||||
/// (District's own courses-on-vs-off number: +0.09-0.21ms against a ~5ms
|
||||
/// baseline, under 5%) so this fills in the two remaining zero-data-point
|
||||
/// cells on the same comparison axis.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_s2_courses_density_at_chunk_and_block() {
|
||||
let (hm, ta) = gj1c_fixture();
|
||||
let dr = drainage::analyze(&hm.data, hm.width, hm.height, hm.sea_level);
|
||||
let rn = dr.river_network.clone();
|
||||
let params = bench_params();
|
||||
let climate = ClimateConstants::default();
|
||||
let seed = SeedChain::root(0xC0FFEE_u64).derive(SeedDomain::Body, 7);
|
||||
|
||||
assert!(
|
||||
!rn.river_cells.is_empty(),
|
||||
"GJ1c at production working resolution must have river cells for this bench to be meaningful"
|
||||
);
|
||||
|
||||
// Find the highest-density river region: scan river cells and pick the
|
||||
// one with the most OTHER river cells within a fixed pixel radius — a
|
||||
// proxy for confluence/braided density, maximizing edges-per-window
|
||||
// rather than picking an arbitrary river cell as the existing single
|
||||
// cross-check bench does.
|
||||
let radius_px = 8i64; // small radius = local confluence density, not just "near any river"
|
||||
let mut best_cell = rn.river_cells[0];
|
||||
let mut best_count = -1i64;
|
||||
for &cell in &rn.river_cells {
|
||||
let mut count = 0i64;
|
||||
for &other in &rn.river_cells {
|
||||
let dr_ = cell.0 as i64 - other.0 as i64;
|
||||
let dc_ = cell.1 as i64 - other.1 as i64;
|
||||
if dr_ * dr_ + dc_ * dc_ <= radius_px * radius_px {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if count > best_count {
|
||||
best_count = count;
|
||||
best_cell = cell;
|
||||
}
|
||||
}
|
||||
|
||||
let r_km = params.body_radius_km.expect("body_radius_km required");
|
||||
let to_world = |row: u16, col: u16| -> (f64, f64) {
|
||||
(
|
||||
col as f64 / ta.w as f64 * (std::f64::consts::TAU * r_km * 1000.0),
|
||||
(row as f64 / (ta.h - 1) as f64 - 0.5) * (std::f64::consts::PI * r_km * 1000.0),
|
||||
)
|
||||
};
|
||||
let center_world = to_world(best_cell.0, best_cell.1);
|
||||
|
||||
println!("\n=== S2: courses-on vs courses-off density bench (chunk 64m + block 128m, densest GJ1c river region) ===");
|
||||
println!(
|
||||
" densest river cell: {best_cell:?} ({best_count} river cells within {radius_px}px radius), world center {center_world:?}"
|
||||
);
|
||||
|
||||
for (label, step_m) in [("chunk (64m)", scale::CHUNK_M as f64), ("block (128m)", scale::BLOCK_M as f64)] {
|
||||
// Realistic-shape window at this spacing: 64x64 cells (4,096, matching
|
||||
// this file's other 4,096-cell sweeps for direct comparability).
|
||||
let grid_side = 64u32;
|
||||
let half_extent_m = (grid_side as f64 / 2.0) * step_m;
|
||||
let win_x0 = center_world.0 - half_extent_m;
|
||||
let win_x1 = center_world.0 + half_extent_m;
|
||||
let win_y0 = center_world.1 - half_extent_m;
|
||||
let win_y1 = center_world.1 + half_extent_m;
|
||||
|
||||
let courses = build_gj1c_courses_near_window(
|
||||
seed, &ta, ¶ms, &rn, win_x0, win_y0, win_x1, win_y1, step_m, step_m,
|
||||
);
|
||||
let total_points: usize = courses.iter().map(|c| c.points.len()).sum();
|
||||
let avg_points = if courses.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
total_points as f64 / courses.len() as f64
|
||||
};
|
||||
|
||||
println!(
|
||||
"\n --- {label}: {grid_side}x{grid_side} window ({:.0}m x {:.0}m), courses_in_window={}, avg_points_per_course={:.1} (station_spacing_m={step_m}) ---",
|
||||
half_extent_m * 2.0,
|
||||
half_extent_m * 2.0,
|
||||
courses.len(),
|
||||
avg_points
|
||||
);
|
||||
|
||||
let n_cells = (grid_side * grid_side) as u64;
|
||||
let iterations = 200; // higher rep count — a single 4,096-cell sweep is sub-10ms, noisy at n=1
|
||||
|
||||
// Courses OFF.
|
||||
let t_off = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
for row in 0..grid_side {
|
||||
for col in 0..grid_side {
|
||||
let wx = win_x0 + col as f64 * step_m;
|
||||
let wy = win_y0 + row as f64 * step_m;
|
||||
let prof = derive_at_metres(
|
||||
seed, "GJ1c", ¶ms, &ta, wx, wy, &climate, step_m, &[],
|
||||
);
|
||||
std::hint::black_box(prof.elev_q);
|
||||
}
|
||||
}
|
||||
}
|
||||
let elapsed_off = t_off.elapsed();
|
||||
|
||||
// Courses ON.
|
||||
let t_on = Instant::now();
|
||||
for _ in 0..iterations {
|
||||
for row in 0..grid_side {
|
||||
for col in 0..grid_side {
|
||||
let wx = win_x0 + col as f64 * step_m;
|
||||
let wy = win_y0 + row as f64 * step_m;
|
||||
let prof = derive_at_metres(
|
||||
seed, "GJ1c", ¶ms, &ta, wx, wy, &climate, step_m, &courses,
|
||||
);
|
||||
std::hint::black_box(prof.elev_q);
|
||||
}
|
||||
}
|
||||
}
|
||||
let elapsed_on = t_on.elapsed();
|
||||
|
||||
let ms_off_per_sweep = elapsed_off.as_secs_f64() * 1000.0 / iterations as f64;
|
||||
let ms_on_per_sweep = elapsed_on.as_secs_f64() * 1000.0 / iterations as f64;
|
||||
let delta_ms = ms_on_per_sweep - ms_off_per_sweep;
|
||||
let delta_pct = 100.0 * delta_ms / ms_off_per_sweep;
|
||||
let ns_per_cell_off = elapsed_off.as_secs_f64() * 1e9 / (n_cells * iterations as u64) as f64;
|
||||
let ns_per_cell_on = elapsed_on.as_secs_f64() * 1e9 / (n_cells * iterations as u64) as f64;
|
||||
|
||||
println!(
|
||||
" courses OFF: {ms_off_per_sweep:.4} ms/sweep ({ns_per_cell_off:.1} ns/cell)"
|
||||
);
|
||||
println!(
|
||||
" courses ON: {ms_on_per_sweep:.4} ms/sweep ({ns_per_cell_on:.1} ns/cell)"
|
||||
);
|
||||
println!(
|
||||
" delta: {delta_ms:+.4} ms/sweep ({delta_pct:+.2}%), {} courses in window",
|
||||
courses.len()
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
@@ -228,6 +228,17 @@ fn determinism_at_330k_cells() {
|
||||
/// solved across the Rayon pool, at the 512×256 production grid size —
|
||||
/// directly answering the workshop's red-flag-2-adjacent question of
|
||||
/// whether per-body-open hydrology is affordable at scale.
|
||||
///
|
||||
/// **Scope note (population-survey gap, closed by
|
||||
/// [`bench_population_survey_all_committed_bodies`] below):** this bench
|
||||
/// solves ONE real body (GJ1c) 273 times — it proves the *wall-clock*
|
||||
/// affordability of running 273 independent solves in parallel (the
|
||||
/// question it was built to answer), but the 273 solves are not 273
|
||||
/// *distinct* bodies, so it says nothing about how basin/endorheic/carved-
|
||||
/// cell distributions vary across the real body population. That is a
|
||||
/// different question, answered by the population survey, not this bench —
|
||||
/// left as-is (not rewritten) since it still correctly answers the
|
||||
/// question it was designed for.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_parallel_273_bodies_at_512x256() {
|
||||
@@ -258,3 +269,429 @@ fn bench_parallel_273_bodies_at_512x256() {
|
||||
elapsed.as_secs_f64() * 1000.0 / body_count as f64
|
||||
);
|
||||
}
|
||||
|
||||
/// **POPULATION SURVEY (post-adversarial, Troblum finding).** Loads and
|
||||
/// solves EVERY real committed heightmap PNG in the repo (267 distinct
|
||||
/// bodies as of this bench, `wiki/star-systems/*/bodies/*/heightmap.png`),
|
||||
/// each downsampled to the real 512×256 production working grid with its
|
||||
/// own PNG-embedded `sea_level`, run independently in parallel across the
|
||||
/// Rayon pool — the actual population-scale question
|
||||
/// [`bench_parallel_273_bodies_at_512x256`] does not answer (that bench
|
||||
/// solves ONE body 273 times, not 273 distinct bodies; see its doc comment
|
||||
/// above). This closes the gap: total wall time for the REAL population,
|
||||
/// and — the load-bearing part for the cliff Phase-4 ruling — the
|
||||
/// distribution of basins/endorheic-basins/carved-cliff-edge-cells ACROSS
|
||||
/// the population, not just on GJ1c.
|
||||
///
|
||||
/// **Known scope limit, stated explicitly (not hidden):** every body uses
|
||||
/// the SAME `ClimateInputs { moisture_q: 55 }` (`default_climate()`) — the
|
||||
/// module's own doc already states real per-body moisture requires wiring
|
||||
/// in `BodyWorldState.districts`/`regions` climate context, which is out of
|
||||
/// scope for this survey (same limitation the original T-1177 prototype
|
||||
/// documented, "No per-basin moisture/climate lookup"). This means the
|
||||
/// endorheic classification specifically is measured under a uniform
|
||||
/// climate assumption, not each body's real hydrosphere/atmosphere-derived
|
||||
/// moisture — a real per-body climate wiring pass could shift the
|
||||
/// endorheic counts. It does NOT limit the carved-cliff-edge finding below:
|
||||
/// carving depends on ELEVATION geometry (the narrow two-basin-saddle
|
||||
/// condition documented in the module), not on `moisture_q` at all — the
|
||||
/// climate input only gates the endorheic/overflow classification of an
|
||||
/// already-identified basin, never whether carving occurs.
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_population_survey_all_committed_bodies() {
|
||||
use rayon::prelude::*;
|
||||
use settled_reach_server::atlas::hydrology_equilibrium::{BasinOutcome, DownstreamTarget};
|
||||
|
||||
let wiki_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki/star-systems");
|
||||
|
||||
// Discover every committed heightmap PNG (deterministic ordering: sort
|
||||
// by path so the survey's own reporting order is stable run-to-run —
|
||||
// not load-bearing for solve() itself, which is pure per-body, but
|
||||
// keeps the printed output diffable).
|
||||
let mut heightmap_paths: Vec<std::path::PathBuf> = Vec::new();
|
||||
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
walk(&path, out);
|
||||
} else if path.file_name().and_then(|n| n.to_str()) == Some("heightmap.png") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(&wiki_root, &mut heightmap_paths);
|
||||
heightmap_paths.sort();
|
||||
|
||||
assert!(
|
||||
heightmap_paths.len() > 200,
|
||||
"expected the real committed body population (~267 as of this bench), found {} — \
|
||||
did the wiki_root path resolve correctly? ({wiki_root:?})",
|
||||
heightmap_paths.len()
|
||||
);
|
||||
|
||||
struct BodySurvey {
|
||||
body_id: String,
|
||||
basins: usize,
|
||||
overflow: usize,
|
||||
endorheic: usize,
|
||||
lake_cells: usize,
|
||||
carved_cells: usize,
|
||||
cliff_edge_cells: usize,
|
||||
solve_ms: f64,
|
||||
}
|
||||
|
||||
let t0 = Instant::now();
|
||||
let surveys: Vec<BodySurvey> = heightmap_paths
|
||||
.par_iter()
|
||||
.map(|path| {
|
||||
// body_id = the directory name one level up from heightmap.png
|
||||
// (wiki/star-systems/<system>/bodies/<body_id>/heightmap.png).
|
||||
let body_id = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("UNKNOWN")
|
||||
.to_string();
|
||||
|
||||
let heightmap = settled_reach_server::atlas::heightmap::load_heightmap_png(
|
||||
path, &body_id, 0.3,
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("decode committed heightmap for {body_id} ({path:?}): {e}"));
|
||||
let small = heightmap.downsample(512, 256);
|
||||
|
||||
let t_solve = Instant::now();
|
||||
let result = solve(&small.data, 512, 256, small.sea_level, default_climate());
|
||||
let solve_ms = t_solve.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
let lake_cells: usize = result.basins.iter().map(|b| b.cells.len()).sum();
|
||||
let carved_cells: usize = result
|
||||
.basins
|
||||
.iter()
|
||||
.filter(|b| {
|
||||
matches!(
|
||||
b.outcome,
|
||||
BasinOutcome::Overflow {
|
||||
downstream_target: DownstreamTarget::Sea
|
||||
| DownstreamTarget::Basin(_)
|
||||
| DownstreamTarget::OpenSpillway,
|
||||
..
|
||||
}
|
||||
) && result.channel_depth_scaled[b.spill_cell] > 0
|
||||
})
|
||||
.count();
|
||||
let cliff_edge_cells = result.cliff_edge.iter().filter(|&&c| c).count();
|
||||
let endorheic = result
|
||||
.basins
|
||||
.iter()
|
||||
.filter(|b| matches!(b.outcome, BasinOutcome::Endorheic { .. }))
|
||||
.count();
|
||||
let overflow = result
|
||||
.basins
|
||||
.iter()
|
||||
.filter(|b| matches!(b.outcome, BasinOutcome::Overflow { .. }))
|
||||
.count();
|
||||
|
||||
BodySurvey {
|
||||
body_id,
|
||||
basins: result.basins.len(),
|
||||
overflow,
|
||||
endorheic,
|
||||
lake_cells,
|
||||
carved_cells,
|
||||
cliff_edge_cells,
|
||||
solve_ms,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let elapsed = t0.elapsed();
|
||||
|
||||
let body_count = surveys.len();
|
||||
let total_basins: usize = surveys.iter().map(|s| s.basins).sum();
|
||||
let total_overflow: usize = surveys.iter().map(|s| s.overflow).sum();
|
||||
let total_endorheic: usize = surveys.iter().map(|s| s.endorheic).sum();
|
||||
let total_lake_cells: usize = surveys.iter().map(|s| s.lake_cells).sum();
|
||||
let total_carved_cells: usize = surveys.iter().map(|s| s.carved_cells).sum();
|
||||
let total_cliff_edge_cells: usize = surveys.iter().map(|s| s.cliff_edge_cells).sum();
|
||||
let bodies_with_any_carving = surveys.iter().filter(|s| s.cliff_edge_cells > 0).count();
|
||||
|
||||
let mut by_cliff_edge: Vec<&BodySurvey> = surveys.iter().collect();
|
||||
by_cliff_edge.sort_by(|a, b| b.cliff_edge_cells.cmp(&a.cliff_edge_cells));
|
||||
|
||||
println!(
|
||||
"\n=== POPULATION SURVEY: {body_count} real committed bodies x 512x256, Rayon par_iter ({} threads available) ===",
|
||||
std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(0)
|
||||
);
|
||||
println!(
|
||||
" {:>9.2} ms total wall time, {:>7.3} ms/body average",
|
||||
elapsed.as_secs_f64() * 1000.0,
|
||||
elapsed.as_secs_f64() * 1000.0 / body_count as f64
|
||||
);
|
||||
println!(
|
||||
" basins: {total_basins} total across population ({total_overflow} overflow, {total_endorheic} endorheic)"
|
||||
);
|
||||
println!(" lake cells (summed across population): {total_lake_cells}");
|
||||
println!(
|
||||
" carved-outlet basins (channel_depth_scaled > 0 at spill cell, summed): {total_carved_cells}"
|
||||
);
|
||||
println!(
|
||||
" cliff_edge=true cells (summed across population): {total_cliff_edge_cells}"
|
||||
);
|
||||
println!(
|
||||
" bodies with ANY cliff_edge cell: {bodies_with_any_carving} / {body_count} ({:.2}%)",
|
||||
100.0 * bodies_with_any_carving as f64 / body_count as f64
|
||||
);
|
||||
println!("\n Top 15 bodies by cliff_edge cell count (the max-carve outliers):");
|
||||
for s in by_cliff_edge.iter().take(15) {
|
||||
println!(
|
||||
" {:>10} cliff_edge_cells={:>4} carved_basins={:>2} basins={:>3} (overflow={:>3}, endorheic={:>2}) lake_cells={:>6} solve={:.2}ms",
|
||||
s.body_id, s.cliff_edge_cells, s.carved_cells, s.basins, s.overflow, s.endorheic, s.lake_cells, s.solve_ms
|
||||
);
|
||||
}
|
||||
let zero_carve_bodies = surveys.iter().filter(|s| s.cliff_edge_cells == 0).count();
|
||||
println!(
|
||||
"\n Bodies with ZERO cliff_edge cells: {zero_carve_bodies} / {body_count} ({:.2}%)",
|
||||
100.0 * zero_carve_bodies as f64 / body_count as f64
|
||||
);
|
||||
|
||||
// Determinism spot-check on the max-carve outlier (if any carving was
|
||||
// found at all) — re-solve it once more and confirm byte-identical
|
||||
// cliff_edge/channel_depth output, so the survey's headline outlier
|
||||
// isn't itself an artifact of non-determinism.
|
||||
if let Some(top) = by_cliff_edge.first() {
|
||||
if top.cliff_edge_cells > 0 {
|
||||
let path = heightmap_paths
|
||||
.iter()
|
||||
.find(|p| {
|
||||
p.parent()
|
||||
.and_then(|d| d.file_name())
|
||||
.and_then(|n| n.to_str())
|
||||
== Some(top.body_id.as_str())
|
||||
})
|
||||
.expect("outlier body path must exist (found via the same walk above)");
|
||||
let heightmap = settled_reach_server::atlas::heightmap::load_heightmap_png(
|
||||
path,
|
||||
&top.body_id,
|
||||
0.3,
|
||||
)
|
||||
.expect("re-decode outlier heightmap for determinism spot-check");
|
||||
let small = heightmap.downsample(512, 256);
|
||||
let r1 = solve(&small.data, 512, 256, small.sea_level, default_climate());
|
||||
let r2 = solve(&small.data, 512, 256, small.sea_level, default_climate());
|
||||
assert_eq!(
|
||||
r1.cliff_edge, r2.cliff_edge,
|
||||
"max-carve outlier {} must be deterministic (cliff_edge)",
|
||||
top.body_id
|
||||
);
|
||||
assert_eq!(
|
||||
r1.channel_depth_scaled, r2.channel_depth_scaled,
|
||||
"max-carve outlier {} must be deterministic (channel_depth_scaled)",
|
||||
top.body_id
|
||||
);
|
||||
println!(
|
||||
"\n Determinism spot-check on max-carve outlier ({}): PASS (byte-identical across two solves)",
|
||||
top.body_id
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// **PER-BASIN SIZE DISTRIBUTION (body-map-viewer workshop, Araminta's
|
||||
/// endorheic-carrier crossover question, routed via the coordinator).**
|
||||
/// [`bench_population_survey_all_committed_bodies`] above reports only
|
||||
/// AGGREGATE lake-cell totals (2,694,012 summed across 22,270 basins) — it
|
||||
/// does not report individual basin SIZES, which is exactly what a
|
||||
/// sparse-list-vs-dense-field crossover analysis needs (a sparse per-cell
|
||||
/// list is cheap for small basins and expensive for large ones; the
|
||||
/// crossover point depends on the actual size DISTRIBUTION, not the
|
||||
/// aggregate). This bench solves every real committed body at the real
|
||||
/// 512×256 production working grid (same population, same grid size as the
|
||||
/// population survey above) and records every individual basin's cell
|
||||
/// count, then reports the full distribution: histogram buckets, percentiles,
|
||||
/// and the largest basins found, so the crossover call is made from a real
|
||||
/// distribution rather than the single synthetic 8.3M-cell grid's one
|
||||
/// data point (412,700 cells — T-1177's original measured appendix, a
|
||||
/// SYNTHETIC ridged-terrain grid at a canvas size no production path
|
||||
/// actually derives hydrology at synchronously, not a real-population
|
||||
/// statistic).
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn bench_per_basin_size_distribution_real_population() {
|
||||
use rayon::prelude::*;
|
||||
|
||||
let wiki_root =
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../wiki/star-systems");
|
||||
let mut heightmap_paths: Vec<std::path::PathBuf> = Vec::new();
|
||||
fn walk(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
|
||||
let Ok(entries) = std::fs::read_dir(dir) else {
|
||||
return;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
walk(&path, out);
|
||||
} else if path.file_name().and_then(|n| n.to_str()) == Some("heightmap.png") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(&wiki_root, &mut heightmap_paths);
|
||||
heightmap_paths.sort();
|
||||
assert!(
|
||||
heightmap_paths.len() > 200,
|
||||
"expected the real committed body population (~267), found {}",
|
||||
heightmap_paths.len()
|
||||
);
|
||||
|
||||
// (body_id, basin_cell_count, outcome_label) for EVERY basin across the
|
||||
// whole population — the raw material for the distribution below.
|
||||
let all_basins: Vec<(String, usize, &'static str)> = heightmap_paths
|
||||
.par_iter()
|
||||
.flat_map(|path| {
|
||||
let body_id = path
|
||||
.parent()
|
||||
.and_then(|p| p.file_name())
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("UNKNOWN")
|
||||
.to_string();
|
||||
let heightmap = settled_reach_server::atlas::heightmap::load_heightmap_png(
|
||||
path, &body_id, 0.3,
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("decode committed heightmap for {body_id} ({path:?}): {e}"));
|
||||
let small = heightmap.downsample(512, 256);
|
||||
let result = solve(&small.data, 512, 256, small.sea_level, default_climate());
|
||||
result
|
||||
.basins
|
||||
.iter()
|
||||
.map(|b| {
|
||||
let outcome_label = match b.outcome {
|
||||
settled_reach_server::atlas::hydrology_equilibrium::BasinOutcome::Overflow {
|
||||
..
|
||||
} => "overflow",
|
||||
settled_reach_server::atlas::hydrology_equilibrium::BasinOutcome::Endorheic {
|
||||
..
|
||||
} => "endorheic",
|
||||
};
|
||||
(body_id.clone(), b.cells.len(), outcome_label)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
all_basins.len(),
|
||||
22_270,
|
||||
"basin count must match the population survey's own total exactly \
|
||||
(same population, same grid size, same solver — a mismatch here \
|
||||
means the two benches drifted, not that either is wrong)"
|
||||
);
|
||||
|
||||
let mut sizes: Vec<usize> = all_basins.iter().map(|(_, n, _)| *n).collect();
|
||||
sizes.sort_unstable();
|
||||
|
||||
let total: usize = sizes.iter().sum();
|
||||
let n = sizes.len();
|
||||
let percentile = |p: f64| -> usize {
|
||||
let idx = ((p / 100.0) * (n - 1) as f64).round() as usize;
|
||||
sizes[idx.min(n - 1)]
|
||||
};
|
||||
|
||||
// Histogram buckets chosen to bracket plausible sparse-vs-dense
|
||||
// crossover points: a Vec<(u16,u16)> cell entry is 4 raw bytes/cell
|
||||
// (MessagePack-framed, somewhat less) vs. the dense `water` field's
|
||||
// PNG-per-field rate of ~1.9 bytes/cell (T-1179) applied ONLY to that
|
||||
// basin's own bounding extent — the buckets below span from "clearly
|
||||
// sparse wins" (tiny basins) through "clearly dense wins" (the largest
|
||||
// basins in this population).
|
||||
let buckets: [(usize, usize, &str); 7] = [
|
||||
(0, 10, "1-10 cells"),
|
||||
(11, 50, "11-50 cells"),
|
||||
(51, 200, "51-200 cells"),
|
||||
(201, 1_000, "201-1,000 cells"),
|
||||
(1_001, 5_000, "1,001-5,000 cells"),
|
||||
(5_001, 20_000, "5,001-20,000 cells"),
|
||||
(20_001, usize::MAX, "20,001+ cells"),
|
||||
];
|
||||
let mut bucket_counts = [0usize; 7];
|
||||
let mut bucket_cell_totals = [0usize; 7];
|
||||
for &size in &sizes {
|
||||
for (i, (lo, hi, _)) in buckets.iter().enumerate() {
|
||||
if size >= *lo && size <= *hi {
|
||||
bucket_counts[i] += 1;
|
||||
bucket_cell_totals[i] += size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut by_size: Vec<&(String, usize, &str)> = all_basins.iter().collect();
|
||||
by_size.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
println!("\n=== PER-BASIN SIZE DISTRIBUTION: {n} basins, real 267-body population, 512x256 working grid ===");
|
||||
println!(" total lake cells (cross-check vs population survey's 2,694,012): {total}");
|
||||
println!(" min={}, max={}, mean={:.1}, median(p50)={}",
|
||||
sizes[0], sizes[n - 1], total as f64 / n as f64, percentile(50.0));
|
||||
println!(" percentiles: p10={} p25={} p50={} p75={} p90={} p95={} p99={} p99.9={}",
|
||||
percentile(10.0), percentile(25.0), percentile(50.0), percentile(75.0),
|
||||
percentile(90.0), percentile(95.0), percentile(99.0), percentile(99.9));
|
||||
|
||||
println!("\n Histogram (basin cell-count buckets):");
|
||||
for (i, (_, _, label)) in buckets.iter().enumerate() {
|
||||
println!(
|
||||
" {label:<18}: {:>6} basins ({:>5.2}% of basins), {:>9} cells total ({:>5.2}% of lake cells)",
|
||||
bucket_counts[i],
|
||||
100.0 * bucket_counts[i] as f64 / n as f64,
|
||||
bucket_cell_totals[i],
|
||||
100.0 * bucket_cell_totals[i] as f64 / total as f64
|
||||
);
|
||||
}
|
||||
|
||||
println!("\n Top 20 largest basins (real population, real bodies):");
|
||||
for (body_id, size, outcome) in by_size.iter().take(20) {
|
||||
println!(" {body_id:>12} cells={size:>7} outcome={outcome}");
|
||||
}
|
||||
|
||||
// Bytes-if-sparse vs bytes-if-dense-per-basin-extent crossover, using
|
||||
// T-1179's measured PNG-per-field rate (638,382 B / 331,776 cells) as
|
||||
// the dense-encoding baseline, and 4 raw bytes/cell (u16,u16 pair,
|
||||
// MessagePack framing not applied — a conservative/pessimistic sparse
|
||||
// estimate, since MessagePack's compact array framing would cost less,
|
||||
// per T-1179's own "6.00 not 7 bytes/cell" finding for a different
|
||||
// field shape) for the sparse Vec<(u16,u16)> carrier.
|
||||
const PNG_BYTES_PER_CELL: f64 = 638_382.0 / 331_776.0;
|
||||
const SPARSE_BYTES_PER_CELL: f64 = 4.0;
|
||||
let mut crossover_basin_count = 0usize;
|
||||
let mut sparse_wins_cells = 0usize;
|
||||
let mut dense_wins_cells = 0usize;
|
||||
for &size in &sizes {
|
||||
let sparse_cost = size as f64 * SPARSE_BYTES_PER_CELL;
|
||||
let dense_cost = size as f64 * PNG_BYTES_PER_CELL;
|
||||
if sparse_cost <= dense_cost {
|
||||
crossover_basin_count += 1;
|
||||
sparse_wins_cells += size;
|
||||
} else {
|
||||
dense_wins_cells += size;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"\n Sparse-vs-dense crossover (sparse={SPARSE_BYTES_PER_CELL} B/cell raw vs dense={:.3} B/cell PNG-per-field):",
|
||||
PNG_BYTES_PER_CELL
|
||||
);
|
||||
println!(
|
||||
" sparse cheaper for {crossover_basin_count} / {n} basins ({:.2}%), covering {sparse_wins_cells} cells ({:.2}% of lake cells)",
|
||||
100.0 * crossover_basin_count as f64 / n as f64,
|
||||
100.0 * sparse_wins_cells as f64 / total as f64
|
||||
);
|
||||
println!(
|
||||
" dense cheaper for {} / {n} basins ({:.2}%), covering {dense_wins_cells} cells ({:.2}% of lake cells)",
|
||||
n - crossover_basin_count,
|
||||
100.0 * (n - crossover_basin_count) as f64 / n as f64,
|
||||
100.0 * dense_wins_cells as f64 / total as f64
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user