diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a116925..79d8c74c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,15 @@ 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`) ### SGI O2 (IP32) - **A new machine: the SGI O2.** `[machine] profile = "o2_ip32"` with an `[o2]` diff --git a/src/ci.rs b/src/ci.rs index c7c6c50b..6509c073 100644 --- a/src/ci.rs +++ b/src/ci.rs @@ -612,17 +612,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 9a043e0f..606809df 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 832580e5..71c579d8 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; }