fix(client): the Atlas was replaying a cache from a build that no longer existed (T-1239)
Ferrath's Global map drew no rivers at native resolution: 375 courses arrived and 0 were drawn. The report suspected the D-261 length cull or the water truncation. Both were innocent, and so was the renderer. The client served the canvas from its own disk cache (T-1183). Every payload for GJ820Bc predated T-1237 (4e503c356), which replaced one-course-per-D8-hop with one-course-per-river -- so the map was drawing 375 hop fragments whose longest run was 106 km, all of them under D-261's read-as-a-line floor. Same build, same scenario, same 3440x1440, cache the only difference: stale courses=375 runs=180 longest=6.0px (~106 km) drawn=0 cold courses=73 runs=23 longest=93.2px (~1,644 km) drawn=18 It looked resolution-dependent because it wasn't a resolution at all: 960x540 resolves to an 814x407 canvas, a key never cached, so it missed and re-derived correctly. 3440x1440 resolves to 1080x540, which had an entry from 2026-08-06. During the stale capture the server logged no course production whatsoever -- the canvas never came from it. The cache's only invalidation signal is project.yaml's version, and4e503c356changed how canvases are generated without touching it, so hop-shaped entries stayed valid. All 13 stale entries are stamped 0.4.5. 0.4.6 forces them to miss; that, not clearing a local directory, is what repairs a player's Atlas. The harness let this hide for eight days, in two ways now fixed. It ran against the developer's persistent user:// cache, so a capture could render a canvas built by a build that no longer existed -- and any golden shot in that window silently inherited it; user:// is now isolated per run. And it sent server stderr to /dev/null via an already-unlinked mktemp file, so no tracing from a capture was ever reachable; the log now lives at .cache/visual-server.log. The capture readout gained runs= and longest= between courses= and drawn=, because "375 arrived, 0 drawn" is not one fact but three stages, and telling them apart is what turned a guess between two suspects into a measurement. Follow-ups filed: T-1241 (current_schema_version() returns its ?.?.? fallback in an exported build, so a shipped game never invalidates on version at all) and T-1242 (nothing enforces the generation-change/version-bump pairing -- this is the fourth bump forced after the fact). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -101,3 +101,70 @@ for a failure of the un-summarisation work.', NULL, '2026-08-06 15:59:39', '2026
|
|||||||
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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'status', 'backlog', 'in_progress', NULL, '2026-08-07 11:36:05', '2026-08-07 11:36:05.551', '2026-08-07 11:36:05.551', NULL, '56946f83904827105a9ae52d64a91a1d', 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'status', 'backlog', 'in_progress', NULL, '2026-08-07 11:36:05', '2026-08-07 11:36:05.551', '2026-08-07 11:36:05.551', NULL, '56946f83904827105a9ae52d64a91a1d', 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'status', 'in_progress', 'in_progress', NULL, '2026-08-07 11:36:20', '2026-08-07 11:36:20.465', '2026-08-07 11:36:20.465', NULL, 'd57ff6176db9e02ac1e97fb8cb8e609b', 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 ('06FT0TZC9MJV6KZTSRVRYQ327M', 'status', 'in_progress', 'in_progress', NULL, '2026-08-07 11:36:20', '2026-08-07 11:36:20.465', '2026-08-07 11:36:20.465', NULL, 'd57ff6176db9e02ac1e97fb8cb8e609b', 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 ('06FSJWSX11WV3C1XXZEV88Q3P0', 'status', 'backlog', 'in_progress', NULL, '2026-08-07 12:44:21', '2026-08-07 12:44:21.620', '2026-08-07 12:44:21.620', NULL, '013321fe4ccaa686875a688ae6174292', 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 ('06FSJWSX11WV3C1XXZEV88Q3P0', 'status', 'backlog', 'in_progress', NULL, '2026-08-07 12:44:21', '2026-08-07 12:44:21.620', '2026-08-07 12:44:21.620', NULL, '013321fe4ccaa686875a688ae6174292', 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 ('06FXF1VDVQDQ8EFGTXX787M90R', 'status', 'backlog', 'in_progress', NULL, '2026-08-13 22:12:59', '2026-08-13 22:12:59.623', '2026-08-13 22:12:59.623', NULL, '679671404e00c4e081f107559acc4c2d', 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 ('06FXF1VDVQDQ8EFGTXX787M90R', 'description', 'Found 2026-08-06 when the visual capture resolution was raised from 960x540 to the native 3440x1440. On Ferrath (GJ820Bc) Global the wire delivers 375 river courses and the annotation layer draws NONE: ''courses=375 drawn=0'' in the view-transform readout. At 960x540 the SAME build drew them as visible strokes, so this is resolution-dependent, not a river-generation failure -- the courses are present and correct on the wire. Suspect the D-261 cull (drop a course below 15 px of on-screen length, 3x the 5 px stroke) or the water-truncation step: T-1237 computes the clipped/culled polylines ONCE on canvas adoption (set_frame) rather than per draw, so if adoption runs against a pre-layout or stale viewport the screen-length measurement is wrong for every course at once -- which matches an all-or-nothing drawn=0 rather than a partial cull. Note the scale moved the RIGHT way for visibility (46.792 km/gridunit at 960x540 vs 35.267 at 3440x1440 -- more gridunits across the body, so a river spans MORE of them), which makes a legitimate cull an unlikely explanation. Reproduce: tests/run-visual --screenshot atlas_GJ820Bc_Global and read the drawn= count.', 'Found 2026-08-06 when the visual capture resolution was raised from 960x540 to the native 3440x1440. On Ferrath (GJ820Bc) Global the wire delivers 375 river courses and the annotation layer draws NONE: ''courses=375 drawn=0'' in the view-transform readout. At 960x540 the SAME build drew them as visible strokes, so this is resolution-dependent, not a river-generation failure -- the courses are present and correct on the wire. Suspect the D-261 cull (drop a course below 15 px of on-screen length, 3x the 5 px stroke) or the water-truncation step: T-1237 computes the clipped/culled polylines ONCE on canvas adoption (set_frame) rather than per draw, so if adoption runs against a pre-layout or stale viewport the screen-length measurement is wrong for every course at once -- which matches an all-or-nothing drawn=0 rather than a partial cull. Note the scale moved the RIGHT way for visibility (46.792 km/gridunit at 960x540 vs 35.267 at 3440x1440 -- more gridunits across the body, so a river spans MORE of them), which makes a legitimate cull an unlikely explanation. Reproduce: tests/run-visual --screenshot atlas_GJ820Bc_Global and read the drawn= count.
|
||||||
|
|
||||||
|
---
|
||||||
|
DIAGNOSED 2026-08-14. Not a client rendering bug. Both suspects in the original
|
||||||
|
report are wrong, and so is the `team: client` label — the defect is a stale
|
||||||
|
client-side disk cache (T-1183/D-255), invalidated by nothing that changed.
|
||||||
|
|
||||||
|
MEASUREMENT. Same build, same scenario, same native 3440x1440, only the cache
|
||||||
|
differs:
|
||||||
|
|
||||||
|
stale cache courses=375 runs=180 longest=6.0px (~106 km) drawn=0
|
||||||
|
cold cache courses=73 runs=23 longest=93.2px (~1,644 km) drawn=18
|
||||||
|
|
||||||
|
Server-side, at the same moment the cold capture ran:
|
||||||
|
`river_cells=615 paths=123 courses=73 longest_path_cells=27 ta_w=512 ta_h=256`.
|
||||||
|
The network is exactly as designed. During the STALE capture the server logged
|
||||||
|
NO course production at all — the canvas never came from it.
|
||||||
|
|
||||||
|
WHY IT LOOKED RESOLUTION-DEPENDENT. It isn''t. 960x540 resolves to an 814x407
|
||||||
|
canvas, a cache key never written before, so it MISSED and re-derived correctly
|
||||||
|
(73 courses, 1,644 km trunk). 3440x1440 resolves to 1080x540, which HAD a cached
|
||||||
|
entry from 2026-08-06 — written before T-1237 (4e503c356) replaced one-course-
|
||||||
|
per-D8-hop with one-course-per-river. Every `.dat` payload for GJ820Bc predates
|
||||||
|
that fix; the oldest is 2026-07-28. So the "high resolution" capture was
|
||||||
|
replaying a pre-fix canvas: 375 hop fragments, none clearing D-261''s
|
||||||
|
read-as-a-line floor, hence drawn=0. The ticket''s own note that the scale "moved
|
||||||
|
the RIGHT way for visibility" was correct and was the clue — a legitimate cull
|
||||||
|
could not explain it, because the cull was never the actor.
|
||||||
|
|
||||||
|
The 375 / 180 / 0 chain also matches, digit for digit, the pre-fix measurement
|
||||||
|
already written into `_cull_short`''s doc comment. That number was being re-read
|
||||||
|
off a cache, not re-measured.
|
||||||
|
|
||||||
|
ROOT CAUSE. The disk cache''s only invalidation signal is `project.yaml: version`
|
||||||
|
(step_canvas_disk_cache.gd `current_schema_version()`). 4e503c356 changed how
|
||||||
|
canvases are GENERATED but touched only the annotation layer, river_course.rs and
|
||||||
|
step_canvas.rs — never project.yaml — so every hop-shaped entry stayed "valid".
|
||||||
|
All 13 stale GJ820Bc entries are stamped 0.4.5, the then-current version. This is
|
||||||
|
the fourth instance of the same class: project.yaml''s own comments record 0.4.2,
|
||||||
|
0.4.3, 0.4.4 and 0.4.5 as bumps forced by exactly this failure.
|
||||||
|
|
||||||
|
FIX, three parts:
|
||||||
|
1. project.yaml 0.4.5 -> 0.4.6, forcing every pre-T-1237 entry to miss. This is
|
||||||
|
what repairs a real player''s Atlas; clearing a local cache is not a fix.
|
||||||
|
2. tests/run-visual isolates `user://` per capture (XDG_DATA_HOME into
|
||||||
|
.cache/visual-user-data, recreated each run). The harness was reading the
|
||||||
|
developer''s persistent cache, so a capture could render a canvas built by a
|
||||||
|
build that no longer existed — and every golden shot in that window silently
|
||||||
|
inherited it. A visual test must exercise the tree it is run against.
|
||||||
|
3. tests/run-visual keeps server stderr (was `2>/dev/null` into an unlinked
|
||||||
|
mktemp file). No tracing output from a capture was reachable, which is why
|
||||||
|
"the server produced nothing" was invisible for eight days.
|
||||||
|
|
||||||
|
Diagnostic left in place: the capture readout now prints `runs=` and `longest=`
|
||||||
|
between `courses=` and `drawn=`, so the three stages of "arrived -> survived the
|
||||||
|
water clip -> survived the length cull" can be told apart from a single capture.
|
||||||
|
That distinction is what made this solvable, and its absence is what made the
|
||||||
|
original report guess between two wrong suspects.
|
||||||
|
|
||||||
|
FOLLOW-UPS worth their own tickets, not done here:
|
||||||
|
- `current_schema_version()` reads `res://../project.yaml`, which does not exist
|
||||||
|
in an exported build — it returns the "?.?.?" fallback, identical for every
|
||||||
|
build, so a shipped game''s cache would never invalidate on version at all.
|
||||||
|
- Nothing enforces the generation-change/version-bump pairing. Four occurrences
|
||||||
|
suggests a check (e.g. a pre-push rule: canvas-generation paths touched =>
|
||||||
|
project.yaml version must move) rather than a fifth comment.', NULL, '2026-08-14 21:13:15', '2026-08-14 21:13:15.779', '2026-08-14 21:13:15.779', NULL, 'a7cfa7d53e62cff64cd95b5738d1ee75', 2) ON CONFLICT(hash) DO NOTHING;
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FXF1VDVQDQ8EFGTXX787M90R', 'T-1239', '2026-08-06 14:43:24.767', '2026-08-06 14:43:24.767', NULL, 'ae5ccb58f885727882f56ce2a32c91e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FXF1VDVQDQ8EFGTXX787M90R', 'T-1239', '2026-08-06 14:43:24.767', '2026-08-06 14:43:24.767', NULL, 'ae5ccb58f885727882f56ce2a32c91e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
|
||||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FXRSY7QWD8J5X6G1N86WMKEC', 'T-1240', '2026-08-07 13:26:56.708', '2026-08-07 13:26:56.708', NULL, 'dc79a976524b951a73f05532772ef86a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FXRSY7QWD8J5X6G1N86WMKEC', 'T-1240', '2026-08-07 13:26:56.708', '2026-08-07 13:26:56.708', NULL, 'dc79a976524b951a73f05532772ef86a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G0495WRHF8ADK82VR8CH1J8R', 'T-1241', '2026-08-14 21:19:17.192', '2026-08-14 21:19:17.192', NULL, '0374016de542b027720ca7a45d39f385', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
|
||||||
|
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G04975H3S7GRVQXHKYCR7BKR', 'T-1242', '2026-08-14 21:19:27.624', '2026-08-14 21:19:27.624', NULL, '410d294a5da4ba7435bc3fd4d1e08c95', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= ticket_idmap.updated_at;
|
||||||
|
|||||||
@@ -167,3 +167,72 @@ ticket''s scope, but it will be visible in the same captures, so do not mistake
|
|||||||
for a failure of the un-summarisation work.', 'in_progress', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-07 11:36:20.464', NULL, '94ccf78431914ea836ba5d59170a3395', 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;
|
for a failure of the un-summarisation work.', 'in_progress', 'high', NULL, NULL, 'D-258', '2026-07-26 21:54:15.245', '2026-08-07 11:36:20.464', NULL, '94ccf78431914ea836ba5d59170a3395', 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;
|
||||||
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 ('06FSJWSX11WV3C1XXZEV88Q3P0', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Biome/relief stipple-density layer on the terrain build (RimWorld technique 4)', 'Follow-up from T-1175''s assess-only item (2026-07-25, stig''s write-up): a stipple/texture-density layer carrying hills/mountains/forest on top of hue, driven by the already-wire-carried vegetation and elev_q L8 planes — presentation-only, D-255(e)-legal (texture-space dithering of already-derived per-cell values, deterministically seeded per cell coordinate + value so it is stable across cache hit/miss; never invents samples between server cells). Would live as a post-process in step_canvas_terrain_layer.gd::rebuild_from_canvas()''s Image.set_pixel build. Design questions to settle at pickup: (a) stipple dots inline in the existing per-cell loop (cheap, same O(wxh) pass) vs a second overlay pass (simpler code, doubles pixel-touch cost); (b) density from vegetation class directly vs a combination with elev_q — relief hachures and forest texture are two different visual grammars in the RimWorld reference, not one slider; (c) own legend toggle (TMP/MST/VEG overlay-bar pattern) vs always-on like the elevation lightness modifier. Reference: docs/design/references/rimworld-world-map-fluency.jpg. Related: T-1175, T-1162 (vegetation patchiness fields), D-255(e).', 'in_progress', 'low', NULL, 'client', NULL, '2026-07-25 13:24:54.152', '2026-08-07 12:44:21.620', NULL, 'c1560b624bd9a9d79fe919f282620f22', 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;
|
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 ('06FSJWSX11WV3C1XXZEV88Q3P0', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Biome/relief stipple-density layer on the terrain build (RimWorld technique 4)', 'Follow-up from T-1175''s assess-only item (2026-07-25, stig''s write-up): a stipple/texture-density layer carrying hills/mountains/forest on top of hue, driven by the already-wire-carried vegetation and elev_q L8 planes — presentation-only, D-255(e)-legal (texture-space dithering of already-derived per-cell values, deterministically seeded per cell coordinate + value so it is stable across cache hit/miss; never invents samples between server cells). Would live as a post-process in step_canvas_terrain_layer.gd::rebuild_from_canvas()''s Image.set_pixel build. Design questions to settle at pickup: (a) stipple dots inline in the existing per-cell loop (cheap, same O(wxh) pass) vs a second overlay pass (simpler code, doubles pixel-touch cost); (b) density from vegetation class directly vs a combination with elev_q — relief hachures and forest texture are two different visual grammars in the RimWorld reference, not one slider; (c) own legend toggle (TMP/MST/VEG overlay-bar pattern) vs always-on like the elevation lightness modifier. Reference: docs/design/references/rimworld-world-map-fluency.jpg. Related: T-1175, T-1162 (vegetation patchiness fields), D-255(e).', 'in_progress', 'low', NULL, 'client', NULL, '2026-07-25 13:24:54.152', '2026-08-07 12:44:21.620', NULL, 'c1560b624bd9a9d79fe919f282620f22', 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;
|
||||||
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 ('06FXRSY7QWD8J5X6G1N86WMKEC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'MIN_WL_BANDS_M octave cutoffs are pre-extent-inversion (stale, currently inert)', 'Found during T-1213 (2026-08-07). MIN_WL_BANDS_M (layer_proxy.rs) is built from 2*DISTRICT_M (4,096 m) and 2*QUARTER_M (1,024 m) -- Nyquist for a sample spacing equal to the rung''s CELL SIZE. That was correct while a rung fixed SPACING; after D-255''s extent inversion a rung fixes EXTENT and District''s spacing is 2048/540 = 3.8 m, so its true Nyquist floor is ~7.6 m. The bands are off by roughly the canvas cell count (~540x). This is the same class of defect as the Global 2x1 wire-extent sentinel the D-258 amendment documents: a constant that was correct under the pre-inversion model and silently outlived it. The code even states the consequence as though intended -- district_profile.rs''s comment reads ''At District''s real Nyquist floor (4,096 m) every VOXEL_OCTAVE_WAVELENGTHS_M entry is truncated, so relief is always exactly 0.0 there''. IMPORTANT SCOPE NOTE, verified before filing: this is currently INERT for the step canvas. step_canvas_viewer._fire_request() calls request_now(body, rung, center, extent) with no min_wl_m, so it defaults to 0, and quantize_min_wl_m(0) returns 0 (the leading sentinel band) -- no truncation happens on the served path. It therefore only affects the legacy layer_proxy district-window consumer. It is NOT the cause of the flat District/Quarter rungs; that is elev_q''s 80 m quantisation (0-100 across MAX_REGION_ELEVATION_KM = 8.0 km), measured at d1 mean 0.02 with the cutoff already disabled. Fix: derive the cutoff from the resolved canvas spacing rather than the rung cell size. Check the layer_proxy consumer''s expectations first -- MIN_WL_BANDS_M is shared, carries a const assert tying band 4 to OCTAVE_WAVELENGTHS_M[3], and is part of the cache key, so a change there is not local.', 'backlog', 'medium', NULL, 'server', 'D-255', '2026-08-07 13:26:56.703', '2026-08-07 13:26:56.703', NULL, '45cb7553effbe05042fe7f3eb09e6483', 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;
|
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 ('06FXRSY7QWD8J5X6G1N86WMKEC', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'MIN_WL_BANDS_M octave cutoffs are pre-extent-inversion (stale, currently inert)', 'Found during T-1213 (2026-08-07). MIN_WL_BANDS_M (layer_proxy.rs) is built from 2*DISTRICT_M (4,096 m) and 2*QUARTER_M (1,024 m) -- Nyquist for a sample spacing equal to the rung''s CELL SIZE. That was correct while a rung fixed SPACING; after D-255''s extent inversion a rung fixes EXTENT and District''s spacing is 2048/540 = 3.8 m, so its true Nyquist floor is ~7.6 m. The bands are off by roughly the canvas cell count (~540x). This is the same class of defect as the Global 2x1 wire-extent sentinel the D-258 amendment documents: a constant that was correct under the pre-inversion model and silently outlived it. The code even states the consequence as though intended -- district_profile.rs''s comment reads ''At District''s real Nyquist floor (4,096 m) every VOXEL_OCTAVE_WAVELENGTHS_M entry is truncated, so relief is always exactly 0.0 there''. IMPORTANT SCOPE NOTE, verified before filing: this is currently INERT for the step canvas. step_canvas_viewer._fire_request() calls request_now(body, rung, center, extent) with no min_wl_m, so it defaults to 0, and quantize_min_wl_m(0) returns 0 (the leading sentinel band) -- no truncation happens on the served path. It therefore only affects the legacy layer_proxy district-window consumer. It is NOT the cause of the flat District/Quarter rungs; that is elev_q''s 80 m quantisation (0-100 across MAX_REGION_ELEVATION_KM = 8.0 km), measured at d1 mean 0.02 with the cutoff already disabled. Fix: derive the cutoff from the resolved canvas spacing rather than the rung cell size. Check the layer_proxy consumer''s expectations first -- MIN_WL_BANDS_M is shared, carries a const assert tying band 4 to OCTAVE_WAVELENGTHS_M[3], and is part of the cache key, so a change there is not local.', 'backlog', 'medium', NULL, 'server', 'D-255', '2026-08-07 13:26:56.703', '2026-08-07 13:26:56.703', NULL, '45cb7553effbe05042fe7f3eb09e6483', 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;
|
||||||
|
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 ('06FXF1VDVQDQ8EFGTXX787M90R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas rivers vanish at native resolution — 375 courses arrive, 0 drawn', 'Found 2026-08-06 when the visual capture resolution was raised from 960x540 to the native 3440x1440. On Ferrath (GJ820Bc) Global the wire delivers 375 river courses and the annotation layer draws NONE: ''courses=375 drawn=0'' in the view-transform readout. At 960x540 the SAME build drew them as visible strokes, so this is resolution-dependent, not a river-generation failure -- the courses are present and correct on the wire. Suspect the D-261 cull (drop a course below 15 px of on-screen length, 3x the 5 px stroke) or the water-truncation step: T-1237 computes the clipped/culled polylines ONCE on canvas adoption (set_frame) rather than per draw, so if adoption runs against a pre-layout or stale viewport the screen-length measurement is wrong for every course at once -- which matches an all-or-nothing drawn=0 rather than a partial cull. Note the scale moved the RIGHT way for visibility (46.792 km/gridunit at 960x540 vs 35.267 at 3440x1440 -- more gridunits across the body, so a river spans MORE of them), which makes a legitimate cull an unlikely explanation. Reproduce: tests/run-visual --screenshot atlas_GJ820Bc_Global and read the drawn= count.', 'in_progress', 'high', NULL, 'client', 'D-261', '2026-08-06 14:43:24.765', '2026-08-13 22:12:59.623', NULL, 'a52ba3f39408c80765dca1eb58e79ae6', 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;
|
||||||
|
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 ('06FXF1VDVQDQ8EFGTXX787M90R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas rivers vanish at native resolution — 375 courses arrive, 0 drawn', 'Found 2026-08-06 when the visual capture resolution was raised from 960x540 to the native 3440x1440. On Ferrath (GJ820Bc) Global the wire delivers 375 river courses and the annotation layer draws NONE: ''courses=375 drawn=0'' in the view-transform readout. At 960x540 the SAME build drew them as visible strokes, so this is resolution-dependent, not a river-generation failure -- the courses are present and correct on the wire. Suspect the D-261 cull (drop a course below 15 px of on-screen length, 3x the 5 px stroke) or the water-truncation step: T-1237 computes the clipped/culled polylines ONCE on canvas adoption (set_frame) rather than per draw, so if adoption runs against a pre-layout or stale viewport the screen-length measurement is wrong for every course at once -- which matches an all-or-nothing drawn=0 rather than a partial cull. Note the scale moved the RIGHT way for visibility (46.792 km/gridunit at 960x540 vs 35.267 at 3440x1440 -- more gridunits across the body, so a river spans MORE of them), which makes a legitimate cull an unlikely explanation. Reproduce: tests/run-visual --screenshot atlas_GJ820Bc_Global and read the drawn= count.
|
||||||
|
|
||||||
|
---
|
||||||
|
DIAGNOSED 2026-08-14. Not a client rendering bug. Both suspects in the original
|
||||||
|
report are wrong, and so is the `team: client` label — the defect is a stale
|
||||||
|
client-side disk cache (T-1183/D-255), invalidated by nothing that changed.
|
||||||
|
|
||||||
|
MEASUREMENT. Same build, same scenario, same native 3440x1440, only the cache
|
||||||
|
differs:
|
||||||
|
|
||||||
|
stale cache courses=375 runs=180 longest=6.0px (~106 km) drawn=0
|
||||||
|
cold cache courses=73 runs=23 longest=93.2px (~1,644 km) drawn=18
|
||||||
|
|
||||||
|
Server-side, at the same moment the cold capture ran:
|
||||||
|
`river_cells=615 paths=123 courses=73 longest_path_cells=27 ta_w=512 ta_h=256`.
|
||||||
|
The network is exactly as designed. During the STALE capture the server logged
|
||||||
|
NO course production at all — the canvas never came from it.
|
||||||
|
|
||||||
|
WHY IT LOOKED RESOLUTION-DEPENDENT. It isn''t. 960x540 resolves to an 814x407
|
||||||
|
canvas, a cache key never written before, so it MISSED and re-derived correctly
|
||||||
|
(73 courses, 1,644 km trunk). 3440x1440 resolves to 1080x540, which HAD a cached
|
||||||
|
entry from 2026-08-06 — written before T-1237 (4e503c356) replaced one-course-
|
||||||
|
per-D8-hop with one-course-per-river. Every `.dat` payload for GJ820Bc predates
|
||||||
|
that fix; the oldest is 2026-07-28. So the "high resolution" capture was
|
||||||
|
replaying a pre-fix canvas: 375 hop fragments, none clearing D-261''s
|
||||||
|
read-as-a-line floor, hence drawn=0. The ticket''s own note that the scale "moved
|
||||||
|
the RIGHT way for visibility" was correct and was the clue — a legitimate cull
|
||||||
|
could not explain it, because the cull was never the actor.
|
||||||
|
|
||||||
|
The 375 / 180 / 0 chain also matches, digit for digit, the pre-fix measurement
|
||||||
|
already written into `_cull_short`''s doc comment. That number was being re-read
|
||||||
|
off a cache, not re-measured.
|
||||||
|
|
||||||
|
ROOT CAUSE. The disk cache''s only invalidation signal is `project.yaml: version`
|
||||||
|
(step_canvas_disk_cache.gd `current_schema_version()`). 4e503c356 changed how
|
||||||
|
canvases are GENERATED but touched only the annotation layer, river_course.rs and
|
||||||
|
step_canvas.rs — never project.yaml — so every hop-shaped entry stayed "valid".
|
||||||
|
All 13 stale GJ820Bc entries are stamped 0.4.5, the then-current version. This is
|
||||||
|
the fourth instance of the same class: project.yaml''s own comments record 0.4.2,
|
||||||
|
0.4.3, 0.4.4 and 0.4.5 as bumps forced by exactly this failure.
|
||||||
|
|
||||||
|
FIX, three parts:
|
||||||
|
1. project.yaml 0.4.5 -> 0.4.6, forcing every pre-T-1237 entry to miss. This is
|
||||||
|
what repairs a real player''s Atlas; clearing a local cache is not a fix.
|
||||||
|
2. tests/run-visual isolates `user://` per capture (XDG_DATA_HOME into
|
||||||
|
.cache/visual-user-data, recreated each run). The harness was reading the
|
||||||
|
developer''s persistent cache, so a capture could render a canvas built by a
|
||||||
|
build that no longer existed — and every golden shot in that window silently
|
||||||
|
inherited it. A visual test must exercise the tree it is run against.
|
||||||
|
3. tests/run-visual keeps server stderr (was `2>/dev/null` into an unlinked
|
||||||
|
mktemp file). No tracing output from a capture was reachable, which is why
|
||||||
|
"the server produced nothing" was invisible for eight days.
|
||||||
|
|
||||||
|
Diagnostic left in place: the capture readout now prints `runs=` and `longest=`
|
||||||
|
between `courses=` and `drawn=`, so the three stages of "arrived -> survived the
|
||||||
|
water clip -> survived the length cull" can be told apart from a single capture.
|
||||||
|
That distinction is what made this solvable, and its absence is what made the
|
||||||
|
original report guess between two wrong suspects.
|
||||||
|
|
||||||
|
FOLLOW-UPS worth their own tickets, not done here:
|
||||||
|
- `current_schema_version()` reads `res://../project.yaml`, which does not exist
|
||||||
|
in an exported build — it returns the "?.?.?" fallback, identical for every
|
||||||
|
build, so a shipped game''s cache would never invalidate on version at all.
|
||||||
|
- Nothing enforces the generation-change/version-bump pairing. Four occurrences
|
||||||
|
suggests a check (e.g. a pre-push rule: canvas-generation paths touched =>
|
||||||
|
project.yaml version must move) rather than a fifth comment.', 'in_progress', 'high', NULL, 'client', 'D-261', '2026-08-06 14:43:24.765', '2026-08-14 21:13:15.779', NULL, 'ee86b57855c05540234c4b0f04ca2a18', 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;
|
||||||
|
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 ('06G0495WRHF8ADK82VR8CH1J8R', 'bug', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Atlas disk cache never invalidates in an exported build — current_schema_version() falls back to ''?.?.?''', 'step_canvas_disk_cache.gd::current_schema_version() reads ProjectSettings.globalize_path(''res://'') + ''/../project.yaml''. That resolves to the repo-root file in a dev run (res:// = client/), but an exported build has no project.yaml one level above res://, so the function returns its ''?.?.?'' fallback. Every exported build therefore stamps and compares the SAME sentinel version, which means the schema-version invalidation path — the cache''s only invalidation signal — is inert in a shipped game: a canvas cached by one build is served forever by every later build. Found while diagnosing T-1239, where the same mechanism failed in dev for a different reason (the version simply was not bumped). Fix direction: bake the version into the client at export time (a generated const, ProjectSettings application/config/version, or an exported resource) rather than reading a repo file at runtime. Note test_current_schema_version_reads_project_yaml passes in dev and would not catch this — it asserts the non-fallback path, in the only environment where that path works.', 'backlog', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:17.188', '2026-08-14 21:19:17.188', NULL, '7880dfa6b9038360c78843ae9f6ca2e2', 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;
|
||||||
|
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 ('06G04975H3S7GRVQXHKYCR7BKR', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Enforce the canvas-generation/project.yaml-version pairing — four silent stale-cache regressions and counting', 'project.yaml''s version is the Atlas disk cache''s only invalidation signal, and nothing enforces that a change to canvas GENERATION also moves it. The file''s own comment block now records four bumps forced after the fact by exactly this failure: 0.4.2 (lake_margin_q semantics), 0.4.3 (coast_warp_px at orbital sampling), 0.4.4 (D-255 extent inversion), 0.4.5 (Global sentinel), and now 0.4.6 (T-1237 one-course-per-river, diagnosed as T-1239 eight days after it shipped). The failure is silent and machine-dependent: it reproduces only where a warm cache exists, so the author with a cold checkout sees nothing wrong. Direction: a pre-push check in .config/hooks/pre-push — if the push touches the canvas-generation paths (server/src/atlas/step_canvas.rs, river_course.rs, layer1.rs, district_profile.rs, the client step_canvas layers) and project.yaml''s version line is unchanged in the same range, reject with the reason. Registry-driven like tooling/generator_sources.py rather than a hand-kept path list in the hook. A false positive is cheap (bump the version, entries miss once); a false negative is another week of a wrong map.', 'backlog', 'medium', NULL, 'client', 'D-255', '2026-08-14 21:19:27.624', '2026-08-14 21:19:27.624', NULL, '5386de8d8c8120e17373cc1c1a59a9cd', 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;
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ func _log_atlas_view_transform(tree_root: Node, scenario_name: String) -> void:
|
|||||||
(
|
(
|
||||||
"visual_capture: view-transform[%s] rung=%s world_center=%s held_extent=%s "
|
"visual_capture: view-transform[%s] rung=%s world_center=%s held_extent=%s "
|
||||||
+ "canvas_position=%s canvas_scale=%s footprint_px=%s canvas_cells=%dx%d "
|
+ "canvas_position=%s canvas_scale=%s footprint_px=%s canvas_cells=%dx%d "
|
||||||
+ "courses=%d drawn=%d settlements=%d"
|
+ "courses=%d runs=%d longest=%.1fpx drawn=%d settlements=%d"
|
||||||
)
|
)
|
||||||
% [
|
% [
|
||||||
scenario_name,
|
scenario_name,
|
||||||
@@ -348,6 +348,8 @@ func _log_atlas_view_transform(tree_root: Node, scenario_name: String) -> void:
|
|||||||
int(summary.get("canvas_width", 0)),
|
int(summary.get("canvas_width", 0)),
|
||||||
int(summary.get("canvas_height", 0)),
|
int(summary.get("canvas_height", 0)),
|
||||||
int(summary.get("course_count", 0)),
|
int(summary.get("course_count", 0)),
|
||||||
|
int(summary.get("runs_built", 0)),
|
||||||
|
float(summary.get("longest_run_px", 0.0)),
|
||||||
int(summary.get("drawn_course_count", 0)),
|
int(summary.get("drawn_course_count", 0)),
|
||||||
int(summary.get("settlement_count", 0)),
|
int(summary.get("settlement_count", 0)),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -206,6 +206,16 @@ var _body_radius_km: float = 0.0
|
|||||||
## (D-261). Entries: {points: PackedVector2Array, terminus: String, taper: bool}.
|
## (D-261). Entries: {points: PackedVector2Array, terminus: String, taper: bool}.
|
||||||
var _prepared_courses: Array = []
|
var _prepared_courses: Array = []
|
||||||
|
|
||||||
|
## Preparation stage counts, for telling apart the three ways a river can fail
|
||||||
|
## to appear (T-1239). "375 courses arrived and 0 were drawn" is not one fact
|
||||||
|
## but a chain: courses arrive, the water clip splits them into runs, the length
|
||||||
|
## cull drops the short runs. Without the intermediate counts every failure
|
||||||
|
## looks identical from a capture, which is how T-1239 sat as "suspect the cull
|
||||||
|
## OR the water truncation" instead of a measurement. Cheap: three integers
|
||||||
|
## written once per canvas adoption, never per frame.
|
||||||
|
var _runs_built: int = 0
|
||||||
|
var _longest_run_px: float = 0.0
|
||||||
|
|
||||||
|
|
||||||
## Adopt a new canvas + its request frame (world center, rung, extent, body
|
## Adopt a new canvas + its request frame (world center, rung, extent, body
|
||||||
## radius) — the world->screen projection for every drawn feature depends on
|
## radius) — the world->screen projection for every drawn feature depends on
|
||||||
@@ -249,6 +259,8 @@ func clear_frame() -> void:
|
|||||||
## dropped entirely rather than drawn as specks.
|
## dropped entirely rather than drawn as specks.
|
||||||
func _prepare_courses() -> void:
|
func _prepare_courses() -> void:
|
||||||
_prepared_courses.clear()
|
_prepared_courses.clear()
|
||||||
|
_runs_built = 0
|
||||||
|
_longest_run_px = 0.0
|
||||||
if not _canvas is Dictionary:
|
if not _canvas is Dictionary:
|
||||||
return
|
return
|
||||||
var d: Dictionary = _canvas
|
var d: Dictionary = _canvas
|
||||||
@@ -298,6 +310,8 @@ func _flush_run(
|
|||||||
) -> void:
|
) -> void:
|
||||||
if run.size() < 2:
|
if run.size() < 2:
|
||||||
return
|
return
|
||||||
|
_runs_built += 1
|
||||||
|
_longest_run_px = maxf(_longest_run_px, _polyline_length_px(run))
|
||||||
_prepared_courses.append(
|
_prepared_courses.append(
|
||||||
{
|
{
|
||||||
"points": run,
|
"points": run,
|
||||||
@@ -351,6 +365,21 @@ func get_drawn_course_count() -> int:
|
|||||||
return _prepared_courses.size()
|
return _prepared_courses.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Runs the water clip produced, before the length cull (T-1239 diagnostic).
|
||||||
|
## Read with get_drawn_course_count(): runs_built == 0 means the water clip ate
|
||||||
|
## everything; runs_built high with drawn 0 means the cull did.
|
||||||
|
func get_runs_built() -> int:
|
||||||
|
return _runs_built
|
||||||
|
|
||||||
|
|
||||||
|
## The longest run's on-screen length in px, before the cull (T-1239). Compared
|
||||||
|
## against MIN_COURSE_LENGTH_PX it says whether the threshold was near-miss or
|
||||||
|
## nowhere close — the difference between "tune the constant" and "the geometry
|
||||||
|
## is wrong".
|
||||||
|
func get_longest_run_px() -> float:
|
||||||
|
return _longest_run_px
|
||||||
|
|
||||||
|
|
||||||
static func _polyline_length_px(pts: PackedVector2Array) -> float:
|
static func _polyline_length_px(pts: PackedVector2Array) -> float:
|
||||||
var total: float = 0.0
|
var total: float = 0.0
|
||||||
for i in range(1, pts.size()):
|
for i in range(1, pts.size()):
|
||||||
|
|||||||
@@ -434,6 +434,10 @@ func get_current_canvas_summary() -> Dictionary:
|
|||||||
"drawn_course_count": (
|
"drawn_course_count": (
|
||||||
_annotation_layer.get_drawn_course_count() if _annotation_layer else 0
|
_annotation_layer.get_drawn_course_count() if _annotation_layer else 0
|
||||||
),
|
),
|
||||||
|
# T-1239: the two stages between "arrived" and "drawn", so a capture can
|
||||||
|
# say WHICH stage dropped the rivers instead of only that they are gone.
|
||||||
|
"runs_built": _annotation_layer.get_runs_built() if _annotation_layer else 0,
|
||||||
|
"longest_run_px": _annotation_layer.get_longest_run_px() if _annotation_layer else 0.0,
|
||||||
"course_count_by_class": course_count_by_class,
|
"course_count_by_class": course_count_by_class,
|
||||||
"cliff_count": (d.get("cliffs", []) as Array).size(),
|
"cliff_count": (d.get("cliffs", []) as Array).size(),
|
||||||
"settlement_count": _count_distinct_settlements(d),
|
"settlement_count": _count_distinct_settlements(d),
|
||||||
|
|||||||
+16
-1
@@ -19,7 +19,22 @@ name: The Settled Reach
|
|||||||
# every Global request resolved to a two-cell canvas regardless of viewport.
|
# every Global request resolved to a two-cell canvas regardless of viewport.
|
||||||
# Those entries were also cached under a key that ignored extent, so they
|
# Those entries were also cached under a key that ignored extent, so they
|
||||||
# answered every later request forever. 0.4.5 forces them to miss.
|
# answered every later request forever. 0.4.5 forces them to miss.
|
||||||
version: 0.4.5
|
# 0.4.5-tagged canvases carry pre-T-1237 river courses: one course per D8 HOP
|
||||||
|
# rather than one per river (build_paths walks whole headwater->mouth rivers
|
||||||
|
# now). The hop-shaped entries survived that fix because it changed only server
|
||||||
|
# and client code, never this version — so Ferrath Global replayed 375 fragments
|
||||||
|
# with a 106 km longest run, all of them below D-261's read-as-a-line floor, and
|
||||||
|
# drew NOTHING, while a cold cache on the same build produced 73 whole rivers
|
||||||
|
# with a 1,644 km trunk and drew 18. Diagnosed as T-1239. 0.4.6 forces them to
|
||||||
|
# miss.
|
||||||
|
#
|
||||||
|
# THE PATTERN, now four bumps deep: a change to how a canvas is GENERATED is
|
||||||
|
# only half the change — the other half is this line. Nothing enforces the
|
||||||
|
# pairing, so a canvas-generation commit that leaves this version alone ships a
|
||||||
|
# silent regression that reproduces only on machines with a warm cache. The
|
||||||
|
# capture harness no longer hides it (tests/run-visual isolates user:// per run,
|
||||||
|
# T-1239), but a real player's cache is still governed by this number alone.
|
||||||
|
version: 0.4.6
|
||||||
repository: settled-reach
|
repository: settled-reach
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -781,6 +781,21 @@ fn invent_courses_for_canvas(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// T-1239: the network is built once per body on the fixed GRID_W x GRID_H
|
||||||
|
// working grid, so these counts MUST NOT vary with canvas extent. They were
|
||||||
|
// observed to (73 courses at one viewport, 375 at another), which is only
|
||||||
|
// visible if the pre-crop network size is logged next to the post-crop wire
|
||||||
|
// size — the client can only ever see the latter.
|
||||||
|
tracing::info!(
|
||||||
|
river_cells = river_network.river_cells.len(),
|
||||||
|
paths = paths.len(),
|
||||||
|
courses = courses.len(),
|
||||||
|
longest_path_cells = paths.iter().map(|p| p.cells.len()).max().unwrap_or(0),
|
||||||
|
ta_w = ta.w,
|
||||||
|
ta_h = ta.h,
|
||||||
|
"step canvas: river course production"
|
||||||
|
);
|
||||||
|
|
||||||
courses
|
courses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+39
-9
@@ -43,6 +43,13 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--filter) shift 2 ;; # Accept and ignore (run-all compat)
|
--filter) shift 2 ;; # Accept and ignore (run-all compat)
|
||||||
--filter=*) shift ;;
|
--filter=*) shift ;;
|
||||||
--interval) INTERVAL="${2:-3}"; shift 2 ;;
|
--interval) INTERVAL="${2:-3}"; shift 2 ;;
|
||||||
|
# Override the config resolution for ONE ad-hoc capture (T-1239). The
|
||||||
|
# Atlas derives canvas extent, and therefore metres-per-gridunit, from
|
||||||
|
# the viewport — so "does this defect depend on resolution?" is a real
|
||||||
|
# diagnostic question, and answering it by hand-editing the committed
|
||||||
|
# tests/visual.json invites leaving it edited. Goldens are shot at the
|
||||||
|
# config resolution; this flag deliberately does not touch them.
|
||||||
|
--resolution) RESOLUTION_OVERRIDE="${2:-}"; shift 2 ;;
|
||||||
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
*) echo "Unknown argument: $1" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
@@ -70,6 +77,10 @@ fi
|
|||||||
GOLDEN_DIR="$ROOT/$(python3 -c "import json; c=json.load(open('$CONFIG')); print(c.get('golden_dir','client/tests/golden/visual'))")"
|
GOLDEN_DIR="$ROOT/$(python3 -c "import json; c=json.load(open('$CONFIG')); print(c.get('golden_dir','client/tests/golden/visual'))")"
|
||||||
TOLERANCE="$(python3 -c "import json; c=json.load(open('$CONFIG')); print(c.get('tolerance', 5))")"
|
TOLERANCE="$(python3 -c "import json; c=json.load(open('$CONFIG')); print(c.get('tolerance', 5))")"
|
||||||
RESOLUTION="$(python3 -c "import json; c=json.load(open('$CONFIG')); r=c.get('resolution',[960,540]); print(f'{r[0]}x{r[1]}')")"
|
RESOLUTION="$(python3 -c "import json; c=json.load(open('$CONFIG')); r=c.get('resolution',[960,540]); print(f'{r[0]}x{r[1]}')")"
|
||||||
|
if [[ -n "${RESOLUTION_OVERRIDE:-}" ]]; then
|
||||||
|
RESOLUTION="$RESOLUTION_OVERRIDE"
|
||||||
|
echo "Note: resolution overridden to $RESOLUTION (goldens are shot at the config resolution)" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
# Read scenario names from config
|
# Read scenario names from config
|
||||||
SCENARIOS=($(python3 -c "
|
SCENARIOS=($(python3 -c "
|
||||||
@@ -117,6 +128,21 @@ godot_capture() {
|
|||||||
local output="$3"
|
local output="$3"
|
||||||
local extra_args=("${@:4}")
|
local extra_args=("${@:4}")
|
||||||
|
|
||||||
|
# Isolate `user://` per run (T-1239). Godot resolves user:// under
|
||||||
|
# XDG_DATA_HOME, which is how the Atlas disk cache (D-255, T-1183) persisted
|
||||||
|
# across captures — including across the server changes that made its
|
||||||
|
# contents wrong. A capture then rendered a canvas generated by a build that
|
||||||
|
# no longer existed: Ferrath Global replayed a pre-T-1237 canvas from weeks
|
||||||
|
# earlier and showed 375 hop-fragments where the live server produces 73
|
||||||
|
# whole rivers, and every golden shot in that window silently inherited it.
|
||||||
|
# A visual test must exercise the code in the tree, so the cache it warms
|
||||||
|
# must not outlive the run. The dir is recreated fresh each capture; the
|
||||||
|
# user's real cache at ~/.local/share/godot is never touched.
|
||||||
|
local user_data="$ROOT/.cache/visual-user-data"
|
||||||
|
rm -rf "$user_data"
|
||||||
|
mkdir -p "$user_data"
|
||||||
|
|
||||||
|
XDG_DATA_HOME="$user_data" \
|
||||||
"${CAPTURE_PREFIX[@]}" "$GODOT" --rendering-driver opengl3 --fixed-fps 60 \
|
"${CAPTURE_PREFIX[@]}" "$GODOT" --rendering-driver opengl3 --fixed-fps 60 \
|
||||||
--resolution "$RESOLUTION" \
|
--resolution "$RESOLUTION" \
|
||||||
--path "$ROOT/client" -s res://tests/visual_capture.gd -- \
|
--path "$ROOT/client" -s res://tests/visual_capture.gd -- \
|
||||||
@@ -154,32 +180,36 @@ ensure_server_built() {
|
|||||||
|
|
||||||
# Start server with --test-mode --port 0, parse LISTENING:{port}
|
# Start server with --test-mode --port 0, parse LISTENING:{port}
|
||||||
start_server() {
|
start_server() {
|
||||||
local stdout_log
|
# Keep the server's own output (T-1239). This was `mktemp` + `2>/dev/null`
|
||||||
stdout_log=$(mktemp)
|
# + `rm` as soon as LISTENING was parsed, which meant two things: every
|
||||||
"$SERVER_BIN" --test-mode --port 0 >"$stdout_log" 2>/dev/null &
|
# tracing::warn!/error! the simulation emitted was discarded, and everything
|
||||||
|
# after startup went to an unlinked file. A server quietly degrading mid-
|
||||||
|
# capture looked identical to a healthy one — the capture only ever showed
|
||||||
|
# what the CLIENT thought it received. Now stderr is merged in and the log
|
||||||
|
# lives at a stable path that survives the run for inspection.
|
||||||
|
local stdout_log="$ROOT/.cache/visual-server.log"
|
||||||
|
mkdir -p "$ROOT/.cache"
|
||||||
|
"$SERVER_BIN" --test-mode --port 0 >"$stdout_log" 2>&1 &
|
||||||
SERVER_PID=$!
|
SERVER_PID=$!
|
||||||
|
|
||||||
local attempts=0
|
local attempts=0
|
||||||
while [[ $attempts -lt 150 ]]; do
|
while [[ $attempts -lt 150 ]]; do
|
||||||
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
if ! kill -0 "$SERVER_PID" 2>/dev/null; then
|
||||||
echo " Error: server exited unexpectedly" >&2
|
echo " Error: server exited unexpectedly — see $stdout_log" >&2
|
||||||
rm -f "$stdout_log"
|
|
||||||
SERVER_PID=""
|
SERVER_PID=""
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
if grep -q "^LISTENING:" "$stdout_log" 2>/dev/null; then
|
if grep -q "^LISTENING:" "$stdout_log" 2>/dev/null; then
|
||||||
SERVER_PORT=$(sed -n 's/^LISTENING://p' "$stdout_log")
|
SERVER_PORT=$(sed -n 's/^LISTENING://p' "$stdout_log")
|
||||||
rm -f "$stdout_log"
|
echo " Server started: pid=$SERVER_PID port=$SERVER_PORT log=$stdout_log"
|
||||||
echo " Server started: pid=$SERVER_PID port=$SERVER_PORT"
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
sleep 0.1
|
sleep 0.1
|
||||||
attempts=$((attempts + 1))
|
attempts=$((attempts + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
echo " Error: no LISTENING signal after 15s" >&2
|
echo " Error: no LISTENING signal after 15s — see $stdout_log" >&2
|
||||||
kill "$SERVER_PID" 2>/dev/null || true
|
kill "$SERVER_PID" 2>/dev/null || true
|
||||||
rm -f "$stdout_log"
|
|
||||||
SERVER_PID=""
|
SERVER_PID=""
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user