Skip to main content

orinium_browser/platform/renderer/
draw_sink.rs

1//! The seam between draw-command production and draw-command execution.
2//!
3//! The browser layer produces a `Vec<DrawCommand>` for a window and nothing
4//! more; executing those commands is a platform concern. Handing the concrete
5//! GPU renderer down through the call stack instead would make every layer
6//! above know that rendering happens on `wgpu`, on a surface bound to an OS
7//! window — which is exactly the assumption that breaks if the surface ever
8//! has to live in another process.
9//!
10//! [`DrawSink`] is the contract. The shell owns the implementation; today that
11//! is [`GpuRenderer`](super::gpu::GpuRenderer), but a recording sink, an
12//! offscreen one, or a remote one are equally valid.
13
14use std::fmt;
15
16use crate::engine::renderer_model::DrawCommand;
17
18/// Failures a [`DrawSink`] can report.
19#[derive(Debug)]
20pub enum DrawError {
21    /// No drawable could be acquired (e.g. a lost or outdated surface).
22    Surface(String),
23    /// The sink refused the submitted command stream.
24    Rejected(String),
25}
26
27impl fmt::Display for DrawError {
28    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29        match self {
30            Self::Surface(s) => write!(f, "Could not acquire a drawable: {s}"),
31            Self::Rejected(s) => write!(f, "Draw commands rejected: {s}"),
32        }
33    }
34}
35
36impl std::error::Error for DrawError {}
37
38/// A destination for one window's draw commands.
39///
40/// Callers produce commands with [`DrawCommand`] and hand them over; the sink
41/// decides when and how they reach a display.
42///
43/// [`upload`](Self::upload) only records, and [`present`](Self::present)
44/// submits, so a caller may feed several command sets before showing
45/// anything — or record without ever presenting, which is what the
46/// offscreen sinks do.
47pub trait DrawSink {
48    /// Records and uploads `commands` for the next frame.
49    fn upload(&mut self, commands: &[DrawCommand]);
50
51    /// Draws and presents whatever [`upload`](Self::upload) recorded.
52    fn present(&mut self) -> Result<(), DrawError>;
53
54    /// Notifies the sink that its drawable changed size, in physical pixels.
55    fn resize(&mut self, size: (u32, u32));
56
57    /// Notifies the sink that the display scale factor changed.
58    fn set_scale_factor(&mut self, scale_factor: f64);
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    /// A sink that only records, standing in for a real GPU. Its existence is
66    /// the point of the port: the browser layer can be driven end to end
67    /// without a window, a device, or `wgpu`.
68    #[derive(Default)]
69    struct RecordingSink {
70        frames: Vec<Vec<DrawCommand>>,
71        pending: Vec<DrawCommand>,
72        size: (u32, u32),
73        scale_factor: f64,
74        present_err: Option<DrawError>,
75    }
76
77    impl DrawSink for RecordingSink {
78        fn upload(&mut self, commands: &[DrawCommand]) {
79            self.pending.extend_from_slice(commands);
80        }
81
82        fn present(&mut self) -> Result<(), DrawError> {
83            match self.present_err.take() {
84                Some(err) => Err(err),
85                None => {
86                    self.frames.push(std::mem::take(&mut self.pending));
87                    Ok(())
88                }
89            }
90        }
91
92        fn resize(&mut self, size: (u32, u32)) {
93            self.size = size;
94        }
95
96        fn set_scale_factor(&mut self, scale_factor: f64) {
97            self.scale_factor = scale_factor;
98        }
99    }
100
101    fn command(x: f32) -> DrawCommand {
102        DrawCommand::PushTransform {
103            transform: crate::engine::renderer_model::AffineTransform::translate(x, 0.0),
104        }
105    }
106
107    #[test]
108    fn upload_records_and_present_flushes() {
109        let mut sink = RecordingSink::default();
110
111        sink.upload(&[command(1.0)]);
112        assert!(sink.frames.is_empty(), "upload must not present");
113
114        sink.present().unwrap();
115        assert_eq!(sink.frames.len(), 1);
116        assert_eq!(sink.frames[0].len(), 1);
117    }
118
119    #[test]
120    fn several_uploads_accumulate_into_one_frame() {
121        let mut sink = RecordingSink::default();
122
123        sink.upload(&[command(1.0), command(2.0)]);
124        sink.upload(&[command(3.0)]);
125        sink.present().unwrap();
126
127        assert_eq!(sink.frames.len(), 1);
128        assert_eq!(sink.frames[0].len(), 3);
129    }
130
131    #[test]
132    fn present_starts_a_fresh_frame() {
133        let mut sink = RecordingSink::default();
134
135        sink.upload(&[command(1.0)]);
136        sink.present().unwrap();
137        sink.upload(&[command(2.0)]);
138        sink.present().unwrap();
139
140        assert_eq!(sink.frames.len(), 2);
141        assert_eq!(sink.frames[1].len(), 1);
142    }
143
144    #[test]
145    fn present_errors_surface_to_the_caller() {
146        let mut sink = RecordingSink {
147            present_err: Some(DrawError::Surface("lost".to_string())),
148            ..Default::default()
149        };
150
151        let err = sink.present().unwrap_err();
152        assert!(matches!(err, DrawError::Surface(_)));
153        assert!(err.to_string().contains("lost"));
154    }
155
156    #[test]
157    fn surface_changes_are_forwarded() {
158        let mut sink = RecordingSink::default();
159        sink.resize((1024, 768));
160        sink.set_scale_factor(2.0);
161
162        assert_eq!(sink.size, (1024, 768));
163        assert_eq!(sink.scale_factor, 2.0);
164    }
165}