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