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>
171 lines
27 KiB
SQL
171 lines
27 KiB
SQL
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 ('06FT0TX2W0BA10PRR7NMJ2362M', 'description', 'D-258: the Atlas cascade gains a single expanded layer (''rung 0.5''), generated once per body from the rung-0 input pair (heightmap.png + reliefmap.png, never displayed), and every zoom tier below it (Region/District/Quarter/Block/Chunk) derives from that layer instead of independently re-deriving from the source files. Hydrology (drainage, course routing, lake fill) resolves ONCE on rung 0.5 and nowhere else -- it is a whole-body computation, not derivable per-window. Global displays rung 0.5 directly (biome-from-orbit, not a photograph). Same-session amendment: lake shorelines run the same shore-morphology code as ocean shorelines (coastline warp applied to both surfaces in the single rung-0.5 pass; shore-morphology gates key on proximity to water, not to ocean; sea-flavored types like TidalFlat/Estuarine-vs-Delta separated by tidal energy, a derived quantity, never by an is-it-the-ocean switch; salinity excluded from morphology entirely). This is a named, principled carve-out from D-227 derive-don''t-store: a whole-body flow solve is not locally computable, so it cannot be re-derived per window at any price -- storage here buys correctness, not convenience. Not yet ticketed prior to this epic (D-258''s own Implementation note). See governance/decisions/architecture.md#d-258.
|
|
|
|
RE-SCOPE REQUIRED BEFORE ANY CHILD STARTS (2026-07-27, D-258 amendment). The structural rationale for this epic was disproven on evidence the day after it was written. D-258 claimed hydrology ''was not derivable at all'' per-window; in fact layer1.rs::run_layer1_with_moisture already solves drainage AND settled-equilibrium hydrology once per body, folds the filled surface into TerrainAnalysis, and every rung bilinearly samples it -- the code''s own comment calls it ''a coarse continuous primitive computed once, sampled fresh at every rung, never re-solved'' (mechanism B, D-255(f)). Compute-once-sample-everywhere already exists. What actually made Global look flat was serve_step_canvas_request zeroing Global''s wire extent (a pre-extent-inversion sentinel), producing a 2x1 canvas -- fixed 2026-07-27; once sized correctly Global reads as a world with no hydrology work at all. Rivers at Global were measured as negligible: 375 courses present, 458 of 518,400 pixels different vs courses-off, because at ~39.7 km/gridunit most courses are shorter than one gridunit. SURVIVES: biome un-summarisation (reliefmap as plurality), composition-on-descent, the conservation invariant, and the lake-shore amendment. WEAKENED: the stored expanded layer and its D-227 carve-out. The live question is no longer ''what does rung 0.5 cost'' but ''does biome un-summarisation need a stored layer at all, or does it ride the existing sample-fresh-at-every-rung mechanism''. Re-scope this epic and T-1212 against that question first.', 'D-258: the Atlas cascade gains a single expanded layer (''rung 0.5''), generated once per body from the rung-0 input pair (heightmap.png + reliefmap.png, never displayed), and every zoom tier below it (Region/District/Quarter/Block/Chunk) derives from that layer instead of independently re-deriving from the source files. Hydrology (drainage, course routing, lake fill) resolves ONCE on rung 0.5 and nowhere else -- it is a whole-body computation, not derivable per-window. Global displays rung 0.5 directly (biome-from-orbit, not a photograph). Same-session amendment: lake shorelines run the same shore-morphology code as ocean shorelines (coastline warp applied to both surfaces in the single rung-0.5 pass; shore-morphology gates key on proximity to water, not to ocean; sea-flavored types like TidalFlat/Estuarine-vs-Delta separated by tidal energy, a derived quantity, never by an is-it-the-ocean switch; salinity excluded from morphology entirely). This is a named, principled carve-out from D-227 derive-don''t-store: a whole-body flow solve is not locally computable, so it cannot be re-derived per window at any price -- storage here buys correctness, not convenience. Not yet ticketed prior to this epic (D-258''s own Implementation note). See governance/decisions/architecture.md#d-258.
|
|
|
|
RE-SCOPE REQUIRED BEFORE ANY CHILD STARTS (2026-07-27, D-258 amendment). The structural rationale for this epic was disproven on evidence the day after it was written. D-258 claimed hydrology ''was not derivable at all'' per-window; in fact layer1.rs::run_layer1_with_moisture already solves drainage AND settled-equilibrium hydrology once per body, folds the filled surface into TerrainAnalysis, and every rung bilinearly samples it -- the code''s own comment calls it ''a coarse continuous primitive computed once, sampled fresh at every rung, never re-solved'' (mechanism B, D-255(f)). Compute-once-sample-everywhere already exists. What actually made Global look flat was serve_step_canvas_request zeroing Global''s wire extent (a pre-extent-inversion sentinel), producing a 2x1 canvas -- fixed 2026-07-27; once sized correctly Global reads as a world with no hydrology work at all. Rivers at Global were measured as negligible: 375 courses present, 458 of 518,400 pixels different vs courses-off, because at ~39.7 km/gridunit most courses are shorter than one gridunit. SURVIVES: biome un-summarisation (reliefmap as plurality), composition-on-descent, the conservation invariant, and the lake-shore amendment. WEAKENED: the stored expanded layer and its D-227 carve-out. The live question is no longer ''what does rung 0.5 cost'' but ''does biome un-summarisation need a stored layer at all, or does it ride the existing sample-fresh-at-every-rung mechanism''. Re-scope this epic and T-1212 against that question first.
|
|
|
|
---
|
|
RE-SCOPED 2026-08-06 (Jeroen''s call) — the epic is unblocked, and its question has changed again.
|
|
|
|
EVIDENCE. A descent ladder was captured on Ferrath (GJ820Bc) at native 3440x1440,
|
|
anchored on land, one shot per rung, no overlays (scenarios atlas_GJ820Bc_land_*
|
|
in tests/visual.json). Ferrath''s heightmap is 1024x512 over a 38,089 km
|
|
circumference = 37.2 km per source pixel. Against that:
|
|
|
|
Global 35.267 km/gridunit ~1:1 with the source pixel a real map
|
|
Region 0.379 km/gridunit 98x finer flat wash
|
|
District 0.0038 km/gridunit 9,800x finer flat wash
|
|
Quarter/Block/Chunk finer still flat
|
|
|
|
The Atlas is legible exactly where it SAMPLES the heightmap and flat everywhere
|
|
it must INVENT. Every rung below Global is a single uniform colour field with
|
|
dither noise; the courses/settlements readout is 0 at all of them.
|
|
|
|
WHAT THIS SETTLES. D-258''s amendment (5) framed the live question as "does biome
|
|
un-summarisation need a stored layer at all, or does it ride the existing
|
|
sample-fresh-at-every-rung mechanism". The ladder answers a PRIOR question:
|
|
un-summarisation is not happening in ANY form. There is no expansion to decide
|
|
the storage policy for. Storage is therefore a downstream optimisation, not the
|
|
decision this epic turns on.
|
|
|
|
NEW SCOPE. Build the expansion first as a PURE FUNCTION, following the mechanism
|
|
that already exists rather than inventing a second one: layer1.rs''s
|
|
compute-once-sample-everywhere primitive (D-255(f) mechanism B) is the model, and
|
|
the amendment established hydrology already works that way. Measure it. Add a
|
|
stored layer ONLY if the measured cost forces it — and if it does, that is when
|
|
the D-227 carve-out argument gets made, on numbers rather than on the disproven
|
|
"not locally computable" claim.
|
|
|
|
Order: T-1213 (the un-summarisation generator) is now the first child and is
|
|
UNBLOCKED. T-1214 (hydrology onto rung 0.5) stays parked — the amendment showed
|
|
that solve already runs once per body and does not need to move. T-1216 (Global
|
|
biome-from-orbit palette) and T-1217 (lake shores through the shared shore path)
|
|
survive unchanged; both were independent of the storage question.
|
|
|
|
T-1212 does not gate this any more (see its own note).', NULL, '2026-08-06 15:59:03', '2026-08-06 15:59:03.757', '2026-08-06 15:59:03.757', NULL, 'eef80bb095e745421957ffdcf7ae7975', 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 ('06FT0TYBD74TQNVKTJMKA8D9KM', 'description', 'MUST run and be reviewed before any other T-1211 child starts -- this measurement could reshape the rung-0.5 design, per the pair session''s explicit sequencing note. Measure, at minimum: (1) per-body derive cost for the expanded layer at a resolution sized so a whole body draws at 2 screen px per gridunit on a large display (the D-258 sizing rule) -- both single-body cold-derive time and the full ~271-body population sum; (2) per-body and total disk footprint if the layer is cached/stored (rung 0.5 is a named D-227 carve-out -- storage is deliberate, but its size must be known, not assumed); (3) whole-body hydrology solve cost on this layer (drainage + course routing + lake fill) at the same resolution, since D-258 requires this to run exactly once per body and nowhere else. Reference point: D-255''s own rung-0 always-keep tier estimate went from ~8.85 MB (measured against a stale ~18K-cell/body figure) to an estimated 226 MB (1080p) / ~900 MB (4K) once the extent inversion made Global viewport-sized -- D-255 amendment item 6 explicitly says ''re-measure against rung 0.5, not against this record.'' This ticket is that re-measurement. Report back to the team before T-1211''s other children are started; if the numbers are structurally bad (e.g. rung 0.5 at the sizing D-258 specifies costs an order of magnitude more than the old rung-0 model), that is grounds to revisit the resolution target with Jeroen before writing generator code. See governance/decisions/architecture.md#d-258 (rationale + Implementation note).
|
|
|
|
SCOPE INVALIDATED 2026-07-27 (see T-1211 and the D-258 amendment). This ticket was written to measure the cost of moving a whole-body hydrology solve onto rung 0.5. That solve does not need to move -- it already runs once per body in layer1.rs and is sampled fresh at every rung. Do NOT run this measurement as written; it would price work that is not required. If a measurement is still wanted after T-1211 is re-scoped, the question is narrower: what does BIOME un-summarisation cost, and does it need storing at all.', 'MUST run and be reviewed before any other T-1211 child starts -- this measurement could reshape the rung-0.5 design, per the pair session''s explicit sequencing note. Measure, at minimum: (1) per-body derive cost for the expanded layer at a resolution sized so a whole body draws at 2 screen px per gridunit on a large display (the D-258 sizing rule) -- both single-body cold-derive time and the full ~271-body population sum; (2) per-body and total disk footprint if the layer is cached/stored (rung 0.5 is a named D-227 carve-out -- storage is deliberate, but its size must be known, not assumed); (3) whole-body hydrology solve cost on this layer (drainage + course routing + lake fill) at the same resolution, since D-258 requires this to run exactly once per body and nowhere else. Reference point: D-255''s own rung-0 always-keep tier estimate went from ~8.85 MB (measured against a stale ~18K-cell/body figure) to an estimated 226 MB (1080p) / ~900 MB (4K) once the extent inversion made Global viewport-sized -- D-255 amendment item 6 explicitly says ''re-measure against rung 0.5, not against this record.'' This ticket is that re-measurement. Report back to the team before T-1211''s other children are started; if the numbers are structurally bad (e.g. rung 0.5 at the sizing D-258 specifies costs an order of magnitude more than the old rung-0 model), that is grounds to revisit the resolution target with Jeroen before writing generator code. See governance/decisions/architecture.md#d-258 (rationale + Implementation note).
|
|
|
|
SCOPE INVALIDATED 2026-07-27 (see T-1211 and the D-258 amendment). This ticket was written to measure the cost of moving a whole-body hydrology solve onto rung 0.5. That solve does not need to move -- it already runs once per body in layer1.rs and is sampled fresh at every rung. Do NOT run this measurement as written; it would price work that is not required. If a measurement is still wanted after T-1211 is re-scoped, the question is narrower: what does BIOME un-summarisation cost, and does it need storing at all.
|
|
|
|
---
|
|
RETIRED AS A GATE 2026-08-06. This no longer blocks T-1213 or the T-1211 epic.
|
|
|
|
It was already SCOPE INVALIDATED (2026-07-27) for pricing a hydrology move that
|
|
does not need to happen. The 2026-08-06 descent ladder (see T-1211) closes the
|
|
remaining reason to keep it as a gate: every rung below Global renders a flat
|
|
wash, so biome un-summarisation is not happening at all. There is no artefact
|
|
whose cost or disk footprint can be measured, because none is produced.
|
|
|
|
The measurement question survives, but it is now DOWNSTREAM of T-1213 rather
|
|
than upstream of it: once the un-summarisation exists as a pure function, measure
|
|
THAT, and only then decide whether a stored layer is warranted. Re-scope this
|
|
ticket to that measurement when T-1213 lands, or close it and let T-1213 carry
|
|
its own measurement step.', NULL, '2026-08-06 15:59:12', '2026-08-06 15:59:12.825', '2026-08-06 15:59:12.825', NULL, '177c4abeb3d8bc4cb2260f81fe9f5d09', 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', 'description', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258.', 'Build the deterministic expansion from rung 0 (heightmap.png 1024x512 16-bit elevation + reliefmap.png 1024x512 colour biome) to rung 0.5, sized per D-258''s 2-px-per-gridunit-at-large-display rule. Same body + same seed must produce the same layer every time (byte-identical, per D-227''s determinism discipline extended by this carve-out). The reliefmap is a PLURALITY, not ground truth: each reliefmap cell already voted-and-discarded the dominant biome across ~38 km of ground, so this generator un-summarises it -- it does not upscale/interpolate it. Three binding invariants from D-258: (1) biome edges are gradients, never lines -- transitions blend so no boundary falls on a rung-0 cell edge (the D-243 climate edge-fuzz rule applied to biome); (2) descending the ladder reveals COMPOSITION not sharpness -- a cell reading ''forest'' globally must be able to contain clearings/marsh/rock/scrub the vote suppressed, emerging deterministically as the ladder descends; (3) CONSERVATIVE invention is the binding acceptance gate -- downsampling rung 0.5 must reproduce the rung-0 summary it came from (a forest cell may gain marsh pockets but must still read as forest from orbit). Blocked on T-1212 (cost/size measurement) landing first. Depends on: nothing else in this epic to start scaffolding, but hydrology (sibling ticket) and this generator are tightly coupled -- coordinate sequencing with whoever picks up hydrology. See governance/decisions/architecture.md#d-258.
|
|
|
|
---
|
|
UNBLOCKED 2026-08-06 (T-1211 re-scope, Jeroen''s call). The T-1212 blocker edge is
|
|
removed: that measurement priced a hydrology move that is not happening, and the
|
|
descent ladder showed there is no expansion artefact to measure yet anyway.
|
|
|
|
BUILD IT AS A PURE FUNCTION FIRST, not as a stored layer. The stored-layer half of
|
|
D-258 was materially weakened by its own 2026-07-27 amendment (the "not locally
|
|
computable" argument for the D-227 carve-out does not hold, because the whole-body
|
|
solve it cited already runs once per body in layer1.rs and is sampled at every
|
|
rung). So follow the mechanism that exists — D-255(f) mechanism B,
|
|
compute-once-sample-everywhere — measure it, and only argue for storage on those
|
|
numbers. Do NOT open with a cache.
|
|
|
|
WHAT "FLAT" MEANS CONCRETELY, so the fix has a target. Ferrath''s heightmap is
|
|
1024x512 over a 38,089 km circumference: 37.2 km per source pixel. Global draws at
|
|
35.267 km/gridunit, roughly 1:1 with the source, and reads as a real map. Region
|
|
draws at 0.379 km/gridunit — 98x finer than any stored datum — and is a single
|
|
uniform colour with dither. District is 0.0038 km/gridunit, ~9,800x finer, also
|
|
uniform. So the acceptance bar is not subtle: at Region, ~98 gridunits across a
|
|
single source pixel must carry visible, deterministic, non-repeating composition
|
|
that still downsamples back to that pixel''s summary (D-258''s conservation
|
|
invariant, the binding gate).
|
|
|
|
VERIFY BY CAPTURE, NOT BY REASONING. The scenarios exist: atlas_GJ820Bc_land_*
|
|
(Region/District/Quarter/Block/Chunk, one land-anchored world point, no overlays)
|
|
in tests/visual.json. Captures run offscreen under gamescope at native 3440x1440
|
|
via tests/run-visual --screenshot <name> — they do not steal the desktop. Re-shoot
|
|
the ladder and look at it; a green unit test proves nothing here.
|
|
|
|
Note the ladder currently also reports courses=0 at every rung below Global —
|
|
rivers vanish on descent. That is tracked separately as T-1239 and is NOT this
|
|
ticket''s scope, but it will be visible in the same captures, so do not mistake it
|
|
for a failure of the un-summarisation work.', NULL, '2026-08-06 15:59:39', '2026-08-06 15:59:39.623', '2026-08-06 15:59:39.623', NULL, 'fff0549b0da56272e5c13e9de4374f76', 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 ('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;
|