Skip to main content

frust_engine/gpu/
atlas.rs

1//! The image atlas array, and the encoded-image record the strip shader reads
2//! it through.
3//!
4//! Two halves, split on the same line the rest of [`crate::gpu`] is split on.
5//!
6//! - **Pure decisions over plain values.** The texture descriptor, the packing
7//!   of an [`AtlasRegion`] and a sampler into a
8//!   [`GpuEncodedImage`](super::GpuEncodedImage), and the natural-to-device
9//!   transform an image paint carries — all host-testable with no device.
10//! - **One live-GPU type.** [`AtlasArray`] owns the `Rgba8Unorm` `D2Array`
11//!   texture the shader's `atlas_texture_array` binding samples, grows it a
12//!   layer at a time, writes newly resident regions and clears evicted ones.
13//!
14//! ## Why the atlas is not pooled
15//!
16//! [`crate::gpu::targets`] pools every *transient* the engine allocates, on the
17//! rule that a target nothing outlives the frame should be reused rather than
18//! reallocated. The atlas is the opposite kind of resource: its whole purpose
19//! is that an image uploaded on one frame is still there on the next thousand,
20//! so it is owned outright for the life of the renderer and reclaimed a region
21//! at a time by [`crate::cache::images`]'s age-based reap. Handing it to the
22//! pool would make residency a lie.
23//!
24//! ## Growth and clearing
25//!
26//! A `wgpu` texture's array-layer count is fixed at creation, so growing the
27//! array means creating a deeper texture and copying every existing layer
28//! across — which is why the array carries `COPY_SRC` alongside `COPY_DST`.
29//! Growth is rare (a layer holds a whole mobile budget's worth of images) and
30//! never shrinks: an atlas that grew to four layers under load keeps them.
31//!
32//! ### Why growth submits a command buffer of its own
33//!
34//! That copy is the one piece of engine work that cannot ride the frame's
35//! encoder. `wgpu` flushes the queued writes pending at a submit *before* the
36//! command buffers of that same submit, so a copy recorded into the frame's
37//! encoder would execute after the frame's own `write_texture` uploads and
38//! clears — restoring the pre-growth contents of every layer they had just
39//! written, permanently (residency schedules an upload on a miss, and the image
40//! is not a miss any more). Ordering is therefore established by submitting the
41//! copy on its own, ahead of the frame's writes being issued at all.
42//!
43//! That is a **maintenance** submit, not scene work: it carries one texture
44//! copy, no pass and no draw, and it happens only on the rare frame that grows
45//! the array. [`crate::renderer::EngineRenderer::encode`]'s contract that the
46//! engine never submits *the caller's* encoder, and records no scene work
47//! anywhere else, is untouched — the caller's encoder is neither read nor
48//! finished here.
49//!
50//! Clearing an evicted region writes transparent texels through the queue
51//! rather than drawing a scissored pass. Both reach the same result; the queue
52//! write needs no pipeline, no render pass and no bind group, and eviction is
53//! a once-in-sixty-frames event whose cost is a zeroed staging buffer the size
54//! of the region.
55//!
56//! ## Rendering *into* the atlas
57//!
58//! Everything above writes the atlas from the host. A glyph is different: it
59//! has no pixels until something rasterizes its outline, and `glifo`'s
60//! [`AtlasCacher::Enabled`](glifo::AtlasCacher::Enabled) path does not
61//! rasterize one — it records the fills into a per-page
62//! [`AtlasCommandRecorder`] and leaves the pixels to whoever owns the atlas.
63//! [`AtlasRenderer`] is that owner on this tier: it replays each dirty page's
64//! commands into a strip pass whose colour attachment is that page's own array
65//! layer.
66//!
67//! Three orderings make the difference between a correct glyph and a stale
68//! one, and all three are this type's to keep.
69//!
70//! 1. **Clears before uploads.** A rectangle an eviction freed can be handed
71//!    straight back out to a different glyph on the same frame, so zeroing it
72//!    after that glyph's pixels landed would erase the glyph that just moved
73//!    in. Both are queue writes, which execute in the order they are issued —
74//!    so the order they are issued in is the whole guarantee.
75//! 2. **Both before the pass.** `wgpu` flushes the writes queued at a submit
76//!    before that submit's command buffers, so a page's replay pass sees the
77//!    clears and the bitmap uploads already applied to the layer it composites
78//!    onto, without anything having to be said about it.
79//! 3. **The pass before the scene's.** A glyph the scene pass samples out of
80//!    the atlas has to be *in* the atlas by then, and the scene pass lives in
81//!    an encoder this crate does not own and never submits. So the replay pass
82//!    goes into an encoder of [`AtlasRenderer`]'s own and is submitted before
83//!    it returns — the sanctioned exception to `frust_gpu::CommandBuffer`'s
84//!    single-submit borrowing contract, named there as the glyph-atlas upload
85//!    carve-out and the same one [`AtlasArray::ensure_layers`] already takes
86//!    for the growth copy.
87//!
88//! One submit per dirty page rather than one for all of them: the coverage a
89//! page's strips index is uploaded to a shared alpha texture, and a second
90//! queue write to that texture in the same submit would overwrite the first
91//! before either pass ran. Pages are dirty only on a frame that missed a glyph,
92//! and there is one page in the overwhelming case, so the extra submit is a
93//! per-miss cost rather than a per-frame one.
94//!
95//! ### What the replay does not lower
96//!
97//! Turning a recorded command stream into strips is compiler work, and
98//! [`crate::compile`] already depends on this module — so the lowering is a
99//! closure the caller supplies ([`AtlasRenderer::render_pending`]) rather than
100//! a dependency taken the other way. What this module contributes to it is
101//! [`push_solid_strips`], the pure expansion of one solid-painted strip run
102//! into instances, which is the whole of an outline glyph's lowering.
103
104use vello_common::encode::EncodedImage;
105use vello_common::kurbo::{Affine, Rect, Vec2};
106use vello_common::paint::{ImageSource, Tint, TintMode};
107use vello_common::strip::Strip;
108
109use frust_gpu::TierCaps;
110use glifo::atlas::PendingBitmapUpload;
111use glifo::{AtlasCommandRecorder, GlyphAtlas, PendingClearRect};
112
113use crate::EngineError;
114use crate::cache::images::{ATLAS_FORMAT_BYTES, AtlasRegion, ResidentImage};
115use crate::diag::{EngineSpan, FrameTimestamps};
116
117use super::GpuEncodedPaint;
118use super::config::GpuConfig;
119use super::paint_texture::GpuEncodedImage;
120use super::strips::{GpuStrip, PaintType, StripDraw, pack_paint_descriptor};
121
122/// The texture format the atlas array stores premultiplied image texels in.
123pub const ATLAS_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
124
125/// The usages the atlas array is created with.
126///
127/// `TEXTURE_BINDING` for the strip shader's own sampling, `COPY_DST` for a
128/// region upload or clear, `COPY_SRC` for the layer copy that growth performs,
129/// and `RENDER_ATTACHMENT` so a scissored clear pass remains available to a
130/// later caller that wants one — the same four the reference renderer creates
131/// its atlas with.
132pub const ATLAS_USAGES: wgpu::TextureUsages = wgpu::TextureUsages::TEXTURE_BINDING
133    .union(wgpu::TextureUsages::COPY_DST)
134    .union(wgpu::TextureUsages::COPY_SRC)
135    .union(wgpu::TextureUsages::RENDER_ATTACHMENT);
136
137/// The most atlas layers the encoded-image record's `atlas_index` field can
138/// name (eight bits).
139pub const MAX_ATLAS_INDEX: u32 = 0xFF;
140
141/// The descriptor for an atlas array of `width` x `height` texels over
142/// `layers` array layers.
143///
144/// `layers` is raised to at least **two**, not one. A texture with zero array
145/// layers cannot be created at all, which would be reason enough for a floor
146/// of one — but wgpu-hal 30.0.1's GLES backend picks a texture's GL target
147/// from the descriptor alone and never consults the view dimension a caller
148/// binds it through (`get_info_from_desc`, wgpu-hal `src/gles/mod.rs:513-530`:
149/// `(false, 1) => TEXTURE_2D`). A one-layer array descriptor therefore binds
150/// as plain `GL_TEXTURE_2D` while [`super::atlas`]'s strip shader always
151/// samples this texture as a `sampler2DArray`
152/// (`crates/frust-engine/shaders/strip.wgsl`'s `texture_2d_array<f32>`
153/// binding) — target and sampler disagree, the texture reads as incomplete
154/// per GLES 3.0 §3.8.2, and every sample returns `(0, 0, 0, 1)`: a solid box
155/// instead of a glyph, a solid black rect instead of an image. Two layers is
156/// the smallest depth the heuristic reads as `TEXTURE_2D_ARRAY`, so it is the
157/// floor a renderer with a single resident image or glyph page still has to
158/// allocate at. See wgpu upstream issues #1614 and #1574; this is a
159/// workaround, not the fix, and is meant to come out once wgpu-hal honours
160/// the view dimension (tracked in `docs/LIMITATIONS.md`).
161#[must_use]
162pub fn atlas_texture_descriptor(
163    width: u32,
164    height: u32,
165    layers: u32,
166) -> wgpu::TextureDescriptor<'static> {
167    wgpu::TextureDescriptor {
168        label: Some("frust-engine image atlas array"),
169        size: wgpu::Extent3d {
170            width: width.max(1),
171            height: height.max(1),
172            depth_or_array_layers: layers.max(2),
173        },
174        mip_level_count: 1,
175        sample_count: 1,
176        dimension: wgpu::TextureDimension::D2,
177        format: ATLAS_FORMAT,
178        usage: ATLAS_USAGES,
179        view_formats: &[],
180    }
181}
182
183/// The `D2Array` view descriptor the strip shader's atlas binding expects.
184#[must_use]
185pub fn atlas_view_descriptor() -> wgpu::TextureViewDescriptor<'static> {
186    wgpu::TextureViewDescriptor {
187        label: Some("frust-engine image atlas array view"),
188        format: None,
189        dimension: Some(wgpu::TextureViewDimension::D2Array),
190        aspect: wgpu::TextureAspect::All,
191        base_mip_level: 0,
192        mip_level_count: None,
193        base_array_layer: 0,
194        array_layer_count: None,
195        usage: None,
196    }
197}
198
199/// The single-layer `D2` view descriptor a render pass attaches one atlas
200/// layer through.
201///
202/// Deliberately not [`atlas_view_descriptor`]'s shape: a colour attachment has
203/// to name exactly one layer, so this is a plain 2D view over
204/// `layer`, not the `D2Array` view the shader samples the whole array through.
205/// The two coexist on the same texture — one bound for sampling by the scene
206/// pass, one attached for writing by the replay pass — which is why
207/// [`ATLAS_USAGES`] carries both `TEXTURE_BINDING` and `RENDER_ATTACHMENT`.
208#[must_use]
209pub fn atlas_layer_view_descriptor(layer: u32) -> wgpu::TextureViewDescriptor<'static> {
210    wgpu::TextureViewDescriptor {
211        label: Some("frust-engine image atlas layer target"),
212        format: None,
213        dimension: Some(wgpu::TextureViewDimension::D2),
214        aspect: wgpu::TextureAspect::All,
215        base_mip_level: 0,
216        mip_level_count: None,
217        base_array_layer: layer,
218        array_layer_count: Some(1),
219        usage: None,
220    }
221}
222
223/// The atlas array texture, its view, and the layer count both were created
224/// at.
225///
226/// Created lazily by the first frame that makes an image resident, then grown
227/// only. The view is kept beside the texture because a bind group is built
228/// against it and has to be rebuilt whenever growth replaces the texture —
229/// [`generation`](Self::generation) is what tells a caller that happened.
230#[derive(Debug)]
231pub struct AtlasArray {
232    texture: wgpu::Texture,
233    view: wgpu::TextureView,
234    width: u32,
235    height: u32,
236    layers: u32,
237    generation: u64,
238}
239
240impl AtlasArray {
241    /// An atlas array of `width` x `height` texels with at least two layers due to
242    /// the floor raised by wgpu-hal 30.0.1's GLES backend heuristic — one resident
243    /// layer is still the logical minimum, but a second layer is allocated to work
244    /// around a target-selection bug in `get_info_from_desc` (see [`atlas_texture_descriptor`]).
245    #[must_use]
246    pub fn new(device: &wgpu::Device, width: u32, height: u32) -> Self {
247        Self::with_layers(device, width, height, 1)
248    }
249
250    /// An atlas array of `width` x `height` texels over `layers` layers.
251    #[must_use]
252    pub fn with_layers(device: &wgpu::Device, width: u32, height: u32, layers: u32) -> Self {
253        let descriptor = atlas_texture_descriptor(width, height, layers);
254        let texture = device.create_texture(&descriptor);
255        let view = texture.create_view(&atlas_view_descriptor());
256
257        Self {
258            texture,
259            view,
260            width: descriptor.size.width,
261            height: descriptor.size.height,
262            layers: descriptor.size.depth_or_array_layers,
263            generation: 0,
264        }
265    }
266
267    /// The array texture the shader samples.
268    #[must_use]
269    pub fn texture(&self) -> &wgpu::Texture {
270        &self.texture
271    }
272
273    /// The `D2Array` view a bind group binds.
274    #[must_use]
275    pub fn view(&self) -> &wgpu::TextureView {
276        &self.view
277    }
278
279    /// Extent of each layer, in texels.
280    #[must_use]
281    pub fn size(&self) -> (u32, u32) {
282        (self.width, self.height)
283    }
284
285    /// How many array layers currently exist.
286    #[must_use]
287    pub fn layers(&self) -> u32 {
288        self.layers
289    }
290
291    /// A render-attachment view over one array layer, or `None` when the array
292    /// has no such layer.
293    ///
294    /// Minted per use rather than cached alongside [`view`](Self::view): a
295    /// layer target is wanted only on a frame that has glyph pixels to
296    /// rasterize, while the sampling view is bound by every frame, and holding
297    /// one view per layer for the array's lifetime would keep a handle alive
298    /// per layer for a path most frames never take.
299    #[must_use]
300    pub fn layer_view(&self, layer: u32) -> Option<wgpu::TextureView> {
301        (layer < self.layers).then(|| {
302            self.texture
303                .create_view(&atlas_layer_view_descriptor(layer))
304        })
305    }
306
307    /// How many times growth has replaced the underlying texture.
308    ///
309    /// A bind group built against [`view`](Self::view) stays valid for as long
310    /// as this value does not change.
311    #[must_use]
312    pub fn generation(&self) -> u64 {
313        self.generation
314    }
315
316    /// Grow the array to hold at least `layers` layers, preserving every
317    /// existing layer's texels.
318    ///
319    /// Returns whether the texture was replaced — the signal a caller needs to
320    /// rebuild its bind group. A request at or below the current depth, or one
321    /// past [`MAX_ATLAS_INDEX`], is a no-op: the encoded-image record cannot
322    /// name a layer the shader could not address, so refusing here is what
323    /// keeps an unaddressable layer from being created at all.
324    ///
325    /// The old-to-new copy is recorded into a command encoder of this method's
326    /// own and submitted before returning, rather than into the frame's. That is
327    /// an ordering requirement rather than a convenience — see the module doc's
328    /// *Why growth submits a command buffer of its own*. Call it before the
329    /// frame's atlas writes are issued; anything already queued is flushed by
330    /// this submit and so lands in the *old* texture.
331    pub fn ensure_layers(
332        &mut self,
333        device: &wgpu::Device,
334        queue: &wgpu::Queue,
335        layers: u32,
336    ) -> bool {
337        if layers <= self.layers || layers > MAX_ATLAS_INDEX + 1 {
338            return false;
339        }
340
341        let grown = Self::with_layers(device, self.width, self.height, layers);
342        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
343            label: Some("frust-engine image atlas growth"),
344        });
345        encoder.copy_texture_to_texture(
346            self.texture.as_image_copy(),
347            grown.texture.as_image_copy(),
348            wgpu::Extent3d {
349                width: self.width,
350                height: self.height,
351                depth_or_array_layers: self.layers,
352            },
353        );
354        queue.submit(std::iter::once(encoder.finish()));
355
356        let generation = self.generation.saturating_add(1);
357        *self = grown;
358        self.generation = generation;
359        true
360    }
361
362    /// Write `pixels` into `region`.
363    ///
364    /// `pixels` must be `region`'s own extent in premultiplied `Rgba8Unorm`,
365    /// row-major and unpadded — exactly what
366    /// [`crate::cache::images::ImageUpload`] carries. A slice that does not
367    /// match, or a region outside the array, is refused rather than handed to
368    /// the queue, which would validate it into a device error mid-frame.
369    pub fn write_region(&self, queue: &wgpu::Queue, region: AtlasRegion, pixels: &[u8]) -> bool {
370        if !self.contains(region) || pixels.len() != region.byte_len() {
371            return false;
372        }
373
374        queue.write_texture(
375            wgpu::TexelCopyTextureInfo {
376                texture: &self.texture,
377                mip_level: 0,
378                origin: wgpu::Origin3d {
379                    x: region.offset[0],
380                    y: region.offset[1],
381                    z: region.layer,
382                },
383                aspect: wgpu::TextureAspect::All,
384            },
385            pixels,
386            wgpu::TexelCopyBufferLayout {
387                offset: 0,
388                bytes_per_row: Some(region.bytes_per_row()),
389                rows_per_image: Some(region.size[1]),
390            },
391            wgpu::Extent3d {
392                width: region.size[0],
393                height: region.size[1],
394                depth_or_array_layers: 1,
395            },
396        );
397        true
398    }
399
400    /// Clear `region` to transparent texels.
401    ///
402    /// This is what makes an eviction observable as *absence* rather than as a
403    /// stale image: the rectangle a reaped entry gave back is zeroed before a
404    /// later allocation can hand part of it to something smaller, so a sample
405    /// that strays into the unwritten remainder reads transparent black rather
406    /// than the previous tenant's pixels.
407    pub fn clear_region(&self, queue: &wgpu::Queue, region: AtlasRegion) -> bool {
408        if !self.contains(region) {
409            return false;
410        }
411        let zeros = vec![0_u8; region.byte_len()];
412        self.write_region(queue, region, &zeros)
413    }
414
415    /// Whether `region` lies wholly inside this array.
416    #[must_use]
417    pub fn contains(&self, region: AtlasRegion) -> bool {
418        !region.is_empty()
419            && region.layer < self.layers
420            && region.offset[0].saturating_add(region.size[0]) <= self.width
421            && region.offset[1].saturating_add(region.size[1]) <= self.height
422    }
423}
424
425/// The bytes a region's texels occupy — the length
426/// [`AtlasArray::write_region`] requires of its slice.
427#[must_use]
428pub fn region_byte_len(region: AtlasRegion) -> usize {
429    (region.size[0] as usize)
430        .saturating_mul(region.size[1] as usize)
431        .saturating_mul(ATLAS_FORMAT_BYTES as usize)
432}
433
434/// The affine mapping an image's natural pixel rectangle
435/// `(0, 0, width, height)` onto `dest`, composed under `transform`.
436///
437/// The same composition `frust-render`'s CPU-tier lowering applies, so the two
438/// tiers place an image identically: a natural-size draw under the widget's own
439/// transform, translated to `dest`'s origin and scaled to `dest`'s extent.
440/// `None` for a degenerate natural size, which has no scale to derive.
441#[must_use]
442pub fn natural_to_dest(transform: Affine, natural: (u32, u32), dest: Rect) -> Option<Affine> {
443    let natural_w = f64::from(natural.0);
444    let natural_h = f64::from(natural.1);
445    if natural_w <= 0.0 || natural_h <= 0.0 {
446        return None;
447    }
448
449    Some(
450        transform
451            * Affine::translate((dest.x0, dest.y0))
452            * Affine::scale_non_uniform(dest.width() / natural_w, dest.height() / natural_h),
453    )
454}
455
456/// The per-pixel advances in image space an encoded image carries, derived
457/// from its already-inverted transform.
458///
459/// The linear part only: an advance is a direction, so the translation is
460/// dropped. `vello_common` computes the same pair internally and keeps it
461/// private, so it is restated here rather than reached for.
462#[must_use]
463pub fn x_y_advances(transform: Affine) -> (Vec2, Vec2) {
464    let c = transform.as_coeffs();
465    (Vec2::new(c[0], c[1]), Vec2::new(c[2], c[3]))
466}
467
468/// Packs an image's width and height into one word, width in the high half.
469#[must_use]
470pub const fn pack_image_size(width: u16, height: u16) -> u32 {
471    ((width as u32) << 16) | (height as u32)
472}
473
474/// Packs an image's atlas offset into one word, x in the high half.
475#[must_use]
476pub const fn pack_image_offset(x: u16, y: u16) -> u32 {
477    ((x as u32) << 16) | (y as u32)
478}
479
480/// Packs sampling quality (bits 0-1), the two extend modes (bits 2-3 and 4-5),
481/// the atlas layer (bits 6-13) and the source kind (bit 14) into one word.
482///
483/// Each field is masked to its width rather than reported: every input is
484/// produced by this crate's own encoding, and the preconditions are checked in
485/// debug builds.
486#[must_use]
487pub fn pack_image_params(
488    quality: u32,
489    extend_x: u32,
490    extend_y: u32,
491    atlas_index: u32,
492    is_external: bool,
493) -> u32 {
494    debug_assert!(quality <= 3, "quality must fit two bits");
495    debug_assert!(extend_x <= 3, "extend_x must fit two bits");
496    debug_assert!(extend_y <= 3, "extend_y must fit two bits");
497    debug_assert!(
498        atlas_index <= MAX_ATLAS_INDEX,
499        "atlas index {atlas_index} exceeds {MAX_ATLAS_INDEX}",
500    );
501
502    (u32::from(is_external) << 14)
503        | ((atlas_index & MAX_ATLAS_INDEX) << 6)
504        | ((extend_y & 0b11) << 4)
505        | ((extend_x & 0b11) << 2)
506        | (quality & 0b11)
507}
508
509/// The premultiplied colour and mode an optional tint packs to.
510///
511/// With no tint the colour is all-ones under [`TintMode::Multiply`], which
512/// leaves the sampled texel exactly as it was — the shader always applies a
513/// tint, so "no tint" has to be expressed as an identity one rather than as a
514/// branch.
515#[must_use]
516pub fn pack_tint(tint: Option<Tint>) -> (u32, u32) {
517    match tint {
518        Some(tint) => (
519            tint.color.premultiply().to_rgba8().to_u32(),
520            tint.mode.as_u32(),
521        ),
522        None => (u32::MAX, TintMode::Multiply.as_u32()),
523    }
524}
525
526/// The shader's extend-mode numbering.
527pub const fn extend_mode(extend: peniko::Extend) -> u32 {
528    match extend {
529        peniko::Extend::Pad => 0,
530        peniko::Extend::Repeat => 1,
531        peniko::Extend::Reflect => 2,
532    }
533}
534
535/// Lower one encoded image paint into the record the strip shader samples.
536///
537/// `resident` is where the image's texels were made resident, which only a
538/// caller that already serviced the frame's residency can supply — the same
539/// shape [`super::paint_texture::lower_encoded_paint`] uses for a gradient's
540/// baked ramp. An image whose source is not the handle `resident` names
541/// answers `None`, so a paint encoded against a different residency becomes a
542/// dropped draw rather than a wrongly-addressed one.
543///
544/// The encoded transform is already the inverse mapping — device space into
545/// image space — because that is the direction the shader applies it in;
546/// narrowing it to `f32` here is what the record and the WGSL both read it at.
547#[must_use]
548pub fn lower_encoded_image(
549    image: &EncodedImage,
550    resident: &ResidentImage,
551) -> Option<GpuEncodedPaint> {
552    match &image.source {
553        ImageSource::OpaqueId { id, .. } if *id == resident.id => {}
554        _ => return None,
555    }
556
557    let region = resident.region;
558    let (tint, tint_mode) = pack_tint(image.tint);
559
560    Some(GpuEncodedPaint::Image(GpuEncodedImage {
561        image_params: pack_image_params(
562            image.sampler.quality as u32,
563            extend_mode(image.sampler.x_extend),
564            extend_mode(image.sampler.y_extend),
565            region.layer,
566            false,
567        ),
568        image_size: pack_image_size(truncate_u16(region.size[0]), truncate_u16(region.size[1])),
569        image_offset: pack_image_offset(
570            truncate_u16(region.offset[0]),
571            truncate_u16(region.offset[1]),
572        ),
573        transform: image.transform.as_coeffs().map(|coeff| coeff as f32),
574        tint,
575        tint_mode,
576        image_padding: resident.padding,
577    }))
578}
579
580/// `value` narrowed to the `u16` the record's packed halves hold.
581///
582/// Saturating rather than wrapping: every caller has already passed the
583/// residency's own `u16` ceiling, so this can only ever be the identity, and a
584/// saturation is the harmless reading if that ever stops being true.
585const fn truncate_u16(value: u32) -> u16 {
586    if value > u16::MAX as u32 {
587        u16::MAX
588    } else {
589        value as u16
590    }
591}
592
593/// The width of the stand-in encoded-paint texture an atlas pass binds.
594///
595/// One texel, because nothing an atlas pass draws reads that texture: a
596/// replayed glyph outline paints solid, and a solid instance carries its colour
597/// in its own payload. A binding still has to be filled for the pass to
598/// validate, and the config the shader reconstructs the texture's width from
599/// has to agree with it — a power of two, which one is.
600const PLACEHOLDER_PAINT_TEX_WIDTH: u32 = 1;
601
602/// Instances an atlas pass's buffer holds before it is grown for a page that
603/// needs more.
604///
605/// A page of glyph outlines is a few hundred strips in the ordinary case; this
606/// floor keeps the first miss from allocating a buffer measured in single
607/// instances and then reallocating it four times on the way up.
608const MIN_ATLAS_INSTANCES: u64 = 256;
609
610/// Expands one solid-painted strip run into the instances that draw it,
611/// appending to `out`.
612///
613/// This is the whole of an outline glyph's lowering: `glifo` records a glyph as
614/// a fill of a path in one colour, so every instance of the run repeats the
615/// same premultiplied `payload` and the same solid paint descriptor, and a
616/// position-sampled paint's per-instance re-evaluation never arises.
617///
618/// `strips` is a *generation's* strips, sentinel included: a strip's extent is
619/// carried by the strip after it, so instances come from consecutive pairs and
620/// the trailing sentinel becomes none. Passing a run without its sentinel
621/// silently drops the last span.
622///
623/// Returns how many instances were appended, which is neither `strips.len()`
624/// nor bounded by it — a pair can contribute a span, a winding gap fill, both,
625/// or neither.
626pub fn push_solid_strips(
627    strips: &[Strip],
628    payload: u32,
629    depth: u32,
630    out: &mut Vec<GpuStrip>,
631) -> u32 {
632    let draw = StripDraw {
633        payload,
634        paint: pack_paint_descriptor(PaintType::Solid, 0),
635        depth_index: depth,
636    };
637
638    let before = out.len();
639    for pair in strips.windows(2) {
640        let span = GpuStrip::from_strip_pair(&pair[0], &pair[1], draw);
641        if span.width > 0 {
642            out.push(span);
643        }
644        if let Some(gap) = GpuStrip::gap_fill(&pair[0], &pair[1], draw) {
645            out.push(gap);
646        }
647    }
648    u32::try_from(out.len().saturating_sub(before)).unwrap_or(u32::MAX)
649}
650
651/// One page's lowered replay: the instances to draw and the coverage they
652/// index.
653///
654/// Reused across pages and across frames rather than allocated per page — the
655/// two vectors are the only heap a replay costs once they have grown, which is
656/// the point of handing them to the lowering closure instead of taking a fresh
657/// pair back from it.
658///
659/// The two are kept together because a strip instance's alpha column is only
660/// meaningful against the coverage buffer generated alongside it, the same
661/// pairing [`crate::compile::CompiledFrame`] keeps for a frame.
662#[derive(Debug, Default)]
663pub struct AtlasPageBuffers {
664    /// The page's strip instances, in the order they are drawn.
665    pub instances: Vec<GpuStrip>,
666    /// The coverage bytes [`instances`](Self::instances) index.
667    pub alphas: Vec<u8>,
668}
669
670impl AtlasPageBuffers {
671    /// Empties both buffers, keeping their capacity for the next page.
672    pub fn clear(&mut self) {
673        self.instances.clear();
674        self.alphas.clear();
675    }
676
677    /// Whether this page would draw nothing.
678    #[must_use]
679    pub fn is_empty(&self) -> bool {
680        self.instances.is_empty()
681    }
682}
683
684/// What one call to [`AtlasRenderer::render_pending`] serviced.
685///
686/// Every field is a count rather than a flag because each names work that
687/// either reached the atlas or did not, and a glyph that silently failed to
688/// rasterize is otherwise indistinguishable from one the text never asked for.
689#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
690pub struct AtlasRenderReport {
691    /// Evicted rectangles zeroed, ahead of every upload.
692    pub cleared: u32,
693    /// Bitmap (and COLR pixmap) glyphs written through the queue.
694    pub uploaded: u32,
695    /// Pages whose recorded commands were replayed into a pass.
696    pub pages: u32,
697    /// Strip instances those passes drew.
698    pub instances: u32,
699    /// Pages, clears and uploads that were refused — a page the lowering
700    /// declined, a layer the array does not have, coverage past the alpha
701    /// texture's ceiling, or a region the array would not take.
702    pub refused: u32,
703}
704
705impl AtlasRenderReport {
706    /// Whether this call did anything at all.
707    ///
708    /// The steady state: text that hit the cache on every glyph queues no
709    /// upload, frees no rectangle and dirties no page, so an atlas renderer
710    /// driven every frame submits nothing on almost all of them.
711    #[must_use]
712    pub fn is_empty(&self) -> bool {
713        *self == Self::default()
714    }
715}
716
717/// One resource texture the atlas pass owns, and the extent it holds.
718#[derive(Debug)]
719struct AtlasResourceTexture {
720    texture: wgpu::Texture,
721    view: wgpu::TextureView,
722    width: u32,
723    height: u32,
724}
725
726impl AtlasResourceTexture {
727    fn new(device: &wgpu::Device, descriptor: &wgpu::TextureDescriptor<'_>) -> Self {
728        let texture = device.create_texture(descriptor);
729        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
730        Self {
731            texture,
732            view,
733            width: descriptor.size.width,
734            height: descriptor.size.height,
735        }
736    }
737
738    fn copy_target(&self) -> wgpu::TexelCopyTextureInfo<'_> {
739        wgpu::TexelCopyTextureInfo {
740            texture: &self.texture,
741            mip_level: 0,
742            origin: wgpu::Origin3d::ZERO,
743            aspect: wgpu::TextureAspect::All,
744        }
745    }
746
747    fn extent(&self) -> wgpu::Extent3d {
748        wgpu::Extent3d {
749            width: self.width,
750            height: self.height,
751            depth_or_array_layers: 1,
752        }
753    }
754}
755
756/// The 1x1 stand-ins for the strip shader's bindings an atlas pass has nothing
757/// real for.
758///
759/// Every declared binding has to be filled for a pass to validate, whether or
760/// not an instance samples it. Three of these five are stand-ins for the same
761/// reason the frame path's are — no layer input, no external texture, no
762/// gradient ramp. The other two are stand-ins for a reason particular to this
763/// pass: the *atlas array* itself is deliberately not bound here, because the
764/// pass is writing one of its layers, and a texture attached for writing cannot
765/// also be bound for sampling in the same pass; and the encoded-paint texture
766/// is unused because a replayed outline paints solid.
767#[derive(Debug)]
768struct AtlasPlaceholders {
769    layer_input: wgpu::TextureView,
770    array: wgpu::TextureView,
771    external: wgpu::TextureView,
772    paints: wgpu::TextureView,
773    gradients: wgpu::TextureView,
774}
775
776impl AtlasPlaceholders {
777    fn new(device: &wgpu::Device) -> Self {
778        Self {
779            layer_input: placeholder(
780                device,
781                "frust-engine atlas pass layer input",
782                ATLAS_FORMAT,
783                false,
784            ),
785            array: placeholder(device, "frust-engine atlas pass array", ATLAS_FORMAT, true),
786            external: placeholder(
787                device,
788                "frust-engine atlas pass external",
789                ATLAS_FORMAT,
790                false,
791            ),
792            paints: placeholder(
793                device,
794                "frust-engine atlas pass paints",
795                super::RESOURCE_TEXTURE_FORMAT,
796                false,
797            ),
798            gradients: placeholder(
799                device,
800                "frust-engine atlas pass gradients",
801                ATLAS_FORMAT,
802                false,
803            ),
804        }
805    }
806}
807
808/// A 1x1 sampled-only texture's view, as a plain 2D texture or a 2D array.
809///
810/// The array variant allocates **two** layers, not one, for the same reason
811/// [`atlas_texture_descriptor`] does: wgpu-hal 30.0.1's GLES backend derives
812/// the GL target from the descriptor's layer count alone, and a one-layer
813/// array descriptor binds as `GL_TEXTURE_2D` rather than
814/// `GL_TEXTURE_2D_ARRAY` (see that function's doc comment for the file:line
815/// and upstream issue refs). Only layer zero of the two is ever sampled here.
816fn placeholder(
817    device: &wgpu::Device,
818    label: &'static str,
819    format: wgpu::TextureFormat,
820    array: bool,
821) -> wgpu::TextureView {
822    let texture = device.create_texture(&wgpu::TextureDescriptor {
823        label: Some(label),
824        size: wgpu::Extent3d {
825            width: 1,
826            height: 1,
827            depth_or_array_layers: if array { 2 } else { 1 },
828        },
829        mip_level_count: 1,
830        sample_count: 1,
831        dimension: wgpu::TextureDimension::D2,
832        format,
833        usage: wgpu::TextureUsages::TEXTURE_BINDING,
834        view_formats: &[],
835    });
836    texture.create_view(&wgpu::TextureViewDescriptor {
837        label: Some(label),
838        dimension: Some(if array {
839            wgpu::TextureViewDimension::D2Array
840        } else {
841            wgpu::TextureViewDimension::D2
842        }),
843        ..Default::default()
844    })
845}
846
847/// The GPU home for glyph pixels: replays `glifo`'s recorded atlas commands
848/// into the atlas array's own layers, and services the two pixel drains that
849/// have to be ordered around them.
850///
851/// Owned for the life of a renderer, beside the [`AtlasArray`] it writes rather
852/// than inside it — the array is a texture a scene pass samples, this is the
853/// machinery a *replay* pass needs, and most renderers never take the replay
854/// path at all. See the module doc's *Rendering into the atlas* for the three
855/// orderings this type exists to keep.
856///
857/// Nothing here is reached by the frame path until the text backend turns
858/// `glifo`'s atlas cacher on; a renderer that holds one and drives it every
859/// frame submits nothing on any frame that missed no glyph
860/// ([`AtlasRenderReport::is_empty`]).
861#[derive(Debug)]
862pub struct AtlasRenderer {
863    /// Coverage for the page currently being drawn. Shared across pages and
864    /// rewritten per page, which is why each page is its own submit.
865    alphas: AtlasResourceTexture,
866    /// The viewport uniform, rewritten per page — identical for every layer of
867    /// one array, since every layer has the array's own extent.
868    config: wgpu::Buffer,
869    /// The instance buffer every page's strips are written to the head of.
870    instances: wgpu::Buffer,
871    instance_capacity: u64,
872    placeholders: AtlasPlaceholders,
873    /// Handed to the lowering closure page by page, so a replay allocates
874    /// nothing once these have grown.
875    buffers: AtlasPageBuffers,
876}
877
878impl AtlasRenderer {
879    /// An atlas renderer for `caps`' adapter.
880    ///
881    /// The coverage texture is created at its minimum height and grown by the
882    /// first page that needs more, the same growth-only rule every other
883    /// resource texture in this crate follows.
884    #[must_use]
885    pub fn new(device: &wgpu::Device, caps: &TierCaps) -> Self {
886        let dim = caps.resource_texture_dim;
887        Self {
888            alphas: AtlasResourceTexture::new(
889                device,
890                &super::alpha_texture_descriptor(dim, super::MIN_RESOURCE_TEXTURE_HEIGHT),
891            ),
892            config: device.create_buffer(&wgpu::BufferDescriptor {
893                label: Some("frust-engine atlas pass config uniform"),
894                size: GpuConfig::SIZE,
895                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
896                mapped_at_creation: false,
897            }),
898            instances: device.create_buffer(&instance_descriptor(
899                MIN_ATLAS_INSTANCES * size_of::<GpuStrip>() as u64,
900            )),
901            instance_capacity: MIN_ATLAS_INSTANCES * size_of::<GpuStrip>() as u64,
902            placeholders: AtlasPlaceholders::new(device),
903            buffers: AtlasPageBuffers::default(),
904        }
905    }
906
907    /// The coverage texture's current extent in texels.
908    #[must_use]
909    pub fn alpha_texture_size(&self) -> (u32, u32) {
910        (self.alphas.width, self.alphas.height)
911    }
912
913    /// The instance buffer's current capacity in bytes.
914    #[must_use]
915    pub fn instance_capacity(&self) -> u64 {
916        self.instance_capacity
917    }
918
919    /// Zero one evicted rectangle, answering whether the array took it.
920    ///
921    /// A queue write, not a scissored pass, for [`AtlasArray::clear_region`]'s
922    /// reason — and issued through the queue rather than an encoder so that it
923    /// is guaranteed to land before any pass of the same submit, which is what
924    /// lets a rectangle freed this frame be drawn into this frame.
925    pub fn clear_rect(
926        &self,
927        queue: &wgpu::Queue,
928        atlas: &AtlasArray,
929        rect: PendingClearRect,
930    ) -> bool {
931        atlas.clear_region(queue, clear_rect_region(rect))
932    }
933
934    /// Write one already-rasterized glyph pixmap into its slot, answering
935    /// whether the array took it.
936    ///
937    /// The slot's extent is what is written, not the pixmap's: a pixmap whose
938    /// dimensions disagree with the slot it was allocated for would be written
939    /// at the wrong stride and smear across the page, so the mismatch is
940    /// refused here rather than handed to the queue.
941    pub fn upload_pixmap(
942        &self,
943        queue: &wgpu::Queue,
944        atlas: &AtlasArray,
945        upload: &PendingBitmapUpload,
946    ) -> bool {
947        let region = slot_region(upload);
948        if u32::from(upload.pixmap.width()) != region.size[0]
949            || u32::from(upload.pixmap.height()) != region.size[1]
950        {
951            return false;
952        }
953        atlas.write_region(queue, region, upload.pixmap.data_as_u8_slice())
954    }
955
956    /// Draw one page's lowered replay into array layer `layer`, on a command
957    /// encoder of this method's own, submitted before it returns.
958    ///
959    /// Returns how many instances were drawn — zero for an empty page, which
960    /// costs no submit at all.
961    ///
962    /// `timestamps` charges this pass to [`EngineSpan::Prepass`] — the atlas
963    /// replay's own recording site, one own-encoder submit per dirty page (see
964    /// this module's header), so several pages in one frame are several passes
965    /// summed into the one span. The fresh pair is asked for only once the
966    /// empty-page early return is behind us, so a page with nothing to draw
967    /// never spends a query pair on a pass that was never opened.
968    ///
969    /// # Errors
970    ///
971    /// [`EngineError::AtlasError`] when the array has no layer `layer`;
972    /// [`EngineError::AlphaCapacity`] when the page's coverage is past what a
973    /// resource texture of this adapter's dimension can hold. Both leave the
974    /// atlas exactly as it was — nothing is recorded before either is checked.
975    #[expect(
976        clippy::too_many_arguments,
977        reason = "one page's whole draw call: device/queue, the pipeline, the \
978                  array it draws into, which layer, the page's own instances, \
979                  and the timestamp sink, each owned by a different caller"
980    )]
981    pub fn render_page(
982        &mut self,
983        device: &wgpu::Device,
984        queue: &wgpu::Queue,
985        pipeline: &wgpu::RenderPipeline,
986        atlas: &AtlasArray,
987        layer: u32,
988        page: &mut AtlasPageBuffers,
989        timestamps: FrameTimestamps<'_>,
990    ) -> Result<u32, EngineError> {
991        if page.is_empty() {
992            return Ok(0);
993        }
994        let Some(target) = atlas.layer_view(layer) else {
995            return Err(EngineError::AtlasError);
996        };
997        let height = super::alpha_texture_height(page.alphas.len(), self.alphas.width)?;
998
999        if height > self.alphas.height {
1000            self.alphas = AtlasResourceTexture::new(
1001                device,
1002                &super::alpha_texture_descriptor(self.alphas.width, height),
1003            );
1004        }
1005        let alphas = &self.alphas;
1006        super::with_padded_alphas(&mut page.alphas, alphas.width, alphas.height, |bytes| {
1007            queue.write_texture(
1008                alphas.copy_target(),
1009                bytes,
1010                wgpu::TexelCopyBufferLayout {
1011                    offset: 0,
1012                    bytes_per_row: Some(super::resource_bytes_per_row(alphas.width)),
1013                    rows_per_image: Some(alphas.height),
1014                },
1015                alphas.extent(),
1016            );
1017        });
1018
1019        let config = super::targets::atlas_layer_config(
1020            atlas.size(),
1021            self.alphas.width,
1022            PLACEHOLDER_PAINT_TEX_WIDTH,
1023        );
1024        queue.write_buffer(&self.config, 0, bytemuck::bytes_of(&config));
1025
1026        let bytes: &[u8] = bytemuck::cast_slice(&page.instances);
1027        self.grow_instances(device, bytes.len() as u64);
1028        queue.write_buffer(&self.instances, 0, bytes);
1029
1030        let groups = self.bind_groups(device, pipeline);
1031        let count = u32::try_from(page.instances.len()).unwrap_or(u32::MAX);
1032
1033        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1034            label: Some("frust-engine atlas replay"),
1035        });
1036        {
1037            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1038                label: Some("frust-engine atlas replay pass"),
1039                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1040                    view: &target,
1041                    depth_slice: None,
1042                    resolve_target: None,
1043                    ops: wgpu::Operations {
1044                        // Loaded, never cleared: the layer holds every glyph
1045                        // resident in it, and this pass adds one page's worth
1046                        // to them. A clear here would evict the whole atlas
1047                        // every time one glyph missed.
1048                        load: wgpu::LoadOp::Load,
1049                        store: wgpu::StoreOp::Store,
1050                    },
1051                })],
1052                depth_stencil_attachment: None,
1053                timestamp_writes: timestamps.writes(EngineSpan::Prepass),
1054                occlusion_query_set: None,
1055                multiview_mask: None,
1056            });
1057            pass.set_pipeline(pipeline);
1058            pass.set_vertex_buffer(0, self.instances.slice(..));
1059            for (index, group) in groups.iter().enumerate() {
1060                pass.set_bind_group(index as u32, group, &[]);
1061            }
1062            pass.draw(GpuStrip::vertex_range(), GpuStrip::instance_range(0, count));
1063        }
1064        queue.submit(std::iter::once(encoder.finish()));
1065
1066        Ok(count)
1067    }
1068
1069    /// Service every drain `glyphs` has pending and replay every page it
1070    /// dirtied, strictly ahead of the caller's own scene pass.
1071    ///
1072    /// The order is the contract: evicted rectangles are zeroed, then bitmap
1073    /// pixmaps are written, then each dirty page's commands are lowered by
1074    /// `lower` and drawn. `lower` answers `false` for a page it declines — a
1075    /// command stream carrying a shape this tier has no lowering for, which is
1076    /// how a colour glyph goes *missing* rather than landing wrong — and the
1077    /// page is counted in [`AtlasRenderReport::refused`] and left undrawn.
1078    ///
1079    /// `pipeline` must be the one built from
1080    /// [`super::pipelines::atlas_strip_desc`]: a bind group is built against
1081    /// the pipeline's own derived layout, so another variant's is rejected even
1082    /// where the two layouts are structurally identical.
1083    ///
1084    /// `timestamps` is passed straight through to [`Self::render_page`] for
1085    /// each dirty page replayed — see that method's docs for the [`EngineSpan`]
1086    /// it charges to and why an empty page spends no query pair on it.
1087    #[expect(
1088        clippy::too_many_arguments,
1089        reason = "the whole replay pass's inputs: device/queue, the pipeline, \
1090                  the array and the glyph source it drains, the timestamp \
1091                  sink, and the caller's own lowering closure, each owned by \
1092                  a different part of the renderer"
1093    )]
1094    pub fn render_pending<L>(
1095        &mut self,
1096        device: &wgpu::Device,
1097        queue: &wgpu::Queue,
1098        pipeline: &wgpu::RenderPipeline,
1099        atlas: &AtlasArray,
1100        glyphs: &mut GlyphAtlas,
1101        timestamps: FrameTimestamps<'_>,
1102        mut lower: L,
1103    ) -> AtlasRenderReport
1104    where
1105        L: FnMut(&AtlasCommandRecorder, &mut AtlasPageBuffers) -> bool,
1106    {
1107        let mut report = AtlasRenderReport::default();
1108
1109        for rect in glyphs.drain_pending_clear_rects() {
1110            if self.clear_rect(queue, atlas, rect) {
1111                report.cleared = report.cleared.saturating_add(1);
1112            } else {
1113                report.refused = report.refused.saturating_add(1);
1114            }
1115        }
1116        for upload in glyphs.drain_pending_uploads() {
1117            if self.upload_pixmap(queue, atlas, &upload) {
1118                report.uploaded = report.uploaded.saturating_add(1);
1119            } else {
1120                report.refused = report.refused.saturating_add(1);
1121            }
1122        }
1123
1124        // Taken out and put back so the closure below can hold the scratch
1125        // mutably while `self` records the pass it feeds.
1126        let mut buffers = core::mem::take(&mut self.buffers);
1127        glyphs.replay_pending_atlas_commands(|recorder| {
1128            buffers.clear();
1129            if !lower(recorder, &mut buffers) {
1130                report.refused = report.refused.saturating_add(1);
1131                return;
1132            }
1133            match self.render_page(
1134                device,
1135                queue,
1136                pipeline,
1137                atlas,
1138                recorder.page_index,
1139                &mut buffers,
1140                timestamps,
1141            ) {
1142                Ok(0) => {}
1143                Ok(instances) => {
1144                    report.pages = report.pages.saturating_add(1);
1145                    report.instances = report.instances.saturating_add(instances);
1146                }
1147                Err(_) => report.refused = report.refused.saturating_add(1),
1148            }
1149        });
1150        self.buffers = buffers;
1151
1152        report
1153    }
1154
1155    /// Grows the instance buffer if `required` bytes outgrew it.
1156    fn grow_instances(&mut self, device: &wgpu::Device, required: u64) {
1157        if self.instance_capacity >= required {
1158            return;
1159        }
1160        let capacity = required
1161            .checked_next_power_of_two()
1162            .unwrap_or(required)
1163            .max(MIN_ATLAS_INSTANCES * size_of::<GpuStrip>() as u64);
1164        self.instances = device.create_buffer(&instance_descriptor(capacity));
1165        self.instance_capacity = capacity;
1166    }
1167
1168    /// The four bind groups an atlas pass sets, built against `pipeline`'s own
1169    /// derived layout.
1170    ///
1171    /// Built per pass rather than cached: a replay happens only on a frame that
1172    /// missed a glyph, and caching would have to be invalidated against both
1173    /// the coverage texture's growth and the pipeline it was derived from — two
1174    /// invalidation sources for a path that is not the frame path.
1175    fn bind_groups(
1176        &self,
1177        device: &wgpu::Device,
1178        pipeline: &wgpu::RenderPipeline,
1179    ) -> [wgpu::BindGroup; 4] {
1180        [
1181            device.create_bind_group(&wgpu::BindGroupDescriptor {
1182                label: Some("frust-engine atlas pass resources"),
1183                layout: &pipeline.get_bind_group_layout(0),
1184                entries: &[
1185                    wgpu::BindGroupEntry {
1186                        binding: 0,
1187                        resource: wgpu::BindingResource::TextureView(&self.alphas.view),
1188                    },
1189                    wgpu::BindGroupEntry {
1190                        binding: 1,
1191                        resource: self.config.as_entire_binding(),
1192                    },
1193                    wgpu::BindGroupEntry {
1194                        binding: 2,
1195                        resource: wgpu::BindingResource::TextureView(
1196                            &self.placeholders.layer_input,
1197                        ),
1198                    },
1199                ],
1200            }),
1201            device.create_bind_group(&wgpu::BindGroupDescriptor {
1202                label: Some("frust-engine atlas pass images"),
1203                layout: &pipeline.get_bind_group_layout(1),
1204                entries: &[
1205                    wgpu::BindGroupEntry {
1206                        binding: 0,
1207                        resource: wgpu::BindingResource::TextureView(&self.placeholders.array),
1208                    },
1209                    wgpu::BindGroupEntry {
1210                        binding: 1,
1211                        resource: wgpu::BindingResource::TextureView(&self.placeholders.external),
1212                    },
1213                ],
1214            }),
1215            device.create_bind_group(&wgpu::BindGroupDescriptor {
1216                label: Some("frust-engine atlas pass paints"),
1217                layout: &pipeline.get_bind_group_layout(2),
1218                entries: &[wgpu::BindGroupEntry {
1219                    binding: 0,
1220                    resource: wgpu::BindingResource::TextureView(&self.placeholders.paints),
1221                }],
1222            }),
1223            device.create_bind_group(&wgpu::BindGroupDescriptor {
1224                label: Some("frust-engine atlas pass gradients"),
1225                layout: &pipeline.get_bind_group_layout(3),
1226                entries: &[wgpu::BindGroupEntry {
1227                    binding: 0,
1228                    resource: wgpu::BindingResource::TextureView(&self.placeholders.gradients),
1229                }],
1230            }),
1231        ]
1232    }
1233}
1234
1235/// The instance buffer's descriptor at `size` bytes.
1236fn instance_descriptor(size: u64) -> wgpu::BufferDescriptor<'static> {
1237    wgpu::BufferDescriptor {
1238        label: Some("frust-engine atlas strip instances"),
1239        size,
1240        usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
1241        mapped_at_creation: false,
1242    }
1243}
1244
1245/// The atlas rectangle one of `glifo`'s pending clear rects names.
1246///
1247/// The rect is already the *padded* region — the allocation an evicted glyph
1248/// gave back, transparent border included — which is exactly the rectangle that
1249/// has to be zeroed: leaving the padding behind would let a later, smaller
1250/// tenant's `Extend::Pad` sampling read the previous glyph's edge texels.
1251#[must_use]
1252pub fn clear_rect_region(rect: PendingClearRect) -> AtlasRegion {
1253    AtlasRegion {
1254        layer: rect.page_index,
1255        offset: [u32::from(rect.x), u32::from(rect.y)],
1256        size: [u32::from(rect.width), u32::from(rect.height)],
1257    }
1258}
1259
1260/// The atlas rectangle one pending bitmap upload's slot occupies.
1261///
1262/// The slot's own extent, not the padded allocation's: the padding around a
1263/// glyph is transparent by construction (a fresh page starts zeroed, an evicted
1264/// one is zeroed by its clear rect), so an upload writes the glyph and leaves
1265/// the border alone.
1266#[must_use]
1267pub fn slot_region(upload: &PendingBitmapUpload) -> AtlasRegion {
1268    let slot = upload.atlas_slot;
1269    AtlasRegion {
1270        layer: slot.page_index,
1271        offset: [u32::from(slot.x), u32::from(slot.y)],
1272        size: [u32::from(slot.width), u32::from(slot.height)],
1273    }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278    use super::*;
1279    use vello_common::kurbo::Point;
1280
1281    fn region(layer: u32, offset: [u32; 2], size: [u32; 2]) -> AtlasRegion {
1282        AtlasRegion {
1283            layer,
1284            offset,
1285            size,
1286        }
1287    }
1288
1289    #[test]
1290    fn the_atlas_is_a_sampled_copyable_renderable_rgba8_array() {
1291        let descriptor = atlas_texture_descriptor(1024, 1024, 4);
1292
1293        assert_eq!(descriptor.format, ATLAS_FORMAT);
1294        assert_eq!(descriptor.dimension, wgpu::TextureDimension::D2);
1295        assert_eq!(descriptor.size.depth_or_array_layers, 4);
1296        assert_eq!(descriptor.mip_level_count, 1);
1297        for usage in [
1298            wgpu::TextureUsages::TEXTURE_BINDING,
1299            wgpu::TextureUsages::COPY_DST,
1300            wgpu::TextureUsages::COPY_SRC,
1301            wgpu::TextureUsages::RENDER_ATTACHMENT,
1302        ] {
1303            assert!(descriptor.usage.contains(usage));
1304        }
1305    }
1306
1307    #[test]
1308    fn a_zero_extent_descriptor_is_raised_to_a_creatable_one() {
1309        let descriptor = atlas_texture_descriptor(0, 0, 0);
1310        assert_eq!(descriptor.size.width, 1);
1311        assert_eq!(descriptor.size.height, 1);
1312        assert_eq!(descriptor.size.depth_or_array_layers, 2);
1313    }
1314
1315    /// The layer floor is two, not one — a fix for wgpu-hal 30.0.1's GLES
1316    /// backend, not an arbitrary minimum.
1317    ///
1318    /// `get_info_from_desc` (wgpu-hal `src/gles/mod.rs:513-530`) chooses the GL
1319    /// target from `TextureDescriptor::size.depth_or_array_layers` alone —
1320    /// `(false, 1) => TEXTURE_2D`, never consulting the view dimension a caller
1321    /// later binds the texture through. A one-layer atlas array descriptor
1322    /// therefore creates a plain `GL_TEXTURE_2D`, while
1323    /// `crates/frust-engine/shaders/strip.wgsl` always samples this texture as
1324    /// `texture_2d_array<f32>` (`sampler2DArray` once naga lowers it to GLSL).
1325    /// Target and sampler disagreeing makes the texture incomplete per GLES
1326    /// 3.0 §3.8.2, so every sample reads `(0, 0, 0, 1)`: a solid box in place
1327    /// of a glyph, a solid black rect in place of an image — reproduced on
1328    /// Chrome's WebGL2 backend by the examples/web-spike probe and fixed
1329    /// by this floor. Tracked upstream as wgpu issues #1614 and #1574; a later
1330    /// tidy-up must not "simplify" this back to `max(1)` without wgpu-hal
1331    /// fixing the heuristic first (see `docs/LIMITATIONS.md`).
1332    #[test]
1333    fn the_layer_floor_is_two_because_wgpu_hal_gles_ignores_the_view_dimension() {
1334        assert_eq!(
1335            atlas_texture_descriptor(64, 64, 0)
1336                .size
1337                .depth_or_array_layers,
1338            2
1339        );
1340        assert_eq!(
1341            atlas_texture_descriptor(64, 64, 1)
1342                .size
1343                .depth_or_array_layers,
1344            2
1345        );
1346        assert_eq!(
1347            atlas_texture_descriptor(64, 64, 3)
1348                .size
1349                .depth_or_array_layers,
1350            3,
1351            "a request already past the floor is not clamped down to it"
1352        );
1353    }
1354
1355    #[test]
1356    fn the_view_the_shader_binds_is_a_layered_one() {
1357        assert_eq!(
1358            atlas_view_descriptor().dimension,
1359            Some(wgpu::TextureViewDimension::D2Array)
1360        );
1361    }
1362
1363    #[test]
1364    fn image_params_round_trip_through_the_shaders_own_field_widths() {
1365        let packed = pack_image_params(1, 2, 3, 200, false);
1366
1367        assert_eq!(packed & 0b11, 1, "quality");
1368        assert_eq!((packed >> 2) & 0b11, 2, "extend_x");
1369        assert_eq!((packed >> 4) & 0b11, 3, "extend_y");
1370        assert_eq!((packed >> 6) & 0xFF, 200, "atlas index");
1371        assert_eq!((packed >> 14) & 1, 0, "source kind");
1372
1373        assert_eq!(pack_image_params(0, 0, 0, 0, true) >> 14 & 1, 1);
1374    }
1375
1376    #[test]
1377    fn size_and_offset_pack_with_the_first_component_high() {
1378        assert_eq!(pack_image_size(0x1234, 0x5678), 0x1234_5678);
1379        assert_eq!(pack_image_offset(0x00FF, 0xAB00), 0x00FF_AB00);
1380    }
1381
1382    #[test]
1383    fn an_absent_tint_is_the_identity_multiply() {
1384        let (color, mode) = pack_tint(None);
1385        assert_eq!(color, u32::MAX);
1386        assert_eq!(mode, TintMode::Multiply.as_u32());
1387    }
1388
1389    #[test]
1390    fn natural_to_dest_lands_the_natural_corners_on_the_dest_corners() {
1391        let dest = Rect::new(5.0, 6.0, 45.0, 46.0);
1392        let transform = natural_to_dest(Affine::IDENTITY, (2, 2), dest).expect("non-degenerate");
1393
1394        assert_eq!(transform * Point::new(0.0, 0.0), Point::new(5.0, 6.0));
1395        assert_eq!(transform * Point::new(2.0, 2.0), Point::new(45.0, 46.0));
1396    }
1397
1398    #[test]
1399    fn natural_to_dest_composes_the_widgets_own_transform_outermost() {
1400        let dest = Rect::new(0.0, 0.0, 4.0, 4.0);
1401        let transform =
1402            natural_to_dest(Affine::translate((10.0, 20.0)), (2, 2), dest).expect("non-degenerate");
1403
1404        assert_eq!(transform * Point::new(0.0, 0.0), Point::new(10.0, 20.0));
1405    }
1406
1407    #[test]
1408    fn natural_to_dest_refuses_a_degenerate_natural_size() {
1409        let dest = Rect::new(0.0, 0.0, 10.0, 10.0);
1410        assert!(natural_to_dest(Affine::IDENTITY, (0, 4), dest).is_none());
1411        assert!(natural_to_dest(Affine::IDENTITY, (4, 0), dest).is_none());
1412    }
1413
1414    #[test]
1415    fn advances_are_the_linear_part_only() {
1416        let transform = Affine::new([2.0, 3.0, 4.0, 5.0, 100.0, 200.0]);
1417        let (x_advance, y_advance) = x_y_advances(transform);
1418
1419        assert_eq!(x_advance, Vec2::new(2.0, 3.0));
1420        assert_eq!(y_advance, Vec2::new(4.0, 5.0));
1421    }
1422
1423    #[test]
1424    fn a_layer_target_names_exactly_one_layer_as_a_plain_2d_view() {
1425        let descriptor = atlas_layer_view_descriptor(3);
1426
1427        assert_eq!(descriptor.dimension, Some(wgpu::TextureViewDimension::D2));
1428        assert_eq!(descriptor.base_array_layer, 3);
1429        assert_eq!(
1430            descriptor.array_layer_count,
1431            Some(1),
1432            "a colour attachment must name exactly one layer"
1433        );
1434        assert_eq!(descriptor.base_mip_level, 0);
1435        // Distinct from the sampling view in the one axis that matters: the
1436        // same texture is bound array-wide for reading and 2D for writing.
1437        assert_ne!(descriptor.dimension, atlas_view_descriptor().dimension);
1438    }
1439
1440    #[test]
1441    fn a_clear_rect_becomes_its_whole_padded_rectangle() {
1442        let region = clear_rect_region(PendingClearRect {
1443            page_index: 2,
1444            x: 40,
1445            y: 8,
1446            width: 18,
1447            height: 22,
1448        });
1449
1450        assert_eq!(region.layer, 2);
1451        assert_eq!(region.offset, [40, 8]);
1452        assert_eq!(region.size, [18, 22]);
1453        assert_eq!(region.byte_len(), 18 * 22 * ATLAS_FORMAT_BYTES as usize);
1454    }
1455
1456    /// One solid-painted run's instances, checked against the shape the shader
1457    /// reads them at.
1458    ///
1459    /// The run carries a trailing sentinel: a strip's width is the *coverage*
1460    /// distance to the strip after it (`Strip::width_to`), not the distance
1461    /// between their x positions, which is why the generator emits a sentinel
1462    /// and why a caller that trimmed it would silently lose the last span.
1463    #[test]
1464    fn a_solid_run_expands_to_alpha_sampled_spans_carrying_one_colour() {
1465        let run = [
1466            Strip::new(4, 0, 0, false),
1467            Strip::new(20, 0, 32, false),
1468            Strip::new(64, 0, 80, false),
1469        ];
1470        let mut out = Vec::new();
1471
1472        let pushed = push_solid_strips(&run, 0xDEAD_BEEF, 7, &mut out);
1473
1474        assert_eq!(pushed, 2, "two pairs, each contributing one span");
1475        assert_eq!(out.len(), 2);
1476        for instance in &out {
1477            assert_eq!(instance.payload, 0xDEAD_BEEF, "a solid colour is per draw");
1478            assert_eq!(instance.depth_index, 7);
1479            assert!(!instance.is_rect());
1480            assert_eq!(
1481                instance.paint(),
1482                pack_paint_descriptor(PaintType::Solid, 0),
1483                "a solid instance indexes no encoded-paint record"
1484            );
1485            assert_eq!(
1486                instance.dense_width_or_rect_height, instance.width,
1487                "every column of a replayed glyph samples coverage"
1488            );
1489        }
1490        assert_eq!(out[0].x, 4);
1491        assert_eq!(
1492            out[0].width, 8,
1493            "32 coverage bytes at 4 per column is 8 pixels"
1494        );
1495        assert_eq!(out[0].col_idx_or_rect_frac, 0);
1496        assert_eq!(out[1].x, 20);
1497        assert_eq!(out[1].width, 12);
1498        assert_eq!(
1499            out[1].col_idx_or_rect_frac, 8,
1500            "the second span starts at the column its coverage does"
1501        );
1502    }
1503
1504    #[test]
1505    fn a_winding_gap_between_two_strips_is_filled_solid() {
1506        // The second strip carries the fill-gap flag on the same row, so the
1507        // span between them is inside the glyph and gets a coverage-free fill.
1508        let run = [
1509            Strip::new(0, 0, 0, false),
1510            Strip::new(32, 0, 16, true),
1511            Strip::new(48, 0, 32, false),
1512        ];
1513        let mut out = Vec::new();
1514
1515        let pushed = push_solid_strips(&run, 0x11, 0, &mut out);
1516
1517        assert_eq!(pushed, 3, "two spans plus the gap between them");
1518        let gap = out
1519            .iter()
1520            .find(|instance| instance.dense_width_or_rect_height == 0)
1521            .copied();
1522        let gap = match gap {
1523            Some(gap) => gap,
1524            None => unreachable!("the flagged pair fills its gap"),
1525        };
1526        assert_eq!(gap.x, 4, "the gap starts where the first strip ends");
1527        assert_eq!(gap.width, 28);
1528        assert_eq!(gap.payload, 0x11, "the fill takes the run's own colour");
1529        assert_eq!(gap.col_idx_or_rect_frac, 0, "a gap samples no coverage");
1530    }
1531
1532    #[test]
1533    fn a_run_with_no_pair_in_it_draws_nothing() {
1534        let mut out = Vec::new();
1535        assert_eq!(push_solid_strips(&[], 0, 0, &mut out), 0);
1536        assert_eq!(
1537            push_solid_strips(&[Strip::new(0, 0, 0, false)], 0, 0, &mut out),
1538            0,
1539            "a lone sentinel is not a span"
1540        );
1541        assert!(out.is_empty());
1542    }
1543
1544    #[test]
1545    fn an_emptied_page_keeps_its_capacity_for_the_next_one() {
1546        let mut buffers = AtlasPageBuffers::default();
1547        assert!(buffers.is_empty());
1548
1549        buffers.instances.push(GpuStrip::solid_fill(
1550            0,
1551            0,
1552            8,
1553            StripDraw {
1554                payload: 0,
1555                paint: 0,
1556                depth_index: 0,
1557            },
1558        ));
1559        buffers.alphas.extend_from_slice(&[1, 2, 3, 4]);
1560        assert!(!buffers.is_empty());
1561
1562        let instances = buffers.instances.capacity();
1563        let alphas = buffers.alphas.capacity();
1564        buffers.clear();
1565
1566        assert!(buffers.is_empty());
1567        assert!(buffers.alphas.is_empty());
1568        assert_eq!(buffers.instances.capacity(), instances);
1569        assert_eq!(buffers.alphas.capacity(), alphas);
1570    }
1571
1572    #[test]
1573    fn a_report_that_serviced_nothing_reads_as_empty() {
1574        let mut report = AtlasRenderReport::default();
1575        assert!(report.is_empty());
1576
1577        report.refused = 1;
1578        assert!(
1579            !report.is_empty(),
1580            "a refusal is work that happened, not an idle frame"
1581        );
1582    }
1583
1584    #[test]
1585    fn the_stand_in_paint_texture_width_is_reconstructable_by_the_shader() {
1586        // The shader rebuilds the width as `1 << bits`, so a stand-in that was
1587        // not a power of two would make the config disagree with the texture
1588        // actually bound.
1589        assert!(PLACEHOLDER_PAINT_TEX_WIDTH.is_power_of_two());
1590        assert_eq!(
1591            super::super::config::tex_width_bits(PLACEHOLDER_PAINT_TEX_WIDTH),
1592            0
1593        );
1594    }
1595
1596    #[test]
1597    fn a_regions_byte_footprint_and_stride_agree_with_rgba8() {
1598        let populated = region(0, [4, 8], [16, 32]);
1599        assert_eq!(populated.bytes_per_row(), 64);
1600        assert_eq!(populated.byte_len(), 64 * 32);
1601        assert_eq!(region_byte_len(populated), populated.byte_len());
1602        assert!(!populated.is_empty());
1603        assert!(region(0, [0, 0], [0, 4]).is_empty());
1604    }
1605}