Skip to main content

retroglyph_software/
surface_native.rs

1//! Native window surface: a softbuffer context + surface.
2//!
3//! Selected by `lib.rs` on every non-wasm32 target. The wasm32 counterpart in
4//! `surface_wasm.rs` exposes the same `WindowSurface::new`/`resize`/`present`
5//! API and its own `SurfaceError`, so the renderer drives either without
6//! `cfg` in its body.
7
8// `WindowSurface` is crate-internal, so `pub(crate)` is the correct visibility
9// (`unreachable_pub` agrees). The nursery `redundant_pub_crate` lint disagrees
10// only because this module is not itself `pub`; the two lints conflict for the
11// module-per-platform pattern, and `pub(crate)` is the honest choice.
12#![allow(clippy::redundant_pub_crate)]
13
14use retroglyph_window::WindowHandle;
15use std::num::NonZeroU32;
16use std::sync::Arc;
17
18/// Softbuffer-backed window surface.
19///
20/// Holds both the `Context` and `Surface`. The `_context` must outlive
21/// `surface` (softbuffer requires it), but is only stored, not read. The
22/// handle type is `Arc<dyn WindowHandle>` (raw-window-handle), not a winit
23/// type: this crate rasterizes and presents, and any windowing library that
24/// yields raw handles can drive it (see `retroglyph_window::Presenter`).
25pub(crate) struct WindowSurface {
26    _context: softbuffer::Context<Arc<dyn WindowHandle>>,
27    surface: softbuffer::Surface<Arc<dyn WindowHandle>, Arc<dyn WindowHandle>>,
28    /// Surface width in pixels, tracked so [`present`](Self::present) can build
29    /// a damage `Rect` (softbuffer's `Buffer` doesn't expose its width).
30    width: u32,
31}
32
33/// Errors creating or presenting the native window surface.
34#[derive(Debug)]
35#[non_exhaustive]
36pub enum SurfaceError {
37    /// Failed to create the softbuffer context from the window.
38    Context(softbuffer::SoftBufferError),
39    /// Failed to create or present the softbuffer surface.
40    Surface(softbuffer::SoftBufferError),
41}
42
43impl core::fmt::Display for SurfaceError {
44    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
45        match self {
46            Self::Context(_) => write!(f, "softbuffer context creation failed"),
47            Self::Surface(_) => write!(f, "softbuffer surface creation or presentation failed"),
48        }
49    }
50}
51
52// Inherits the default `is_recoverable() -> true`: softbuffer's error enum has no
53// `Lost`/`Outdated`/`Timeout` discrimination the way `wgpu::SurfaceError` does, so every present
54// failure here is treated as potentially transient, matching this crate's existing (pre-trait)
55// behavior.
56impl retroglyph_window::RecoverableError for SurfaceError {}
57
58impl std::error::Error for SurfaceError {
59    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
60        match self {
61            Self::Context(e) | Self::Surface(e) => Some(e),
62        }
63    }
64}
65
66impl WindowSurface {
67    /// Creates a softbuffer context and surface from a raw window handle.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`SurfaceError`] if the softbuffer context or surface cannot be
72    /// created.
73    pub(crate) fn new(window: Arc<dyn WindowHandle>) -> Result<Self, SurfaceError> {
74        let context = softbuffer::Context::new(window.clone()).map_err(SurfaceError::Context)?;
75        let surface = softbuffer::Surface::new(&context, window).map_err(SurfaceError::Surface)?;
76        Ok(Self {
77            _context: context,
78            surface,
79            width: 0,
80        })
81    }
82
83    /// Resizes the surface to `width` x `height` pixels.
84    pub(crate) fn resize(&mut self, width: u32, height: u32) {
85        if let (Some(w), Some(h)) = (NonZeroU32::new(width), NonZeroU32::new(height)) {
86            let _ = self.surface.resize(w, h);
87            self.width = width;
88        }
89    }
90
91    /// Copies `pixels` (`0x00RRGGBB`) into the surface buffer and presents only
92    /// the changed row band `[y0, y1)` via `present_with_damage`, so softbuffer
93    /// blits just those rows to the window.
94    ///
95    /// Falls back to a full present if the band or width is degenerate, or if
96    /// the buffer size does not match (a resize is mid-flight).
97    ///
98    /// # Errors
99    ///
100    /// Returns [`SurfaceError::Surface`] if the softbuffer buffer cannot be
101    /// acquired or presented.
102    pub(crate) fn present(
103        &mut self,
104        pixels: &[u32],
105        damage: (u32, u32),
106    ) -> Result<(), SurfaceError> {
107        let mut buffer = self.surface.buffer_mut().map_err(SurfaceError::Surface)?;
108        if needs_full_present_fallback(pixels.len(), buffer.len()) {
109            buffer.fill(0);
110            return buffer.present().map_err(SurfaceError::Surface);
111        }
112        buffer.copy_from_slice(pixels);
113        match damage_rect(self.width, damage) {
114            Some(rect) => buffer
115                .present_with_damage(&[rect])
116                .map_err(SurfaceError::Surface),
117            None => buffer.present().map_err(SurfaceError::Surface),
118        }
119    }
120}
121
122/// Decides whether [`WindowSurface::present`] must fall back to a full-buffer
123/// present because `pixels` doesn't match the softbuffer buffer's current
124/// length (a resize is mid-flight).
125const fn needs_full_present_fallback(pixels_len: usize, buffer_len: usize) -> bool {
126    pixels_len != buffer_len
127}
128
129/// Converts a damage row band `[y0, y1)` and the surface `width` into a
130/// softbuffer damage [`Rect`](softbuffer::Rect) spanning the full row width,
131/// or `None` if the band or width is degenerate, in which case
132/// [`WindowSurface::present`] falls back to a full present instead.
133const fn damage_rect(width: u32, damage: (u32, u32)) -> Option<softbuffer::Rect> {
134    let (y0, y1) = damage;
135    match (
136        NonZeroU32::new(width),
137        NonZeroU32::new(y1.saturating_sub(y0)),
138    ) {
139        (Some(width), Some(height)) => Some(softbuffer::Rect {
140            x: 0,
141            y: y0,
142            width,
143            height,
144        }),
145        _ => None,
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use super::{NonZeroU32, damage_rect, needs_full_present_fallback};
152
153    #[test]
154    fn full_present_fallback_on_length_mismatch() {
155        assert!(needs_full_present_fallback(3, 4));
156        assert!(needs_full_present_fallback(0, 4));
157    }
158
159    #[test]
160    fn no_fallback_when_lengths_match() {
161        assert!(!needs_full_present_fallback(4, 4));
162        assert!(!needs_full_present_fallback(0, 0));
163    }
164
165    #[test]
166    fn damage_rect_converts_band_to_full_width_rect() {
167        let rect = damage_rect(80, (4, 10)).expect("non-degenerate band");
168        assert_eq!(rect.x, 0);
169        assert_eq!(rect.y, 4);
170        assert_eq!(rect.width, NonZeroU32::new(80).unwrap());
171        assert_eq!(rect.height, NonZeroU32::new(6).unwrap());
172    }
173
174    #[test]
175    fn damage_rect_none_when_width_is_zero() {
176        assert!(damage_rect(0, (0, 10)).is_none());
177    }
178
179    #[test]
180    fn damage_rect_none_when_band_is_empty() {
181        assert!(damage_rect(80, (5, 5)).is_none());
182    }
183
184    #[test]
185    fn damage_rect_none_when_band_is_inverted() {
186        // y1 < y0: saturating_sub yields 0, treated the same as an empty band.
187        assert!(damage_rect(80, (10, 5)).is_none());
188    }
189}