diff --git a/iris-hostgl/src/draw.rs b/iris-hostgl/src/draw.rs index 87055064..161426fc 100644 --- a/iris-hostgl/src/draw.rs +++ b/iris-hostgl/src/draw.rs @@ -365,7 +365,10 @@ pub enum Sink { Queued, } -/// Hand a frame to `d`: at a screen position when the program gave one. +/// Hand a frame to `d`: at a screen position when the program gave one. A +/// colour-index window's frame (`index`: Some(overlay)) goes as indices, and +/// only with a position (see iris_hostcall::Display::present_index_at). +#[allow(clippy::too_many_arguments)] pub(crate) fn present_to( d: &dyn iris_hostcall::Display, window: u32, @@ -374,35 +377,31 @@ pub(crate) fn present_to( stride: usize, w: usize, h: usize, + index: Option, ) -> bool { - match at { - Some((x, y)) => d.present_at(window, x, y, bgra, stride, w, h), - None => d.present(window, bgra, stride, w, h), + match (index, at) { + (Some(overlay), Some((x, y))) => d.present_index_at(window, x, y, bgra, stride, w, h, overlay), + (Some(_), None) => false, + (None, Some((x, y))) => d.present_at(window, x, y, bgra, stride, w, h), + (None, None) => d.present(window, bgra, stride, w, h), } } 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, 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, - }; - } + #[allow(clippy::too_many_arguments)] + fn deliver(self, window: u32, at: Option<(i32, i32)>, bgra: &[u8], stride: usize, w: usize, h: usize, index: Option, flush: bool) -> bool { match self { Sink::Now => { let ok = match iris_hostcall::display() { - Some(d) => present_to(d.as_ref(), window, at, bgra, stride, w, h), + Some(d) => present_to(d.as_ref(), window, at, bgra, stride, w, h, index), None => false, }; crate::present::note(window, ok); ok } Sink::Queued if crate::present::presents(window) => { - crate::present::queue(window, at, bgra, stride, w, h); + crate::present::queue(window, at, bgra, stride, w, h, index, flush); true } // Not a drawable we have seen present. Whether presenting works is @@ -411,7 +410,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, None), + Sink::Queued => Sink::Now.deliver(window, at, bgra, stride, w, h, index, flush), } } } @@ -420,8 +419,11 @@ 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)] +/// `flush`: the frame is shown because the program flushed, not swapped (a +/// front buffer, a colour-index window): it never waits for the presenter, +/// and replaces a frame still waiting (see present::queue). #[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> { +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, flush: bool) -> 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]; @@ -489,11 +491,11 @@ 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, None) = (&flip.surface, sink, index) { + if let (Some(sf), Sink::Queued) = (&flip.surface, sink) { if crate::present::presents(window) { glFlush(); flip.busy.store(true, Ordering::Release); - crate::present::queue_surface(window, at, sf.clone(), flip.busy.clone(), rw, rh); + crate::present::queue_surface(window, at, sf.clone(), flip.busy.clone(), rw, rh, index, flush); for (&n, &v) in names.iter().zip(pack.iter()) { glPixelStorei(n, v); } @@ -532,7 +534,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, index); + presented = sink.deliver(window, at, px, stride, rw, rh, index, flush); if !presented && px.len() >= stride * rh && stride >= rw * 4 { hand_back(px, stride); } @@ -547,7 +549,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, index); + presented = sink.deliver(window, at, &bgra, rw * 4, rw, rh, index, flush); if !presented { hand_back(&bgra, rw * 4); } diff --git a/iris-hostgl/src/lib.rs b/iris-hostgl/src/lib.rs index 820bd860..b8893a1b 100644 --- a/iris-hostgl/src/lib.rs +++ b/iris-hostgl/src/lib.rs @@ -72,7 +72,7 @@ use exec::Exec; #[cfg(any(target_os = "macos", target_os = "linux"))] pub use service::{ GlService, OP_BATCH, OP_BATCH_ASYNC, OP_CREATE, OP_DESTROY, OP_DRAWABLE_GONE, OP_FINISH, OP_GETSTRING, OP_GLX_STRING, OP_GOODBYE, OP_HELLO, - OP_MAKECURRENT, OP_PBUFFER, OP_RELEASE, OP_SWAP, OP_SWAP_INTERVAL, OP_VIDEO_SYNC, PROTOCOL, + OP_MAKECURRENT, OP_PBUFFER, OP_PRESENT_LATER, OP_RELEASE, OP_SWAP, OP_SWAP_INTERVAL, OP_VIDEO_SYNC, PROTOCOL, }; /// Answer host call [`iris_hostcall::GL`] with a new host GL service. diff --git a/iris-hostgl/src/present.rs b/iris-hostgl/src/present.rs index 991d1954..fe2c4cfd 100644 --- a/iris-hostgl/src/present.rs +++ b/iris-hostgl/src/present.rs @@ -98,6 +98,8 @@ struct Frame { src: Src, w: usize, h: usize, + /// A colour-index window's frame (Some(overlay)), shown as indices. + index: Option, } /// A frame's pixels: a copy, or the surface the GPU is rendering them into, @@ -123,7 +125,10 @@ impl Src { #[derive(Default)] struct State { - queued: Option, + /// Frames waiting, oldest first: at most one a window (a newer frame of + /// the same window replaces it), so one window's flushes never drop + /// another window's frame. + queued: Vec, /// A frame is being presented right now. busy: bool, /// Buffers of frames already presented or replaced. @@ -180,25 +185,37 @@ pub fn forget(window: u32) { /// /// `bgra` is `h` rows of `stride` bytes, `w` pixels each, as /// `Display::present` wants them. -pub fn queue(window: u32, at: Option<(i32, i32)>, bgra: &[u8], stride: usize, w: usize, h: usize) { - let mut st = admit(); +/// +/// `flush`: the program flushed rather than swapped (front-buffer or +/// colour-index drawing). Such a frame never waits: it replaces the one +/// still waiting, so the screen gets the newest drawing as fast as this +/// thread can put it there, and the last one always. SGI's IRIS GL flushes +/// after every primitive it draws into the front buffer -- flight drew its +/// cockpit with 550 flushes a second -- and waiting for the presenter on +/// each made the drawing crawl. +#[allow(clippy::too_many_arguments)] +pub fn queue(window: u32, at: Option<(i32, i32)>, bgra: &[u8], stride: usize, w: usize, h: usize, index: Option, flush: bool) { + let mut st = admit(flush); let mut buf = st.spare.pop().unwrap_or_default(); buf.clear(); buf.extend_from_slice(bgra); - put(st, Frame { window, at, src: Src::Bytes { bgra: buf, stride }, w, h }); + put(st, Frame { window, at, src: Src::Bytes { bgra: buf, stride }, w, h, index }); } /// Queue a frame still being rendered into `sf`: this thread maps it (waiting /// for the GPU there, not in the swap) and presents `w` x `h` of it, then -/// clears `busy` so its drawable may draw into it again. -pub fn queue_surface(window: u32, at: Option<(i32, i32)>, sf: Arc, busy: Arc, w: usize, h: usize) { - let st = admit(); - put(st, Frame { window, at, src: Src::Surface { sf, busy }, w, h }); +/// clears `busy` so its drawable may draw into it again. `index`, `flush`: +/// as [`queue`]. +#[allow(clippy::too_many_arguments)] +pub fn queue_surface(window: u32, at: Option<(i32, i32)>, sf: Arc, busy: Arc, w: usize, h: usize, index: Option, flush: bool) { + let st = admit(flush); + put(st, Frame { window, at, src: Src::Surface { sf, busy }, w, h, index }); } /// The state, once there is room for another frame: starts the thread, and -/// waits while [`MAX_IN_FLIGHT`] frames are already in flight. -fn admit() -> MutexGuard<'static, State> { +/// waits while [`MAX_IN_FLIGHT`] frames are already in flight -- unless the +/// frame is a flush's, which replaces the waiting one instead. +fn admit(flush: bool) -> MutexGuard<'static, State> { let p = presenter(); let mut st = lock(p); if !st.started { @@ -210,7 +227,7 @@ fn admit() -> MutexGuard<'static, State> { // One being presented and one already waiting: wait for the first to // finish rather than let the guest run further ahead. let mut waited = false; - while st.queued.is_some() as usize + st.busy as usize >= MAX_IN_FLIGHT { + while !flush && st.queued.len() + st.busy as usize >= MAX_IN_FLIGHT { waited = true; st = wait(&p.idle, st); } @@ -220,18 +237,21 @@ fn admit() -> MutexGuard<'static, State> { st } -/// Put `f` in the queue, replacing whatever is waiting there: it is older. +/// Put `f` in the queue, replacing a frame of its window still waiting there: +/// that one is older. fn put(mut st: MutexGuard<'static, State>, f: Frame) { - if let Some(old) = st.queued.take() { + if let Some(at) = st.queued.iter().position(|q| q.window == f.window) { + let old = std::mem::replace(&mut st.queued[at], f); REPLACED.fetch_add(1, Ordering::Relaxed); if let Some(buf) = old.src.release() { if st.spare.len() < SPARE_BUFFERS { st.spare.push(buf); } } + } else { + st.queued.push(f); } QUEUED.fetch_add(1, Ordering::Relaxed); - st.queued = Some(f); drop(st); presenter().work.notify_one(); } @@ -240,7 +260,7 @@ fn put(mut st: MutexGuard<'static, State>, f: Frame) { pub fn drain() { let p = presenter(); let mut st = lock(p); - while st.queued.is_some() || st.busy { + while !st.queued.is_empty() || st.busy { st = wait(&p.idle, st); } } @@ -252,7 +272,7 @@ fn run() { let frame = { let mut st = lock(p); loop { - match st.queued.take() { + match (!st.queued.is_empty()).then(|| st.queued.remove(0)) { Some(f) => { st.busy = true; break f; @@ -265,12 +285,12 @@ fn run() { // queuer must not be held up by it. let ok = match iris_hostcall::display() { Some(d) => match &frame.src { - Src::Bytes { bgra, stride } => crate::draw::present_to(d.as_ref(), frame.window, frame.at, bgra, *stride, frame.w, frame.h), + Src::Bytes { bgra, stride } => crate::draw::present_to(d.as_ref(), frame.window, frame.at, bgra, *stride, frame.w, frame.h, frame.index), Src::Surface { sf, .. } => { let mut ok = false; let mapped = sf.with_pixels(&mut |px, stride| { if px.len() >= stride * frame.h && stride >= frame.w * 4 { - ok = crate::draw::present_to(d.as_ref(), frame.window, frame.at, px, stride, frame.w, frame.h); + ok = crate::draw::present_to(d.as_ref(), frame.window, frame.at, px, stride, frame.w, frame.h, frame.index); } }); mapped && ok diff --git a/iris-hostgl/src/service.rs b/iris-hostgl/src/service.rs index f087ee5f..ebd0ca84 100644 --- a/iris-hostgl/src/service.rs +++ b/iris-hostgl/src/service.rs @@ -94,6 +94,20 @@ pub const OP_FRONT_BUFFERS: u64 = 0x1_0011; /// 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; +/// OP_SWAP's slots for a flush's present (a front buffer, a colour-index +/// window) the library knows the display composites: nothing comes back, so +/// on the GL thread it is answered 1 at once and queued behind the buffers +/// sent before it (threaded.rs), and a client's window is shown at most once +/// a `FLUSH_GAP` -- the rest when the gap is up (see present_later). SGI's +/// IRIS GL flushes after every primitive it draws into the front buffer, two +/// thousand a second in flight's cockpit; each a whole frame on the screen +/// made the cockpit draw itself over a second where the board takes a few +/// hundred milliseconds. Without the GL thread it is OP_SWAP; a host without +/// it answers EINVAL, and the library uses OP_SWAP. +pub const OP_PRESENT_LATER: u64 = 0x1_0012; + +/// How often a client's flushes reach the screen at most: a display's frame. +pub const FLUSH_GAP: Duration = Duration::from_micros(16_667); /// The protocol: HELLO's second result, which the library checks. pub const PROTOCOL: u64 = 2; @@ -162,6 +176,10 @@ struct Client { since: u64, /// It said OP_FRONT_BUFFERS. fronts: bool, + /// When an OP_PRESENT_LATER of its was last shown, and the one waiting + /// for FLUSH_GAP to be up since, with when it is due. + flush_shown: Option, + flush_waiting: Option<([u64; 7], Instant)>, } /// Where a call stopped, to be carried on by its retry. @@ -192,6 +210,9 @@ pub struct GlService { /// Command buffers run, and calls in them: how much batching is saving. batches: u64, commands: u64, + /// FLUSH_GAP; a test sets its own, so as not to depend on how fast the + /// host draws. + pub(crate) flush_gap: Duration, } impl GlService { @@ -210,6 +231,7 @@ impl GlService { started: Instant::now(), batches: 0, commands: 0, + flush_gap: FLUSH_GAP, } } @@ -276,6 +298,122 @@ impl GlService { self.resumes.push_back(Resume { args: *args, stage }); } + /// OP_SWAP for the client's current context and drawable, `slots` its + /// seven: the frame to hand back (None: it is on the screen), and how long + /// the library is to wait (GLX_SGI_swap_control), or an errno. + fn swap(&mut self, client: u32, slots: [u64; 7]) -> Result<(Option>, u64), u64> { + // The fourth slot: nonzero when the program's visual has red + // in the low byte, so the frame comes back in that order. + // Then the window's top-left on the screen, valid when bit 0 + // of the seventh is set, for a display that composites. + let [_, w, h, abgr, x, y, place] = slots; + 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 (w, h) = ((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); + // A frame with a screen position goes to a display that + // composites it: it is queued whatever the swap interval, so + // the wait for the GPU and the present happen off the CPU + // thread (the first swap of each drawable is still inline: + // see present::presents). Pacing is the `wait` below, slept + // in the guest. + // A colour-index frame takes the same way as an RGBA one: the + // first of each drawable now, to learn whether the display + // takes it, then queued. + let sink = if c.swap_async || at.is_some() { draw::Sink::Queued } else { draw::Sink::Now }; + // Shown because the program flushed (front buffer, or a + // colour-index window, bit 4), not swapped: never waits. + let flush = front || place & 16 != 0; + let Some(group) = c.current.and_then(|id| self.ctxs.get(&id)).map(|c| c.group) else { + return Err(EINVAL); + }; + let Some(d) = self.groups.get(&group).and_then(|g| g.draws.get(&window)) else { return Err(EINVAL) }; + // GLX_SGI_swap_control: this swap is due `interval` frames + // after the last. The program is told how long that is. Only + // swaps are paced: showing the front buffer (bit 1), or a + // colour-index window, after a flush (bit 4) is not a swap, + // and neither waits nor moves the next swap's due time. + // Paced, SGI's IRIS GL drew flight at a few primitives a + // second: it flushes after each one it draws into the front + // buffer, and asks for swapinterval(2). + let now = Instant::now(); + let mut wait = Duration::ZERO; + if !front && place & 16 == 0 { + if interval > 1 { + if let Some(last) = last_swap { + let due = last + Duration::from_nanos((FRAME_NS * interval as u128).min(MAX_WAIT_NS) as u64); + wait = due.saturating_duration_since(now); + } + } + 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, index, flush); + // 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 + // showing the front: that is a flush, after which the program + // still has its back buffer (gltest's window, sized by the + // window manager after its context was made current, lost + // what it had drawn). + if !front && (w, h) != (dw, dh) && w > 0 && h > 0 { + if let Some(g) = self.groups.get_mut(&group) { + draw::ensure(&mut g.draws, self.backend.as_mut(), window, w, h, samples); + } + } + self.rebind(client); + Ok((frame, wait.as_nanos() as u64)) + } + + /// OP_PRESENT_LATER, on the GL thread: shown now when the client's last + /// was shown at least FLUSH_GAP ago, otherwise when the gap is up -- this + /// one replacing any still waiting, since what it shows is the drawable as + /// it is then. + pub fn present_later(&mut self, client: u32, slots: [u64; 7]) { + let gap = self.flush_gap; + let Some(c) = self.clients.get_mut(&client) else { return }; + let now = Instant::now(); + match c.flush_shown { + Some(t) if now < t + gap => c.flush_waiting = Some((slots, t + gap)), + _ => self.show_later(client, slots, now), + } + } + + fn show_later(&mut self, client: u32, slots: [u64; 7], now: Instant) { + let Some(c) = self.clients.get_mut(&client) else { return }; + c.flush_shown = Some(now); + c.flush_waiting = None; + self.select_client(client); + // Composited, so nothing comes back; a display that did not take it + // has nobody to hand it to. + let _ = self.swap(client, slots); + } + + /// The client's present still waiting for FLUSH_GAP, shown now. + fn show_waiting(&mut self, client: u32) { + if let Some((slots, _)) = self.clients.get_mut(&client).and_then(|c| c.flush_waiting.take()) { + self.show_later(client, slots, Instant::now()); + } + } + + /// When the next waiting present is due, if one is. + pub fn next_due(&self) -> Option { + self.clients.values().filter_map(|c| c.flush_waiting.map(|(_, due)| due)).min() + } + + /// Shows every waiting present that is due. + pub fn show_due(&mut self) { + let now = Instant::now(); + let due: Vec = self.clients.iter().filter(|(_, c)| c.flush_waiting.is_some_and(|(_, d)| d <= now)).map(|(&id, _)| id).collect(); + for client in due { + self.show_waiting(client); + } + } + /// An OP_BATCH_ASYNC buffer, run on the GL thread after its call was /// answered. Its bytes were read then; nothing else of the program's /// memory can be, since the program has moved on. A command that tries @@ -449,6 +587,11 @@ impl Service for GlService { return Reply::Err(EINVAL); } self.select_client(client); + // A present still waiting is of the drawable the client is drawing + // to now. Only a call changes that, so it is shown first. + if op != OP_BATCH && op != OP_BATCH_ASYNC { + self.show_waiting(client); + } // The retry of a call that stopped part-way. if let Some(i) = self.resumes.iter().position(|r| r.args == *args) { @@ -627,65 +770,15 @@ impl Service for GlService { self.backend.make_current(None); Reply::Ok(0, 0) } - OP_SWAP => { - // The fourth slot: nonzero when the program's visual has red - // in the low byte, so the frame comes back in that order. - // Then the window's top-left on the screen, valid when bit 0 - // of the seventh is set, for a display that composites. - 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); - // A frame with a screen position goes to a display that - // composites it: it is queued whatever the swap interval, so - // the wait for the GPU and the present happen off the CPU - // thread (the first swap of each drawable is still inline: - // see present::presents). Pacing is the `wait` below, slept - // in the guest. - // 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); - }; - let Some(d) = self.groups.get(&group).and_then(|g| g.draws.get(&window)) else { return Reply::Err(EINVAL) }; - // GLX_SGI_swap_control: this swap is due `interval` frames - // after the last. The program is told how long that is. - let now = Instant::now(); - let mut wait = Duration::ZERO; - if interval > 1 { - if let Some(last) = last_swap { - let due = last + Duration::from_nanos((FRAME_NS * interval as u128).min(MAX_WAIT_NS) as u64); - wait = due.saturating_duration_since(now); - } - } - 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, 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 - // showing the front: that is a flush, after which the program - // still has its back buffer (gltest's window, sized by the - // window manager after its context was made current, lost - // what it had drawn). - if !front && (w, h) != (dw, dh) && w > 0 && h > 0 { - if let Some(g) = self.groups.get_mut(&group) { - draw::ensure(&mut g.draws, self.backend.as_mut(), window, w, h, samples); - } - } - self.rebind(client); - let wait = wait.as_nanos() as u64; - match frame { + OP_SWAP | OP_PRESENT_LATER => { + let slots = slots!(7); + let buf = slots[0]; + match self.swap(client, slots) { + Err(e) => Reply::Err(e), // 1 tells the library the frame is already on screen. - None => Reply::Ok(1, wait), - Some(_) if buf == 0 => Reply::Err(EFAULT), - Some(frame) => self.finish(mem, args, vec![(buf, frame)], Reply::Ok(0, wait)), + Ok((None, wait)) => Reply::Ok(1, wait), + Ok((Some(_), _)) if buf == 0 => Reply::Err(EFAULT), + Ok((Some(frame), wait)) => self.finish(mem, args, vec![(buf, frame)], Reply::Ok(0, wait)), } } OP_INDEX_FRAMES => Reply::Ok(1, 0), diff --git a/iris-hostgl/src/tests.rs b/iris-hostgl/src/tests.rs index 2ad7bf86..809cfa29 100644 --- a/iris-hostgl/src/tests.rs +++ b/iris-hostgl/src/tests.rs @@ -1760,3 +1760,68 @@ fn index_frames_are_asked_for() { let mut g = Guest::new(64); assert_eq!(g.call(OP_INDEX_FRAMES, &[0]), Reply::Ok(1, 0)); } + +/// OP_PRESENT_LATER shows a client's flushes at most once a FLUSH_GAP: the +/// first at once, a second inside the gap waits -- for the gap to be up, or +/// for the client's next call that is not a buffer, whichever comes first. +#[test] +fn flush_presents_wait_out_the_gap() { + let mut g = Guest::new(512); + assert_eq!(g.call(OP_FRONT_BUFFERS, &[0]), Reply::Ok(1, 0)); + g.context(0x0040_0009, 40, 30); + let client = g.client as u32; + let slots = [0, 40, 30, 0, 0, 0, 2]; + // A gap far longer than a present takes, however slow the host's GL: a + // virtual machine's took longer than a real frame, and the second + // present came after the gap. + let gap = std::time::Duration::from_secs(60); + g.svc.borrow_mut().flush_gap = gap; + g.svc.borrow_mut().present_later(client, slots); + assert_eq!(g.svc.borrow().next_due(), None, "the first is shown at once"); + g.svc.borrow_mut().present_later(client, slots); + let due = g.svc.borrow().next_due().expect("the second waits"); + assert!(due > std::time::Instant::now() && due <= std::time::Instant::now() + gap); + // A buffer does not change what the client draws to: it still waits. + let mut e = Enc::default(); + e.cmd("glClear", &[V::I(GL_COLOR_BUFFER_BIT)]); + assert_eq!(g.batch(&e.take()), Reply::Ok(0, 0)); + assert!(g.svc.borrow().next_due().is_some(), "still waiting after a buffer"); + // Any other call shows it first. + assert_eq!(g.call(OP_FINISH, &[0]), Reply::Ok(0, 0)); + assert_eq!(g.svc.borrow().next_due(), None, "shown before the call"); + // And with nothing else to do, the gap -- a short one, still well over + // what the last present took. + let gap = std::time::Duration::from_millis(200); + g.svc.borrow_mut().flush_gap = gap; + g.svc.borrow_mut().present_later(client, slots); + assert!(g.svc.borrow().next_due().is_some()); + g.svc.borrow_mut().show_due(); + assert!(g.svc.borrow().next_due().is_some(), "not due yet"); + std::thread::sleep(gap); + g.svc.borrow_mut().show_due(); + assert_eq!(g.svc.borrow().next_due(), None, "shown when the gap was up"); +} + +/// On the GL thread OP_PRESENT_LATER is answered at once, even with the +/// client's buffers still queued, and runs in order with them. +#[test] +fn present_later_is_answered_at_once_and_runs_in_order() { + let mut t = Threads::new(); + let mut e = Enc::default(); + ortho(&mut e, 64, 64); + e.cmd("glClearColor", &[V::F(0.0), V::F(0.0), V::F(1.0), V::F(1.0)]).cmd("glClear", &[V::I(GL_COLOR_BUFFER_BIT)]); + let clear = e.take(); + for _ in 0..4 { + assert_eq!(t.batch(OP_BATCH_ASYNC, &clear), Reply::Ok(0, 0)); + let s: Vec = [0u64, 64, 64, 0, 0, 0, 0].iter().flat_map(|v| v.to_be_bytes()).collect(); + t.mem.put(t.slots, &s); + t.serial += 1; + let r = t.svc.call(&mut t.mem, &[OP_PRESENT_LATER, t.slots, t.client, t.serial, 0, 0, 0, 0]); + assert_eq!(r, Reply::Ok(1, 0), "answered without waiting for the buffers"); + } + let px = t.mem.alloc(64 * 64 * 4, 0); + let mut e = Enc::default(); + e.cmd("glReadPixels", &[V::I(0), V::I(0), V::I(64), V::I(64), V::I(GL_RGBA), V::I(GL_UNSIGNED_BYTE), V::A(px)]); + assert_eq!(t.batch(OP_BATCH, &e.take()), Reply::Ok(0, 0)); + assert_eq!(pixel(t.mem.get(px, 64 * 64 * 4), 64, 8, 8), [0, 0, 255, 255], "the buffers ran"); +} diff --git a/iris-hostgl/src/threaded.rs b/iris-hostgl/src/threaded.rs index e4f15967..f9945069 100644 --- a/iris-hostgl/src/threaded.rs +++ b/iris-hostgl/src/threaded.rs @@ -25,6 +25,10 @@ //! frame's swap. Only a library that sends OP_BATCH_ASYNC ever has buffers //! queued, so only one that knows ENOTYET is ever told it. //! +//! - OP_PRESENT_LATER is the same: its slots are read here and it is answered +//! at once, and the GL thread shows it in its turn, or when FLUSH_GAP is up +//! (service.rs) -- waking for that when nothing else comes. +//! //! The queue is bounded: a program that sends faster than the host can run //! waits for room, rather than piling up frames. Each side spins a few //! microseconds before it sleeps: most answers come sooner than a thread @@ -34,14 +38,14 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::mpsc::{self, Receiver, SyncSender, TryRecvError}; +use std::sync::mpsc::{self, Receiver, RecvTimeoutError, SyncSender, TryRecvError}; use std::sync::Arc; use std::time::{Duration, Instant}; use iris_hostcall::{GuestMemory, Reply, Service, ENOTYET}; use crate::exec::read_guest; -use crate::service::{read_slots, GlService, EFAULT, EINVAL, MAX_BATCH, OP_BATCH_ASYNC}; +use crate::service::{read_slots, GlService, EFAULT, EINVAL, MAX_BATCH, OP_BATCH_ASYNC, OP_PRESENT_LATER}; /// Buffers queued at most: 16 of the library's 128 KB ones. const QUEUE: usize = 16; @@ -63,6 +67,41 @@ enum Work { Call([u64; 8], Lent), /// An OP_BATCH_ASYNC buffer, already answered. Detached { client: u32, bytes: Vec, left: Arc }, + /// An OP_PRESENT_LATER, already answered. + Present { client: u32, slots: [u64; 7] }, +} + +/// What `next_until` found. +enum Next { + Got(T), + /// `until` came first. + Due, + Gone, +} + +/// `rx`'s next message, or `Due` at `until`: `next` with a deadline. +fn next_until(rx: &Receiver, until: Instant) -> Next { + let spin = Instant::now() + SPIN; + loop { + match rx.try_recv() { + Ok(m) => return Next::Got(m), + Err(TryRecvError::Disconnected) => return Next::Gone, + Err(TryRecvError::Empty) => {} + } + let now = Instant::now(); + if now >= until { + return Next::Due; + } + if now < spin { + std::hint::spin_loop(); + continue; + } + return match rx.recv_timeout(until - now) { + Ok(m) => Next::Got(m), + Err(RecvTimeoutError::Timeout) => Next::Due, + Err(RecvTimeoutError::Disconnected) => Next::Gone, + }; + } } /// `rx`'s next message: spin for `SPIN`, then sleep. `None` once the other @@ -97,7 +136,21 @@ impl Threaded { crate::qos::latency_critical("host GL"); // Ends when the proxy goes (the service is registered again), // dropping the service -- and its contexts -- on this thread. - while let Some(w) = next(&work_rx) { + loop { + let w = match svc.next_due() { + None => match next(&work_rx) { + Some(w) => w, + None => break, + }, + Some(due) => match next_until(&work_rx, due) { + Next::Got(w) => w, + Next::Due => { + svc.show_due(); + continue; + } + Next::Gone => break, + }, + }; match w { Work::Call(args, Lent(mem)) => { // SAFETY: the CPU thread lent this for the call and @@ -111,7 +164,10 @@ impl Threaded { svc.run_detached(client, bytes); left.fetch_sub(1, Ordering::Release); } + Work::Present { client, slots } => svc.present_later(client, slots), } + // A program that never stops sending still gets its due ones. + svc.show_due(); } }) .expect("host GL: no thread for the GL service"); @@ -152,6 +208,17 @@ impl Service for Threaded { if args[0] == OP_BATCH_ASYNC { return self.queue_batch(mem, args); } + if args[0] == OP_PRESENT_LATER { + // Not held back by the client's buffers: it is queued behind them. + let slots = match read_slots::<7>(mem, args[1]) { + Ok(s) => s, + Err(f) => return Reply::NeedPage(f), + }; + return match self.work.send(Work::Present { client: args[2] as u32, slots }) { + Ok(()) => Reply::Ok(1, 0), + Err(_) => Reply::Err(EINVAL), + }; + } if self.queued.get(&(args[2] as u32)).is_some_and(|n| n.load(Ordering::Acquire) > 0) { return Reply::Err(ENOTYET); }