Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]`
Expand Down
37 changes: 33 additions & 4 deletions src/ci.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand Down
1 change: 1 addition & 0 deletions src/dev/ng1/rex3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4865,6 +4865,7 @@ impl crate::gfx_display::GfxDisplay for Rex3 {
fn renderer_slot(&self) -> &Mutex<Option<Box<dyn Renderer>>> { &self.renderer }
fn screen(&self) -> &Mutex<Rex3Screen> { &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() }
}

Expand Down
7 changes: 7 additions & 0 deletions src/gfx_display.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading