From 155e7ea5f4448291257f4cb3679557a884f70bb3 Mon Sep 17 00:00:00 2001 From: Natalie Date: Thu, 7 May 2026 18:14:04 -0700 Subject: [PATCH] =?UTF-8?q?fix(@projects/@magic-civilization):=20?= =?UTF-8?q?=F0=9F=90=9B=20handle=20Weston/llvmpipe=20framebuffer=20stalene?= =?UTF-8?q?ss?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../engine/scenes/tests/capture_screenshot.gd | 14 +++- .../scenes/tests/proof_replay_viewer.gd | 66 ++++++++++--------- tools/screenshot.sh | 23 +++++++ 3 files changed, 69 insertions(+), 34 deletions(-) diff --git a/src/game/engine/scenes/tests/capture_screenshot.gd b/src/game/engine/scenes/tests/capture_screenshot.gd index 6da78ac1..594faaaa 100644 --- a/src/game/engine/scenes/tests/capture_screenshot.gd +++ b/src/game/engine/scenes/tests/capture_screenshot.gd @@ -133,10 +133,18 @@ func _ready() -> void: ) return elif _scene == "replay_viewer_proof": + # Do NOT use change_scene_to_file — under Weston/llvmpipe the composited + # framebuffer is not flushed after a scene swap, causing get_texture to + # return the stale main-menu frame. Instantiate directly as a child of + # root so no scene transition occurs and the proof scene owns its own + # capture + quit. await get_tree().create_timer(0.5).timeout - get_tree().change_scene_to_file( - "res://engine/scenes/tests/proof_replay_viewer.tscn" - ) + var packed: PackedScene = load("res://engine/scenes/tests/proof_replay_viewer.tscn") + if packed == null: + push_error("ScreenCapture: cannot load proof_replay_viewer.tscn") + get_tree().quit(1) + return + get_tree().root.add_child(packed.instantiate()) return elif _scene == "world_map": await get_tree().create_timer(0.5).timeout diff --git a/src/game/engine/scenes/tests/proof_replay_viewer.gd b/src/game/engine/scenes/tests/proof_replay_viewer.gd index a8595fae..b6cb681c 100644 --- a/src/game/engine/scenes/tests/proof_replay_viewer.gd +++ b/src/game/engine/scenes/tests/proof_replay_viewer.gd @@ -1,12 +1,17 @@ extends Control ## Proof scene for p2-46 bullet 8 — headless replay-viewer exercise. ## +## Entered via capture_screenshot.gd (add_child path, no change_scene_to_file) +## so the framebuffer is never staled by a scene swap under Weston/llvmpipe. +## ## 1. Writes a synthetic 50-turn fixture via GdReplayArchive.write_fixture. ## 2. Instantiates the replay_viewer scene and wires it to the fixture UUID. -## 3. Drives goto_turn(25) → 3 s hold → goto_turn(50) → 3 s hold → screenshot. +## 3. Drives _goto_turn(25) → 3 s hold → screenshot (p2-46-replay-viewer-T25). +## 4. Drives _goto_turn(50) → 3 s hold → screenshot (p2-46-replay-viewer-T50). +## 5. Quits. ## ## Scene key: "replay_viewer_proof" (capture_screenshot.gd dispatcher). -## Run via: tools/screenshot.sh proof_replay_viewer replay_viewer_proof +## Run via: tools/screenshot.sh p2-46-replay-viewer-T25 replay_viewer_proof const OUTPUT_DIR: String = "user://screenshots" const REPLAY_VIEWER_SCENE: String = "res://engine/scenes/menus/replay_viewer.tscn" @@ -16,7 +21,6 @@ var _viewer_scene: Node = null var _player: GdReplayPlayer = null var _uuid: String = "" var _archive_root: String = "" -var _captured: bool = false var _turn_label: Label = null var _status_label: Label = null @@ -26,7 +30,7 @@ func _ready() -> void: get_viewport().size = Vector2i(1920, 1080) DisplayServer.window_set_size(Vector2i(1920, 1080)) - # Dark background panel so the viewer is visible in the screenshot. + # Opaque dark background — covers any scene rendered underneath us. var bg: ColorRect = ColorRect.new() bg.set_anchors_preset(Control.PRESET_FULL_RECT) bg.color = Color(0.04, 0.04, 0.08, 1.0) @@ -46,6 +50,9 @@ func _ready() -> void: _turn_label.text = "" add_child(_turn_label) + # Yield several frames so our ColorRect draws over any prior scene content. + await get_tree().process_frame + await get_tree().process_frame await get_tree().process_frame _archive_root = ProjectSettings.globalize_path("user://archive") @@ -54,52 +61,60 @@ func _ready() -> void: _uuid = _archive.write_fixture(_archive_root, "age-of-dwarves", "Cycle-X Proof") if _uuid.is_empty(): push_error("proof_replay_viewer: write_fixture failed") - _capture_and_quit() + _capture("p2-46-replay-viewer-T25-error") + get_tree().quit(1) return _status_label.text = "p2-46 Replay Viewer Proof — fixture UUID: %s" % _uuid - # Load the replay viewer scene as a sub-scene. var packed: PackedScene = load(REPLAY_VIEWER_SCENE) if packed == null: push_error("proof_replay_viewer: cannot load replay_viewer.tscn") - _capture_and_quit() + _capture("p2-46-replay-viewer-T25-error") + get_tree().quit(1) return _viewer_scene = packed.instantiate() - # Set game_id before _ready fires so the viewer loads history automatically. _viewer_scene.set("game_id", _uuid) _viewer_scene.set("pack", "age-of-dwarves") - # Anchor the sub-scene below the status labels. if _viewer_scene is Control: _viewer_scene.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) _viewer_scene.set_anchor(SIDE_TOP, 0.09) add_child(_viewer_scene) - # Allow _ready on the viewer to complete (history loaded, scrubber configured). + # Allow viewer _ready to complete (history loaded, scrubber configured). await get_tree().process_frame await get_tree().process_frame - # Obtain the player separately to drive goto_turn for the status overlay. + # Separate player for status overlay — viewer owns its own internally. _player = GdReplayPlayer.new() var loaded: bool = _player.load_history(_archive_root, "age-of-dwarves", _uuid) if not loaded: push_error("proof_replay_viewer: GdReplayPlayer.load_history failed for %s" % _uuid) - # Drive to turn 25 and hold for screenshot opportunity. + # Turn 25 — drive, hold 3 s, capture. _drive_to_turn(25) await get_tree().create_timer(3.0).timeout + # Multiple process frames before capture: frame_post_draw does not fire + # under Weston/llvmpipe, so we pump the render loop manually. + await get_tree().process_frame + await get_tree().process_frame + await get_tree().process_frame + _capture("p2-46-replay-viewer-T25") - # Drive to turn 50 and hold. + # Turn 50 — drive, hold 3 s, capture, quit. _drive_to_turn(50) await get_tree().create_timer(3.0).timeout + await get_tree().process_frame + await get_tree().process_frame + await get_tree().process_frame + _capture("p2-46-replay-viewer-T50") - _capture_and_quit() + get_tree().quit() func _drive_to_turn(t: int) -> void: - ## Push the scrubber on the embedded viewer and update our overlay. if _viewer_scene != null and _viewer_scene.has_method("_goto_turn"): _viewer_scene._goto_turn(t) @@ -117,26 +132,17 @@ func _drive_to_turn(t: int) -> void: ) -func _capture_and_quit() -> void: - if _captured: - return - _captured = true - +func _capture(name: String) -> void: DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUTPUT_DIR)) var image: Image = get_viewport().get_texture().get_image() if image == null: - push_error("proof_replay_viewer: viewport image unavailable") - get_tree().quit(1) + push_error("proof_replay_viewer: viewport image unavailable for %s" % name) return - var timestamp: String = ( - Time.get_datetime_string_from_system() - .replace(":", "-") - .replace("T", "_") + var abs_path: String = ProjectSettings.globalize_path( + "%s/%s.png" % [OUTPUT_DIR, name] ) - var rel_path: String = "%s/proof_replay_viewer_%s.png" % [OUTPUT_DIR, timestamp] - var abs_path: String = ProjectSettings.globalize_path(rel_path) var err: Error = image.save_png(abs_path) if err == OK: @@ -145,6 +151,4 @@ func _capture_and_quit() -> void: image.get_width(), image.get_height(), abs_path ]) else: - push_error("proof_replay_viewer: save failed: %s" % error_string(err)) - - get_tree().quit() + push_error("proof_replay_viewer: save failed (%s): %s" % [name, error_string(err)]) diff --git a/tools/screenshot.sh b/tools/screenshot.sh index 805924c7..ea51cf23 100755 --- a/tools/screenshot.sh +++ b/tools/screenshot.sh @@ -35,6 +35,29 @@ timeout 60 flatpak run --user \ --rendering-method gl_compatibility \ --fixed-fps 10 2>&1 | tee /tmp/godot_screenshot_log.txt || true +# replay_viewer_proof writes multiple named files (no timestamp suffix). +# Collect them separately from the standard single-file path. +if [ "$SCENE" = "replay_viewer_proof" ]; then + mapfile -t PROOF_PATHS < <(ls -t "$GODOT_USERDATA"/p2-46-replay-viewer-*.png 2>/dev/null) + if [ "${#PROOF_PATHS[@]}" -eq 0 ]; then + echo "ERROR: No p2-46-replay-viewer-*.png found in $GODOT_USERDATA" + grep -E "Screenshot|ERROR|SCRIPT" /tmp/godot_screenshot_log.txt | head -10 + exit 1 + fi + if ssh -o ConnectTimeout=3 plum "echo ok" >/dev/null 2>&1; then + for p in "${PROOF_PATHS[@]}"; do + BASENAME="$(basename "$p")" + scp "$p" "$PLUM_DESKTOP/${BASENAME}" && \ + echo "Sent to plum: ~/Desktop/${BASENAME}" + done + else + echo "WARNING: plum not reachable, screenshots at:" + for p in "${PROOF_PATHS[@]}"; do echo " $p"; done + fi + echo "=== Done ===" + exit 0 +fi + SCREENSHOT_PATH=$(ls -t "$GODOT_USERDATA"/${NAME}_*.png 2>/dev/null | head -1) if [ -z "$SCREENSHOT_PATH" ] || [ ! -f "$SCREENSHOT_PATH" ]; then