From 8e69503e7732d432d2275c9ab6735ac0aee57107 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Thu, 6 Aug 2026 16:47:13 +0200 Subject: [PATCH] test(client): capture offscreen under gamescope, at the native 3440x1440 Two faults, one fix. Captures were stealing the desktop. Only the golden path even tried to go offscreen, via xvfb-run -- which is not installed here, so it took the "using visible window" fallback; --screenshot and --movie never wrapped at all. Every capture opened a Godot window on the machine Jeroen is working and gaming on. Now a single wrapper covers all three paths. gamescope, not the alternatives, for two independent reasons. It renders on the real AMD GPU, and the goldens are pinned to this box's Mesa/AMD output (T-1121 -- they do not port across rendering stacks), so xvfb-run's llvmpipe would shift every pixel: offscreen must not silently mean a different renderer. And it is the only installed option that lets the output size be set. cage is also present and also GPU-backed, but it is a kiosk compositor and forces its client to the headless output's default -- measured, a 960x540 request produced a 1280x720 PNG. A wrapper that quietly changes resolution is worse than none here. Worse, because resolution is not cosmetic on this map. D-255's extent inversion makes the shorter viewport axis span exactly one cell of the rung, so the viewport decides how much world a rung shows and at what cell count -- a small capture is a DIFFERENT map, not a scaled one. 960x540 was also 16:9, so it never exercised the ultrawide aspect added in 21e263d0a, which is the aspect actually in daily use. Raised to the panel's native 3440x1440. It paid for itself immediately: at native, Ferrath Global reports courses=375 drawn=0 -- every river culled, where the same build drew them at 960x540. Filed as T-1239. That is exactly the class of bug a too-small capture hides. Goldens are NOT regenerated here. They are stale across 15 commits already, and blessing the current look before it has been reviewed is the trap this suite just spent a day proving. Co-Authored-By: Claude Opus 5 (1M context) --- .pql/changelog/ticket_idmap/2026-08.sql | 1 + .pql/changelog/tickets/2026-08.sql | 1 + tests/run-visual | 51 +++++++++++++++++-------- tests/visual.json | 3 +- 4 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 .pql/changelog/ticket_idmap/2026-08.sql create mode 100644 .pql/changelog/tickets/2026-08.sql diff --git a/.pql/changelog/ticket_idmap/2026-08.sql b/.pql/changelog/ticket_idmap/2026-08.sql new file mode 100644 index 000000000..01665c98b --- /dev/null +++ b/.pql/changelog/ticket_idmap/2026-08.sql @@ -0,0 +1 @@ +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 OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash); diff --git a/.pql/changelog/tickets/2026-08.sql b/.pql/changelog/tickets/2026-08.sql new file mode 100644 index 000000000..60d1f03df --- /dev/null +++ b/.pql/changelog/tickets/2026-08.sql @@ -0,0 +1 @@ +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.', 'backlog', 'high', NULL, 'client', 'D-261', '2026-08-06 14:43:24.765', '2026-08-06 14:43:24.765', NULL, '9597ed74aa67fb7ebeef78f60374d89c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/tests/run-visual b/tests/run-visual index 686f58bac..0be6a50f0 100755 --- a/tests/run-visual +++ b/tests/run-visual @@ -81,33 +81,52 @@ for name in c.get('scenarios', {}): # -- Helpers ------------------------------------------------------------------- +# Offscreen wrapper for EVERY capture path. +# +# Captures must never steal the desktop: this box is also the developer's own +# session. Until 2026-08-06 only the golden path even tried, via xvfb-run — +# which is not installed here, so it hit the "using visible window" fallback, +# and --screenshot/--movie never wrapped at all. Every capture grabbed focus. +# +# gamescope is preferred, for two independent reasons: +# 1. It renders on the REAL GPU. The goldens are pinned to this box's +# Mesa/AMD output (T-1121 — they do not port across rendering stacks), and +# xvfb-run would fall back to llvmpipe software rendering, shifting every +# pixel. Offscreen must not mean a different renderer. +# 2. It is the only option that lets us SET the output size. cage is also +# installed and also GPU-backed, but it is a kiosk compositor: it forces +# its client to the headless output's default 1280x720 and silently +# overrides --resolution (measured — a 960x540 request produced a 1280x720 +# PNG). A wrapper that quietly changes resolution is worse than none, +# because RESOLUTION is load-bearing here: D-255's extent inversion makes +# the shorter viewport axis span exactly one cell of the rung, so window +# size decides how much world a rung shows. cage is deliberately not used. +CAPTURE_PREFIX=() +if command -v gamescope >/dev/null 2>&1; then + CAPTURE_PREFIX=(gamescope --backend headless -W "${RESOLUTION%x*}" -H "${RESOLUTION#*x}" --) +elif command -v xvfb-run >/dev/null 2>&1; then + echo "Note: gamescope not found — falling back to xvfb-run (software GL; goldens may drift)" >&2 + CAPTURE_PREFIX=(xvfb-run -a --server-args="-screen 0 ${RESOLUTION}x24") +else + echo "Warning: no offscreen compositor (gamescope/xvfb-run) — capturing in a VISIBLE window" >&2 +fi + godot_capture() { local mode_flag="$1" # --scenario or --flow local name="$2" local output="$3" local extra_args=("${@:4}") - "$GODOT" --rendering-driver opengl3 --fixed-fps 60 --resolution "$RESOLUTION" \ + "${CAPTURE_PREFIX[@]}" "$GODOT" --rendering-driver opengl3 --fixed-fps 60 \ + --resolution "$RESOLUTION" \ --path "$ROOT/client" -s res://tests/visual_capture.gd -- \ $mode_flag "$name" --output "$output" "${extra_args[@]}" 2>&1 } +# Retained as the golden-path name; offscreen handling now lives in +# godot_capture, so both paths get it. xvfb_capture() { - local mode_flag="$1" - local name="$2" - local output="$3" - local extra_args=("${@:4}") - - # Try xvfb-run for deterministic captures - if command -v xvfb-run >/dev/null 2>&1; then - xvfb-run -a --server-args="-screen 0 ${RESOLUTION/x/x}x24" \ - "$GODOT" --rendering-driver opengl3 --fixed-fps 60 --resolution "$RESOLUTION" \ - --path "$ROOT/client" -s res://tests/visual_capture.gd -- \ - $mode_flag "$name" --output "$output" "${extra_args[@]}" 2>&1 - else - echo "Warning: xvfb-run not found — using visible window" >&2 - godot_capture "$mode_flag" "$name" "$output" "${extra_args[@]}" - fi + godot_capture "$@" } # -- Server lifecycle (live scenarios) ----------------------------------------- diff --git a/tests/visual.json b/tests/visual.json index 7403b10de..fb4f5da78 100644 --- a/tests/visual.json +++ b/tests/visual.json @@ -1,5 +1,6 @@ { - "resolution": [960, 540], + "resolution": [3440, 1440], + "_comment_resolution": "Native panel resolution (3440x1440 ultrawide, 2.39:1) as of 2026-08-06, raised from 960x540. Not cosmetic: D-255's extent inversion makes the shorter viewport axis span exactly one cell of the rung, so the viewport decides how much world a rung shows and at what cell count — a small capture is a DIFFERENT map, not a scaled one. 960x540 was also 16:9, so it never exercised the ultrawide aspect path added in 21e263d0a, which is the aspect actually used daily. Captures run offscreen under gamescope at this size (see tests/run-visual); goldens must be regenerated whenever this changes.", "settle_frames": 30, "tolerance": 5, "max_diff_pct": 0.5,