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` (future) | Submits render pass + presents swap chain | Creates `wgpu::Surface` + `Device` |
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 std::fmt;
23use std::sync::Arc;
24
25/// A window/display handle pair, as one trait.
26///
27/// Presenters receive [`raw-window-handle`](raw_window_handle) types, not a concrete
28/// `winit::window::Window`: softbuffer, wgpu, and glutin all accept these handles directly, so
29/// any windowing library that produces them can drive the same presenter, and only this crate
30/// depends on winit itself.
31///
32/// `raw-window-handle` has no combined trait, and surface libraries need to *own* the handle
33/// (softbuffer stores it for the surface's lifetime), so presenters receive `Arc<dyn
34/// WindowHandle>`: rwh implements the handle traits for `Arc<H: ?Sized>`, so the trait object
35/// passes straight into `softbuffer::Surface::new` / `wgpu::Instance::create_surface`.
36///
37/// # Examples
38///
39/// Blanket-implemented for any type implementing both `raw-window-handle` traits; there is
40/// nothing to implement directly on `WindowHandle` itself.
41///
42/// ```
43/// use raw_window_handle::{
44/// DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle,
45/// WindowHandle as RawWindowHandle,
46/// };
47/// use retroglyph_window::WindowHandle;
48///
49/// struct NoWindow;
50///
51/// impl HasWindowHandle for NoWindow {
52/// fn window_handle(&self) -> Result<RawWindowHandle<'_>, HandleError> {
53/// Err(HandleError::NotSupported)
54/// }
55/// }
56///
57/// impl HasDisplayHandle for NoWindow {
58/// fn display_handle(&self) -> Result<DisplayHandle<'_>, HandleError> {
59/// Err(HandleError::NotSupported)
60/// }
61/// }
62///
63/// fn assert_is_window_handle<T: WindowHandle>(_handle: &T) {}
64/// assert_is_window_handle(&NoWindow);
65/// ```
66pub trait WindowHandle: HasWindowHandle + HasDisplayHandle {}
67
68impl<T: HasWindowHandle + HasDisplayHandle + ?Sized> WindowHandle for T {}
69
70/// A surface-lifecycle error that can optionally signal whether it's worth retrying.
71///
72/// [`Presenter::SurfaceError`] is a per-implementation associated type: softbuffer's error enum
73/// has no `Lost`/`Outdated`/`Timeout` discrimination the way `wgpu::SurfaceError` does, so today's
74/// only backend (`SoftwareRenderer`) has no structured way to say "this specific failure is
75/// fatal, don't bother retrying." [`is_recoverable`](Self::is_recoverable) is that hook: a
76/// presenter with real error categories can override it to return `false` for a truly fatal
77/// failure, while every presenter that doesn't need the distinction (including every backend that
78/// exists in this crate today) can implement this trait with an empty body and inherit the
79/// default `true`.
80///
81/// Deliberately not blanket-implemented for every `Debug + Display` type: that would make it
82/// impossible for any concrete error type to override [`is_recoverable`](Self::is_recoverable) at
83/// all (a specific `impl` would conflict with the blanket one), defeating the point of the trait.
84/// Instead, each `SurfaceError` type needs one explicit (and usually empty) `impl
85/// RecoverableError for ...` block: see `retroglyph_software`'s `SurfaceError` for the minimal
86/// case that just inherits the default.
87///
88/// # Examples
89///
90/// ```
91/// use core::fmt;
92/// use retroglyph_window::RecoverableError;
93///
94/// #[derive(Debug)]
95/// enum MySurfaceError {
96/// Init,
97/// Lost,
98/// }
99///
100/// impl fmt::Display for MySurfaceError {
101/// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102/// match self {
103/// Self::Init => write!(f, "surface init failed"),
104/// Self::Lost => write!(f, "surface lost"),
105/// }
106/// }
107/// }
108///
109/// impl RecoverableError for MySurfaceError {
110/// fn is_recoverable(&self) -> bool {
111/// // Init failures are fatal; a lost surface may come back.
112/// matches!(self, Self::Lost)
113/// }
114/// }
115///
116/// assert!(!MySurfaceError::Init.is_recoverable());
117/// assert!(MySurfaceError::Lost.is_recoverable());
118/// ```
119pub trait RecoverableError: core::fmt::Debug + core::fmt::Display {
120 /// Whether this error represents a transient failure worth retrying, as opposed to a fatal
121 /// one.
122 ///
123 /// Defaults to `true`: absent any structured error categorization, every failure is treated
124 /// as potentially transient, matching the generic consecutive-failure recovery heuristic
125 /// `winit::run::present_failure_action` already applies. Override to return `false` only for
126 /// an error variant known to be unrecoverable regardless of retries (e.g. a `wgpu::SurfaceError
127 /// ::Lost` variant that persists until the surface is fully rebuilt from a different code
128 /// path than a simple retry).
129 #[must_use]
130 fn is_recoverable(&self) -> bool {
131 true
132 }
133}
134
135// `Infallible` is uninhabited: no value of it can ever exist, so `is_recoverable` can never
136// actually be called on one, but a presenter that can't fail (e.g. a test mock) still needs
137// `type SurfaceError = core::convert::Infallible` to satisfy the `RecoverableError` bound, so
138// this impl exists purely for that convenience.
139impl RecoverableError for core::convert::Infallible {}
140
141/// A ready-made, string-backed [`SurfaceError`](Presenter::SurfaceError) for presenters whose
142/// underlying surface library reports failures as opaque strings rather than a structured error
143/// enum.
144///
145/// Several presenter backends (e.g. `retroglyph-gl`'s native/wasm split, or a future softbuffer
146/// backend) need only two buckets ("surface/context creation failed" (fatal) and "presenting a
147/// frame failed" (potentially recoverable)) and would otherwise each hand-roll the same `enum {
148/// Init(String), Present(String) }` plus [`RecoverableError`] impl. This type is that common
149/// shape, provided once here so backends can reuse it directly instead of duplicating it.
150#[derive(Debug)]
151pub enum GenericSurfaceError {
152 /// Creating the surface or its underlying context failed. Treated as fatal (not
153 /// recoverable): a presenter cannot proceed without a surface, and retrying the same
154 /// creation path is very unlikely to succeed.
155 Init(String),
156 /// Presenting a frame failed. Treated as potentially recoverable so the event loop's
157 /// consecutive-failure heuristic can retry before giving up.
158 Present(String),
159}
160
161impl fmt::Display for GenericSurfaceError {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match self {
164 Self::Init(msg) => write!(f, "surface init: {msg}"),
165 Self::Present(msg) => write!(f, "surface present: {msg}"),
166 }
167 }
168}
169
170impl std::error::Error for GenericSurfaceError {}
171
172impl RecoverableError for GenericSurfaceError {
173 fn is_recoverable(&self) -> bool {
174 // Init failures are fatal (nothing to retry into); present failures may be transient.
175 matches!(self, Self::Present(_))
176 }
177}
178
179/// A renderer that rasterizes grid content and presents it to a window surface.
180///
181/// A supertrait of [`Output`], adding the surface lifecycle (`init_surface`, `resize_surface`,
182/// `present`, `cell_size`) that the event loop drives. Every `Presenter` implementation is an
183/// `Output` implementation for free: [`WindowBackend`](crate::WindowBackend) delegates its own
184/// `Output` impl straight through to `P: Presenter`, with no duplicated method bodies.
185///
186/// # Sub-cell offsets and spill
187///
188/// A [`Tile`](retroglyph_core::tile::Tile)'s `dx`/`dy` shift its glyph within, and past, its cell.
189/// This is a cross-backend rendering contract: the CPU rasterizer (`retroglyph-software`) and the
190/// GPU one (`retroglyph-gl`) must produce the same pixels, so it is specified here once instead of
191/// in mirrored per-backend comments that reference each other (and drift when only one is
192/// touched). A `Presenter` that honors sub-cell offsets must obey all four points:
193///
194/// - `dx`/`dy` are in **unscaled font pixels** (a presenter multiplies by its own integer scale);
195/// negative `dx` shifts the glyph left, negative `dy` up.
196/// - The cell's **background fill is always the full, unshifted cell** rectangle. An offset moves
197/// only the glyph, never the background.
198/// - An offset glyph **may spill past its cell edge into neighboring cells**, and that spill is
199/// **uniform in all four directions**: a glyph pushed right/down onto a later neighbor spills
200/// the same way as one pushed left/up onto an earlier neighbor.
201/// - The mechanism that guarantees that uniformity is a **two-pass draw**: lay down *every* cell's
202/// background first, then draw *every* cell's (offset) glyph over the result. Interleaving the
203/// two per cell would let a later cell's background overwrite an earlier neighbor's spilled
204/// glyph, breaking spill in the right/down directions only.
205///
206/// The offset *application* is deliberately not shared code: `retroglyph-gl` shifts a quad's vertex
207/// position in its vertex shader, `retroglyph-software` shifts `origin_x`/`origin_y` in a CPU blit:
208/// irreducibly different mechanics that must nonetheless agree on the four points above.
209///
210/// # Examples
211///
212/// ```
213/// use retroglyph_core::backend::{DrawCell, Output};
214/// use retroglyph_core::grid::Size;
215/// use retroglyph_window::{Presenter, WindowHandle};
216/// use std::sync::Arc;
217///
218/// struct NullPresenter;
219///
220/// impl Output for NullPresenter {
221/// type Error = core::convert::Infallible;
222///
223/// fn draw_layers<'a, I>(&mut self, _content: I) -> Result<(), Self::Error>
224/// where
225/// I: Iterator<Item = DrawCell<'a>>,
226/// {
227/// Ok(())
228/// }
229///
230/// fn flush(&mut self) -> Result<(), Self::Error> {
231/// Ok(())
232/// }
233///
234/// fn size(&self) -> Size {
235/// Size {
236/// width: 4,
237/// height: 2,
238/// }
239/// }
240///
241/// fn clear(&mut self) -> Result<(), Self::Error> {
242/// Ok(())
243/// }
244/// }
245///
246/// impl Presenter for NullPresenter {
247/// type SurfaceError = core::convert::Infallible;
248///
249/// fn init_surface(&mut self, _window: Arc<dyn WindowHandle>) -> Result<(), Self::SurfaceError> {
250/// Ok(())
251/// }
252///
253/// fn resize_surface(&mut self, _width: u32, _height: u32) {}
254///
255/// fn present(&mut self) -> Result<(), Self::SurfaceError> {
256/// Ok(())
257/// }
258///
259/// fn cell_size(&self) -> (u32, u32) {
260/// (8, 16)
261/// }
262/// }
263/// ```
264pub trait Presenter: Output {
265 /// Surface lifecycle error (context creation, buffer acquisition,
266 /// present).
267 type SurfaceError: RecoverableError;
268
269 /// Initialize the window surface.
270 ///
271 /// Called once from the loop's `resumed` handler. The presenter creates
272 /// its platform surface (softbuffer surface, wgpu device+surface, GL
273 /// context) from the raw window/display handles.
274 ///
275 /// # Errors
276 ///
277 /// Returns [`Self::SurfaceError`] if surface or context creation from `window` fails (an
278 /// unsupported display/window system connection, a missing graphics API, or, on wasm32,
279 /// a canvas element that can't be located). Unlike a failed [`present`](Self::present),
280 /// the `winit` driver treats this as fatal rather than retryable: it logs the error and
281 /// exits the event loop immediately, since there is no surface to draw into and no later
282 /// hook that calls `init_surface` again.
283 fn init_surface(&mut self, window: Arc<dyn WindowHandle>) -> Result<(), Self::SurfaceError>;
284
285 /// Resize the window surface to a new physical pixel size.
286 ///
287 /// Called on every window resize event with `width`/`height` already resolved by the
288 /// caller: for the `winit` driver (see `winit::run::WindowApp::resize_to`), that means
289 /// `cols * cell_w` x `rows * cell_h`, where `cols`/`rows` are the window's physical size
290 /// divided down to whole cells. Any sub-cell remainder is truncated, not centered or
291 /// cleared: when the window's physical size isn't an exact multiple of the cell size,
292 /// `width`/`height` here are the largest whole-cell-multiple that fits, which can be
293 /// smaller than the window's actual physical size. The OS window itself is never resized
294 /// to compensate, so a non-exact-multiple resize leaves a thin strip at the window's
295 /// trailing edge outside the surface: retroglyph does not paint or clear that strip;
296 /// whatever the OS/windowing backend leaves there remains visible until a subsequent
297 /// resize covers it.
298 fn resize_surface(&mut self, width: u32, height: u32);
299
300 /// Notify the presenter that the window's scale factor (DPI) changed.
301 ///
302 /// Called when the window moves to a display with a different pixel density, or the
303 /// system DPI setting changes. The event loop follows this with
304 /// [`resize_surface`](Self::resize_surface) for the window's new physical size, so
305 /// this hook only needs to handle DPI-dependent state that isn't a plain buffer
306 /// resize (e.g. regenerating a font atlas rasterized for a particular scale).
307 ///
308 /// Defaults to a no-op: presenters whose rasterization doesn't depend on DPI (like
309 /// `SoftwareRenderer`'s integer `scale` config, set once at construction) need no
310 /// action here.
311 fn scale_factor_changed(&mut self, _scale_factor: f64) {}
312
313 /// Present the rasterized frame to the window surface.
314 ///
315 /// Called after each app tick. A lost frame is not fatal; the caller
316 /// logs the error and continues.
317 ///
318 /// # Errors
319 ///
320 /// Returns [`Self::SurfaceError`] if the surface buffer can't be acquired
321 /// or presented (e.g. context lost on wasm, page flip pending on
322 /// DRI/KMS).
323 fn present(&mut self) -> Result<(), Self::SurfaceError>;
324
325 /// Cell size in physical pixels `(width, height)`.
326 ///
327 /// Physical pixels, not logical/DPI-scaled pixels, and never auto-scaled by this crate for
328 /// display DPI: see the crate-level "DPI, scale, and the resize contract" docs. A presenter
329 /// whose cells should grow on a `HiDPI` display must change what this returns itself (from
330 /// [`resize`](Output::resize) or [`scale_factor_changed`](Self::scale_factor_changed)); absent
331 /// that, it stays constant for the presenter's lifetime.
332 ///
333 /// `(u32, u32)` rather than [`Size`](retroglyph_core::grid::Size) because grid coordinates
334 /// are `u16` but pixel arithmetic uses `u32` (winit `PhysicalSize`).
335 #[must_use]
336 fn cell_size(&self) -> (u32, u32);
337}
338
339#[cfg(test)]
340mod generic_surface_error_tests {
341 use super::{GenericSurfaceError, RecoverableError};
342
343 #[test]
344 fn init_is_not_recoverable() {
345 let err = GenericSurfaceError::Init("boom".to_string());
346 assert!(!err.is_recoverable());
347 }
348
349 #[test]
350 fn present_is_recoverable() {
351 let err = GenericSurfaceError::Present("boom".to_string());
352 assert!(err.is_recoverable());
353 }
354
355 #[test]
356 fn display_includes_message() {
357 let init = GenericSurfaceError::Init("init failed".to_string());
358 assert!(init.to_string().contains("init failed"));
359
360 let present = GenericSurfaceError::Present("present failed".to_string());
361 assert!(present.to_string().contains("present failed"));
362 }
363}