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) <noreply@anthropic.com>
This commit is contained in:
2026-08-06 16:47:13 +02:00
co-authored by Claude Opus 5
parent c91096a3c9
commit 8e69503e77
4 changed files with 39 additions and 17 deletions
+1
View File
@@ -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);
+1
View File
@@ -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);
+35 -16
View File
@@ -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) -----------------------------------------
+2 -1
View File
@@ -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,