Skip to main content

rlx_core/render/
preview_readback.rs

1//! A non-blocking readback of the preview intermediate, consumed one frame late.
2//!
3//! The operator console already draws the show into an intermediate and copies
4//! it out (ADR-0143). A studio wants those same pixels on the CPU — and the one
5//! thing it must not cost is a **stall in the display loop**, which answers to a
6//! present deadline the headless tap does not have.
7//!
8//! ## One frame in flight, and nothing waits
9//!
10//! Each cycle is three steps across two frames:
11//!
12//! 1. **Record** — frame *N*'s encoder gets a scaling blit from the intermediate
13//!    into the fixed-size tap and a `copy_texture_to_buffer` out of that tap into
14//!    this buffer, both riding the submission the frame makes anyway.
15//! 2. **Arm** — after that submission, `map_async` is asked for the buffer.
16//! 3. **Consume** — frame *N+1* polls **without waiting**
17//!    ([`wgpu::PollType::Poll`]) and takes the mapping if it has landed. If it
18//!    has not, this frame yields nothing and the buffer stays in flight; the
19//!    display loop does not notice.
20//!
21//! So a consumer sees frame *N* while frame *N+1* is being drawn. That is the
22//! whole latency, and it is the price of never blocking.
23//!
24//! **The buffer cannot be re-recorded while it is mapped**, which is why the
25//! consume step precedes the record step within a frame and why a map that has
26//! not landed skips the record: a second copy into a mapped buffer is a
27//! validation error, not a dropped frame.
28//!
29//! ## Why not the frame tap's readback
30//!
31//! `capture.rs`'s `read_back` waits indefinitely, deliberately: a headless loop
32//! outruns the GPU and the wait is what paces it. That is the correct policy
33//! there and the wrong one here, and the two are not merged for exactly that
34//! reason — `core/tests/suite/console_preview.rs` holds `render/` to no indefinite
35//! wait outside the two capture files.
36
37// Hot-path panic-denial pragma (Plan 0002 Phase 2; render/ is scanned by the
38// hygiene guard).
39#![deny(
40    clippy::unwrap_used,
41    clippy::expect_used,
42    clippy::indexing_slicing,
43    clippy::panic,
44    clippy::unreachable
45)]
46
47// A continuation of one module split across several files, so it needs the
48// names `render/mod.rs` has in scope.
49use super::*;
50
51use std::sync::mpsc::{Receiver, TryRecvError};
52
53/// The fixed-size tap, the staging buffer, and the state of the map in flight.
54pub(super) struct PreviewReadback {
55    /// The tap the buffer copies out of. Owned here rather than beside the
56    /// intermediate because its whole purpose is to give this readback one
57    /// geometry for the life of the run (ADR-0187).
58    pub(super) tap: preview::PreviewTap,
59    buffer: wgpu::Buffer,
60    width: u32,
61    height: u32,
62    /// `width * 4` rounded up to wgpu's 256-byte row alignment. The mapped
63    /// range carries this stride and the frame handed out does not.
64    padded_bpr: u32,
65    /// The armed map's result channel, `None` when the buffer is free to record
66    /// into.
67    armed: Option<Receiver<Result<(), wgpu::BufferAsyncError>>>,
68}
69
70impl PreviewReadback {
71    /// Build a readback that yields `width`x`height` frames at `format`.
72    ///
73    /// Neither follows the intermediate: the tap is built here and stays, so a
74    /// renderer resize changes what the blit reads and nothing about what this
75    /// hands out.
76    pub(super) fn new(
77        device: &wgpu::Device,
78        format: wgpu::TextureFormat,
79        width: u32,
80        height: u32,
81    ) -> Self {
82        let tap = preview::PreviewTap::new(device, format, width, height);
83        let (width, height) = tap.size();
84        let (buffer, padded_bpr) = capture::create_readback(device, width, height);
85        Self {
86            tap,
87            buffer,
88            width,
89            height,
90            padded_bpr,
91            armed: None,
92        }
93    }
94
95    /// The size of the frames this yields.
96    pub(super) fn size(&self) -> (u32, u32) {
97        (self.width, self.height)
98    }
99
100    /// Take the frame the **previous** submission's map produced, if it has
101    /// landed.
102    ///
103    /// Polls without waiting. A map still in flight yields `None` and leaves the
104    /// buffer armed; the next frame asks again. Call this before
105    /// [`record`](Self::record) within a frame — the buffer cannot be copied
106    /// into while it is mapped.
107    pub(super) fn consume(&mut self, device: &wgpu::Device) -> Option<CaptureImage> {
108        let armed = self.armed.as_ref()?;
109        // The one poll on this path, and it is the non-blocking kind. A `Wait`
110        // here is the defect the whole module exists to avoid.
111        let _ = device.poll(wgpu::PollType::Poll);
112        match armed.try_recv() {
113            Ok(Ok(())) => {}
114            // Still in flight. Not an error and not a dropped frame — the buffer
115            // stays armed and the next frame asks again.
116            Err(TryRecvError::Empty) => return None,
117            // Mapping failed, or the callback was dropped without firing. Either
118            // way this cycle is over: disarm so the next frame records afresh.
119            Ok(Err(_)) | Err(TryRecvError::Disconnected) => {
120                self.armed = None;
121                return None;
122            }
123        }
124        self.armed = None;
125        let slice = self.buffer.slice(..);
126        let image = slice.get_mapped_range().ok().map(|mapped| CaptureImage {
127            width: self.width,
128            height: self.height,
129            rgba: capture::unpad_rows(&mapped, self.width, self.height, self.padded_bpr),
130        });
131        // Unmapped whether or not the range was readable: a buffer left mapped
132        // is one this readback can never record into again.
133        self.buffer.unmap();
134        image
135    }
136
137    /// Fill the tap from `preview` and record the copy out of it, if the buffer
138    /// is free.
139    ///
140    /// Two recorded steps rather than one: the blit scales and letterboxes the
141    /// show into the tap's fixed shape, and the copy that follows reads a
142    /// texture whose extent has not moved since this readback was opened.
143    ///
144    /// Returns whether it recorded, which the caller needs: [`arm`](Self::arm)
145    /// must be called after the submission if and only if this did.
146    pub(super) fn record(
147        &mut self,
148        device: &wgpu::Device,
149        encoder: &mut wgpu::CommandEncoder,
150        preview: &preview::PreviewTarget,
151    ) -> bool {
152        if self.armed.is_some() || !self.tap.record_fill_from(device, encoder, preview) {
153            return false;
154        }
155        capture::record_copy(
156            encoder,
157            self.tap.texture(),
158            &self.buffer,
159            self.padded_bpr,
160            self.width,
161            self.height,
162        );
163        true
164    }
165
166    /// Ask for the mapping, after the submission carrying the recorded copy.
167    ///
168    /// The callback only sends; every decision is taken on the display thread
169    /// when it next polls, so nothing wgpu calls back into does work.
170    pub(super) fn arm(&mut self) {
171        let (tx, rx) = std::sync::mpsc::channel();
172        self.buffer
173            .slice(..)
174            .map_async(wgpu::MapMode::Read, move |res| {
175                let _ = tx.send(res);
176            });
177        self.armed = Some(rx);
178    }
179}
180
181/// The renderer-facing half: opening and closing the readback, and taking what
182/// it produced.
183///
184/// Every one of these delegates to the `PreviewService` in `render::preview`
185/// the renderer holds — the readback and the intermediate it copies out of are
186/// one concern with one owner, and a `Renderer` accessor that reached past that
187/// owner could open half of it.
188impl Renderer {
189    /// Open a non-blocking readback of the preview, yielding `width`x`height`
190    /// frames.
191    ///
192    /// Requires an open preview — the blit that fills the readback's tap samples
193    /// that intermediate and has nothing to read without one. The **size is the
194    /// caller's** and is answered for the life of the readback: a renderer
195    /// resize rebuilds the intermediate under the blit and moves nothing here
196    /// (ADR-0187).
197    ///
198    /// Calling it again replaces the readback, which is how a caller changes the
199    /// size it asked for.
200    ///
201    /// Refused when the frames would come out at a format no consumer can be
202    /// told the order of, so the announcement that follows can always be true
203    /// (ADR-0187).
204    pub fn open_preview_readback(&mut self, width: u32, height: u32) -> Result<(), RenderError> {
205        self.preview.open_readback(&self.ctx.device, width, height)
206    }
207
208    /// Close the readback and free its staging buffer. A closed readback yields
209    /// nothing and costs the frame one `Option` test.
210    pub fn close_preview_readback(&mut self) {
211        self.preview.close_readback();
212    }
213
214    /// The size of the frames the readback yields, or `None` when it is closed.
215    ///
216    /// **The size the caller asked for, and not the output's.** The frames are a
217    /// scaled, letterboxed copy of the intermediate rather than an exact one, so
218    /// this is a fixed property of the open readback: it answers the same pair
219    /// across every resize, and a consumer told it once never has to be told
220    /// again.
221    pub fn preview_readback_size(&self) -> Option<(u32, u32)> {
222        self.preview.readback_size()
223    }
224
225    /// Take the frame the readback produced, if one has landed.
226    ///
227    /// A frame is available at most every other call in the steady state, and
228    /// `None` means "not yet" rather than "never": the caller sends what it gets
229    /// and does not wait.
230    pub fn take_preview_frame(&mut self) -> Option<CaptureImage> {
231        self.preview.take_frame()
232    }
233}