Skip to main content

retroglyph_wgpu/
lib.rs

1//! GPU rendering backend for retroglyph: Vulkan, Metal, and D3D12 from a single codebase via
2//! [`wgpu`].
3//!
4//! # Architecture
5//!
6//! [`WgpuBackendBuilder`] holds configuration (fonts, grid size, integer scale) and
7//! [`build`](WgpuBackendBuilder::build)s a [`WgpuRenderer`]. The glyph source is a static
8//! [`FontChain`] (a single [`BitmapFont`] is a chain of one); every font in the chain is
9//! grid-packed into one `R8` array-texture atlas and addressed by a flat slot id (see
10//! [`retroglyph_window::atlas`]). The renderer keeps one CPU-side instance array per grid layer and
11//! creates its device lazily, when the windowing loop calls [`Presenter::init_surface`]:
12//!
13//! ```text
14//! WgpuBackendBuilder (font, grid size, scale)
15//!   |  .build()
16//!   v
17//! WgpuRenderer
18//!   implements retroglyph_window::Presenter (an Output supertrait)
19//!   wrapped by retroglyph_window::WindowBackend to become a full Backend
20//!   (WindowBackend owns the input event queue and the no-op Cursor)
21//!   |
22//!   |  init_surface(window) -> wgpu Device + Queue + Surface, then GpuResources
23//!   v
24//! one render pass per present(): every grid layer back to front, each drawn as
25//! backgrounds then coverage-blended glyphs (then sprites), from one instance
26//! buffer uploaded once per frame.
27//! ```
28//!
29//! A cell costs 16 bytes and no index buffer: the vertex shader derives the quad's corners from the
30//! vertex index and the cell's `(column, row)` from the instance index, so the only per-instance
31//! data is the glyph slot, two colors, the sub-cell offset, and the compositing flags.
32//!
33//! This backend composites grid layers itself on the GPU
34//! ([`composites_layers`](Output::composites_layers) returns `true`): it receives the raw layered
35//! stream from the core `Terminal` and draws each layer back to front, so an empty cell in a higher
36//! layer lets the layer beneath show through while an occupied cell is opaque, matching
37//! `retroglyph-software`'s per-pixel occlusion. It requests full frames
38//! ([`needs_full_frame`](Output::needs_full_frame) returns `true`) and redraws every cell of every
39//! layer each frame, so there is no orphaned-pixel problem from sub-cell glyph spill.
40//!
41//! # Choosing between this and `retroglyph-gl`
42//!
43//! Both are GPU backends drawing the same instanced-quad pipeline, and both produce pixel-identical
44//! output (each is checked against the `retroglyph-software` CPU rasterizer). They differ in which
45//! driver stack they reach and what they cost to depend on:
46//!
47//! | | `retroglyph-wgpu` | `retroglyph-gl` |
48//! | --- | --- | --- |
49//! | APIs | Vulkan, Metal, D3D12 | OpenGL 3.3, WebGL2 |
50//! | Browser | yes, WebGPU (see [Platform support](#platform-support)) | yes, WebGL2 |
51//! | `unsafe` in the backend | none | unavoidable (every GL call) |
52//! | Direct dependencies, transitively | 85 crates | 54 crates |
53//! | Clean debug build of the crate | 15s | 7s |
54//! | Offscreen render tests | every platform | Linux/EGL only |
55//!
56//! The dependency and build-time figures are for one host (macOS, `--all-features`); the ratio is
57//! what matters, not the absolute numbers. The difference is `naga` (the shader front end that
58//! compiles this crate's WGSL) plus `wgpu-core` (the validation and state-tracking layer that
59//! makes the API safe), not the three backend APIs: only one hardware abstraction layer compiles
60//! per platform, since `wgpu-hal`'s backends are target-gated. A macOS build pulls the Metal
61//! stack and no Vulkan; a Linux build pulls `ash` and no Metal.
62//!
63//! Both cover the browser today (WebGPU here, WebGL2 there); pick `retroglyph-gl` when a smaller
64//! dependency tree matters more than the rest, or this one for validation-layer diagnostics, a
65//! modern driver path, and a backend with no `unsafe` in it. On wasm32 this backend's device is
66//! ready asynchronously (see [Platform support](#platform-support)), where `retroglyph-gl`'s WebGL2
67//! context is ready synchronously; that is the one real difference the browser adds to the choice.
68//!
69//! # Performance
70//!
71//! A 200x60 grid with three layers (36000 cells, 562 KiB of instance data) costs 266us per frame
72//! end to end: flattening the layers, uploading, encoding six draws, submitting, and waiting for
73//! the GPU to finish. That is roughly 4% of a 60 Hz frame budget, on an M-series laptop at
74//! `--release`, and it is larger than any grid the examples use.
75//!
76//! The number is here to set expectations, not to invite tuning: a character grid is a trivial
77//! workload for a modern GPU, and the design choices above (deriving position from the instance
78//! index, packing every layer into one upload) are about keeping the per-frame *bandwidth* small
79//! rather than about winning a benchmark. Emitting four real vertices per cell instead, with
80//! explicit positions and UVs, is the other common shape for this renderer and would cost roughly
81//! six times the per-frame bytes.
82//!
83//! # Platform support
84//!
85//! Native (Vulkan, Metal, D3D12) and the browser (WebGPU) both work.
86//!
87//! [`Presenter::init_surface`] is synchronous, but `request_adapter` and `request_device` are not,
88//! and a browser's main thread has no way to block on a future the way native does. So the two
89//! targets take different paths through the same call: `Instance::create_surface` *is* synchronous
90//! everywhere, so on wasm32 `init_surface` creates the surface from the window's canvas, spawns the
91//! adapter and device request with `wasm_bindgen_futures::spawn_local`, and returns immediately;
92//! [`present`](Presenter::present) polls the result each frame and draws nothing until it lands.
93//! Native, by contrast, blocks on the same requests with `pollster::block_on` inside
94//! `init_surface` itself, so the device is always ready by the time it returns. See
95//! `crate::gpu::PendingGpu` for the shared-state cell this deferral is built on.
96//!
97//! The one user-visible consequence is on wasm32 only: the first frames after startup are blank
98//! (whatever was drawn before the device exists is not shown) until the adapter and device resolve,
99//! typically well under a second. Native has no such gap.
100//!
101//! `retroglyph-gl` also covers the browser, through WebGL2, whose context creation is synchronous
102//! and so needs no equivalent deferral. Pick between the two using the table above and the
103//! dependency/build-time comparison it links to.
104//!
105//! # Environment variables
106//!
107//! `wgpu` reads its own configuration from the environment, and this crate passes it through
108//! rather than overriding it:
109//!
110//! | Variable | Effect |
111//! | --- | --- |
112//! | `WGPU_BACKEND` | Restricts the backends to try (`vulkan`, `metal`, `dx12`). |
113//! | `WGPU_POWER_PREF` | `low` or `high`; defaults to `low`, since a character grid is a handful of draw calls per frame. |
114//! | `WGPU_VALIDATION` / `WGPU_DEBUG` | Toggle the driver's validation and debug layers. |
115//!
116//! # Features
117//!
118//! <!-- gen-features:start -->
119//! This crate has no default features; every feature below is optional and off unless enabled.
120//!
121//! ### `default-font`
122//!
123//! ⚪ Optional.
124//!
125//! Embeds the Unscii 16 default font so a caller can build a renderer with no font of its own.
126//!
127//! Forwards to `retroglyph-window`'s `default-font` feature.
128//!
129//! ### `dev`
130//!
131//! ⚪ Optional.
132//!
133//! Forwards `retroglyph-core`'s `dev` feature, which forces development diagnostics on in a build
134//! that would otherwise compile them out (see [`retroglyph_core::dev`]).
135//!
136//! ### `tilesets`
137//!
138//! ⚪ Optional.
139//!
140//! PNG sprite/tileset support: decodes sprite sheets into an RGBA array-texture atlas and draws
141//! them in a third, source-over blended pass per grid layer.
142//!
143//! Forwards to `retroglyph-window`'s shared tileset decode.
144//! <!-- gen-features:end -->
145
146#![cfg_attr(docsrs, feature(doc_cfg))]
147
148pub mod config;
149
150mod error;
151mod gpu;
152mod instance;
153mod renderer;
154mod shaders;
155#[cfg(feature = "tilesets")]
156mod sprite_set;
157
158// Offscreen render tests: create a device with no surface at all, run the real pipeline into a
159// texture, and read the pixels back to assert on them. Unlike `retroglyph-gl`'s EGL-surfaceless
160// equivalent these need no platform-specific setup, so they run wherever wgpu finds an adapter.
161#[cfg(all(test, feature = "default-font"))]
162mod headless;
163
164pub use config::{WgpuBackendBuilder, WgpuBackendError};
165pub use error::SurfaceError;
166// Re-export the font types so a consumer can build a custom atlas without a separate dependency.
167pub use retroglyph_window::font::{self as font, BitmapFont, FontChain};
168
169use gpu::{GpuContext, PendingGpu, WindowSurface, WindowedResult};
170use instance::{Cell, FLAG_HAS_BG, FLAG_HAS_GLYPH};
171use renderer::{GpuResources, LayerRange};
172use retroglyph_core::backend::{DrawCell, Output};
173use retroglyph_core::color::Color;
174use retroglyph_core::grid::HasSize;
175use retroglyph_core::grid::Size;
176use retroglyph_core::tile::Tile;
177use retroglyph_window::atlas::GlyphAtlas;
178use retroglyph_window::palette::{DEFAULT_BG, DEFAULT_FG};
179#[cfg(feature = "tilesets")]
180use retroglyph_window::sprite_cache::SpriteTint;
181use retroglyph_window::{CellGeometry, Presenter, WindowHandle, cell_art_glyph};
182#[cfg(feature = "tilesets")]
183use sprite_set::{SpriteInstance, SpriteSet, SpriteSlot};
184use std::sync::Arc;
185
186// Compile the crate README's code blocks as doctests so the quick start can't silently rot.
187#[cfg(doctest)]
188#[doc = include_str!("../README.md")]
189struct ReadmeDoctests;
190
191/// The live wgpu renderer: a [`Presenter`], wrapped in
192/// [`WindowBackend`](retroglyph_window::WindowBackend) to form a full
193/// [`Backend`](retroglyph_core::backend::Backend) for the windowing loop.
194///
195/// It does not implement [`Input`](retroglyph_core::backend::Input) or
196/// [`Cursor`](retroglyph_core::backend::Cursor) itself: a GPU renderer cannot present without a
197/// live device, so there is no headless-with-input use for a bare `Terminal<WgpuRenderer>`. In
198/// windowed use `WindowBackend` owns the input queue (with its `Mouse(Moved)` coalescing) and the
199/// no-op cursor, so a duplicate queue here would only ever be dead. See the sub-cell offset note on
200/// [`Presenter`] for the shared rendering contract.
201///
202/// Build one with [`WgpuBackendBuilder`]. Before the windowing loop calls
203/// [`init_surface`](Presenter::init_surface) there is no device; drawing updates only the CPU-side
204/// instance arrays, and [`present`](Presenter::present) is a no-op. Once the device exists,
205/// `present` uploads the frame and encodes one render pass.
206pub struct WgpuRenderer {
207    /// Character-to-atlas-slot map for the bitmap font chain.
208    glyphs: GlyphAtlas,
209    cols: u16,
210    rows: u16,
211    /// Cell/surface pixel geometry (glyph size x scale); the single source of the `cell_size`
212    /// contract, delegated to by [`Presenter::cell_size`].
213    geometry: CellGeometry,
214    /// Atlas slot for the space glyph, used to initialize blank cells.
215    space_glyph: u16,
216    /// Per-layer instance arrays (index = grid layer id), each `cols * rows` in row-major cell
217    /// order. `layers[0]` is the always-opaque base; higher layers composite over it back to front.
218    /// Rebuilt each frame by [`Output::draw_layers`], since this backend requests full frames.
219    /// There is always at least the base layer.
220    layers: Vec<Vec<Cell>>,
221    /// Scratch buffer holding every layer's cells back to back for the per-frame upload. Kept as a
222    /// field so a steady-state frame reuses one allocation instead of building a new one.
223    upload: Vec<Cell>,
224    /// Per-layer slices of `upload`, parallel to `layers`.
225    ranges: Vec<LayerRange>,
226    /// The decoded sprite atlas, if a tileset was loaded. Retained so the GPU atlas can be rebuilt
227    /// if the device is recreated.
228    #[cfg(feature = "tilesets")]
229    sprite_set: Option<SpriteSet>,
230    /// Per-layer sprite instances, parallel to `layers`, rebuilt each frame by
231    /// [`Output::draw_layers`].
232    #[cfg(feature = "tilesets")]
233    sprite_layers: Vec<Vec<SpriteInstance>>,
234    /// Sprite equivalents of `upload`/`ranges`.
235    #[cfg(feature = "tilesets")]
236    sprite_upload: Vec<SpriteInstance>,
237    #[cfg(feature = "tilesets")]
238    sprite_ranges: Vec<LayerRange>,
239    /// Glyphs already reported as needing a span, so a redraw loop logs each one once instead of
240    /// every frame. See `retroglyph_window::sprite_cache::warn_sprite_needs_span`.
241    #[cfg(feature = "tilesets")]
242    warned_oversized: std::collections::BTreeSet<char>,
243    /// Glyphs already reported as having a dropped tint, so a redraw loop logs each one once
244    /// instead of every frame. See `retroglyph_window::sprite_cache::warn_tint_needs_sprite`.
245    #[cfg(feature = "tilesets")]
246    warned_dropped_tint: std::collections::BTreeSet<char>,
247    /// The current surface size in physical pixels (set by
248    /// [`resize_surface`](Presenter::resize_surface)).
249    surface_size: (u32, u32),
250    /// Device, surface, and GPU resources. `None` until the device is ready: before
251    /// [`init_surface`](Presenter::init_surface) is ever called, and on wasm32 for every frame
252    /// while [`pending`](Self::pending) is still resolving.
253    gpu: Option<Gpu>,
254    /// wasm32 only in practice: the deferred adapter/device request started by
255    /// [`init_surface`](Presenter::init_surface), polled by [`present`](Presenter::present) each
256    /// frame until it resolves. Always `None` on native, where `init_surface` never returns
257    /// without the device already installed in `Self::gpu`; `PendingGpu` is uninhabited there, so
258    /// this field costs nothing to poll.
259    pending: Option<PendingGpu>,
260}
261
262/// The live device, surface, and resources, present only after
263/// [`init_surface`](Presenter::init_surface).
264struct Gpu {
265    context: GpuContext,
266    /// The window's swap chain. `None` for the offscreen render tests, which render into a texture
267    /// they own rather than a frame they acquire.
268    surface: Option<WindowSurface>,
269    resources: GpuResources,
270}
271
272impl WgpuRenderer {
273    /// Builds a renderer for the given glyph atlas, grid size, and scale. Called by
274    /// [`WgpuBackendBuilder::build`].
275    ///
276    /// Glyph cells wider or taller than 255 unscaled pixels are clamped to 255 (the
277    /// [`CellGeometry`] limit).
278    #[allow(clippy::cast_possible_truncation)]
279    pub(crate) fn new(glyphs: GlyphAtlas, cols: u16, rows: u16, scale: u16) -> Self {
280        let (cell_w, cell_h) = glyphs.cell_size();
281        let geometry = CellGeometry::new(cell_w.min(255) as u8, cell_h.min(255) as u8, scale);
282        let space_glyph = glyphs.space_slot();
283        let count = usize::from(cols) * usize::from(rows);
284        Self {
285            glyphs,
286            cols,
287            rows,
288            geometry,
289            space_glyph,
290            layers: vec![vec![base_blank(space_glyph); count]],
291            upload: Vec::new(),
292            ranges: Vec::new(),
293            #[cfg(feature = "tilesets")]
294            sprite_set: None,
295            #[cfg(feature = "tilesets")]
296            sprite_layers: Vec::new(),
297            #[cfg(feature = "tilesets")]
298            sprite_upload: Vec::new(),
299            #[cfg(feature = "tilesets")]
300            sprite_ranges: Vec::new(),
301            #[cfg(feature = "tilesets")]
302            warned_oversized: std::collections::BTreeSet::new(),
303            #[cfg(feature = "tilesets")]
304            warned_dropped_tint: std::collections::BTreeSet::new(),
305            surface_size: geometry.surface_size(cols, rows),
306            gpu: None,
307            pending: None,
308        }
309    }
310
311    /// Attaches a decoded sprite atlas. Called by [`WgpuBackendBuilder::build`] when a tileset was
312    /// registered; the GPU atlas is built later, in [`build_resources`](Self::build_resources).
313    #[cfg(feature = "tilesets")]
314    pub(crate) fn set_sprites(&mut self, set: SpriteSet) {
315        self.sprite_set = Some(set);
316    }
317
318    /// The base-layer blank instance: space glyph, default colors, opaque default background, no
319    /// glyph drawn. Layer 0 always paints its background (the opaque base), so an untouched base
320    /// cell is the default background.
321    const fn base_blank(&self) -> Cell {
322        base_blank(self.space_glyph)
323    }
324
325    /// Total cell count for the current grid.
326    fn cell_count(&self) -> usize {
327        usize::from(self.cols) * usize::from(self.rows)
328    }
329
330    /// Reports a sprite drawn larger than one cell without a span to reserve the cells it covers.
331    ///
332    /// Shares `retroglyph-window`'s diagnostic with the other backends so all of them name the same
333    /// fix. A tile that already declares a span is fine and says nothing.
334    #[cfg(feature = "tilesets")]
335    fn warn_if_sprite_needs_span(&mut self, tile: &Tile, sprite: SpriteSlot) {
336        if tile.is_span_anchor() {
337            return;
338        }
339        retroglyph_window::sprite_cache::warn_sprite_needs_span(
340            &mut self.warned_oversized,
341            tile.glyph(),
342            (u32::from(sprite.w), u32::from(sprite.h)),
343            (
344                u32::from(self.geometry.glyph_w),
345                u32::from(self.geometry.glyph_h),
346            ),
347        );
348    }
349
350    /// Reports a tint set on a cell whose glyph resolved to a bitmap font rather than a sprite, so
351    /// the tint was silently dropped (retroglyph#564).
352    #[cfg(feature = "tilesets")]
353    fn warn_if_tint_needs_sprite(&mut self, glyph: char, tint: retroglyph_core::color::Tint) {
354        retroglyph_window::sprite_cache::warn_tint_needs_sprite(
355            &mut self.warned_dropped_tint,
356            glyph,
357            tint,
358        );
359    }
360
361    /// Builds the GPU resources for the current grid on an existing device: compiles the pipelines,
362    /// uploads the glyph and sprite atlases, and allocates the instance buffers.
363    ///
364    /// Shared by [`Presenter::init_surface`] (windowed) and the offscreen render tests, so both
365    /// exercise the same setup: the point of those tests is to catch a break in exactly this
366    /// pipeline, so it must not diverge from the real one.
367    ///
368    /// # Errors
369    ///
370    /// Returns [`SurfaceError::Init`] if either atlas exceeds the device's texture limits.
371    pub(crate) fn build_resources(
372        &self,
373        context: &GpuContext,
374        target_format: wgpu::TextureFormat,
375    ) -> Result<GpuResources, SurfaceError> {
376        let atlas = self.glyphs.data();
377        let capacity = u32::try_from(self.cell_count()).unwrap_or(u32::MAX);
378        #[cfg_attr(not(feature = "tilesets"), allow(unused_mut))]
379        let mut resources = GpuResources::new(
380            &context.device,
381            &context.queue,
382            target_format,
383            &atlas,
384            capacity,
385        )?;
386        #[cfg(feature = "tilesets")]
387        if let Some(set) = &self.sprite_set {
388            resources.attach_sprites(&context.device, &context.queue, target_format, set)?;
389        }
390        Ok(resources)
391    }
392
393    /// Flattens the per-layer instance arrays into [`Self::upload`] and records each layer's slice
394    /// in [`Self::ranges`].
395    ///
396    /// One contiguous buffer per frame is what lets the whole frame be a single `write_buffer` and
397    /// a single render pass; see [`renderer`]'s module docs for why re-uploading between layers
398    /// would be wrong rather than merely slower.
399    fn flatten_layers(&mut self) {
400        self.upload.clear();
401        self.ranges.clear();
402        for layer in &self.layers {
403            let start = u32::try_from(self.upload.len()).unwrap_or(u32::MAX);
404            let count = u32::try_from(layer.len()).unwrap_or(u32::MAX);
405            self.upload.extend_from_slice(layer);
406            self.ranges.push(LayerRange { start, count });
407        }
408
409        #[cfg(feature = "tilesets")]
410        {
411            self.sprite_upload.clear();
412            self.sprite_ranges.clear();
413            for layer in &self.sprite_layers {
414                let start = u32::try_from(self.sprite_upload.len()).unwrap_or(u32::MAX);
415                let count = u32::try_from(layer.len()).unwrap_or(u32::MAX);
416                self.sprite_upload.extend_from_slice(layer);
417                self.sprite_ranges.push(LayerRange { start, count });
418            }
419        }
420    }
421
422    /// Installs a device and resources without a surface, for the offscreen render tests.
423    ///
424    /// The windowed path installs the same pair in [`Presenter::init_surface`]; splitting this out
425    /// is what lets `headless` drive [`encode_frame`](Self::encode_frame) (and therefore the whole
426    /// production render path) against a texture it owns.
427    #[cfg(all(test, feature = "default-font"))]
428    fn install_offscreen(&mut self, context: GpuContext, resources: GpuResources) {
429        self.gpu = Some(Gpu {
430            context,
431            surface: None,
432            resources,
433        });
434    }
435
436    /// The installed device, for the offscreen render tests' readback.
437    #[cfg(all(test, feature = "default-font"))]
438    fn offscreen_context(&self) -> Option<&GpuContext> {
439        self.gpu.as_ref().map(|gpu| &gpu.context)
440    }
441
442    /// Builds resources for a newly ready device and surface, and installs both as [`Self::gpu`].
443    ///
444    /// Shared by [`Presenter::init_surface`] (native: the device is always ready by the time it
445    /// returns) and [`Presenter::present`] (wasm32: called once the deferred adapter/device
446    /// request resolves).
447    ///
448    /// # Errors
449    ///
450    /// Returns [`SurfaceError::Init`] if either atlas exceeds the device's texture limits.
451    fn install_device(
452        &mut self,
453        context: GpuContext,
454        surface: WindowSurface,
455    ) -> Result<(), SurfaceError> {
456        log::info!("retroglyph-wgpu: {}", context.describe());
457        let resources = self.build_resources(&context, surface.view_format)?;
458        self.gpu = Some(Gpu {
459            context,
460            surface: Some(surface),
461            resources,
462        });
463        Ok(())
464    }
465
466    /// The sprite atlas layer size in texels, or `(0, 0)` without a tileset.
467    fn sprite_tex_size(&self) -> (u32, u32) {
468        #[cfg(feature = "tilesets")]
469        {
470            self.sprite_set.as_ref().map_or((0, 0), SpriteSet::tex_size)
471        }
472        #[cfg(not(feature = "tilesets"))]
473        {
474            (0, 0)
475        }
476    }
477
478    /// Uploads the current frame and encodes it into `view`, without acquiring or presenting a
479    /// surface frame.
480    ///
481    /// Split out from [`present`](Presenter::present) so the offscreen render tests can point the
482    /// same code at a texture they own.
483    fn encode_frame(&mut self, view: &wgpu::TextureView) {
484        self.flatten_layers();
485        let (screen, cell, glyph, cols, sprite_tex) = (
486            self.surface_size,
487            self.geometry.cell_size(),
488            self.glyphs.cell_size(),
489            self.cols,
490            self.sprite_tex_size(),
491        );
492        let Some(gpu) = self.gpu.as_mut() else {
493            return;
494        };
495        gpu.resources
496            .set_uniforms(&gpu.context.queue, screen, cell, glyph, cols, sprite_tex);
497        gpu.resources
498            .upload_cells(&gpu.context.device, &gpu.context.queue, &self.upload);
499        #[cfg(feature = "tilesets")]
500        gpu.resources
501            .upload_sprites(&gpu.context.device, &gpu.context.queue, &self.sprite_upload);
502
503        let mut encoder =
504            gpu.context
505                .device
506                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
507                    label: Some("retroglyph frame"),
508                });
509        gpu.resources.render(
510            &mut encoder,
511            view,
512            &self.ranges,
513            #[cfg(feature = "tilesets")]
514            &self.sprite_ranges,
515        );
516        gpu.context.queue.submit(Some(encoder.finish()));
517    }
518}
519
520/// `(u8, u8, u8)` -> `[u8; 3]`, for packing resolved colors into a [`Cell`].
521const fn to_arr(rgb: (u8, u8, u8)) -> [u8; 3] {
522    [rgb.0, rgb.1, rgb.2]
523}
524
525/// The base-layer blank instance for `space_glyph`: opaque default background, no glyph. A free
526/// function so [`WgpuRenderer::new`] can build it before `self` exists.
527const fn base_blank(space_glyph: u16) -> Cell {
528    Cell::new(
529        space_glyph,
530        to_arr(DEFAULT_FG),
531        to_arr(DEFAULT_BG),
532        0,
533        0,
534        FLAG_HAS_BG,
535    )
536}
537
538/// Builds the base-layer (layer 0) [`Cell`] for `tile` at the already-resolved atlas `slot`: the
539/// background is always opaque (default-substituted), and the glyph is drawn only when
540/// [`cell_art_glyph`] says this tile draws art (see its docs for the blank/span-covered rules).
541///
542/// A `slot` of `None` is a character no font in the chain can draw, not even as the substituted
543/// solid block; the cell keeps its background and draws no glyph, matching `retroglyph-software`.
544const fn base_instance(slot: Option<u16>, tile: &Tile) -> Cell {
545    let fg = to_arr(tile.style().foreground().resolve_rgb(DEFAULT_FG));
546    let bg = to_arr(tile.style().background().resolve_rgb(DEFAULT_BG));
547    let (slot, drawable) = match slot {
548        Some(slot) => (slot, FLAG_HAS_GLYPH),
549        None => (0, 0),
550    };
551    let flags = FLAG_HAS_BG
552        | if cell_art_glyph(tile).is_none() {
553            0
554        } else {
555            drawable
556        };
557    Cell::new(slot, fg, bg, tile.dx(), tile.dy(), flags)
558}
559
560// ── Output ───────────────────────────────────────────────────────────────────
561
562impl Output for WgpuRenderer {
563    // Drawing only touches CPU memory (the instance arrays); it never fails. Device failures
564    // surface through `Presenter::present`'s `SurfaceError` instead.
565    type Error = core::convert::Infallible;
566
567    // No `draw` override: this backend always composites (`composites_layers` returns `true`
568    // below), so `Terminal::present` never calls single-layer `draw` and the default implementation
569    // (which forwards to `draw_layers`) is exactly right. See retroglyph#561 for why a second,
570    // hand-maintained body is worse than none.
571    #[allow(clippy::too_many_lines)]
572    fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
573    where
574        I: Iterator<Item = DrawCell<'a>>,
575    {
576        // This backend requests full frames, so `content` is every cell of every allocated layer in
577        // layer-major (0..=max) then row-major order (see `Grid::layers`). Rebuild the per-layer
578        // arrays from scratch: reset the base to blanks and drop higher layers, growing them back
579        // as the stream references them. Cells a layer doesn't stream stay transparent (flags == 0).
580        let base = self.base_blank();
581        let cell_count = self.cell_count();
582        self.layers.truncate(1);
583        let base_layer = &mut self.layers[0];
584        if base_layer.len() == cell_count {
585            base_layer.fill(base);
586        } else {
587            *base_layer = vec![base; cell_count];
588        }
589
590        // Per-cell running background, updated bottom-up as layers are processed. An occupied
591        // higher-layer tile with a `Color::Default` background inherits this instead of being
592        // transparent: matching `retroglyph-software`'s `resolve_bg_fill`, an occupied tile is
593        // opaque and erases the glyph beneath it, repainting whichever background a lower layer
594        // last established (down to layer 0's default). This relies on the layer-major stream order
595        // above, so a layer's lower neighbours are always processed first.
596        let mut inherited_bg = vec![to_arr(DEFAULT_BG); cell_count];
597
598        // Per-cell record of whether the occupant drawn at that index dispatched to a sprite, keyed
599        // the same way as `inherited_bg`. A span's covered cells hold only a text-fallback glyph
600        // that never has a sprite of its own, so the covered-cell branch below consults this at the
601        // *anchor's* index to answer "does this span dispatch to a sprite", matching
602        // `retroglyph-software`'s `resolve_cell_bg` (retroglyph#726). Reused across layers: a lower
603        // layer's `true` is always overwritten before a higher layer's covered cell can read it,
604        // because the anchor of any span is written before its covered cells (row-major stream).
605        let mut sprite_bg = vec![false; cell_count];
606
607        // Sprite instances are collected per layer in lockstep with `self.layers`: reset to just
608        // the (empty) base layer; higher layers are grown alongside `self.layers`.
609        #[cfg(feature = "tilesets")]
610        {
611            self.sprite_layers.truncate(1);
612            if self.sprite_layers.is_empty() {
613                self.sprite_layers.push(Vec::new());
614            }
615            self.sprite_layers[0].clear();
616        }
617        let cols = usize::from(self.cols);
618        let rows = usize::from(self.rows);
619        for draw_cell in content {
620            let (layer_id, pos, tile) = (draw_cell.layer, draw_cell.pos, draw_cell.tile);
621            let (x, y) = (usize::from(pos.x), usize::from(pos.y));
622            if x >= cols || y >= rows {
623                continue;
624            }
625            let l = usize::from(layer_id);
626            while self.layers.len() <= l {
627                // Higher layers default to fully transparent cells (flags == 0).
628                self.layers
629                    .push(vec![Cell::transparent(self.space_glyph); cell_count]);
630                #[cfg(feature = "tilesets")]
631                self.sprite_layers.push(Vec::new());
632            }
633            let idx = y * cols + x;
634
635            #[cfg(feature = "tilesets")]
636            #[allow(clippy::cast_possible_truncation)]
637            let (cx, cy) = (x as u16, y as u16);
638
639            // A cell covered by a multi-cell span (retroglyph#412) draws no glyph of its own: the
640            // span's anchor emitted one sprite across the whole footprint, and this cell's glyph is
641            // that sprite's text fallback, for backends that can't draw it. Every cell of a span
642            // shares one `Style` (see `Grid::write_span_cells`), so this cell's own tile already
643            // carries the same colors as the anchor; the anchor is consulted only to answer "does
644            // this span dispatch to a sprite" (via `sprite_bg`), the same split
645            // `retroglyph-software`'s `resolve_cell_bg` documents. Resolving the running inherited
646            // background at this cell's own index, not the anchor's, keeps a span from smearing one
647            // column's inheritance across the whole footprint (retroglyph#726).
648            if tile.span_offset().is_some() {
649                let anchor_idx = tile
650                    .span_anchor_index(idx, cols)
651                    .filter(|&anchor_idx| anchor_idx < cell_count);
652                if let Some(anchor_idx) = anchor_idx {
653                    let has_sprite = sprite_bg[anchor_idx];
654                    let fg = to_arr(tile.style().foreground().resolve_rgb(DEFAULT_FG));
655                    let bg_color = tile.style().background();
656                    let (bg, has_bg) = if l == 0 || bg_color != Color::Default {
657                        (to_arr(bg_color.resolve_rgb(DEFAULT_BG)), FLAG_HAS_BG)
658                    } else if has_sprite {
659                        (inherited_bg[idx], 0)
660                    } else {
661                        (inherited_bg[idx], FLAG_HAS_BG)
662                    };
663                    if has_bg != 0 {
664                        inherited_bg[idx] = bg;
665                    }
666                    self.layers[l][idx] = Cell::new(self.space_glyph, fg, bg, 0, 0, has_bg);
667                    continue;
668                }
669            }
670
671            if layer_id == 0 {
672                let slot = self.glyphs.resolve(tile.glyph());
673                let inst = base_instance(slot, tile);
674                // Sprite dispatch is gated on `cell_art_glyph`, not the raw `tile.glyph()`: a blank
675                // layer-0 cell (`is_empty()`, e.g. an untouched grid cell) draws no art at all,
676                // even if its glyph happens to have a registered sprite (retroglyph#762).
677                #[cfg(feature = "tilesets")]
678                {
679                    let art_glyph = cell_art_glyph(tile);
680                    if let Some(sprite) =
681                        art_glyph.and_then(|g| self.sprite_set.as_ref().and_then(|s| s.slot(g)))
682                    {
683                        // Keep layer 0's opaque background; drop the glyph, the sprite covers it.
684                        let sprite_inst = Cell::new(
685                            inst.glyph,
686                            [inst.fg[0], inst.fg[1], inst.fg[2]],
687                            [inst.bg[0], inst.bg[1], inst.bg[2]],
688                            0,
689                            0,
690                            inst.flags & FLAG_HAS_BG,
691                        );
692                        inherited_bg[idx] = [inst.bg[0], inst.bg[1], inst.bg[2]];
693                        sprite_bg[idx] = true;
694                        self.layers[0][idx] = sprite_inst;
695                        let (span_w, span_h) = tile.span();
696                        let align = sprite.align_offset(
697                            span_w,
698                            span_h,
699                            self.geometry.glyph_w,
700                            self.geometry.glyph_h,
701                        );
702                        self.warn_if_sprite_needs_span(tile, sprite);
703                        self.sprite_layers[0].push(SpriteInstance::new(
704                            cx,
705                            cy,
706                            sprite.layer,
707                            sprite.w,
708                            sprite.h,
709                            tile.dx() + align.0,
710                            tile.dy() + align.1,
711                            SpriteTint::resolve(
712                                sprite.color,
713                                tile.style().foreground(),
714                                draw_cell.tint,
715                                DEFAULT_FG,
716                            ),
717                        ));
718                        continue;
719                    }
720                    if let Some(g) = art_glyph {
721                        self.warn_if_tint_needs_sprite(g, draw_cell.tint);
722                    }
723                }
724                inherited_bg[idx] = [inst.bg[0], inst.bg[1], inst.bg[2]];
725                self.layers[0][idx] = inst;
726                continue;
727            }
728            if cell_art_glyph(tile).is_none() {
729                // Transparent: nothing drawn, and the running background is unchanged. This branch
730                // runs after the span-covered `continue` above, so a `None` here always means
731                // blank, never span-covered.
732                self.layers[l][idx] = Cell::transparent(self.space_glyph);
733                continue;
734            }
735            // Occupied higher-layer tile: opaque background (its own color, or the inherited one
736            // when the tile's background is `Default`) plus its glyph, unless no font in the chain
737            // can draw that character at all (see `base_instance`).
738            let resolved = self.glyphs.resolve(tile.glyph());
739            let glyph = resolved.unwrap_or(0);
740            let has_glyph = if resolved.is_some() {
741                FLAG_HAS_GLYPH
742            } else {
743                0
744            };
745            let fg = to_arr(tile.style().foreground().resolve_rgb(DEFAULT_FG));
746            let bg_color = tile.style().background();
747            let bg = if bg_color == Color::Default {
748                inherited_bg[idx]
749            } else {
750                let resolved = to_arr(bg_color.resolve_rgb(DEFAULT_BG));
751                inherited_bg[idx] = resolved;
752                resolved
753            };
754            #[cfg(feature = "tilesets")]
755            if let Some(sprite) = self.sprite_set.as_ref().and_then(|s| s.slot(tile.glyph())) {
756                // No bitmap glyph. An occupied higher-layer sprite cell with a `Default` background
757                // paints no background (the sprite's own alpha provides coverage, so lower layers
758                // show through its transparent pixels), matching `resolve_bg_fill`'s has_sprite
759                // rule; an explicit background is still painted opaque.
760                let has_bg = if bg_color == Color::Default {
761                    0
762                } else {
763                    FLAG_HAS_BG
764                };
765                sprite_bg[idx] = true;
766                self.layers[l][idx] = Cell::new(glyph, fg, bg, 0, 0, has_bg);
767                let (span_w, span_h) = tile.span();
768                let align = sprite.align_offset(
769                    span_w,
770                    span_h,
771                    self.geometry.glyph_w,
772                    self.geometry.glyph_h,
773                );
774                self.warn_if_sprite_needs_span(tile, sprite);
775                self.sprite_layers[l].push(SpriteInstance::new(
776                    cx,
777                    cy,
778                    sprite.layer,
779                    sprite.w,
780                    sprite.h,
781                    tile.dx() + align.0,
782                    tile.dy() + align.1,
783                    SpriteTint::resolve(
784                        sprite.color,
785                        tile.style().foreground(),
786                        draw_cell.tint,
787                        DEFAULT_FG,
788                    ),
789                ));
790                continue;
791            }
792            #[cfg(feature = "tilesets")]
793            self.warn_if_tint_needs_sprite(tile.glyph(), draw_cell.tint);
794            sprite_bg[idx] = false;
795            self.layers[l][idx] =
796                Cell::new(glyph, fg, bg, tile.dx(), tile.dy(), FLAG_HAS_BG | has_glyph);
797        }
798        Ok(())
799    }
800
801    fn needs_full_frame(&self) -> bool {
802        // Composited layers plus sub-cell glyph spill mean a partial redraw could leave orphaned
803        // pixels; redraw every cell of every layer each frame.
804        true
805    }
806
807    fn composites_layers(&self) -> bool {
808        // Draw the raw layered stream back to front on the GPU instead of letting the core flatten
809        // it, so per-layer transparency works the same as on `retroglyph-software`.
810        true
811    }
812
813    fn flush(&mut self) -> Result<(), Self::Error> {
814        // Upload is deferred to `present`, which owns the device.
815        Ok(())
816    }
817
818    fn size(&self) -> Size {
819        Size::new(self.cols, self.rows)
820    }
821
822    fn clear(&mut self) -> Result<(), Self::Error> {
823        let base = self.base_blank();
824        self.layers.truncate(1);
825        self.layers[0].fill(base);
826        // Sprite instances are collected per layer in lockstep with `self.layers`; a stale, larger
827        // `sprite_layers` would otherwise survive the clear and get redrawn by `present`
828        // (retroglyph#727).
829        #[cfg(feature = "tilesets")]
830        {
831            self.sprite_layers.truncate(1);
832            if self.sprite_layers.is_empty() {
833                self.sprite_layers.push(Vec::new());
834            }
835            self.sprite_layers[0].clear();
836        }
837        Ok(())
838    }
839
840    fn resize(&mut self, size: Size) {
841        self.cols = size.width();
842        self.rows = size.height();
843        let base = self.base_blank();
844        self.layers = vec![vec![base; self.cell_count()]];
845        // See the comment in `clear`: `sprite_layers` must stay in lockstep with `layers` so
846        // `present` doesn't redraw sprites left over from before the resize (retroglyph#727).
847        #[cfg(feature = "tilesets")]
848        {
849            self.sprite_layers = vec![Vec::new()];
850        }
851    }
852}
853
854// WgpuRenderer implements neither `Input` nor `Cursor`: `WindowBackend<WgpuRenderer>` supplies both
855// for windowed use (its input queue coalesces `Mouse(Moved)`; its cursor is a no-op), and a GPU
856// renderer has no headless-with-input path that would need its own. See the type-level docs.
857
858// ── Presenter ────────────────────────────────────────────────────────────────
859
860impl Presenter for WgpuRenderer {
861    type SurfaceError = SurfaceError;
862
863    fn init_surface(&mut self, window: Arc<dyn WindowHandle>) -> Result<(), SurfaceError> {
864        // Re-entry (surface-loss recovery, retroglyph#728): a previous device may still be
865        // installed, e.g. from the winit driver re-calling this after repeated present failures.
866        // Drop it before building the replacement, so the old surface releases the window it owns
867        // before a new surface asks for it. A previous deferred request (wasm32) is dropped too:
868        // its result, once it lands, would otherwise install a device for a window that is no
869        // longer current.
870        self.gpu = None;
871        self.pending = None;
872
873        let (w, h) = self.surface_size;
874        match GpuContext::windowed(window, w, h)? {
875            WindowedResult::Ready(context, surface) => self.install_device(context, surface)?,
876            // wasm32 only in practice (see `Self::pending`'s docs): the device isn't ready yet.
877            // `present` polls `self.pending` each frame until it resolves, drawing nothing in the
878            // meantime; drawing itself still updates the CPU-side arrays normally.
879            WindowedResult::Pending(pending) => self.pending = Some(pending),
880        }
881        Ok(())
882    }
883
884    fn resize_surface(&mut self, width: u32, height: u32) {
885        self.surface_size = (width, height);
886        if let Some(gpu) = &mut self.gpu
887            && let Some(surface) = gpu.surface.as_mut()
888        {
889            surface.resize(&gpu.context, width, height);
890        }
891    }
892
893    fn present(&mut self) -> Result<(), SurfaceError> {
894        // wasm32 only in practice: poll the deferred adapter/device request. While it is still in
895        // flight, drop through to the no-device branch below and draw nothing this frame; once it
896        // resolves, install the device (reconciling any resize that landed while it was pending)
897        // and fall through to render the very same frame.
898        if let Some(pending) = self.pending.as_ref() {
899            match pending.poll() {
900                None => {}
901                Some(Ok((context, mut surface))) => {
902                    self.pending = None;
903                    let (w, h) = self.surface_size;
904                    surface.resize(&context, w, h);
905                    self.install_device(context, surface)?;
906                }
907                Some(Err(err)) => {
908                    self.pending = None;
909                    return Err(err);
910                }
911            }
912        }
913
914        // No device yet: nothing to present. Drawing has still updated the CPU-side arrays, so the
915        // first frame after the device becomes ready shows the current grid rather than a blank
916        // one. On wasm32 this is also the ordinary state for every frame before the deferred
917        // request above resolves.
918        let Some(gpu) = self.gpu.as_mut() else {
919            return Ok(());
920        };
921        let Some(surface) = gpu.surface.as_ref() else {
922            return Ok(());
923        };
924        let frame = surface.acquire(&gpu.context)?;
925        self.encode_frame(&frame.view);
926        // Reborrow: `encode_frame` needs `&mut self`, so the earlier borrow can't survive it.
927        let gpu = self.gpu.as_ref().expect("context installed above");
928        frame.present(&gpu.context);
929        Ok(())
930    }
931
932    fn cell_size(&self) -> (u32, u32) {
933        self.geometry.cell_size()
934    }
935
936    fn geometry(&self) -> CellGeometry {
937        self.geometry
938    }
939}
940
941#[cfg(all(test, feature = "default-font"))]
942mod compositing_tests {
943    use super::{FLAG_HAS_BG, FLAG_HAS_GLYPH, WgpuBackendBuilder};
944    use retroglyph_core::backend::{DrawCell, Output};
945    use retroglyph_core::color::{Color, Style};
946    use retroglyph_core::grid::Pos;
947    use retroglyph_core::tile::Tile;
948
949    const RED: Color = Color::Rgb { r: 255, g: 0, b: 0 };
950
951    #[test]
952    fn draw_records_sub_cell_offset_and_flags_in_the_base_layer() {
953        let mut r = WgpuBackendBuilder::new()
954            .grid_size(4, 2)
955            .build()
956            .expect("default-font builds");
957        let tile = Tile::new('A', Style::new()).with_offset(-3, 5);
958        // `Output::draw` has no override on this backend: this exercises the trait's default, which
959        // forwards to `draw_layers` tagged onto layer 0.
960        r.draw(core::iter::once(DrawCell::new(Pos::new(1, 0), &tile)))
961            .expect("draw is infallible");
962
963        let inst = r.layers[0][1];
964        assert_eq!((inst.dx, inst.dy), (-3, 5));
965        let a_slot = r.glyphs.resolve('A').expect("'A' is in CP437");
966        assert_eq!(inst.glyph, a_slot);
967        // A non-empty tile on the base layer draws both its glyph and its (base) background.
968        assert_eq!(inst.flags, FLAG_HAS_BG | FLAG_HAS_GLYPH);
969    }
970
971    #[test]
972    fn base_layer_blank_cells_are_opaque_background_only() {
973        let r = WgpuBackendBuilder::new()
974            .grid_size(3, 3)
975            .build()
976            .expect("default-font builds");
977        assert!(
978            r.layers[0]
979                .iter()
980                .all(|i| i.dx == 0 && i.dy == 0 && i.flags == FLAG_HAS_BG)
981        );
982    }
983
984    #[test]
985    fn composites_layers_and_requests_full_frames() {
986        let r = WgpuBackendBuilder::new()
987            .grid_size(2, 1)
988            .build()
989            .expect("default-font builds");
990        assert!(r.composites_layers());
991        assert!(r.needs_full_frame());
992    }
993
994    #[test]
995    fn draw_layers_encodes_the_occlusion_rule_per_layer() {
996        let mut r = WgpuBackendBuilder::new()
997            .grid_size(3, 1)
998            .build()
999            .expect("default-font builds");
1000
1001        // Layer 0: an opaque glyph with a real background at (0,0).
1002        let base = Tile::new('X', Style::new().bg(RED));
1003        // Layer 1: (0,0) empty (transparent), (1,0) glyph with default bg (transparent bg),
1004        // (2,0) glyph with a real bg (opaque).
1005        let empty = Tile::default();
1006        let glyph_default_bg = Tile::new('Y', Style::new());
1007        let glyph_real_bg = Tile::new('Z', Style::new().bg(RED));
1008        let stream = [
1009            DrawCell::on_layer(0, Pos::new(0, 0), &base),
1010            DrawCell::on_layer(1, Pos::new(0, 0), &empty),
1011            DrawCell::on_layer(1, Pos::new(1, 0), &glyph_default_bg),
1012            DrawCell::on_layer(1, Pos::new(2, 0), &glyph_real_bg),
1013        ];
1014        r.draw_layers(stream.iter().copied())
1015            .expect("draw_layers is infallible");
1016
1017        // Base layer cell 0 draws both.
1018        assert_eq!(r.layers[0][0].flags, FLAG_HAS_BG | FLAG_HAS_GLYPH);
1019        // A second layer was allocated.
1020        assert_eq!(r.layers.len(), 2);
1021        // Higher-layer empty cell: fully transparent, so the lower layer shows.
1022        assert_eq!(r.layers[1][0].flags, 0);
1023        // Higher-layer occupied cell with a Default background is opaque (it erases the glyph
1024        // beneath), inheriting the background from below: here the untouched base cell.
1025        assert_eq!(r.layers[1][1].flags, FLAG_HAS_BG | FLAG_HAS_GLYPH);
1026        assert_eq!(r.layers[1][1].bg, r.layers[0][1].bg);
1027        // Higher-layer glyph with a real background: both, with its own color.
1028        assert_eq!(r.layers[1][2].flags, FLAG_HAS_BG | FLAG_HAS_GLYPH);
1029        assert_eq!(r.layers[1][2].bg, [255, 0, 0, 0]);
1030    }
1031
1032    #[test]
1033    fn draw_layers_full_frame_drops_a_removed_higher_layer() {
1034        let mut r = WgpuBackendBuilder::new()
1035            .grid_size(2, 1)
1036            .build()
1037            .expect("default-font builds");
1038        let tile = Tile::new('Q', Style::new());
1039        r.draw_layers(core::iter::once(DrawCell::on_layer(
1040            1,
1041            Pos::new(0, 0),
1042            &tile,
1043        )))
1044        .expect("draw_layers");
1045        assert_eq!(r.layers.len(), 2);
1046        // Frame 2: only the base layer is streamed, so the higher layer must not linger.
1047        r.draw_layers(core::iter::once(DrawCell::on_layer(
1048            0,
1049            Pos::new(0, 0),
1050            &tile,
1051        )))
1052        .expect("draw_layers");
1053        assert_eq!(r.layers.len(), 1);
1054    }
1055
1056    /// A span's covered cells draw no glyph of their own and take the anchor's background, so one
1057    /// piece of artwork sits on one uniform backdrop (retroglyph#412).
1058    #[test]
1059    fn draw_layers_gives_span_covered_cells_the_anchors_background_and_no_glyph() {
1060        use retroglyph_core::grid::Grid;
1061
1062        let mut r = WgpuBackendBuilder::new()
1063            .grid_size(3, 1)
1064            .build()
1065            .expect("default-font builds");
1066
1067        let mut grid = Grid::new(3, 1);
1068        grid.write_span(0, 0, 0, &["C="], Style::new().bg(RED))
1069            .expect("2x1 span fits");
1070        let anchor = *grid.tile(0, (0, 0)).expect("anchor");
1071        let covered = *grid.tile(0, (1, 0)).expect("covered");
1072        assert!(covered.span_offset().is_some());
1073
1074        r.draw_layers(
1075            [
1076                DrawCell::on_layer(0, Pos::new(0, 0), &anchor),
1077                DrawCell::on_layer(0, Pos::new(1, 0), &covered),
1078            ]
1079            .into_iter(),
1080        )
1081        .expect("draw_layers");
1082
1083        // The covered cell paints the span's background and no glyph of its own.
1084        assert_eq!(r.layers[0][1].flags, FLAG_HAS_BG);
1085        assert_eq!(r.layers[0][1].bg, [255, 0, 0, 0]);
1086    }
1087
1088    #[test]
1089    fn flatten_packs_every_layer_into_one_contiguous_upload() {
1090        let mut r = WgpuBackendBuilder::new()
1091            .grid_size(2, 2)
1092            .build()
1093            .expect("default-font builds");
1094        let tile = Tile::new('#', Style::new());
1095        r.draw_layers(core::iter::once(DrawCell::on_layer(
1096            1,
1097            Pos::new(0, 0),
1098            &tile,
1099        )))
1100        .expect("draw_layers");
1101
1102        r.flatten_layers();
1103        assert_eq!(r.upload.len(), 8, "two layers of four cells");
1104        assert_eq!(r.ranges.len(), 2);
1105        assert_eq!((r.ranges[0].start, r.ranges[0].count), (0, 4));
1106        assert_eq!((r.ranges[1].start, r.ranges[1].count), (4, 4));
1107        // Each layer's slice must hold that layer's own cells, in order.
1108        assert_eq!(&r.upload[0..4], r.layers[0].as_slice());
1109        assert_eq!(&r.upload[4..8], r.layers[1].as_slice());
1110    }
1111
1112    #[test]
1113    fn resize_rebuilds_the_grid_and_reports_the_new_size() {
1114        use retroglyph_core::grid::Size;
1115        let mut r = WgpuBackendBuilder::new()
1116            .grid_size(4, 4)
1117            .build()
1118            .expect("default-font builds");
1119        r.resize(Size::new(10, 3));
1120        assert_eq!(r.size(), Size::new(10, 3));
1121        assert_eq!(r.layers.len(), 1);
1122        assert_eq!(r.layers[0].len(), 30);
1123    }
1124
1125    #[test]
1126    fn present_without_a_device_is_a_no_op() {
1127        use retroglyph_window::Presenter as _;
1128        let mut r = WgpuBackendBuilder::new()
1129            .grid_size(2, 1)
1130            .build()
1131            .expect("default-font builds");
1132        assert!(r.present().is_ok(), "no device is not an error");
1133    }
1134
1135    /// retroglyph#726: a `Color::Default`-background span on a higher layer must not smear the
1136    /// anchor's column across the whole footprint. Layer 0 has a different background under each
1137    /// half of the span (red under the anchor, blue under the covered cell); the covered cell's
1138    /// `Default` background must inherit from *its own* column (blue), matching
1139    /// `retroglyph-software`'s `resolve_cell_bg`, not the anchor's (red).
1140    #[test]
1141    fn draw_layers_resolves_a_span_covered_cells_default_background_at_its_own_column() {
1142        use retroglyph_core::grid::Grid;
1143
1144        const BLUE: Color = Color::Rgb { r: 0, g: 0, b: 255 };
1145
1146        let mut r = WgpuBackendBuilder::new()
1147            .grid_size(2, 1)
1148            .build()
1149            .expect("default-font builds");
1150
1151        let mut grid = Grid::new(2, 1);
1152        grid.put_tile(0, (0, 0), Tile::new(' ', Style::new().bg(RED)));
1153        grid.put_tile(0, (1, 0), Tile::new(' ', Style::new().bg(BLUE)));
1154        grid.write_span(1, 0, 0, &["C="], Style::new())
1155            .expect("2x1 span fits");
1156
1157        let mut tiles: Vec<(u8, Pos, Tile)> = (0..2)
1158            .map(|x| (0u8, Pos::new(x, 0), *grid.tile(0, (x, 0)).unwrap()))
1159            .collect();
1160        tiles.extend((0..2).map(|x| (1u8, Pos::new(x, 0), *grid.tile(1, (x, 0)).unwrap())));
1161        r.draw_layers(
1162            tiles
1163                .iter()
1164                .map(|(l, pos, t)| DrawCell::on_layer(*l, *pos, t)),
1165        )
1166        .expect("draw_layers is infallible");
1167
1168        let covered = r.layers[1][1];
1169        assert_eq!(covered.flags, FLAG_HAS_BG, "covered cell draws no glyph");
1170        assert_eq!(
1171            covered.bg,
1172            [0, 0, 255, 0],
1173            "covered cell inherits its own column's background, not the anchor's"
1174        );
1175    }
1176
1177    /// Covered-cell suppression is grid state, not a tileset feature, so it holds with the
1178    /// `tilesets` feature off too: a span with no sprite behind it renders as its anchor glyph
1179    /// alone, the same on every pixel backend.
1180    #[test]
1181    fn draw_layers_suppresses_covered_glyphs_without_a_sprite() {
1182        use retroglyph_core::grid::Grid;
1183
1184        let mut r = WgpuBackendBuilder::new()
1185            .grid_size(2, 1)
1186            .build()
1187            .expect("default-font builds");
1188        let mut grid = Grid::new(2, 1);
1189        grid.write_span(0, 0, 0, &["AB"], Style::new()).unwrap();
1190        let tiles: Vec<(u8, Pos, Tile)> = (0..2)
1191            .map(|x| (0u8, Pos::new(x, 0), *grid.tile(0, (x, 0)).unwrap()))
1192            .collect();
1193        r.draw_layers(
1194            tiles
1195                .iter()
1196                .map(|(l, pos, t)| DrawCell::on_layer(*l, *pos, t)),
1197        )
1198        .expect("draw_layers is infallible");
1199
1200        assert_eq!(r.layers[0][0].flags & FLAG_HAS_GLYPH, FLAG_HAS_GLYPH);
1201        assert_eq!(r.layers[0][1].flags & FLAG_HAS_GLYPH, 0);
1202    }
1203}
1204
1205/// Sprite bookkeeping that has to stay in lockstep with the glyph layers (retroglyph#727) and the
1206/// diagnostic for a tint that had no sprite to land on (retroglyph#564).
1207#[cfg(all(test, feature = "default-font", feature = "tilesets"))]
1208mod sprite_layer_tests {
1209    use crate::{WgpuBackendBuilder, WgpuRenderer};
1210    use retroglyph_core::backend::{DrawCell, Output};
1211    use retroglyph_core::color::{Style, Tint};
1212    use retroglyph_core::grid::{Pos, Size};
1213    use retroglyph_core::tile::Tile;
1214    use retroglyph_window::tileset::{Codepage, TilesetOptions};
1215
1216    /// A one-tile 8x16 opaque PNG mapped to `'S'`, encoded once at test time.
1217    fn renderer_with_sprite(cols: u16, rows: u16) -> WgpuRenderer {
1218        let mut img = image::RgbaImage::new(8, 16);
1219        for px in img.pixels_mut() {
1220            *px = image::Rgba([0xFF, 0xFF, 0xFF, 0xFF]);
1221        }
1222        let mut png = Vec::new();
1223        image::DynamicImage::ImageRgba8(img)
1224            .write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
1225            .expect("encode png");
1226        let opts = TilesetOptions::builder(png)
1227            .tile_size(8, 16)
1228            .columns(1)
1229            .codepage(Codepage::Custom(vec!['S']))
1230            .build()
1231            .expect("valid one-tile tileset");
1232        WgpuBackendBuilder::new()
1233            .grid_size(cols, rows)
1234            .tileset(opts)
1235            .build()
1236            .expect("tileset builds")
1237    }
1238
1239    /// Draws a sprite on two layers, so `sprite_layers` has more than the (always present) base
1240    /// layer entry to be reset.
1241    fn renderer_with_a_sprite_on_two_layers() -> WgpuRenderer {
1242        let mut r = renderer_with_sprite(1, 1);
1243        let sprite = Tile::new('S', Style::new());
1244        r.draw_layers(
1245            [
1246                DrawCell::on_layer(0, Pos::new(0, 0), &sprite),
1247                DrawCell::on_layer(1, Pos::new(0, 0), &sprite),
1248            ]
1249            .into_iter(),
1250        )
1251        .expect("draw_layers is infallible");
1252        r
1253    }
1254
1255    #[test]
1256    fn clear_resets_sprite_layers_to_a_single_empty_layer() {
1257        let mut r = renderer_with_a_sprite_on_two_layers();
1258        assert_eq!(r.sprite_layers.len(), 2);
1259        assert!(!r.sprite_layers[0].is_empty());
1260
1261        r.clear().expect("clear is infallible");
1262
1263        assert_eq!(r.sprite_layers.len(), 1);
1264        assert!(r.sprite_layers[0].is_empty());
1265    }
1266
1267    #[test]
1268    fn resize_resets_sprite_layers_to_a_single_empty_layer() {
1269        let mut r = renderer_with_a_sprite_on_two_layers();
1270        assert_eq!(r.sprite_layers.len(), 2);
1271        assert!(!r.sprite_layers[0].is_empty());
1272
1273        r.resize(Size::new(2, 2));
1274
1275        assert_eq!(r.sprite_layers.len(), 1);
1276        assert!(r.sprite_layers[0].is_empty());
1277    }
1278
1279    #[test]
1280    fn flatten_packs_sprite_layers_in_lockstep_with_the_glyph_layers() {
1281        let mut r = renderer_with_a_sprite_on_two_layers();
1282        r.flatten_layers();
1283        assert_eq!(r.sprite_ranges.len(), r.ranges.len());
1284        assert_eq!(r.sprite_upload.len(), 2, "one sprite per layer");
1285        assert_eq!((r.sprite_ranges[0].start, r.sprite_ranges[0].count), (0, 1));
1286        assert_eq!((r.sprite_ranges[1].start, r.sprite_ranges[1].count), (1, 1));
1287    }
1288
1289    /// retroglyph#564: a tint on a cell whose glyph resolved to a bitmap font rather than a sprite
1290    /// is silently dropped, so it is reported once per glyph.
1291    #[test]
1292    fn a_tint_on_a_font_glyph_is_reported_once() {
1293        let mut r = renderer_with_sprite(1, 1);
1294        let tile = Tile::new('X', Style::new());
1295        for _ in 0..3 {
1296            r.draw_layers(core::iter::once(
1297                DrawCell::on_layer(0, Pos::new(0, 0), &tile).with_tint(Tint::multiply(1, 2, 3)),
1298            ))
1299            .expect("draw_layers is infallible");
1300        }
1301        assert!(r.warned_dropped_tint.contains(&'X'));
1302        assert_eq!(
1303            r.warned_dropped_tint.len(),
1304            1,
1305            "reported once, not per frame"
1306        );
1307    }
1308
1309    #[test]
1310    fn a_tint_on_a_sprite_cell_is_not_reported() {
1311        let mut r = renderer_with_sprite(1, 1);
1312        let tile = Tile::new('S', Style::new());
1313        r.draw_layers(core::iter::once(
1314            DrawCell::on_layer(0, Pos::new(0, 0), &tile).with_tint(Tint::multiply(1, 2, 3)),
1315        ))
1316        .expect("draw_layers is infallible");
1317        assert!(r.warned_dropped_tint.is_empty(), "the tint was applied");
1318    }
1319
1320    #[test]
1321    fn tint_none_is_never_reported() {
1322        let mut r = renderer_with_sprite(1, 1);
1323        let tile = Tile::new('X', Style::new());
1324        r.draw_layers(core::iter::once(DrawCell::on_layer(
1325            0,
1326            Pos::new(0, 0),
1327            &tile,
1328        )))
1329        .expect("draw_layers is infallible");
1330        assert!(r.warned_dropped_tint.is_empty());
1331    }
1332}
1333
1334/// Cross-backend `Output` conformance (retroglyph#763).
1335///
1336/// `WgpuRenderer` implements neither `Input` nor `Cursor`, so only
1337/// [`assert_output_contract`](retroglyph_core::testing::conformance::assert_output_contract)
1338/// applies; the other two harnesses have no facet here to check.
1339#[cfg(all(test, feature = "default-font"))]
1340mod conformance {
1341    use crate::{Cell, WgpuBackendBuilder, WgpuRenderer};
1342    use retroglyph_core::backend::{DrawCell, Output};
1343    use retroglyph_core::grid::HasSize as _;
1344    use retroglyph_core::grid::Size;
1345    use retroglyph_core::testing::conformance::{Observable, fnv1a};
1346
1347    /// A renderer plus the instance data as of the previous [`snapshot`](Observable::snapshot), so
1348    /// each call can hash what changed rather than the whole frame.
1349    ///
1350    /// `WgpuRenderer` has no CPU-readable framebuffer without a device (see `headless`'s pixel
1351    /// readback tests), but its `layers` field is the exact per-cell data every draw uploads
1352    /// verbatim on the next present, so hashing that is equivalent to hashing the frame for
1353    /// everything this contract checks: clear, resize, and out-of-range handling never reach the
1354    /// GPU at all.
1355    struct WgpuObserver {
1356        renderer: WgpuRenderer,
1357        previous: Vec<Vec<Cell>>,
1358    }
1359
1360    impl WgpuObserver {
1361        fn new(size: Size) -> Self {
1362            let renderer = WgpuBackendBuilder::new()
1363                .grid_size(size.width().max(1), size.height().max(1))
1364                .build()
1365                .expect("default-font builds");
1366            let previous = renderer.layers.clone();
1367            Self { renderer, previous }
1368        }
1369    }
1370
1371    impl Output for WgpuObserver {
1372        type Error = core::convert::Infallible;
1373
1374        fn draw_layers<'a, I>(&mut self, content: I) -> Result<(), Self::Error>
1375        where
1376            I: Iterator<Item = DrawCell<'a>>,
1377        {
1378            self.renderer.draw_layers(content)
1379        }
1380
1381        fn needs_full_frame(&self) -> bool {
1382            self.renderer.needs_full_frame()
1383        }
1384
1385        fn composites_layers(&self) -> bool {
1386            self.renderer.composites_layers()
1387        }
1388
1389        fn flush(&mut self) -> Result<(), Self::Error> {
1390            self.renderer.flush()
1391        }
1392
1393        fn size(&self) -> Size {
1394            self.renderer.size()
1395        }
1396
1397        fn clear(&mut self) -> Result<(), Self::Error> {
1398            self.renderer.clear()
1399        }
1400
1401        fn resize(&mut self, size: Size) {
1402            self.renderer.resize(size);
1403        }
1404    }
1405
1406    impl Observable for WgpuObserver {
1407        fn snapshot(&mut self) -> u64 {
1408            let current = &self.renderer.layers;
1409            let mut hash = fnv1a(b"wgpu-diff");
1410            for (layer, (was, now)) in self.previous.iter().zip(current.iter()).enumerate() {
1411                for (index, (was, now)) in was.iter().zip(now.iter()).enumerate() {
1412                    if was != now {
1413                        hash ^= fnv1a(&(layer as u64).to_ne_bytes());
1414                        hash ^= fnv1a(&(index as u64).to_ne_bytes());
1415                        hash ^= fnv1a(bytemuck::bytes_of(now));
1416                    }
1417                }
1418            }
1419            // A resize changes the number of layers/cells outright: fold that in too, or a
1420            // shrink-then-grow back to the same per-cell content would hash identically to no
1421            // change at all.
1422            hash ^= fnv1a(&(current.len() as u64).to_ne_bytes());
1423            for layer in current {
1424                hash ^= fnv1a(&(layer.len() as u64).to_ne_bytes());
1425            }
1426            self.previous = current.clone();
1427            hash
1428        }
1429    }
1430
1431    #[test]
1432    fn output_contract() {
1433        retroglyph_core::testing::conformance::assert_output_contract(WgpuObserver::new);
1434    }
1435}