Jeroen hit a black mosaic zooming into a body from a fresh make-atlas spawn. Live diagnosis showed all six tiles held with colored textures and no repaint; the original line-specific diagnosis (unpaired queue_redraw in _on_tile_ready) turned out WRONG — the pairing already existed (lead's truncated grep misread the function; Stig verified via git log -p before acting). Rather than chase the exact dropped signal edge, _process() now self-heals: both viewer and overlay redraw every frame while the tile set has pending tiles (has_pending_tiles(), new) — a strict superset that closes the black regardless of which edge drops, pinned by a _draw()-counting real-subclass spy test. Legend stacking root cause found by trace, not guess: ImplantApp. _on_screen_changed() re-runs enter() unconditionally on repeat same-screen pushes — each re-entry tore down and rebuilt all six tile requests (the round-6 orphaning fingerprint via a new trigger) and stacked another legend (~10 deep, full-height dark panel). Fixed both ends: ImplantPanel.clear() frees immediately (same-frame re-entrant refresh can never observe stale children — protects every implant app), and RegionalScreen.enter() no-ops for the same body (different body still re-enters fresh). The load-bearing regression asserts an ARRIVED TILE'S DATA survives a repeat push — node identity would not catch the teardown (the tile-set Node is a fixed field; only its internals reset). Cold-start UX: pending tiles now draw the single-window path's COLOR_BORDER_FADE wash instead of raw background — a deriving mosaic reads as loading, not broken. Suites green (zoom_ladder 48, viewer 74, tile_set 20, overlay 30, overlays 46 + 2 new files), gdlint clean, revert-verified throughout.
408 lines
19 KiB
GDScript
408 lines
19 KiB
GDScript
## Live round 4: a REAL draw smoke test — the "does anything draw at all"
|
|
## gap has now bitten twice (round 4's tile-mosaic coordinate bug AND its
|
|
## per-tile-texture-lifetime bug, both invisible to test_atlas_window_overlay.gd's
|
|
## existing suite, which only asserts on the CACHE FIELDS being populated —
|
|
## never on an actual composited pixel). This file closes that gap
|
|
## structurally: render AtlasWindowOverlay into a REAL SubViewport, force a
|
|
## GPU sync, grab the rendered Image, and assert a meaningful fraction of
|
|
## pixels differ from the background color — for BOTH the single-window path
|
|
## (a) and the tile-mosaic path (b), matching the coordinator's explicit ask.
|
|
##
|
|
## **REQUIRES A REAL RENDERING DRIVER — SKIPS (not fails) under
|
|
## `tests/run-godot`'s hardcoded `--headless`** (dummy driver, no GPU texture
|
|
## output; confirmed directly: SubViewport.get_texture().get_image() returns
|
|
## an all-zero/unusable image under it). This matters beyond "the assertions
|
|
## are meaningless there": the push gate runs the FULL suite through
|
|
## `tests/run-godot --headless` for every push, for everyone — a loud FAILURE
|
|
## here would bounce every future push project-wide, not just report a local
|
|
## false negative. Every test below carries the gdUnit4 fuzzer-arg skip
|
|
## convention (`_do_skip`/`_skip_reason`, matching test_input_gate_live.gd's
|
|
## own server-binary-not-built skip) keyed on `_dummy_renderer_active()`, so
|
|
## `tests/run-godot --filter test_atlas_window_overlay_draw_smoke` reports
|
|
## green-with-skips under headless, not red.
|
|
##
|
|
## To actually exercise this file's assertions, run it with a real driver:
|
|
## godot4 --display-driver x11 --rendering-driver opengl3 \
|
|
## -s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c \
|
|
## -a res://tests/test_atlas_window_overlay_draw_smoke.gd
|
|
## (matching tests/visual_capture.gd's own documented "requires a real
|
|
## rendering driver" precedent — see docs/DEVOPS.md's own note on this file.)
|
|
class_name TestAtlasWindowOverlayDrawSmoke
|
|
extends GdUnitTestSuite
|
|
|
|
const AtlasWindowOverlay := preload("res://ui/implant/apps/atlas/atlas_window_overlay.gd")
|
|
const AtlasWindowGeometry := preload("res://ui/implant/apps/atlas/atlas_window_geometry.gd")
|
|
|
|
const COLOR_BG: Color = Color("#0d1117") # AtlasWindowViewer.COLOR_BG, mirrored (private const)
|
|
const VIEWPORT_SIZE: Vector2i = Vector2i(512, 512)
|
|
|
|
## Minimum fraction of the captured image that must differ from COLOR_BG for
|
|
## a draw to count as "genuinely rendered something" — low enough to tolerate
|
|
## a mostly-water/mostly-one-color composite (round 4's own repro shots were
|
|
## legitimately near-uniform ocean at some zooms), high enough that a
|
|
## fully-blank/fully-background/fully-white frame (both round 4 bugs) fails it.
|
|
const MIN_NON_BACKGROUND_FRACTION: float = 0.05
|
|
|
|
const SKIP_REASON: String = (
|
|
"no real rendering driver (dummy/headless) — run with e.g."
|
|
+ " `godot4 --display-driver x11 --rendering-driver opengl3"
|
|
+ " -s addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -c"
|
|
+ " -a res://tests/test_atlas_window_overlay_draw_smoke.gd` to exercise this file"
|
|
)
|
|
|
|
|
|
## True under Godot's `--display-driver headless` (the dummy renderer
|
|
## `tests/run-godot`'s hardcoded `--headless` flag selects) — `DisplayServer.
|
|
## get_name()` reports `"headless"` there and the real driver name (`"X11"`,
|
|
## `"Wayland"`, etc.) otherwise, confirmed directly against both this
|
|
## worktree's `tests/run-godot` invocation and a real `--display-driver x11
|
|
## --rendering-driver opengl3` run. Named as a function, not a const, since
|
|
## `DisplayServer` singleton state isn't available at script-parse time.
|
|
static func _dummy_renderer_active() -> bool:
|
|
return DisplayServer.get_name() == "headless"
|
|
|
|
|
|
## Single-window viewer stub — mirrors test_atlas_window_overlay.gd's
|
|
## _ViewerStub exactly (is_tile_mode() -> false), so this exercises the
|
|
## SAME single-window draw path that suite's cache tests cover, just
|
|
## through a REAL render instead of inspecting `_cached_texture` directly.
|
|
class _SingleWindowViewerStub:
|
|
var window: Variant = null
|
|
|
|
func get_district_window() -> Variant:
|
|
return window
|
|
|
|
func is_overlay_visible(_overlay_id: String) -> bool:
|
|
return false
|
|
|
|
func get_cell_pixel_size() -> float:
|
|
return 16.0
|
|
|
|
func is_tile_mode() -> bool:
|
|
return false
|
|
|
|
|
|
## Tile-mode viewer stub — is_tile_mode() -> true, get_tile_set() returns a
|
|
## bare object exposing get_tiles() (AtlasWindowOverlay's own duck-typed
|
|
## contract, matching AtlasWindowTileSet.get_tiles()'s public shape exactly:
|
|
## Array of {"center": Vector2i, "window": Variant}).
|
|
class _TileModeViewerStub:
|
|
# PR #192 cold-start dossier: AtlasWindowOverlay reads viewer.COLOR_BORDER_FADE
|
|
# directly for a pending tile's wash (avoids a cyclic preload of the
|
|
# viewer's own script — see that read site's own doc) — mirrored here
|
|
# byte-for-byte (AtlasWindowViewer.COLOR_BORDER_FADE, private const).
|
|
const COLOR_BORDER_FADE: Color = Color(0.20, 0.24, 0.30, 0.55)
|
|
|
|
var tiles: Array = []
|
|
var held_center: Vector2i = Vector2i.ZERO
|
|
var held_n: int = 0
|
|
# Live round 5: nearest_wrap_image()'s cols input — 0 here (a no-radius
|
|
# passthrough) is fine for these tests, which don't exercise the wrap
|
|
# seam itself (that's test_atlas_window_geometry.gd's own coverage);
|
|
# this stub only needs to satisfy _draw_tile_mosaic()'s duck-typed call.
|
|
var body_radius_km: float = 0.0
|
|
|
|
func get_district_window() -> Variant:
|
|
return null
|
|
|
|
func is_overlay_visible(_overlay_id: String) -> bool:
|
|
return false
|
|
|
|
func get_cell_pixel_size() -> float:
|
|
return 16.0
|
|
|
|
func is_tile_mode() -> bool:
|
|
return true
|
|
|
|
func get_tile_set() -> Variant:
|
|
return _TileSetStub.new(tiles)
|
|
|
|
func get_held_center() -> Vector2i:
|
|
return held_center
|
|
|
|
func get_held_n() -> int:
|
|
return held_n
|
|
|
|
func get_body_radius_km() -> float:
|
|
return body_radius_km
|
|
|
|
|
|
class _TileSetStub:
|
|
var _tiles: Array = []
|
|
|
|
func _init(tiles: Array) -> void:
|
|
_tiles = tiles
|
|
|
|
func get_tiles() -> Array:
|
|
return _tiles
|
|
|
|
|
|
## A `Node2D._draw()`-based flat-fill background — deliberately NOT a
|
|
## `ColorRect` (a `Control`). A `ColorRect` parented directly under a bare
|
|
## `SubViewport` (no intervening `Control` container establishing its own
|
|
## layout rect) did not reliably render in this harness: sampled pixels came
|
|
## back fully transparent `(0,0,0,0)` regardless of the ColorRect's `color`/
|
|
## `size`, even after entering the tree before sizing. `AtlasWindowOverlay`
|
|
## itself is a bare `Node2D` using `draw_rect()` for its own background wash
|
|
## (`AtlasWindowViewer._draw()`'s own `COLOR_BG` fill) — matching that same,
|
|
## already-proven-working `Node2D.draw_rect()` pattern here sidesteps
|
|
## whatever `Control`-specific layout/compositing gap caused the ColorRect
|
|
## failure, rather than debugging that gap for its own sake.
|
|
class _BackgroundRect extends Node2D:
|
|
var fill_color: Color = Color.BLACK
|
|
var fill_size: Vector2 = Vector2.ZERO
|
|
|
|
func _draw() -> void:
|
|
draw_rect(Rect2(Vector2.ZERO, fill_size), fill_color)
|
|
|
|
|
|
static func _mock_window(center: Vector2i = Vector2i.ZERO, n: int = 64) -> Dictionary:
|
|
# A checkerboard-ish morphology spread (not all-one-zone) so the built
|
|
# composite has genuine color VARIATION, not just "one flat non-background
|
|
# color" — closer to what a real terrain response looks like.
|
|
var cells: int = n * n
|
|
var morphology := PackedByteArray()
|
|
var elev_q := PackedByteArray()
|
|
morphology.resize(cells)
|
|
elev_q.resize(cells)
|
|
for i in range(cells):
|
|
morphology[i] = (i % 4) as int # cycles through the 4 morphology zones
|
|
elev_q[i] = (i * 7) % 100 as int
|
|
return {
|
|
"center": [center.x, center.y],
|
|
"n": n,
|
|
"granularity_v2": "District",
|
|
"morphology": morphology,
|
|
"elev_q": elev_q,
|
|
"temp_dc": [],
|
|
"moisture_q": PackedByteArray(),
|
|
"vegetation": PackedByteArray(),
|
|
"glaciation": PackedByteArray(),
|
|
}
|
|
|
|
|
|
## Renders `overlay` (any Node2D with its own `_draw()` — an
|
|
## AtlasWindowOverlay for (a)/(b) below, or a bare `_BackgroundRect` probe for
|
|
## the harness sanity check) parented under a Node2D positioned/scaled the
|
|
## way AtlasWindowViewer._canvas would be, into a fresh SubViewport, and
|
|
## returns the captured Image. `zoom` mirrors AtlasWindowViewer._canvas.scale
|
|
## (real `fit_window_view()` output is a small fraction, e.g. ~0.006 for a
|
|
## whole-body tile mosaic per live round 4's own repro) — WITHOUT it, a
|
|
## tile's real-world extent (TILE_N * cell_px = 102,400 local units) is so
|
|
## much larger than any realistic test viewport that a mis-POSITIONED tile
|
|
## still overlaps the frame purely by being gigantic, making the position
|
|
## math this test exists to catch silently unfalsifiable (confirmed
|
|
## directly: an earlier version of this test without a zoom scale kept
|
|
## passing even with live round 4's tile-coordinate bug deliberately
|
|
## reintroduced). Also confirmed directly: a hand-rolled duplicate of this
|
|
## same SubViewport/settle-loop setup (the harness sanity check's ORIGINAL
|
|
## standalone version) was measurably less reliable under a real driver than
|
|
## going through this shared path — reuse over duplication here isn't just
|
|
## tidiness, it's the more reliable rendering path.
|
|
func _render_to_image(overlay: Node2D, zoom: float = 1.0) -> Image:
|
|
var sub_viewport := SubViewport.new()
|
|
sub_viewport.size = VIEWPORT_SIZE
|
|
sub_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
|
sub_viewport.transparent_bg = false
|
|
add_child(sub_viewport)
|
|
auto_free(sub_viewport)
|
|
|
|
var bg := _BackgroundRect.new()
|
|
bg.fill_color = COLOR_BG
|
|
bg.fill_size = Vector2(VIEWPORT_SIZE)
|
|
sub_viewport.add_child(bg)
|
|
bg.queue_redraw()
|
|
|
|
var canvas := Node2D.new()
|
|
canvas.position = Vector2(VIEWPORT_SIZE) * 0.5 # center the composite's local (0,0)
|
|
canvas.scale = Vector2(zoom, zoom)
|
|
sub_viewport.add_child(canvas)
|
|
canvas.add_child(overlay)
|
|
overlay.queue_redraw()
|
|
|
|
# Bounded settle wait, NOT `await RenderingServer.frame_post_draw` — that
|
|
# signal never fires under the dummy/headless driver (confirmed directly:
|
|
# a first version of this file using it hung for the full 300s
|
|
# tests/run-godot wall-clock cap and was force-killed, producing a FALSE
|
|
# "0 tests, passed" result — exactly the silent-hang failure mode the
|
|
# `_do_skip`/`_dummy_renderer_active()` gate (this file's header doc) now
|
|
# avoids structurally instead). A fixed small number of `process_frame`
|
|
# awaits settles real rendering — confirmed sufficient against a real
|
|
# driver during this fix's own live verification.
|
|
for _i in range(6):
|
|
await get_tree().process_frame
|
|
|
|
return sub_viewport.get_texture().get_image()
|
|
|
|
|
|
## Fraction of `image`'s pixels whose RGB differs from COLOR_BG (alpha
|
|
## ignored — the ColorRect background is opaque, everything drawn on top of
|
|
## it is what's under test).
|
|
static func _non_background_fraction(image: Image) -> float:
|
|
var w: int = image.get_width()
|
|
var h: int = image.get_height()
|
|
if w <= 0 or h <= 0:
|
|
return 0.0
|
|
var total: int = w * h
|
|
var differing: int = 0
|
|
var bg_rgb: Color = Color(COLOR_BG.r, COLOR_BG.g, COLOR_BG.b, 1.0)
|
|
for y in range(h):
|
|
for x in range(w):
|
|
var px: Color = image.get_pixel(x, y)
|
|
var px_rgb: Color = Color(px.r, px.g, px.b, 1.0)
|
|
if not px_rgb.is_equal_approx(bg_rgb):
|
|
differing += 1
|
|
return float(differing) / float(total)
|
|
|
|
|
|
## (a) Single-window path: a District-rung response must render as visibly
|
|
## non-background pixels through AtlasWindowOverlay._draw()'s own
|
|
## Rect2(0,0,extent,extent) draw call — the "does the OVERLAY actually paint
|
|
## something for a real window" half of the gap. Positions it at the
|
|
## SubViewport's center via the surrounding Node2D, mirroring _canvas's role
|
|
## in the real viewer.
|
|
##
|
|
## Honest scope note: live round 4's SECOND bug (leaving `_view_offset`
|
|
## stale across a rung crossing in `_maybe_reselect_rung()`) lived entirely
|
|
## in AtlasWindowViewer's transform bookkeeping, ONE LAYER ABOVE this
|
|
## overlay-only test's boundary — it never touched `_draw()` itself, so a
|
|
## pure-overlay smoke test structurally cannot reproduce it (there is no
|
|
## "stale vs. fresh offset" state to compare inside the overlay alone). That
|
|
## regression's coverage is `_maybe_reselect_rung()`'s own unit tests in
|
|
## test_atlas_zoom_ladder.gd. This test's job is narrower and still real:
|
|
## proving the overlay's draw call itself produces visible output for
|
|
## legitimate window data, closing the "the composite Rect2 call is
|
|
## silently a no-op" class of bug regardless of which layer caused it.
|
|
func test_single_window_draw_produces_visible_pixels() -> void:
|
|
# Guarded early-return instead of the _do_skip fuzzer-arg convention:
|
|
# gdUnit4 leaks one internal <Node> per fuzzer-skipped test, tripping the
|
|
# orphan detector (exit 101) and bouncing the push gate even at 0 failures
|
|
# (PR gate run 2026-07-22: "2 skipped | 2 orphans | Exit code: 101").
|
|
if _dummy_renderer_active():
|
|
print(SKIP_REASON)
|
|
return
|
|
var overlay: AtlasWindowOverlay = AtlasWindowOverlay.new()
|
|
var stub := _SingleWindowViewerStub.new()
|
|
stub.window = _mock_window(Vector2i.ZERO, 32)
|
|
overlay.viewer = stub
|
|
|
|
var image: Image = await _render_to_image(overlay)
|
|
var fraction: float = _non_background_fraction(image)
|
|
|
|
assert_float(fraction).override_failure_message(
|
|
(
|
|
"single-window composite must render VISIBLE non-background pixels — got"
|
|
+ " only %.2f%% of the frame differing from COLOR_BG. This is exactly the"
|
|
+ " shape of live round 4's second bug: _view_offset left stale across a"
|
|
+ " rung crossing pushed the composite off-canvas, so nothing but"
|
|
+ " background/chrome ever appeared, despite the underlying window data"
|
|
+ " and draw calls being individually 'correct' in isolation."
|
|
)
|
|
% (fraction * 100.0)
|
|
).is_greater(MIN_NON_BACKGROUND_FRACTION)
|
|
|
|
|
|
## (b) Tile-mosaic path: a tile at a district center AWAY from the body's
|
|
## own origin must still render as visibly non-background pixels once
|
|
## correctly placed via `district_to_canvas_local()`'s shared held_center/
|
|
## held_n convention — this is the path round 4's FIRST and THIRD bugs
|
|
## (tile-local-origin math ignoring that convention, and per-tile
|
|
## ImageTexture objects with no persistent reference being garbage-
|
|
## collected/GPU-desynced before their draw command flushed) would both
|
|
## have failed. Uses production-realistic scale (live round 4's own Lendel
|
|
## repro: raw circumference ~19,139 districts) — see the tile-center
|
|
## comment below for why scale matters here specifically.
|
|
func test_tile_mosaic_draw_produces_visible_pixels() -> void:
|
|
# Guarded early-return, not _do_skip — see the sibling test's comment.
|
|
if _dummy_renderer_active():
|
|
print(SKIP_REASON)
|
|
return
|
|
var overlay: AtlasWindowOverlay = AtlasWindowOverlay.new()
|
|
var stub := _TileModeViewerStub.new()
|
|
var tile_n: int = AtlasWindowGeometry.TILE_N
|
|
var cell_px: float = stub.get_cell_pixel_size()
|
|
stub.held_center = Vector2i.ZERO
|
|
stub.held_n = 19139
|
|
# A SINGLE tile chosen so the CORRECT canvas-local formula
|
|
# (`district_to_canvas_local()`, anchored at `held_center - held_n/2`)
|
|
# lands it centered in the viewport, while the round-4 BUGGY formula
|
|
# (anchored at absolute district (0,0) directly) lands it almost
|
|
# `held_n/2 * cell_px` local units away — tens of thousands of units at
|
|
# this scale, i.e. genuinely fully off a 512x512 viewport, not just
|
|
# "shifted but still overlapping" (confirmed by hand-computation: a
|
|
# smaller/toy-scale version of this test stayed green with the bug
|
|
# reintroduced, because the shift stayed within the viewport bounds
|
|
# either way — this scale/center combination is chosen specifically to
|
|
# avoid that false-negative).
|
|
var half_tile: float = float(tile_n) * 0.5
|
|
var half_body: float = float(stub.held_n) * 0.5
|
|
var lone_tile_center := Vector2i(roundi(half_tile - half_body), roundi(half_tile - half_body))
|
|
stub.tiles = [
|
|
{"center": lone_tile_center, "window": _mock_window(lone_tile_center, 64)},
|
|
]
|
|
overlay.viewer = stub
|
|
|
|
# A zoom small enough that the buggy-vs-correct shift (~half_body * cell_px
|
|
# local units) is comfortably larger than the viewport — see the center
|
|
# choice's own doc above for why this specific magnitude matters.
|
|
var zoom: float = float(VIEWPORT_SIZE.x) * 1.5 / (half_body * cell_px)
|
|
var image: Image = await _render_to_image(overlay, zoom)
|
|
var fraction: float = _non_background_fraction(image)
|
|
|
|
assert_float(fraction).override_failure_message(
|
|
(
|
|
"tile mosaic must render VISIBLE non-background pixels across its tiles —"
|
|
+ " got only %.2f%% of the frame differing from COLOR_BG. This is exactly"
|
|
+ " the shape of live round 4's bugs: (1) tile local-origin computed"
|
|
+ " relative to absolute district (0,0) instead of the shared"
|
|
+ " held_center/held_n canvas-local convention pushed the whole mosaic"
|
|
+ " off-canvas, and (2) even once correctly positioned, an unstored"
|
|
+ " per-draw-call ImageTexture rendered as a blank/white gap despite"
|
|
+ " provably-correct CPU-side pixel data — both invisible to any test that"
|
|
+ " only inspects Dictionary/cache state, never an actual composited pixel."
|
|
)
|
|
% (fraction * 100.0)
|
|
).is_greater(MIN_NON_BACKGROUND_FRACTION)
|
|
|
|
|
|
## (c) PR #192 cold-start dossier, BUG 3: a mosaic tile with NO window yet
|
|
## (the cold-server "still working" state, every tile in the mosaic at once
|
|
## right after enter_orbital() on a real cold server) must render the SAME
|
|
## border-fade wash the single-window path already gives its own
|
|
## no-composite-yet wait — not a bare COLOR_BG gap that reads as broken.
|
|
## Same real-render infrastructure as (a)/(b): a pending tile (`window: null`)
|
|
## must still produce visible non-background pixels, proving the wash
|
|
## genuinely draws rather than the loop just `continue`-ing past it silently.
|
|
func test_pending_tile_gets_a_visible_border_fade_wash() -> void:
|
|
if _dummy_renderer_active():
|
|
print(SKIP_REASON)
|
|
return
|
|
var overlay: AtlasWindowOverlay = AtlasWindowOverlay.new()
|
|
var stub := _TileModeViewerStub.new()
|
|
var tile_n: int = AtlasWindowGeometry.TILE_N
|
|
var cell_px: float = stub.get_cell_pixel_size()
|
|
stub.held_center = Vector2i.ZERO
|
|
stub.held_n = 19139 # GJ380c/Lendel's own raw circumference (live round 4)
|
|
# Same centered-tile placement as (b) above, but with `window: null` —
|
|
# the pending state this fix targets, instead of a real response.
|
|
var half_tile: float = float(tile_n) * 0.5
|
|
var half_body: float = float(stub.held_n) * 0.5
|
|
var lone_tile_center := Vector2i(roundi(half_tile - half_body), roundi(half_tile - half_body))
|
|
stub.tiles = [{"center": lone_tile_center, "window": null}]
|
|
overlay.viewer = stub
|
|
|
|
var zoom: float = float(VIEWPORT_SIZE.x) * 1.5 / (half_body * cell_px)
|
|
var image: Image = await _render_to_image(overlay, zoom)
|
|
var fraction: float = _non_background_fraction(image)
|
|
|
|
assert_float(fraction).override_failure_message(
|
|
(
|
|
"a pending tile (window == null) must still render a visible border-fade"
|
|
+ " wash — got only %.2f%% of the frame differing from COLOR_BG, meaning"
|
|
+ " the tile is a bare background gap during the cold-server wait, which"
|
|
+ " reads as broken rather than 'still working' (coordinator's closing"
|
|
+ " question, PR #192 cold-start dossier)."
|
|
)
|
|
% (fraction * 100.0)
|
|
).is_greater(MIN_NON_BACKGROUND_FRACTION)
|