diff --git a/iris-hostcall/src/lib.rs b/iris-hostcall/src/lib.rs index 4affec59..0c7affda 100644 --- a/iris-hostcall/src/lib.rs +++ b/iris-hostcall/src/lib.rs @@ -277,6 +277,19 @@ pub trait Display: Send + Sync { let _ = (x, y); self.present(window, bgra, stride, width, height) } + + /// A colour-index window's frame, for a window at `x`, `y` as above: each + /// pixel's index is its red byte (the low 8 bits) and the low nibble of + /// its green (the next 4), as the host draws for an index context. + /// `overlay`: the window is in the overlay planes. A display that keeps + /// colour-index pixels and shows them through a colour map (an emulated + /// board) puts the indices into the window and returns true; the default + /// takes none, and the program puts the frame up itself. + #[allow(clippy::too_many_arguments)] + fn present_index_at(&self, window: u32, x: i32, y: i32, bgra: &[u8], stride: usize, width: usize, height: usize, overlay: bool) -> bool { + let _ = (window, x, y, bgra, stride, width, height, overlay); + false + } } static DISPLAY: parking_lot::RwLock>> = parking_lot::RwLock::new(None); diff --git a/iris-hostgl/src/draw.rs b/iris-hostgl/src/draw.rs index 9ed94bcc..87055064 100644 --- a/iris-hostgl/src/draw.rs +++ b/iris-hostgl/src/draw.rs @@ -383,7 +383,15 @@ pub(crate) fn present_to( impl Sink { /// True if the frame has been dealt with and the program needs no pixels. - fn deliver(self, window: u32, at: Option<(i32, i32)>, bgra: &[u8], stride: usize, w: usize, h: usize) -> bool { + fn deliver(self, window: u32, at: Option<(i32, i32)>, bgra: &[u8], stride: usize, w: usize, h: usize, index: Option) -> bool { + // A colour-index frame goes to the display now, as indices (see + // iris_hostcall::Display::present_index_at), or back to the program. + if let Some(overlay) = index { + return match (iris_hostcall::display(), at) { + (Some(d), Some((x, y))) => d.present_index_at(window, x, y, bgra, stride, w, h, overlay), + _ => false, + }; + } match self { Sink::Now => { let ok = match iris_hostcall::display() { @@ -403,7 +411,7 @@ impl Sink { // let the next swap of this drawable be the fast one. A drawable // that cannot be presented into stays here for ever and behaves // exactly as it did before any of this. - Sink::Queued => Sink::Now.deliver(window, at, bgra, stride, w, h), + Sink::Queued => Sink::Now.deliver(window, at, bgra, stride, w, h, None), } } } @@ -412,7 +420,8 @@ impl Sink { /// rather than swap. `keep_front`: the front buffer is kept, so a swap copies /// the back into it. #[allow(clippy::too_many_arguments)] -pub fn swap(d: &Draw, window: u32, at: Option<(i32, i32)>, w: i32, h: i32, sink: Sink, abgr: bool, front: bool, keep_front: bool) -> Option> { +#[allow(clippy::too_many_arguments)] +pub fn swap(d: &Draw, window: u32, at: Option<(i32, i32)>, w: i32, h: i32, sink: Sink, abgr: bool, front: bool, keep_front: bool, index: Option) -> Option> { let (w, h) = (w.max(0), h.max(0)); let (rw, rh) = (w.min(d.w), h.min(d.h)); let mut frame = vec![0u8; w as usize * h as usize * 4]; @@ -480,7 +489,7 @@ pub fn swap(d: &Draw, window: u32, at: Option<(i32, i32)>, w: i32, h: i32, sink: // returns now. Mapping it here stopped the whole emulated machine for // that wait on every frame (powerflip: over half the CPU thread's // time in IOSurfaceLock). - if let (Some(sf), Sink::Queued) = (&flip.surface, sink) { + if let (Some(sf), Sink::Queued, None) = (&flip.surface, sink, index) { if crate::present::presents(window) { glFlush(); flip.busy.store(true, Ordering::Release); @@ -523,7 +532,7 @@ pub fn swap(d: &Draw, window: u32, at: Option<(i32, i32)>, w: i32, h: i32, sink: glFlush(); let mut done = false; let mapped = sf.with_pixels(&mut |px, stride| { - presented = sink.deliver(window, at, px, stride, rw, rh); + presented = sink.deliver(window, at, px, stride, rw, rh, index); if !presented && px.len() >= stride * rh && stride >= rw * 4 { hand_back(px, stride); } @@ -538,7 +547,7 @@ pub fn swap(d: &Draw, window: u32, at: Option<(i32, i32)>, w: i32, h: i32, sink: glReadBuffer(GL_COLOR_ATTACHMENT0); let mut bgra = vec![0u8; rw * rh * 4]; glReadPixels(0, 0, rw as i32, rh as i32, GL_BGRA, GL_UNSIGNED_BYTE, bgra.as_mut_ptr() as *mut c_void); - presented = sink.deliver(window, at, &bgra, rw * 4, rw, rh); + presented = sink.deliver(window, at, &bgra, rw * 4, rw, rh, index); if !presented { hand_back(&bgra, rw * 4); } diff --git a/iris-hostgl/src/service.rs b/iris-hostgl/src/service.rs index 576de1b0..f087ee5f 100644 --- a/iris-hostgl/src/service.rs +++ b/iris-hostgl/src/service.rs @@ -88,6 +88,12 @@ pub const OP_PRESENT_STATS: u64 = 0x1_000F; /// a host without it answers EINVAL, and the library keeps the one buffer. /// For this client from now on, its contexts made before as well. pub const OP_FRONT_BUFFERS: u64 = 0x1_0011; +/// Answered 1: OP_SWAP takes a colour-index window's frame as indices (bits +/// 2 and 3 of the seventh slot) for a display that keeps them. A host +/// without it answers EINVAL, and would composite such a frame's encoded +/// RGBA as colours -- so the library asks first, and without it has the +/// frame handed back and puts it up itself. +pub const OP_INDEX_FRAMES: u64 = 0x1_0013; /// The protocol: HELLO's second result, which the library checks. pub const PROTOCOL: u64 = 2; @@ -629,6 +635,9 @@ impl Service for GlService { let [buf, w, h, abgr, x, y, place] = slots!(7); let at = (place & 1 != 0).then_some((x as i32, y as i32)); let front = place & 2 != 0; + // Bit 2: a colour-index window's frame, its indices for a + // display that keeps them (bit 3: in the overlay planes). + let index = (place & 4 != 0).then_some(place & 8 != 0); let (buf, w, h) = (buf, (w as i32).clamp(0, MAX_DIM), (h as i32).clamp(0, MAX_DIM)); let c = &self.clients[&client]; let (window, interval, last_swap, keep_front) = (c.draw, c.swap_interval, c.last_swap, c.fronts); @@ -638,7 +647,9 @@ impl Service for GlService { // thread (the first swap of each drawable is still inline: // see present::presents). Pacing is the `wait` below, slept // in the guest. - let sink = if c.swap_async || at.is_some() { draw::Sink::Queued } else { draw::Sink::Now }; + // Colour-index frames go now: the display takes them or hands + // them back to the program, which waits for the answer. + let sink = if index.is_none() && (c.swap_async || at.is_some()) { draw::Sink::Queued } else { draw::Sink::Now }; let Some(group) = c.current.and_then(|id| self.ctxs.get(&id)).map(|c| c.group) else { return Reply::Err(EINVAL); }; @@ -655,7 +666,7 @@ impl Service for GlService { } self.clients.get_mut(&client).unwrap().last_swap = Some(now + wait); let (dw, dh, samples) = (d.w, d.h, d.samples); - let frame = draw::swap(d, window, at, w, h, sink, abgr != 0, front, keep_front); + let frame = draw::swap(d, window, at, w, h, sink, abgr != 0, front, keep_front, index); // The window is a different size from the drawable, so the // next frame is drawn at the new one. After the read, not // before: the pixels presented are in the old buffer. Not on @@ -677,6 +688,7 @@ impl Service for GlService { Some(frame) => self.finish(mem, args, vec![(buf, frame)], Reply::Ok(0, wait)), } } + OP_INDEX_FRAMES => Reply::Ok(1, 0), OP_FRONT_BUFFERS => { if let Some(c) = self.clients.get_mut(&client) { c.fronts = true; diff --git a/iris-hostgl/src/tests.rs b/iris-hostgl/src/tests.rs index 6175b76b..2ad7bf86 100644 --- a/iris-hostgl/src/tests.rs +++ b/iris-hostgl/src/tests.rs @@ -1752,3 +1752,11 @@ fn color_matrix_limits_are_answered_and_raise_no_error() { assert!(v(0) >= 2, "a colour matrix stack of at least 2, as IMPACT's: {}", v(0)); assert_eq!(v(4), 1, "one deep"); } + +/// A library asks before it sends a colour-index frame as indices: a host +/// that cannot take them answers EINVAL (see OP_INDEX_FRAMES). +#[test] +fn index_frames_are_asked_for() { + let mut g = Guest::new(64); + assert_eq!(g.call(OP_INDEX_FRAMES, &[0]), Reply::Ok(1, 0)); +} diff --git a/src/dev/mgras/disp.rs b/src/dev/mgras/disp.rs index 516dd8d5..cb222c3d 100644 --- a/src/dev/mgras/disp.rs +++ b/src/dev/mgras/disp.rs @@ -83,6 +83,111 @@ pub fn composite(rss: &mut Rss, dcb: &Dcb, x: i32, y: i32, bgra: &[u8], stride: true } +/// The window ID a colour-index frame's window has, in the main planes (a +/// colour-index display mode) or the overlay planes (one that is on): the +/// commonest such ID over a grid of samples inside the window, as +/// `window_did` -- and only when it covers three quarters of the samples on +/// the screen. A window just mapped may not have its ID in the tables yet, +/// and the commonest ID around it is then someone else's: the frame went +/// there, the program was told it was shown, and gltest's overlay read back +/// 0s now and then. None, and the program puts the frame up itself. +fn index_window_did(dcb: &Dcb, x: i32, y: i32, w: usize, h: usize, overlay: bool) -> Option { + let mut total = 0u32; + let mut counts = [0u32; 32]; + let mut runs = Vec::new(); + let (screen_w, screen_h) = frame::display_size(dcb); + for sy in 0..16 { + let py = y + (h as i32 * (2 * sy + 1)) / 32; + if !(0..screen_h as i32).contains(&py) { + continue; + } + if overlay { + dcb.vc3.overlay_did_runs(py as usize, &mut runs); + } else { + dcb.vc3.main_did_runs(py as usize, &mut runs); + } + for sx in 0..16 { + let px = x + (w as i32 * (2 * sx + 1)) / 32; + if !(0..screen_w as i32).contains(&px) { + continue; + } + let did = runs.iter().rev().find(|r| r.0 as i32 <= px).map_or(0, |r| r.1) as u32; + let index = if overlay { dcb.xmap.overlay_mode(did) != 0 } else { dcb.xmap.main_mode(did) & 0x1F < 4 }; + total += 1; + if index { + counts[did as usize & 31] += 1; + } + } + } + let (did, n) = counts.iter().enumerate().max_by_key(|&(_, n)| *n)?; + (*n > 0 && *n * 4 >= total * 3).then_some(did as u8) +} + +/// Paint a colour-index window's host GL frame into the framebuffer, as +/// `composite` does an RGB one, but as indices: each pixel's index is its +/// red byte and the low nibble of its green (`bgra`: B, G, R, A), and goes +/// where the board keeps that window's indices -- the low 8 or 12 main +/// planes (by the window ID's display mode), the high 12 when the ID shows +/// buffer B (`frame::Frame::snapshot`), or the overlay planes for an overlay +/// window -- for the colour map to show, as GL's drawing would on the board. +/// Before this, the program read every frame back, turned it into indices +/// and put it to the X server itself: SGI's IRIS GL on host OpenGL ran +/// flight's full-screen colour-index window at a frame every few seconds. +/// False when no window ID fits, or there are no overlay planes. +#[allow(clippy::too_many_arguments)] +pub fn composite_index(rss: &mut Rss, dcb: &Dcb, x: i32, y: i32, bgra: &[u8], stride: usize, w: usize, h: usize, overlay: bool) -> bool { + let (main, over) = frame::scanout_buffers(rss, dcb); + let buf = match (overlay, over) { + (false, _) => main, + (true, Some(o)) => o, + (true, None) => return false, + }; + let Some(target) = index_window_did(dcb, x, y, w, h, overlay) else { return false }; + let b_half = !overlay && dcb.xmap.buf_select() >> target & 1 != 0; + let (mask, shift): (u64, u32) = if overlay { + (0xFF, 0) + } else if b_half { + (0xFFF << 12, 12) + } else if dcb.xmap.main_mode(target as u32) & 0x1F == 1 { + (0xFF, 0) + } else { + (0xFFF, 0) + }; + let mut runs = Vec::new(); + let (screen_w, screen_h) = frame::display_size(dcb); + for row in 0..h { + let sy = y + row as i32; + if !(0..screen_h as i32).contains(&sy) { + continue; + } + if overlay { + dcb.vc3.overlay_did_runs(sy as usize, &mut runs); + } else { + dcb.vc3.main_did_runs(sy as usize, &mut runs); + } + if runs.is_empty() || runs[0].0 != 0 { + runs.insert(0, (0, 0)); + } + let fb_y = (screen_h - 1 - sy as usize) as u32; + for (k, &(x0, did)) in runs.iter().enumerate() { + if did != target { + continue; + } + let x1 = runs.get(k + 1).map_or(screen_w as i32, |r| r.0 as i32); + let lo = (x0 as i32).max(x).max(0); + let hi = x1.min(x + w as i32).min(screen_w as i32); + for sx in lo..hi { + let i = row * stride + (sx - x) as usize * 4; + let Some(p) = bgra.get(i..i + 4) else { break }; + let index = p[2] as u64 | (p[1] as u64 & 0xF) << 8; + let old = rss.mem.get(&buf, sx as u32, fb_y); + rss.mem.put(&buf, sx as u32, fb_y, (old & !mask) | (index << shift & mask)); + } + } + } + true +} + /// Write a scanned-out frame (`0xFF_BB_GG_RR`, stride `OUT_STRIDE`) as a PNG. pub fn save_png(path: &str, frame: &[u32], width: usize, height: usize) -> Result<(), String> { let file = std::fs::File::create(path).map_err(|e| e.to_string())?; diff --git a/src/dev/mgras/mod.rs b/src/dev/mgras/mod.rs index dd2cdd0f..77589bce 100644 --- a/src/dev/mgras/mod.rs +++ b/src/dev/mgras/mod.rs @@ -783,6 +783,23 @@ impl Mgras { ok } + /// A colour-index window's host GL frame, as indices (`disp::composite_index`). + #[allow(clippy::too_many_arguments)] + pub fn composite_index(&self, x: i32, y: i32, bgra: &[u8], stride: usize, w: usize, h: usize, overlay: bool) -> bool { + let sub = self.submit.lock(); + self.wait_idle(); + // SAFETY: idle with `submit` held. + let rss = unsafe { &mut *self.rss.get() }; + let ok = disp::composite_index(rss, &self.front().dcb, x, y, bgra, stride, w, h, overlay); + if ok { + if let Some(rec) = sub.as_ref() { + rec.lock().put(record::Rec::Composite); + } + self.dirty.store(true, Ordering::Release); + } + ok + } + /// Framebuffer pixel (its 24 colour planes) at display (`x`, `y`), y = 0 /// the top row, once the board has run everything queued (tests). @@ -1492,4 +1509,8 @@ impl iris_hostcall::Display for ImpactScreen { fn present_at(&self, _window: u32, x: i32, y: i32, bgra: &[u8], stride: usize, width: usize, height: usize) -> bool { self.0.composite(x, y, bgra, stride, width, height) } + + fn present_index_at(&self, _window: u32, x: i32, y: i32, bgra: &[u8], stride: usize, width: usize, height: usize, overlay: bool) -> bool { + self.0.composite_index(x, y, bgra, stride, width, height, overlay) + } }