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