Skip to main content

retroglyph_window/
presenter.rs

1//! The [`Presenter`] trait: what a renderer crate implements to rasterize a grid and present it
2//! to a window surface.
3//!
4//! `Presenter` is an [`Output`](retroglyph_core::backend::Output) supertrait plus window-surface
5//! operations, with no input methods: the event loop owns input, and
6//! [`WindowBackend`](crate::WindowBackend) forwards translated events into its own queue instead.
7//!
8//! | Presenter | `present()` | `init_surface()` |
9//! |---|---|---|
10//! | `SoftwareRenderer` (retroglyph-software) | Copies pixel buffer to softbuffer surface | Creates `softbuffer::Context` + `Surface` |
11//! | `GlRenderer` (retroglyph-gl) | Instanced draw + swaps buffers | Creates a GL context (glutin native / WebGL2 wasm) from the window |
12//! | `WgpuRenderer` (retroglyph-wgpu) | Submits a render pass + presents the swap chain | Creates a `wgpu::Surface`, `Device`, and `Queue` |
13//!
14//! See the crate-level docs (`crate` root, "DPI, scale, and the resize contract" and
15//! "Threading model" sections) for the physical-pixel/no-auto-scaling contract on
16//! [`cell_size`](Presenter::cell_size), the sub-cell-remainder behavior on
17//! [`resize_surface`](Presenter::resize_surface), and the single-threaded execution model
18//! every `Presenter` implementation runs under.
19
20use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
21use retroglyph_core::backend::Output;
22use retroglyph_core::tile::Tile;
23use std::fmt;
24use std::sync::Arc;
25
26use crate::geometry::CellGeometry;
27
28/// A window/display handle pair, as one trait.
29///
30/// Presenters receive [`raw-window-handle`](raw_window_handle) types, not a concrete
31/// `winit::window::Window`: softbuffer, wgpu, and glutin all accept these handles directly, so
32/// any windowing library that produces them can drive the same presenter, and only this crate
33/// depends on winit itself.
34///
35/// `raw-window-handle` has no combined trait, and surface libraries need to *own* the handle
36/// (softbuffer stores it for the surface's lifetime), so presenters receive `Arc<dyn
37/// WindowHandle>`: rwh implements the handle traits for `Arc<H: ?Sized>`, so the trait object
38/// passes straight into `softbuffer::Surface::new` / `wgpu::Instance::create_surface`.
39///
40/// `Send + Sync` is part of the trait rather than left to each implementation because a trait
41/// object erases auto traits its trait doesn't name, and `wgpu::Instance::create_surface` requires
42/// them: its safe entry point takes ownership of a `Send + Sync` handle, and the alternative that
43/// doesn't is `unsafe`. Declaring them here is what makes `Arc<dyn WindowHandle>` usable with it.
44/// Every windowing library that produces `raw-window-handle` types satisfies this already
45/// (`winit::window::Window` does on every platform).
46///
47/// # Examples
48///
49/// Blanket-implemented for any type implementing both `raw-window-handle` traits; there is
50/// nothing to implement directly on `WindowHandle` itself.
51///
52/// ```
53/// use raw_window_handle::{
54///     DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle,
55///     WindowHandle as RawWindowHandle,
56/// };
57/// use retroglyph_window::WindowHandle;
58///
59/// struct NoWindow;
60///
61/// impl HasWindowHandle for NoWindow {
62///     fn window_handle(&self) -> Result<RawWindowHandle<'_>, HandleError> {
63///         Err(HandleError::NotSupported)
64///     }
65/// }
66///
67/// impl HasDisplayHandle for NoWindow {
68///     fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
69///         Err(HandleError::NotSupported)
70///     }
71/// }
72///
73/// fn assert_is_window_handle<T: WindowHandle>(_handle: &T) {}
74/// assert_is_window_handle(&NoWindow);
75/// ```
76pub trait WindowHandle: HasWindowHandle + HasDisplayHandle + Send + Sync {}
77
78impl<T: HasWindowHandle + HasDisplayHandle + Send + Sync + ?Sized> WindowHandle for T {}
79
80/// A surface-lifecycle error that can optionally signal whether it's worth retrying.
81///
82/// [`Presenter::SurfaceError`] is a per-implementation associated type: softbuffer's error enum
83/// has no `Lost`/`Outdated`/`Timeout` discrimination the way `wgpu::SurfaceError` does, so today's
84/// only backend (`SoftwareRenderer`) has no structured way to say "this specific failure is
85/// fatal, don't bother retrying." [`is_recoverable`](Self::is_recoverable) is that hook: a
86/// presenter with real error categories can override it to return `false` for a truly fatal
87/// failure, while every presenter that doesn't need the distinction (including every backend that
88/// exists in this crate today) can implement this trait with an empty body and inherit the
89/// default `true`.
90///
91/// Not blanket-implemented for every `Debug + Display` type: that would make it
92/// impossible for any concrete error type to override [`is_recoverable`](Self::is_recoverable) at
93/// all (a specific `impl` would conflict with the blanket one), defeating the point of the trait.
94/// Instead, each `SurfaceError` type needs one explicit (and usually empty) `impl
95/// RecoverableError for ...` block: see `retroglyph_software`'s `SurfaceError` for the minimal
96/// case that just inherits the default.
97///
98/// # Examples
99///
100/// ```
101/// use core::fmt;
102/// use retroglyph_window::RecoverableError;
103///
104/// #[derive(Debug)]
105/// enum MySurfaceError {
106///     Init,
107///     Lost,
108/// }
109///
110/// impl fmt::Display for MySurfaceError {
111///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112///         match self {
113///             Self::Init => write!(f, "surface init failed"),
114///             Self::Lost => write!(f, "surface lost"),
115///         }
116///     }
117/// }
118///
119/// impl RecoverableError for MySurfaceError {
120///     fn is_recoverable(&self) -> bool {
121///         // Init failures are fatal; a lost surface may come back.
122///         matches!(self, Self::Lost)
123///     }
124/// }
125///
126/// assert!(!MySurfaceError::Init.is_recoverable());
127/// assert!(MySurfaceError::Lost.is_recoverable());
128/// ```
129pub trait RecoverableError: core::fmt::Debug + core::fmt::Display {
130    /// Whether this error represents a transient failure worth retrying, as opposed to a fatal
131    /// one.
132    ///
133    /// Defaults to `true`: absent any structured error categorization, every failure is treated
134    /// as potentially transient, matching the generic consecutive-failure recovery heuristic
135    /// `winit::run::present_failure_action` already applies. Override to return `false` only for
136    /// an error variant known to be unrecoverable regardless of retries (e.g. a `wgpu::SurfaceError
137    /// ::Lost` variant that persists until the surface is fully rebuilt from a different code
138    /// path than a simple retry).
139    #[must_use]
140    fn is_recoverable(&self) -> bool {
141        true
142    }
143}
144
145// `Infallible` is uninhabited: no value of it can ever exist, so `is_recoverable` can never
146// actually be called on one, but a presenter that can't fail (e.g. a test mock) still needs
147// `type SurfaceError = core::convert::Infallible` to satisfy the `RecoverableError` bound, so
148// this impl exists purely for that convenience.
149impl RecoverableError for core::convert::Infallible {}
150
151/// A ready-made, string-backed [`SurfaceError`](Presenter::SurfaceError) for presenters whose
152/// underlying surface library reports failures as opaque strings rather than a structured error
153/// enum.
154///
155/// Several presenter backends (e.g. `retroglyph-gl`'s native/wasm split, or a future softbuffer
156/// backend) need only two buckets ("surface/context creation failed" (fatal) and "presenting a
157/// frame failed" (potentially recoverable)) and would otherwise each hand-roll the same `enum {
158/// Init(String), Present(String) }` plus [`RecoverableError`] impl. This type is that common
159/// shape, provided once here so backends can reuse it directly instead of duplicating it.
160#[derive(Debug)]
161#[non_exhaustive]
162pub enum GenericSurfaceError {
163    /// Creating the surface or its underlying context failed. Treated as fatal (not
164    /// recoverable): a presenter cannot proceed without a surface, and retrying the same
165    /// creation path is very unlikely to succeed.
166    Init(String),
167    /// Presenting a frame failed. Treated as potentially recoverable so the event loop's
168    /// consecutive-failure heuristic can retry before giving up.
169    Present(String),
170}
171
172impl fmt::Display for GenericSurfaceError {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        match self {
175            Self::Init(msg) => write!(f, "surface init: {msg}"),
176            Self::Present(msg) => write!(f, "surface present: {msg}"),
177        }
178    }
179}
180
181impl std::error::Error for GenericSurfaceError {}
182
183impl RecoverableError for GenericSurfaceError {
184    fn is_recoverable(&self) -> bool {
185        // Init failures are fatal (nothing to retry into); present failures may be transient.
186        matches!(self, Self::Present(_))
187    }
188}
189
190/// A renderer that rasterizes grid content and presents it to a window surface.
191///
192/// A supertrait of [`Output`], adding the surface lifecycle (`init_surface`, `resize_surface`,
193/// `present`, `cell_size`) that the event loop drives. Every `Presenter` implementation is an
194/// `Output` implementation for free: [`WindowBackend`](crate::WindowBackend) delegates its own
195/// `Output` impl straight through to `P: Presenter`, with no duplicated method bodies.
196///
197/// # Sub-cell offsets and spill
198///
199/// A [`Tile`]'s `dx`/`dy` shift its glyph within, and past, its cell.
200/// This is a cross-backend rendering contract: the CPU rasterizer (`retroglyph-software`) and the
201/// GPU ones (`retroglyph-gl`, `retroglyph-wgpu`) must produce the same pixels, so it is specified
202/// here once instead of in mirrored per-backend comments that reference each other (and drift when
203/// only one is touched). A `Presenter` that honors sub-cell offsets must obey all four points:
204///
205/// - `dx`/`dy` are in **unscaled font pixels** (a presenter multiplies by its own integer scale);
206///   negative `dx` shifts the glyph left, negative `dy` up.
207/// - The cell's **background fill is always the full, unshifted cell** rectangle. An offset moves
208///   only the glyph, never the background.
209/// - An offset glyph **may spill past its cell edge into neighboring cells**, and that spill is
210///   **uniform in all four directions**: a glyph pushed right/down onto a later neighbor spills
211///   the same way as one pushed left/up onto an earlier neighbor.
212/// - The mechanism that guarantees that uniformity is a **two-pass draw**: lay down *every* cell's
213///   background first, then draw *every* cell's (offset) glyph over the result. Interleaving the
214///   two per cell would let a later cell's background overwrite an earlier neighbor's spilled
215///   glyph, breaking spill in the right/down directions only.
216///
217/// The offset *application* is not shared code: the GPU backends shift a quad's vertex position in
218/// their vertex shader, `retroglyph-software` shifts `origin_x`/`origin_y` in a CPU blit:
219/// irreducibly different mechanics that must nonetheless agree on the four points above.
220///
221/// # Examples
222///
223/// ```
224/// use retroglyph_core::backend::{DrawCell, Output};
225/// use retroglyph_core::grid::Size;
226/// use retroglyph_window::{Presenter, WindowHandle};
227/// use std::sync::Arc;
228///
229/// struct NullPresenter;
230///
231/// impl Output for NullPresenter {
232///     type Error = core::convert::Infallible;
233///
234///     fn draw_layers<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
235///     where
236///         I: Iterator<Item = DrawCell<'a>>,
237///     {
238///         Ok(())
239///     }
240///
241///     fn flush(&mut self) -> Result<(), Self::Error> {
242///         Ok(())
243///     }
244///
245///     fn size(&self) -> Size {
246///         Size::new(4, 2)
247///     }
248///
249///     fn clear(&mut self) -> Result<(), Self::Error> {
250///         Ok(())
251///     }
252/// }
253///
254/// impl Presenter for NullPresenter {
255///     type SurfaceError = core::convert::Infallible;
256///
257///     fn init_surface(&mut self, _window: Arc<dyn WindowHandle>) -> Result<(), Self::SurfaceError> {
258///         Ok(())
259///     }
260///
261///     fn resize_surface(&mut self, _width: u32, _height: u32) {}
262///
263///     fn present(&mut self) -> Result<(), Self::SurfaceError> {
264///         Ok(())
265///     }
266///
267///     fn cell_size(&self) -> (u32, u32) {
268///         (8, 16)
269///     }
270/// }
271/// ```
272pub trait Presenter: Output {
273    /// Surface lifecycle error (context creation, buffer acquisition, present).
274    type SurfaceError: RecoverableError;
275
276    /// Initialize the window surface.
277    ///
278    /// Called once from the loop's `resumed` handler. The presenter creates its platform surface
279    /// (softbuffer surface, wgpu device+surface, GL context) from the raw window/display handles.
280    ///
281    /// # Errors
282    ///
283    /// Returns [`Self::SurfaceError`] if surface or context creation from `window` fails (an
284    /// unsupported display/window system connection, a missing graphics API, or, on wasm32,
285    /// a canvas element that can't be located). Unlike a failed [`present`](Self::present),
286    /// the `winit` driver treats this as fatal rather than retryable: it logs the error and
287    /// exits the event loop immediately, since there is no surface to draw into and no later
288    /// hook that calls `init_surface` again.
289    fn init_surface(&mut self, window: Arc<dyn WindowHandle>) -> Result<(), Self::SurfaceError>;
290
291    /// Resize the window surface to a new physical pixel size.
292    ///
293    /// Called on every window resize event with `width`/`height` already resolved by the
294    /// caller: for the `winit` driver (see `winit::run::WindowApp::resize_to`), that means
295    /// `cols * cell_w` x `rows * cell_h`, where `cols`/`rows` are the window's physical size
296    /// divided down to whole cells. Any sub-cell remainder is truncated, not centered or
297    /// cleared: when the window's physical size isn't an exact multiple of the cell size,
298    /// `width`/`height` here are the largest whole-cell-multiple that fits, which can be
299    /// smaller than the window's actual physical size. The OS window itself is never resized
300    /// to compensate, so a non-exact-multiple resize leaves a thin strip at the window's
301    /// trailing edge outside the surface: retroglyph does not paint or clear that strip;
302    /// whatever the OS/windowing backend leaves there remains visible until a subsequent
303    /// resize covers it.
304    fn resize_surface(&mut self, width: u32, height: u32);
305
306    /// Notify the presenter that the window's scale factor (DPI) changed.
307    ///
308    /// Called when the window moves to a display with a different pixel density, or the
309    /// system DPI setting changes. The event loop follows this with
310    /// [`resize_surface`](Self::resize_surface) for the window's new physical size, so
311    /// this hook only needs to handle DPI-dependent state that isn't a plain buffer
312    /// resize (e.g. regenerating a font atlas rasterized for a particular scale).
313    ///
314    /// Defaults to a no-op: presenters whose rasterization doesn't depend on DPI (like
315    /// `SoftwareRenderer`'s integer `scale` config, set once at construction) need no
316    /// action here.
317    fn scale_factor_changed(&mut self, _scale_factor: f64) {}
318
319    /// Present the rasterized frame to the window surface.
320    ///
321    /// Called after each app tick. A lost frame is not fatal; the caller logs the error and
322    /// continues.
323    ///
324    /// # Errors
325    ///
326    /// Returns [`Self::SurfaceError`] if the surface buffer can't be acquired or presented (e.g.
327    /// context lost on wasm, page flip pending on DRI/KMS).
328    fn present(&mut self) -> Result<(), Self::SurfaceError>;
329
330    /// Cell size in physical pixels `(width, height)`.
331    ///
332    /// Physical pixels, not logical/DPI-scaled pixels, and never auto-scaled by this crate for
333    /// display DPI: see the crate-level "DPI, scale, and the resize contract" docs. A presenter
334    /// whose cells should grow on a `HiDPI` display must change what this returns itself (from
335    /// [`resize`](Output::resize) or [`scale_factor_changed`](Self::scale_factor_changed)); absent
336    /// that, it stays constant for the presenter's lifetime.
337    ///
338    /// `(u32, u32)` rather than [`Size`](retroglyph_core::grid::Size) because grid coordinates
339    /// are `u16` but pixel arithmetic uses `u32` (winit `PhysicalSize`).
340    #[must_use]
341    fn cell_size(&self) -> (u32, u32);
342
343    /// This presenter's cell geometry, as a [`CellGeometry`] rather than the raw
344    /// `(width, height)` pair [`cell_size`](Self::cell_size) returns.
345    ///
346    /// Lets callers (e.g. `winit::run`'s cursor/mouse handlers) use
347    /// [`CellGeometry::pixel_to_cell`] directly instead of pairing a raw `cell_size()` with the
348    /// [`translate_pixel_to_cell`](crate::winit::translate::translate_pixel_to_cell) free
349    /// function. The default implementation derives a geometry from [`cell_size`](Self::cell_size)
350    /// at `scale` 1, clamping each dimension to `u8::MAX`: exact for presenters whose cell size
351    /// fits in a `u8` (true of every glyph/tile size in practice), lossy only past that, which
352    /// only affects [`pixel_to_cell`](CellGeometry::pixel_to_cell) precision for callers that
353    /// don't override this method (test doubles, not `retroglyph-software`/`retroglyph-gl`, both
354    /// of which override it to return their real internal geometry).
355    #[must_use]
356    fn geometry(&self) -> CellGeometry {
357        let (cell_w, cell_h) = self.cell_size();
358        #[allow(clippy::cast_possible_truncation)]
359        CellGeometry::new(cell_w.min(255) as u8, cell_h.min(255) as u8, 1)
360    }
361}
362
363/// The glyph a `Presenter` should paint art (a bitmap-font glyph or a tileset sprite) for, or
364/// `None` when this cell draws none.
365///
366/// Both pixel backends (`retroglyph-software`, `retroglyph-gl`) ask this same question at
367/// several points in their draw path (sprite-vs-font dispatch, font fallback, whether a cell
368/// counts as "occupied" for compositing), and used to each answer it independently, which let
369/// them drift (retroglyph#762). This is the one place that decides it:
370///
371/// - A [`TileFlags::SPAN_COVERED`](retroglyph_core::tile::TileFlags::SPAN_COVERED) cell (see
372///   [`Tile::span_offset`]) draws no art of its own: the span's anchor already drew one piece of
373///   artwork across the whole footprint, and this cell's glyph is only that artwork's text
374///   fallback for backends that can't draw it.
375/// - An [`is_empty`](Tile::is_empty) tile draws no art: nothing has been written to it, so it is
376///   transparent when compositing layers. This is the canonical blank rule, matching
377///   [`Grid::flatten_into`](retroglyph_core::grid::Grid::flatten_into) and the cell backends;
378///   comparing the glyph itself against `' '` is both slower (it can't be decided without the
379///   glyph) and wrong for a font whose space glyph isn't blank.
380///
381/// Neither check depends on whether a sprite exists for the glyph: that dispatch (sprite vs.
382/// bitmap font) is a separate, backend-specific decision made *after* this one, once a caller
383/// knows a cell draws art at all.
384#[must_use]
385pub const fn cell_art_glyph(tile: &Tile) -> Option<char> {
386    if tile.span_offset().is_some() || tile.is_empty() {
387        None
388    } else {
389        Some(tile.glyph())
390    }
391}
392
393#[cfg(test)]
394mod cell_art_glyph_tests {
395    use super::cell_art_glyph;
396    use retroglyph_core::color::Style;
397    use retroglyph_core::grid::Grid;
398    use retroglyph_core::tile::Tile;
399
400    #[test]
401    fn none_for_an_empty_tile() {
402        assert_eq!(cell_art_glyph(&Tile::default()), None);
403    }
404
405    #[test]
406    fn some_for_an_occupied_tile() {
407        let tile = Tile::new('@', Style::new());
408        assert_eq!(cell_art_glyph(&tile), Some('@'));
409    }
410
411    #[test]
412    fn none_for_a_span_covered_tile() {
413        let mut grid = Grid::new(2, 1);
414        grid.write_span(0, 0, 0, &["AB"], Style::new()).unwrap();
415        let covered = *grid.tile(0, (1, 0)).unwrap();
416        assert!(covered.span_offset().is_some());
417        assert_eq!(cell_art_glyph(&covered), None);
418    }
419
420    #[test]
421    fn some_for_a_non_blank_space_glyph() {
422        // A space glyph is not inherently blank: an explicit `Tile::new(' ', ...)` is occupied
423        // (not `is_empty()`), and a font may draw something for it (`BitmapFont::new` allows a
424        // non-blank space). The blank rule is `is_empty()`, not `glyph == ' '`.
425        let tile = Tile::new(' ', Style::new());
426        assert_eq!(cell_art_glyph(&tile), Some(' '));
427    }
428}
429
430#[cfg(test)]
431mod generic_surface_error_tests {
432    use super::{GenericSurfaceError, RecoverableError};
433
434    #[test]
435    fn init_is_not_recoverable() {
436        let err = GenericSurfaceError::Init("boom".to_string());
437        assert!(!err.is_recoverable());
438    }
439
440    #[test]
441    fn present_is_recoverable() {
442        let err = GenericSurfaceError::Present("boom".to_string());
443        assert!(err.is_recoverable());
444    }
445
446    #[test]
447    fn display_includes_message() {
448        let init = GenericSurfaceError::Init("init failed".to_string());
449        assert!(init.to_string().contains("init failed"));
450
451        let present = GenericSurfaceError::Present("present failed".to_string());
452        assert!(present.to_string().contains("present failed"));
453    }
454}