From 711637a532a8226720155df069ddf513901fce7c Mon Sep 17 00:00:00 2001 From: Jupiter Belic Date: Sun, 25 Oct 2026 06:53:15 +1000 Subject: [PATCH] ci: make screenshot work without a front-end renderer A --ci instance installs no renderer, so nothing ever fills Rex3Screen::rgba and every iris-ci screenshot was black. Wait for the frame the display thread was asked to produce (bounded) and, when no renderer is present, composite the guest screen from the display caches the refresh thread just updated. A GUI renderer still supplies the readback as before. --- CHANGELOG.md | 10 ++++++++++ src/ci.rs | 37 +++++++++++++++++++++++++++++++++---- src/dev/ng1/rex3.rs | 1 + src/gfx_display.rs | 7 +++++++ 4 files changed, 51 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a538ce5f..c5d9e8c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ hashes are given where a change is easiest to understand by reading the commit. ## October 2026 +### CI and automation + +- **`iris-ci screenshot` produces the guest screen in `--ci`.** A `--ci` + instance installs no front-end renderer, so nothing filled the readback + buffer and every screenshot came out black. The command now waits for the + frame it requested instead of sampling whatever was current, and composites + the guest screen from the display caches when no renderer is present. With + a GUI renderer attached it still returns the readback that renderer + produced. (`src/ci.rs`, `src/gfx_display.rs`) + ### NAT gateway - **Gateway replies no longer wait for a poll tick.** A reply to a guest diff --git a/src/ci.rs b/src/ci.rs index 86cc651cc..24c26d202 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -571,17 +571,46 @@ fn cmd_screenshot(server: &CiServer, args: &Value) -> Response { return Response::err("screenshot: missing 'path' arg"); }; + display.request_screenshot(); + // The board consumes the request when its refresh thread next renders. + // Wait for that (bounded) instead of guessing a frame time; the screen + // lock below then waits for that render to finish copying into `rgba`. + // Boards that do not consume the request report no pending work and fall + // through immediately. + let deadline = std::time::Instant::now() + std::time::Duration::from_millis(250); + while display.screenshot_pending() && std::time::Instant::now() < deadline { + std::thread::sleep(std::time::Duration::from_millis(2)); + } // Snapshot the framebuffer under the screen lock; unlock before the PNG // encode so the refresh thread isn't blocked during disk I/O. + let has_renderer = display.renderer_slot().lock().is_some(); let (width, height, rgba_copy) = { let screen = display.screen().lock(); let w = screen.width; let h = screen.height; + if w == 0 || h == 0 { + return Response::err("screenshot: the display has no video mode yet"); + } let mut out = Vec::with_capacity(w * h); - // `rgba` has row stride 2048; copy the visible window. - for y in 0..h { - let base = y * 2048; - out.extend_from_slice(&screen.rgba[base..base + w]); + if has_renderer { + // The front-end's renderer already composited this frame into + // `rgba`; copy the visible window (row stride 2048). + for y in 0..h { + let base = y * 2048; + out.extend_from_slice(&screen.rgba[base..base + w]); + } + } else { + // `--ci` installs no renderer, so nothing has filled `rgba`. + // Composite the guest screen here from the caches the refresh + // thread just updated. The host status bar and debug overlay are + // deliberately absent: this is the guest's screen. + let mut compositor = crate::compositor::SwCompositor::new(); + compositor.compose_pixels(&screen.compositor_source()); + let pixels = compositor.pixels(); + for y in 0..h { + let base = y * 2048; + out.extend_from_slice(&pixels[base..base + w]); + } } (w, h, out) }; diff --git a/src/dev/ng1/rex3.rs b/src/dev/ng1/rex3.rs index 9a043e0ff..606809dfe 100644 --- a/src/dev/ng1/rex3.rs +++ b/src/dev/ng1/rex3.rs @@ -4865,6 +4865,7 @@ impl crate::gfx_display::GfxDisplay for Rex3 { fn renderer_slot(&self) -> &Mutex>> { &self.renderer } fn screen(&self) -> &Mutex { &self.screen } fn request_screenshot(&self) { self.screenshot_pending.store(true, Ordering::Relaxed); } + fn screenshot_pending(&self) -> bool { self.screenshot_pending.load(Ordering::Relaxed) } fn cycles(&self) -> crate::cpu::mips_core::CyclesPtr { self.cycles.get() } } diff --git a/src/gfx_display.rs b/src/gfx_display.rs index 832580e53..71c579d8a 100644 --- a/src/gfx_display.rs +++ b/src/gfx_display.rs @@ -22,6 +22,13 @@ pub trait GfxDisplay: Send + Sync { /// Ask the display thread to read the next frame back into `screen().rgba`. fn request_screenshot(&self); + /// True while a [`request_screenshot`](Self::request_screenshot) readback is + /// still pending. Callers wait for this to clear (bounded) so they copy the + /// requested frame rather than the one that happened to be current. The + /// default is `false`: a board that does not consume the request has + /// nothing to wait for. + fn screenshot_pending(&self) -> bool { false } + /// CPU cycle counter the board was wired to (GUI's live MIPS estimate). fn cycles(&self) -> CyclesPtr; }