Skip to main content

cranpose_render_wgpu/
render.rs

1use std::{
2    borrow::Cow,
3    cell::Cell,
4    hash::{Hash, Hasher},
5    rc::Rc,
6    sync::{Arc, mpsc},
7    time::Duration,
8};
9
10use bytemuck::{Pod, Zeroable};
11use cranpose_core::{
12    NodeId,
13    collections::{bounded_lru::BoundedLruCache, map::HashMap},
14    hash::default as default_hash,
15};
16use cranpose_render_common::{
17    geometry::blur_reach,
18    graph::{DrawCommandId, quad_bounds},
19    software_text_raster::{
20        SoftwareGlyphAtlasGlyph, SoftwareGlyphAtlasKey, SoftwareGlyphAtlasPlacement,
21        SoftwareGlyphAtlasRunGlyph, SoftwareGlyphRasterCache, SoftwareTextFontSet,
22        collect_solid_text_atlas_run, rasterize_annotated_text_region,
23        rasterize_text_to_image_with_glyph_cache,
24    },
25    text_mask_gamma::TextLuminance,
26};
27use cranpose_ui_graphics::{
28    BlendMode, Color, ColorFilter, FRAGMENT_KIND_ARC, FRAGMENT_KIND_FILL, FRAGMENT_KIND_LINE,
29    FRAGMENT_KIND_STROKE, FxHasher, ImageBitmap, ImageSampling, Point, RecordSegment, Rect,
30    RenderHash, TileMode,
31};
32use smallvec::SmallVec;
33use web_time::Instant;
34
35use crate::{
36    DebugCpuAllocationStats,
37    ablation::{Ablation, ShapeAblation},
38    arc_trig_fill::ArcTrigFill,
39    collect::LayerScene,
40    debug_toggles::DebugToggle,
41    draw_pass::{PassSegment, PassTarget, ResolvedComposite, ResolvedCompositeKind, SourceContent},
42    effect_renderer::{CompositeSampleMode, EffectRenderer, RoundedCompositeMask},
43    frame::{AdmissionGate, FrameExecutor},
44    frame_graph::{
45        BufferUpload, FrameCommandRecorder, FrameCommandStats, FrameTextureDescriptor,
46        FrameUploadAllocators, UniformUpload, UploadAllocatorId, UploadAllocatorSpec,
47        WgpuFrameGraph, WgpuFrameGraphExecutor,
48    },
49    frame_packet::{CancelReason, FramePacket, FrameSceneStorage, PresentOutcome, RenderReturns},
50    geometry::{
51        DevicePixelBounds, SegmentTransform, anchored_device_rect, axis_aligned_quad_rect,
52        canonicalize_device_coordinate, canonicalized_scaled_quad, offscreen_byte_size,
53        scaled_quad, snap_delta_for_anchor, translate_quad,
54        translation_stable_anchored_device_pixel_bounds,
55    },
56    glyph_run::{RunGlyphScratch, RunGlyphs},
57    glyph_run_arena::{GlyphRunArena, GlyphRunSpan},
58    gpu_stats::{self, gpu_stats_enabled},
59    layer_cache::LayerCache,
60    lazy_resource::LazyGpuResource,
61    offscreen::{OffscreenTarget, composition_bytes_per_pixel},
62    output_conversion::OutputConverter,
63    pipeline_compiler::{CompilerSend, PipelineCompilation, PipelineCompiler},
64    record_columns::record_vertex_layouts,
65    rect_to_quad,
66    run_store::{ArenaBinding, PlacementData, RunBufferMode, RunDrawCall, RunStore},
67    scene::{
68        CompositorScene, DrawOp, DrawOpKind, ImageDraw, RunDraw, ShadowDraw, SnapAnchor, TextDraw,
69    },
70    shaders,
71    shape_pipelines::{ShapePipelineFactory, ShapePipelines},
72    shared_shader::SharedShader,
73};
74const MAX_SHADOW_SURFACE_CACHE_ITEMS: usize = 512;
75const MAX_TRANSPARENT_SOURCES: usize = 16;
76const MAX_SHADOW_SURFACE_CACHE_BYTES: u64 = 384 * 1024 * 1024;
77
78static SKIP_SHADOWS: DebugToggle = DebugToggle::new("CRANPOSE_SKIP_SHADOWS");
79
80fn skip_shadow_draws() -> bool {
81    SKIP_SHADOWS.flag()
82}
83const MAX_TEXT_IMAGE_CACHE_ITEMS: usize = 1024;
84const MAX_TEXT_GLYPH_MASK_CACHE_ITEMS: usize = 8192;
85const MAX_TEXT_GLYPH_ATLAS_ITEMS: usize = 8192;
86const MAX_TEXT_GLYPH_RUN_CACHE_ITEMS: usize = 1024;
87const MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS: usize = 1024;
88/// Frames a text run may go undrawn before its glyphs and quads are freed,
89/// on the CPU and in retained GPU buffers alike. Shorter than the shape
90/// store's span: a list scrolling at speed leaves several screens of text
91/// behind each second, which at 120 frames held 12 to 19 MB of GPU quads on
92/// a scrolling feed, and the CPU runs, bounded only by their count, held
93/// another 4.6 MB of placements and quads.
94const TEXT_GLYPH_RUN_IDLE_FRAMES: u64 = 30;
95/// Text runs with at least this many glyphs keep their quads in retained GPU
96/// buffers and draw on their own; shorter ones are written into the frame's
97/// shared quads each frame, where consecutive runs share a draw.
98const RETAINED_TEXT_GLYPH_RUN_MIN_QUADS: usize = 64;
99
100const TEXT_GLYPH_ATLAS_MIN_SIZE: u32 = 512;
101const TEXT_GLYPH_ATLAS_MAX_SIZE: u32 = 4096;
102const _: () = assert!(TEXT_GLYPH_ATLAS_MAX_SIZE <= u16::MAX as u32);
103const TEXT_GLYPH_ATLAS_PADDING: u32 = 1;
104const MAX_TEXT_LINE_INDEX_CACHE_ITEMS: usize = 512;
105const MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER: usize = 2;
106
107const CACHE_MISS_WARMUP_FRAMES: u8 = 1;
108pub(crate) const CLEAR_COLOR: wgpu::Color = wgpu::Color {
109    r: cranpose_render_common::FRAME_CLEAR_COLOR[0] as f64,
110    g: cranpose_render_common::FRAME_CLEAR_COLOR[1] as f64,
111    b: cranpose_render_common::FRAME_CLEAR_COLOR[2] as f64,
112    a: cranpose_render_common::FRAME_CLEAR_COLOR[3] as f64,
113};
114const MAX_TEXTURE_CACHE_ITEMS: usize = 256;
115const MAX_IMAGE_TEXTURE_CACHE_BYTES: usize = 256 * 1024 * 1024;
116
117const DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
118
119fn wgpu_render_stage_telemetry_threshold_ms() -> Option<f64> {
120    static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
121    *THRESHOLD_MS.get_or_init(|| {
122        let explicit =
123            crate::debug_toggles::debug_toggle("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY_MS")
124                .and_then(|value| value.parse::<f64>().ok())
125                .filter(|value| value.is_finite() && *value >= 0.0);
126        explicit.or_else(|| {
127            std::env::var_os("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY")
128                .is_some()
129                .then_some(DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS)
130        })
131    })
132}
133
134pub(crate) fn instant_ms(start: Instant, end: Instant) -> f64 {
135    end.duration_since(start).as_secs_f64() * 1000.0
136}
137
138pub(crate) fn should_log_wgpu_render_stage(start: Instant, end: Instant) -> Option<f64> {
139    let threshold_ms = wgpu_render_stage_telemetry_threshold_ms()?;
140    let total_ms = instant_ms(start, end);
141    (total_ms >= threshold_ms).then_some(total_ms)
142}
143
144pub static PRESENTED_FRAMES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
145
146pub fn frames_presented() -> u64 {
147    PRESENTED_FRAMES.load(std::sync::atomic::Ordering::Relaxed)
148}
149
150fn text_atlas_fallback_diag_enabled() -> bool {
151    cranpose_core::env_flag!("CRANPOSE_TEXT_ATLAS_FALLBACK_DIAG")
152}
153
154#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
155struct ShadowSurfaceCacheKey {
156    content_hash: u64,
157    pixel_size: [u32; 2],
158    root_scale_bits: u32,
159    blur_radius_bits: u32,
160}
161
162struct CachedShadowSurface {
163    target: Rc<OffscreenTarget>,
164    byte_size: u64,
165}
166
167type DeviceRect4 = (f32, f32, f32, f32);
168/// A rect of whole target pixels: x, y, width, height.
169pub(crate) type TargetRect = (u32, u32, u32, u32);
170
171/// The target pixels a clipped shape variant keeps of `placement`'s records
172/// under `viewport`, as a scissor that stands in for its clip test: every
173/// pixel whose centre, moved by the viewport's offset, lies inside the
174/// device clip, inclusive of its edges, as the shader compares them. `None`
175/// without a clip or under a turn, where the variant tests the clip itself.
176/// An empty clip keeps an empty scissor.
177pub(crate) fn clip_scissor(
178    placement: &crate::scene::Placement,
179    root_scale: f32,
180    viewport: ViewportUniformParams,
181) -> Option<TargetRect> {
182    if !viewport.transform.is_identity() {
183        return None;
184    }
185    let [x, y, width, height] = crate::run_store::device_clip(placement, root_scale)?;
186    let columns = kept_pixels(x, x + width, viewport.offset[0], viewport.width);
187    let rows = kept_pixels(y, y + height, viewport.offset[1], viewport.height);
188    Some((
189        columns.start,
190        rows.start,
191        columns.end - columns.start,
192        rows.end - rows.start,
193    ))
194}
195
196/// The pixels along one axis whose centre, moved by `offset`, lies within
197/// `low..=high`, within `0..extent`: stepped with the shader's own float
198/// sums so the edges fall where its comparisons put them.
199fn kept_pixels(low: f32, high: f32, offset: f32, extent: u32) -> std::ops::Range<u32> {
200    let centre = |pixel: u32| pixel as f32 + 0.5 + offset;
201    let guess = |edge: f32| (edge - offset - 0.5).ceil().clamp(0.0, extent as f32) as u32;
202    let mut start = guess(low);
203    while start > 0 && centre(start - 1) >= low {
204        start -= 1;
205    }
206    while start < extent && centre(start) < low {
207        start += 1;
208    }
209    let mut end = guess(high).max(start);
210    while end > start && centre(end - 1) > high {
211        end -= 1;
212    }
213    while end < extent && centre(end) <= high {
214        end += 1;
215    }
216    start..end
217}
218
219/// The pixels both scissors keep, `None` when they share none.
220pub(crate) fn intersect_target_rects(a: TargetRect, b: TargetRect) -> Option<TargetRect> {
221    let left = a.0.max(b.0);
222    let top = a.1.max(b.1);
223    let right = (a.0 + a.2).min(b.0 + b.2);
224    let bottom = (a.1 + a.3).min(b.1 + b.3);
225    (right > left && bottom > top).then(|| (left, top, right - left, bottom - top))
226}
227
228/// A draw's scissor cut down to the pixels its pass segment may touch;
229/// `None` when nothing of it remains.
230pub(crate) fn bounded_scissor(
231    scissor: (u32, u32, u32, u32),
232    bound: Option<(u32, u32, u32, u32)>,
233) -> Option<(u32, u32, u32, u32)> {
234    let Some((bx, by, bw, bh)) = bound else {
235        return Some(scissor);
236    };
237    let (x, y, width, height) = scissor;
238    let left = x.max(bx);
239    let top = y.max(by);
240    let right = (x + width).min(bx + bw);
241    let bottom = (y + height).min(by + bh);
242    (right > left && bottom > top).then(|| (left, top, right - left, bottom - top))
243}
244
245/// The scissor an unturned shared glyph run needs and the target pixels it
246/// touches. A run whose quads all lie inside `scissor` needs none of its own,
247/// which lets it share a draw with its neighbours. A turned run never needs
248/// one: its quads were cut to its clip before the turn.
249fn shared_glyph_clip(
250    glyphs: &[GlyphInstance],
251    scissor: TargetRect,
252    viewport: ViewportUniformParams,
253) -> (Option<TargetRect>, TargetRect) {
254    if glyphs.is_empty() {
255        return (Some(scissor), scissor);
256    }
257    let (mut left, mut top) = (f32::INFINITY, f32::INFINITY);
258    let (mut right, mut bottom) = (f32::NEG_INFINITY, f32::NEG_INFINITY);
259    for glyph in glyphs {
260        left = left.min(glyph.rect[0]);
261        top = top.min(glyph.rect[1]);
262        right = right.max(glyph.rect[2]);
263        bottom = bottom.max(glyph.rect[3]);
264    }
265    let left = (left - viewport.offset[0]).floor().max(0.0);
266    let top = (top - viewport.offset[1]).floor().max(0.0);
267    let right = (right - viewport.offset[0]).ceil();
268    let bottom = (bottom - viewport.offset[1]).ceil();
269    let (x, y, width, height) = scissor;
270    let inside = left >= x as f32
271        && top >= y as f32
272        && right <= (x + width) as f32
273        && bottom <= (y + height) as f32;
274    if !inside || right <= left || bottom <= top {
275        return (Some(scissor), scissor);
276    }
277    let bounds = (
278        left as u32,
279        top as u32,
280        (right - left) as u32,
281        (bottom - top) as u32,
282    );
283    (None, bounds)
284}
285
286fn intersect_device_rects(a: DeviceRect4, b: DeviceRect4) -> Option<DeviceRect4> {
287    let left = a.0.max(b.0);
288    let top = a.1.max(b.1);
289    let right = (a.0 + a.2).min(b.0 + b.2);
290    let bottom = (a.1 + a.3).min(b.1 + b.3);
291    (right > left && bottom > top).then_some((left, top, right - left, bottom - top))
292}
293
294fn anchored_rect_to_device(
295    rect: Rect,
296    snap_anchor: Option<SnapAnchor>,
297    root_scale: f32,
298) -> DeviceRect4 {
299    let device = anchored_device_rect(rect, snap_anchor, root_scale);
300    (device.x, device.y, device.width, device.height)
301}
302
303fn mask_rect(rect: Rect) -> [f32; 4] {
304    [rect.x, rect.y, rect.width, rect.height]
305}
306
307/// The parts of a shadow's covered device rect that lie outside its
308/// occluder: up to four disjoint bands (above, below, left of and right of
309/// the occluder) that together tile the coverage minus the occluder's whole
310/// interior pixels. A fractional occluder shrinks inward so no covered pixel
311/// is skipped.
312fn shadow_bands(
313    coverage: DeviceRect4,
314    occluder: Option<DeviceRect4>,
315) -> SmallVec<[DeviceRect4; 4]> {
316    let mut bands = SmallVec::new();
317    let (cx, cy, cw, ch) = coverage;
318    let (cr, cb) = (cx + cw, cy + ch);
319    let Some((ox, oy, ow, oh)) = occluder else {
320        bands.push(coverage);
321        return bands;
322    };
323    let left = ox.ceil().max(cx);
324    let top = oy.ceil().max(cy);
325    let right = (ox + ow).floor().min(cr);
326    let bottom = (oy + oh).floor().min(cb);
327    if right <= left || bottom <= top {
328        bands.push(coverage);
329        return bands;
330    }
331    if top > cy {
332        bands.push((cx, cy, cw, top - cy));
333    }
334    if bottom < cb {
335        bands.push((cx, bottom, cw, cb - bottom));
336    }
337    if left > cx {
338        bands.push((cx, top, left - cx, bottom - top));
339    }
340    if right < cr {
341        bands.push((right, top, cr - right, bottom - top));
342    }
343    bands
344}
345
346fn banded_pixels(bands: &[DeviceRect4]) -> u64 {
347    bands
348        .iter()
349        .map(|band| (band.2 as u64).saturating_mul(band.3 as u64))
350        .sum()
351}
352
353#[cfg(test)]
354#[path = "tests/render_shadow_band_tests.rs"]
355mod shadow_band_tests;
356#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
357struct TextImageCacheKey(u64);
358
359struct CachedTextImage {
360    image: ImageBitmap,
361}
362
363#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
364struct TextGlyphRunCacheKey(u64);
365
366#[derive(Clone, Copy)]
367struct CachedTextGlyphQuad {
368    x: i32,
369    y: i32,
370    width: usize,
371    height: usize,
372    color: (f32, f32, f32, f32),
373    uv: ImageUvRect,
374}
375
376struct CachedTextGlyphRun {
377    /// The run's glyphs that draw: zero-sized and transparent ones are left
378    /// out when the run is collected.
379    glyphs: RunGlyphs,
380    bounds: GlyphRunBounds,
381    /// Where each of `glyphs` sits in the atlas at `atlas_generation`. A
382    /// glyph's quad is derived from the two as it is drawn, so no glyph is
383    /// held twice.
384    atlas_entries: Option<Rc<[GlyphAtlasEntry]>>,
385    atlas_generation: u64,
386    /// The frame the run last drew in; see [`TEXT_GLYPH_RUN_IDLE_FRAMES`].
387    last_frame: Cell<u64>,
388}
389
390/// A cached run drawn as quads: its glyphs and their atlas entries, each
391/// quad derived as it is read.
392#[derive(Clone, Copy)]
393struct GlyphRunQuads<'a> {
394    glyphs: &'a RunGlyphs,
395    entries: &'a [GlyphAtlasEntry],
396    atlas_size: u32,
397    bounds: GlyphRunBounds,
398}
399
400/// The pixel box a run's drawing glyphs cover, from its raster origin.
401#[derive(Clone, Copy, Debug, PartialEq)]
402struct GlyphRunBounds {
403    min: [i32; 2],
404    max: [i32; 2],
405}
406
407impl GlyphRunBounds {
408    fn of(glyphs: impl IntoIterator<Item = SoftwareGlyphAtlasPlacement>) -> Self {
409        let mut bounds = Self {
410            min: [i32::MAX; 2],
411            max: [i32::MIN; 2],
412        };
413        for glyph in glyphs {
414            let width = i32::try_from(glyph.width).unwrap_or(i32::MAX);
415            let height = i32::try_from(glyph.height).unwrap_or(i32::MAX);
416            bounds.min = [bounds.min[0].min(glyph.x), bounds.min[1].min(glyph.y)];
417            bounds.max = [
418                bounds.max[0].max(glyph.x.saturating_add(width)),
419                bounds.max[1].max(glyph.y.saturating_add(height)),
420            ];
421        }
422        bounds
423    }
424
425    /// Whether every glyph of the run at `source_raster_rect` lies inside
426    /// what `viewport` shows of `clip`, so none needs its own test.
427    fn within_viewport(
428        self,
429        source_raster_rect: Rect,
430        clip: Option<Rect>,
431        viewport: ViewportUniformParams,
432        root_scale: f32,
433    ) -> bool {
434        if self.min[0] > self.max[0] || !root_scale.is_finite() || root_scale <= 0.0 {
435            return false;
436        }
437        let left = (source_raster_rect.x + self.min[0] as f32) / root_scale;
438        let top = (source_raster_rect.y + self.min[1] as f32) / root_scale;
439        let right = (source_raster_rect.x + self.max[0] as f32) / root_scale;
440        let bottom = (source_raster_rect.y + self.max[1] as f32) / root_scale;
441        let viewport_rect = viewport.scene_rect(root_scale);
442        let visible = match clip {
443            Some(clip) => clip.intersect(viewport_rect),
444            None => Some(viewport_rect),
445        };
446        visible
447            .is_some_and(|visible| visible.contains(left, top) && visible.contains(right, bottom))
448    }
449}
450
451/// A text's glyph run as the frame found it.
452struct TextGlyphRunLookup {
453    glyphs: RunGlyphs,
454    bounds: GlyphRunBounds,
455    /// Whether the run came from the cache rather than from this frame's
456    /// collection.
457    cached: bool,
458    /// The glyphs' atlas entries, while the atlas they were taken from holds.
459    entries: Option<Rc<[GlyphAtlasEntry]>>,
460}
461
462/// A text's glyph run and where it draws.
463struct TextGlyphRunDraw<'a> {
464    text_draw: &'a TextDraw,
465    raster_rect: Rect,
466    run_key: TextGlyphRunCacheKey,
467    run: TextGlyphRunLookup,
468}
469
470/// Logs why a text fell back from the glyph atlas, when
471/// `CRANPOSE_TEXT_ATLAS_FALLBACK_DIAG` asks.
472fn log_text_atlas_fallback(text_draw: &TextDraw) {
473    if !text_atlas_fallback_diag_enabled() {
474        return;
475    }
476    let preview: String = text_draw.text.text().chars().take(96).collect();
477    log::warn!(
478        "[text-atlas-fallback] node={:?} spans={} links={} text_len={} preview={:?} span_style={:?} paragraph_style={:?}",
479        text_draw.node_id,
480        text_draw.text.span_styles().len(),
481        text_draw.text.links().len(),
482        text_draw.text.text().len(),
483        preview,
484        text_draw.text_style.span_style,
485        text_draw.text_style.paragraph_style,
486    );
487}
488
489impl GlyphRunQuads<'_> {
490    fn len(&self) -> usize {
491        self.entries.len()
492    }
493
494    fn iter(&self) -> impl Iterator<Item = CachedTextGlyphQuad> + '_ {
495        self.glyphs
496            .iter()
497            .zip(self.entries)
498            .map(|(glyph, entry)| cached_text_glyph_quad(&glyph, *entry, self.atlas_size))
499    }
500}
501
502struct CachedGpuTextGlyphRun {
503    span: GlyphRunSpan,
504    atlas_generation: u64,
505    /// The frame the run last drew in; a run idle for
506    /// [`TEXT_GLYPH_RUN_IDLE_FRAMES`] gives its quads back.
507    last_frame: Cell<u64>,
508}
509
510#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
511struct TextLineIndexCacheKey(usize);
512
513struct CachedTextLineIndex {
514    text: std::sync::Weak<cranpose_ui::text::RenderString>,
515    len: usize,
516    starts: Rc<[usize]>,
517}
518
519struct TextLineIndexCache {
520    entries: BoundedLruCache<TextLineIndexCacheKey, CachedTextLineIndex>,
521}
522
523impl TextLineIndexCache {
524    fn new(capacity: usize) -> Self {
525        Self {
526            entries: BoundedLruCache::with_capacity_at_least_one(capacity),
527        }
528    }
529
530    fn line_starts(&mut self, text: &Arc<cranpose_ui::text::RenderString>) -> Rc<[usize]> {
531        let key = TextLineIndexCacheKey(Arc::as_ptr(text) as usize);
532        if let Some(cached) = self.entries.get(&key)
533            && cached.len == text.text().len()
534            && cached
535                .text
536                .upgrade()
537                .is_some_and(|cached_text| Arc::ptr_eq(&cached_text, text))
538        {
539            return cached.starts.clone();
540        }
541
542        let starts = Rc::<[usize]>::from(line_start_offsets(text.text()));
543        self.entries.put(
544            key,
545            CachedTextLineIndex {
546                text: Arc::downgrade(text),
547                len: text.text().len(),
548                starts: starts.clone(),
549            },
550        );
551        starts
552    }
553}
554
555#[derive(Default)]
556struct DeviceErrorSentry {
557    errors: std::sync::atomic::AtomicU64,
558    poisoned: std::sync::atomic::AtomicBool,
559}
560
561impl DeviceErrorSentry {
562    fn record(&self, error: &wgpu::Error) {
563        use std::sync::atomic::Ordering;
564        self.poisoned.store(true, Ordering::Release);
565        let count = self.errors.fetch_add(1, Ordering::Relaxed) + 1;
566        if count.is_power_of_two() {
567            log::error!("[gpu-device] uncaptured wgpu error #{count}: {error}");
568        }
569    }
570
571    fn take_poison(&self) -> bool {
572        self.poisoned
573            .swap(false, std::sync::atomic::Ordering::AcqRel)
574    }
575
576    fn error_count(&self) -> u64 {
577        self.errors.load(std::sync::atomic::Ordering::Relaxed)
578    }
579}
580
581fn is_blend_mode_supported(mode: BlendMode) -> bool {
582    matches!(
583        mode,
584        BlendMode::Src | BlendMode::SrcOver | BlendMode::DstOut
585    )
586}
587
588fn blend_state_for_mode(mode: BlendMode) -> wgpu::BlendState {
589    match mode {
590        BlendMode::Src => wgpu::BlendState::REPLACE,
591        BlendMode::DstOut => wgpu::BlendState {
592            color: wgpu::BlendComponent {
593                src_factor: wgpu::BlendFactor::Zero,
594                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
595                operation: wgpu::BlendOperation::Add,
596            },
597            alpha: wgpu::BlendComponent {
598                src_factor: wgpu::BlendFactor::Zero,
599                dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
600                operation: wgpu::BlendOperation::Add,
601            },
602        },
603        _ => wgpu::BlendState::ALPHA_BLENDING,
604    }
605}
606
607pub(crate) fn supported_blend_mode(mode: BlendMode) -> BlendMode {
608    if is_blend_mode_supported(mode) {
609        return mode;
610    }
611
612    BlendMode::SrcOver
613}
614
615pub(crate) fn hash_f32_for_cache<H: Hasher>(value: f32, state: &mut H) {
616    value.to_bits().hash(state);
617}
618
619fn hash_text_raster_geometry_for_cache<H: Hasher>(
620    rect: Rect,
621    static_text_motion: bool,
622    state: &mut H,
623) {
624    hash_f32_for_cache(rect.width, state);
625    hash_f32_for_cache(rect.height, state);
626    static_text_motion.hash(state);
627    if !static_text_motion {
628        hash_f32_for_cache(rect.x.fract(), state);
629        hash_f32_for_cache(rect.y.fract(), state);
630    }
631}
632
633fn text_logical_geometry_for_draw(text_draw: &TextDraw, root_scale: f32) -> Option<(Rect, f32)> {
634    if text_draw.text.is_empty()
635        || text_draw.rect.width <= 0.0
636        || text_draw.rect.height <= 0.0
637        || !root_scale.is_finite()
638        || root_scale <= 0.0
639    {
640        return None;
641    }
642
643    let text_scale = text_draw.scale * root_scale;
644    if !text_scale.is_finite() || text_scale <= 0.0 {
645        return None;
646    }
647
648    let snap_delta = text_draw
649        .snap_anchor
650        .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
651        .unwrap_or_default();
652    let logical_rect = text_draw.rect.translate(snap_delta.x, snap_delta.y);
653    Some((logical_rect, text_scale))
654}
655
656fn text_raster_geometry_for_draw(
657    text_draw: &TextDraw,
658    root_scale: f32,
659) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
660    let (logical_rect, text_scale) = text_logical_geometry_for_draw(text_draw, root_scale)?;
661    let static_text_motion = text_draw
662        .text_style
663        .paragraph_style
664        .text_motion
665        .unwrap_or(cranpose_ui::text::TextMotion::Static)
666        == cranpose_ui::text::TextMotion::Static;
667    let clip = text_draw.clip;
668    let mut raster_rect = Rect {
669        x: logical_rect.x * root_scale,
670        y: logical_rect.y * root_scale,
671        width: logical_rect.width * root_scale,
672        height: logical_rect.height * root_scale,
673    };
674    if text_draw.snap_anchor.is_some() {
675        raster_rect.x = canonicalize_device_coordinate(raster_rect.x);
676        raster_rect.y = canonicalize_device_coordinate(raster_rect.y);
677    }
678    if static_text_motion {
679        raster_rect.x = raster_rect.x.round();
680        raster_rect.y = raster_rect.y.round();
681    }
682    raster_rect.width = raster_rect.width.ceil().max(1.0);
683    raster_rect.height = raster_rect.height.ceil().max(1.0);
684    Some((
685        logical_rect,
686        raster_rect,
687        clip,
688        text_scale,
689        static_text_motion,
690    ))
691}
692
693fn text_draw_is_visible_in_viewport(
694    logical_rect: Rect,
695    clip: Option<Rect>,
696    viewport: ViewportUniformParams,
697    root_scale: f32,
698) -> bool {
699    draw_rect_is_visible_in_viewport(logical_rect, clip, viewport, root_scale)
700}
701
702fn expand_rect(rect: Rect, margin_x: f32, margin_y: f32) -> Rect {
703    Rect {
704        x: rect.x - margin_x,
705        y: rect.y - margin_y,
706        width: rect.width + margin_x * 2.0,
707        height: rect.height + margin_y * 2.0,
708    }
709}
710
711fn draw_rect_is_visible_in_viewport(
712    rect: Rect,
713    clip: Option<Rect>,
714    viewport: ViewportUniformParams,
715    root_scale: f32,
716) -> bool {
717    if !root_scale.is_finite() || root_scale <= 0.0 {
718        return false;
719    }
720    rect_is_visible_in_rect(rect, clip, viewport.scene_rect(root_scale))
721}
722
723fn rect_is_visible_in_rect(rect: Rect, clip: Option<Rect>, viewport_rect: Rect) -> bool {
724    let visible_rect = match clip {
725        Some(clip) => clip.intersect(viewport_rect),
726        None => Some(viewport_rect),
727    };
728    visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
729}
730
731fn snapped_quad_bounds(quad: [[f32; 2]; 4], anchor: Option<SnapAnchor>, root_scale: f32) -> Rect {
732    let snap_delta = anchor
733        .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
734        .unwrap_or_default();
735    quad_bounds(translate_quad(quad, snap_delta))
736}
737
738/// The logical rect a draw may touch: its snapped bounds within its clip,
739/// `None` when the clip leaves nothing.
740fn clipped_bounds(rect: Rect, clip: Option<Rect>) -> Option<Rect> {
741    match clip {
742        Some(clip) => rect.intersect(clip),
743        None => Some(rect),
744    }
745}
746
747pub(crate) fn text_draw_bounds(text: &TextDraw, root_scale: f32) -> Option<Rect> {
748    text_logical_geometry_for_draw(text, root_scale)
749        .and_then(|(logical_rect, _)| clipped_bounds(logical_rect, text.clip))
750}
751
752pub(crate) fn image_draw_bounds(image: &ImageDraw, root_scale: f32) -> Option<Rect> {
753    clipped_bounds(
754        snapped_quad_bounds(image.quad, image.snap_anchor, root_scale),
755        image.clip,
756    )
757}
758
759pub(crate) fn run_draw_bounds(run: &RunDraw, root_scale: f32) -> Option<Rect> {
760    let snap_delta = run
761        .placement
762        .snap_anchor
763        .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
764        .unwrap_or_default();
765    clipped_bounds(
766        run.bounds.translate(snap_delta.x, snap_delta.y),
767        run.placement.clip,
768    )
769}
770
771pub(crate) fn text_draw_is_visible_in_rect(
772    text: &TextDraw,
773    viewport_rect: Rect,
774    root_scale: f32,
775) -> bool {
776    text_draw_bounds(text, root_scale)
777        .is_some_and(|bounds| bounds.intersect(viewport_rect).is_some())
778}
779
780pub(crate) fn run_draw_is_visible_in_rect(
781    run: &RunDraw,
782    viewport_rect: Rect,
783    root_scale: f32,
784) -> bool {
785    run_draw_bounds(run, root_scale).is_some_and(|bounds| bounds.intersect(viewport_rect).is_some())
786}
787
788fn cached_text_glyph_quad(
789    glyph: &SoftwareGlyphAtlasPlacement,
790    entry: GlyphAtlasEntry,
791    atlas_size: u32,
792) -> CachedTextGlyphQuad {
793    CachedTextGlyphQuad {
794        x: glyph.x,
795        y: glyph.y,
796        width: glyph.width,
797        height: glyph.height,
798        color: (
799            glyph.color.0.clamp(0.0, 1.0),
800            glyph.color.1.clamp(0.0, 1.0),
801            glyph.color.2.clamp(0.0, 1.0),
802            glyph.color.3.clamp(0.0, 1.0),
803        ),
804        uv: glyph_atlas_uv_rect(entry, atlas_size),
805    }
806}
807
808/// `quad` at `source_raster_rect`'s origin. `None` for a quad that draws
809/// nothing.
810fn cached_text_glyph_instance(
811    source_raster_rect: Rect,
812    quad: &CachedTextGlyphQuad,
813) -> Option<GlyphInstance> {
814    if quad.width == 0 || quad.height == 0 || quad.color.3 <= 0.0 {
815        return None;
816    }
817    let x0 = source_raster_rect.x + quad.x as f32;
818    let y0 = source_raster_rect.y + quad.y as f32;
819    Some(GlyphInstance {
820        rect: [x0, y0, x0 + quad.width as f32, y0 + quad.height as f32],
821        uv: [
822            quad.uv.min[0],
823            quad.uv.min[1],
824            quad.uv.max[0],
825            quad.uv.max[1],
826        ],
827        uv_bounds: quad.uv.sample_bounds,
828        color: [quad.color.0, quad.color.1, quad.color.2, quad.color.3],
829    })
830}
831
832fn cached_text_glyph_quad_logical_rect(
833    source_raster_rect: Rect,
834    quad: &CachedTextGlyphQuad,
835    root_scale: f32,
836) -> Option<Rect> {
837    if !root_scale.is_finite() || root_scale <= 0.0 {
838        return None;
839    }
840    Some(Rect {
841        x: (source_raster_rect.x + quad.x as f32) / root_scale,
842        y: (source_raster_rect.y + quad.y as f32) / root_scale,
843        width: quad.width as f32 / root_scale,
844        height: quad.height as f32 / root_scale,
845    })
846}
847
848fn cached_text_glyph_quad_is_visible_in_viewport(
849    source_raster_rect: Rect,
850    quad: &CachedTextGlyphQuad,
851    clip: Option<Rect>,
852    viewport: ViewportUniformParams,
853    root_scale: f32,
854) -> bool {
855    cached_text_glyph_quad_logical_rect(source_raster_rect, quad, root_scale)
856        .is_some_and(|rect| draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale))
857}
858
859const SHADOW_CACHE_DEVICE_QUANT: f32 = 16.0;
860
861pub(crate) fn hash_shadow_device_offset<H: Hasher>(
862    value: f32,
863    origin: f32,
864    root_scale: f32,
865    state: &mut H,
866) {
867    let quantized = ((value - origin) * root_scale * SHADOW_CACHE_DEVICE_QUANT).round();
868    (quantized as i64).hash(state);
869}
870
871pub(crate) fn hash_shadow_device_rect<H: Hasher>(
872    rect: Rect,
873    origin_x: f32,
874    origin_y: f32,
875    root_scale: f32,
876    state: &mut H,
877) {
878    hash_shadow_device_offset(rect.x, origin_x, root_scale, state);
879    hash_shadow_device_offset(rect.y, origin_y, root_scale, state);
880    hash_shadow_device_offset(rect.width, 0.0, root_scale, state);
881    hash_shadow_device_offset(rect.height, 0.0, root_scale, state);
882}
883
884fn hash_placement<H: Hasher>(
885    placement: &crate::scene::Placement,
886    clip: Option<Rect>,
887    origin_x: f32,
888    origin_y: f32,
889    root_scale: f32,
890    state: &mut H,
891) {
892    hash_shadow_device_offset(placement.offset.x, origin_x, root_scale, state);
893    hash_shadow_device_offset(placement.offset.y, origin_y, root_scale, state);
894    match placement.snap_anchor {
895        Some(anchor) => {
896            1u8.hash(state);
897            hash_shadow_device_offset(anchor.origin.x, origin_x, root_scale, state);
898            hash_shadow_device_offset(anchor.origin.y, origin_y, root_scale, state);
899            hash_f32_for_cache(anchor.device_pixel_step, state);
900        }
901        None => 0u8.hash(state),
902    }
903    match clip {
904        Some(clip) => {
905            1u8.hash(state);
906            hash_shadow_device_rect(clip, origin_x, origin_y, root_scale, state);
907            hash_f32_for_cache(placement.clip_radius * root_scale, state);
908        }
909        None => 0u8.hash(state),
910    }
911    hash_f32_for_cache(placement.alpha, state);
912    match placement.color_filter {
913        Some(filter) => {
914            1u8.hash(state);
915            filter.render_hash().hash(state);
916        }
917        None => 0u8.hash(state),
918    }
919}
920
921/// Hashes what a run draws relative to `origin`: its records by
922/// fingerprint and segment range, and its placement in device units, so a
923/// run moving rigidly by whole pixels hashes the same.
924pub(crate) fn hash_run_item<H: Hasher>(
925    run: &RunDraw,
926    origin_x: f32,
927    origin_y: f32,
928    root_scale: f32,
929    state: &mut H,
930) {
931    hash_run_item_with_clip(
932        run,
933        run.placement.clip,
934        origin_x,
935        origin_y,
936        root_scale,
937        state,
938    );
939}
940
941pub(crate) fn hash_run_item_with_clip<H: Hasher>(
942    run: &RunDraw,
943    clip: Option<Rect>,
944    origin_x: f32,
945    origin_y: f32,
946    root_scale: f32,
947    state: &mut H,
948) {
949    run.tables().fingerprint().hash(state);
950    run.segments.start.hash(state);
951    run.segments.end.hash(state);
952    hash_shadow_device_rect(run.bounds, origin_x, origin_y, root_scale, state);
953    hash_placement(&run.placement, clip, origin_x, origin_y, root_scale, state);
954}
955
956/// What a shadow's casters draw, independent of where the shadow sits to
957/// the whole device pixel: the recordings, and the placement relative to
958/// the casters' bounds.
959pub(crate) fn shadow_content_hash(shadow: &ShadowDraw, root_scale: f32) -> u64 {
960    let mut hasher = FxHasher::default();
961    let origin = shape_shadow_bounds(shadow).unwrap_or(Rect {
962        x: 0.0,
963        y: 0.0,
964        width: 0.0,
965        height: 0.0,
966    });
967    for run in shadow.shapes.iter().chain(&shadow.post_blur_cutouts) {
968        hash_run_item(run, origin.x, origin.y, root_scale, &mut hasher);
969    }
970    hasher.finish()
971}
972
973fn shape_shadow_surface_cache_key(
974    shadow: &ShadowDraw,
975    device_bounds: DevicePixelBounds,
976    pixel_radius: f32,
977    root_scale: f32,
978) -> Option<ShadowSurfaceCacheKey> {
979    (root_scale.is_finite() && root_scale > 0.0).then(|| ShadowSurfaceCacheKey {
980        content_hash: shadow_content_hash(shadow, root_scale),
981        pixel_size: [device_bounds.width, device_bounds.height],
982        root_scale_bits: root_scale.to_bits(),
983        blur_radius_bits: pixel_radius.to_bits(),
984    })
985}
986
987fn shape_shadow_bounds(shadow: &ShadowDraw) -> Option<Rect> {
988    shadow.shapes.as_ref().map(|run| run.bounds)
989}
990
991pub(crate) fn shadow_draw_bounds(shadow: &ShadowDraw) -> Option<Rect> {
992    shape_shadow_bounds(shadow)
993        .into_iter()
994        .chain(shadow.texts.iter().map(|text| text.rect))
995        .reduce(|a, b| Rect {
996            x: a.x.min(b.x),
997            y: a.y.min(b.y),
998            width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
999            height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1000        })
1001}
1002
1003/// The shape shader's source for `mode`, built when its module is parsed.
1004fn shape_shader_source(mode: RunBufferMode) -> fn() -> Cow<'static, str> {
1005    if mode.trig_fill {
1006        || Cow::Owned(shaders::filling_shape_shader())
1007    } else if mode.storage {
1008        || Cow::Owned(shaders::storage_shape_shader())
1009    } else {
1010        || Cow::Borrowed(shaders::SHADER)
1011    }
1012}
1013
1014/// A pipeline that draws one full-screen triangle strip from `fullscreen_vs`
1015/// into a single color target, the shape every effect and composite pass
1016/// shares; `constants` fixes the shader's override constants.
1017#[expect(clippy::too_many_arguments)]
1018pub(crate) fn create_fullscreen_strip_pipeline(
1019    device: &wgpu::Device,
1020    cache: Option<&wgpu::PipelineCache>,
1021    log_label: &str,
1022    label: &'static str,
1023    layout: &wgpu::PipelineLayout,
1024    module: &wgpu::ShaderModule,
1025    fragment_entry: &'static str,
1026    constants: &[(&str, f64)],
1027    target: wgpu::ColorTargetState,
1028) -> wgpu::RenderPipeline {
1029    create_render_pipeline_logged(
1030        device,
1031        cache,
1032        log_label,
1033        wgpu::RenderPipelineDescriptor {
1034            label: Some(label),
1035            layout: Some(layout),
1036            vertex: wgpu::VertexState {
1037                module,
1038                entry_point: Some("fullscreen_vs"),
1039                buffers: &[],
1040                compilation_options: wgpu::PipelineCompilationOptions {
1041                    constants,
1042                    ..wgpu::PipelineCompilationOptions::default()
1043                },
1044            },
1045            fragment: Some(wgpu::FragmentState {
1046                module,
1047                entry_point: Some(fragment_entry),
1048                targets: &[Some(target)],
1049                compilation_options: wgpu::PipelineCompilationOptions {
1050                    constants,
1051                    ..wgpu::PipelineCompilationOptions::default()
1052                },
1053            }),
1054            primitive: wgpu::PrimitiveState {
1055                topology: wgpu::PrimitiveTopology::TriangleStrip,
1056                strip_index_format: None,
1057                front_face: wgpu::FrontFace::Ccw,
1058                cull_mode: None,
1059                ..Default::default()
1060            },
1061            depth_stencil: None,
1062            multisample: wgpu::MultisampleState::default(),
1063            multiview_mask: None,
1064            cache: None,
1065        },
1066    )
1067}
1068
1069pub(crate) fn create_render_pipeline_logged<'a>(
1070    device: &wgpu::Device,
1071    cache: Option<&'a wgpu::PipelineCache>,
1072    tag: &str,
1073    mut descriptor: wgpu::RenderPipelineDescriptor<'a>,
1074) -> wgpu::RenderPipeline {
1075    descriptor.cache = cache;
1076    build_pipeline_logged(tag, || device.create_render_pipeline(&descriptor))
1077}
1078
1079pub(crate) fn build_pipeline_logged<T>(tag: &str, build: impl FnOnce() -> T) -> T {
1080    let started = Instant::now();
1081    let pipeline = build();
1082    log::info!(
1083        "[pipeline-create] {tag} {:.1}ms on {}",
1084        instant_ms(started, Instant::now()),
1085        std::thread::current().name().unwrap_or("unnamed thread"),
1086    );
1087    if OFF_FRAME_BUILDS.with(Cell::get) {
1088        PIPELINES_CREATED_OFF_FRAME.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1089    } else {
1090        PIPELINES_CREATED.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1091    }
1092    #[cfg(not(target_arch = "wasm32"))]
1093    crate::pipeline_disk_cache::note_pipeline_built();
1094    pipeline
1095}
1096
1097/// Pipelines this process has built on a thread that draws.
1098///
1099/// A build runs the backend's shader compiler, and whoever asks for one while
1100/// drawing waits for it there. A count that grows across an interaction names
1101/// work a person waited on, whatever the driver's own caches made a compile
1102/// cost on this machine. Builds handed to [`crate::pipeline_compiler`] are
1103/// counted apart, by [`pipelines_created_off_frame`]: they cost a frame
1104/// nothing.
1105static PIPELINES_CREATED: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1106static PIPELINES_CREATED_OFF_FRAME: std::sync::atomic::AtomicU64 =
1107    std::sync::atomic::AtomicU64::new(0);
1108
1109thread_local! {
1110    static OFF_FRAME_BUILDS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1111}
1112
1113/// Declares that pipelines built on this thread are built away from any
1114/// frame. The compiler thread says so once, when it starts.
1115#[cfg(not(target_arch = "wasm32"))]
1116pub(crate) fn mark_thread_off_frame() {
1117    OFF_FRAME_BUILDS.with(|off_frame| off_frame.set(true));
1118}
1119
1120pub fn pipelines_created() -> u64 {
1121    PIPELINES_CREATED.load(std::sync::atomic::Ordering::Relaxed)
1122}
1123
1124/// Pipelines built away from every frame, on the compiler thread.
1125pub fn pipelines_created_off_frame() -> u64 {
1126    PIPELINES_CREATED_OFF_FRAME.load(std::sync::atomic::Ordering::Relaxed)
1127}
1128
1129#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1130#[repr(u8)]
1131pub(crate) enum RunTier {
1132    Store = 0,
1133    Arena = 1,
1134    Either = 2,
1135}
1136
1137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1138pub(crate) struct ShapeVariant {
1139    kinds: u8,
1140    brush: Option<u8>,
1141    solid: bool,
1142    /// How its records meet their placements' clips. A rounded clip gives
1143    /// every fragment its coverage, on the general entry points.
1144    clip: SegmentClip,
1145    interior: bool,
1146    dither: bool,
1147    ablation: ShapeAblation,
1148}
1149
1150const FLAT_FILL_ENTRIES: [[(&str, &str); 2]; 2] = [
1151    [
1152        ("vs_record_plain_fill", "fs_plain_fill"),
1153        ("vs_record_dithered_fill", "fs_dithered_fill"),
1154    ],
1155    [
1156        ("vs_record_clipped_fill", "fs_clipped_fill"),
1157        ("vs_record_solid", "fs_solid"),
1158    ],
1159];
1160
1161const ALL_SHAPE_KINDS: u8 = (1 << FRAGMENT_KIND_FILL)
1162    | (1 << FRAGMENT_KIND_STROKE)
1163    | (1 << FRAGMENT_KIND_ARC)
1164    | (1 << FRAGMENT_KIND_LINE);
1165const LINE_KIND: u8 = 1 << FRAGMENT_KIND_LINE;
1166
1167fn variant_kinds(kinds: u8) -> u8 {
1168    if kinds.count_ones() <= 1 {
1169        kinds
1170    } else if kinds & LINE_KIND != 0 {
1171        ALL_SHAPE_KINDS
1172    } else {
1173        ALL_SHAPE_KINDS & !LINE_KIND
1174    }
1175}
1176
1177impl ShapeVariant {
1178    const GENERAL: Self = Self {
1179        kinds: ALL_SHAPE_KINDS,
1180        brush: None,
1181        solid: false,
1182        clip: SegmentClip::Tested,
1183        interior: true,
1184        dither: false,
1185        ablation: ShapeAblation {
1186            material: false,
1187            fill: false,
1188        },
1189    };
1190
1191    pub(crate) fn of_segment(
1192        segment: &RecordSegment,
1193        clip: SegmentClip,
1194        ablation: ShapeAblation,
1195        laid: bool,
1196        vertex_gradients: bool,
1197    ) -> Self {
1198        if !shape_variants_enabled() || clip == SegmentClip::Rounded {
1199            return Self {
1200                ablation,
1201                ..Self::GENERAL
1202            }
1203            .clipped_by(clip);
1204        }
1205        let gradient = segment.gradient || (segment.vertex_gradient && !vertex_gradients);
1206        Self {
1207            kinds: variant_kinds(segment.kinds),
1208            brush: gradient
1209                .then(|| segment.uniform_brush())
1210                .flatten()
1211                .map(|brush| brush as u8),
1212            solid: !gradient,
1213            clip,
1214            interior: gradient
1215                && segment.interiors
1216                && !(laid && segment.occluders && !segment.bare_interiors),
1217            dither: segment.vertex_gradient && vertex_gradients,
1218            ablation,
1219        }
1220    }
1221
1222    fn kind(self) -> Option<u8> {
1223        (self.kinds.count_ones() == 1).then(|| self.kinds.trailing_zeros() as u8)
1224    }
1225
1226    /// The vertex and fragment entry points of this variant, for records
1227    /// drawn `flat` (none of them turned).
1228    fn entries(self, flat: bool) -> (&'static str, &'static str) {
1229        let fill = self.kind() == Some(FRAGMENT_KIND_FILL as u8);
1230        if self.rounded() {
1231            ("vs_record", "fs_main")
1232        } else if self.solid && fill && flat {
1233            FLAT_FILL_ENTRIES[usize::from(self.clip == SegmentClip::Tested)]
1234                [usize::from(self.dither)]
1235        } else if self.solid {
1236            ("vs_record_solid", "fs_solid")
1237        } else if fill && !self.ablation.material {
1238            ("vs_record_gradient_fill", "fs_gradient_fill")
1239        } else {
1240            ("vs_record", "fs_main")
1241        }
1242    }
1243
1244    fn general(self) -> Self {
1245        Self {
1246            kinds: (ALL_SHAPE_KINDS & !LINE_KIND) | (self.kinds & LINE_KIND),
1247            ablation: self.ablation,
1248            ..Self::GENERAL
1249        }
1250        .clipped_by(self.clip)
1251    }
1252
1253    /// The variant, keeping the rounded clip `clip` names: the general
1254    /// variant tests every rect clip, but no fallback may drop a rounding.
1255    fn clipped_by(self, clip: SegmentClip) -> Self {
1256        if clip == SegmentClip::Rounded {
1257            Self { clip, ..self }
1258        } else {
1259            self
1260        }
1261    }
1262
1263    fn rounded(self) -> bool {
1264        self.clip == SegmentClip::Rounded
1265    }
1266
1267    fn joined(self) -> Self {
1268        if self.solid {
1269            Self {
1270                dither: true,
1271                ..self
1272            }
1273        } else {
1274            Self {
1275                interior: true,
1276                ..self
1277            }
1278        }
1279    }
1280}
1281
1282/// How a segment's draw meets its placement's clip: a scissor or no clip
1283/// leaves the shader nothing to test, a rect clip off the scissor's grid is
1284/// tested per fragment, and a rounded clip gives every fragment coverage.
1285#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1286pub(crate) enum SegmentClip {
1287    Untested,
1288    Tested,
1289    Rounded,
1290}
1291
1292impl SegmentClip {
1293    /// The clip a draw under `placement` tests, given whether its rect clip
1294    /// became the draw's scissor.
1295    pub(crate) fn of(placement: &crate::scene::Placement, scissored: bool) -> Self {
1296        if placement.clip_rounded() {
1297            Self::Rounded
1298        } else if placement.clip.is_some() && !scissored {
1299            Self::Tested
1300        } else {
1301            Self::Untested
1302        }
1303    }
1304}
1305
1306static SHAPE_VARIANTS: DebugToggle = DebugToggle::new("CRANPOSE_SHAPE_VARIANTS");
1307
1308fn shape_variants_enabled() -> bool {
1309    !SHAPE_VARIANTS.equals("0")
1310}
1311
1312/// What every draw of one pass shares: its target's size and whether the
1313/// pass has a depth buffer its opaque interiors fill first.
1314#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1315pub(crate) struct PassFrame {
1316    pub(crate) size: (u32, u32),
1317    pub(crate) depth: bool,
1318}
1319
1320impl PassFrame {
1321    /// The scissor a draw bounded by `scissor`, or by nothing, sets.
1322    pub(crate) fn scissor(self, scissor: Option<(u32, u32, u32, u32)>) -> (u32, u32, u32, u32) {
1323        scissor.unwrap_or((0, 0, self.size.0, self.size.1))
1324    }
1325}
1326
1327/// Which of a pass's two stages a shape draw records: the front-to-back
1328/// opaque interiors, or the paint in draw order.
1329#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1330pub(crate) enum RunStage {
1331    Interiors,
1332    Paint,
1333}
1334
1335/// The depth buffer of a pass that lays opaque interiors down first.
1336pub(crate) const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
1337
1338/// How a pipeline meets its pass's depth buffer.
1339#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1340pub(crate) enum ShapeDepth {
1341    /// The pass has no depth buffer.
1342    Off,
1343    /// Paints what the opaque interiors of later records leave visible.
1344    Tested,
1345    /// Lays down opaque interiors, front to back, ahead of the paint.
1346    Interior,
1347}
1348
1349impl ShapeDepth {
1350    fn stencil_state(self) -> Option<wgpu::DepthStencilState> {
1351        let (depth_write_enabled, depth_compare) = match self {
1352            Self::Off => return None,
1353            Self::Tested => (false, wgpu::CompareFunction::Less),
1354            Self::Interior => (true, wgpu::CompareFunction::Less),
1355        };
1356        Some(wgpu::DepthStencilState {
1357            format: DEPTH_FORMAT,
1358            depth_write_enabled: Some(depth_write_enabled),
1359            depth_compare: Some(depth_compare),
1360            stencil: wgpu::StencilState::default(),
1361            bias: wgpu::DepthBiasState::default(),
1362        })
1363    }
1364}
1365
1366/// The depth state of a glyph or image pipeline: in a pass with a depth
1367/// buffer it paints what later opaque interiors leave visible, as shapes do.
1368fn overlay_depth_state(depth: bool) -> Option<wgpu::DepthStencilState> {
1369    if depth {
1370        ShapeDepth::Tested.stencil_state()
1371    } else {
1372        None
1373    }
1374}
1375
1376/// Which records a shape pipeline draws turned, as `SHAPE_TURNS` in the
1377/// shape shader: none, every one, or each as its placement says.
1378#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1379pub(crate) enum ShapeTurns {
1380    None,
1381    All,
1382    Mixed,
1383}
1384
1385impl ShapeTurns {
1386    /// The turns of records placed under `turn`, in a pass whose flat and
1387    /// turned records alternate often when `mixed`.
1388    pub(crate) fn of(turn: SegmentTransform, mixed: bool) -> Self {
1389        if mixed {
1390            Self::Mixed
1391        } else if turn.is_identity() {
1392            Self::None
1393        } else {
1394            Self::All
1395        }
1396    }
1397
1398    fn constant(self) -> f64 {
1399        match self {
1400            Self::None => 0.0,
1401            Self::All => 1.0,
1402            Self::Mixed => 2.0,
1403        }
1404    }
1405}
1406
1407#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
1408pub(crate) struct ShapePipelineKey {
1409    pub(crate) blend_mode: BlendMode,
1410    pub(crate) tier: RunTier,
1411    pub(crate) variant: ShapeVariant,
1412    pub(crate) turns: ShapeTurns,
1413    pub(crate) depth: ShapeDepth,
1414}
1415
1416/// Packs a key's fields into a number, low bit first.
1417#[cfg(not(target_arch = "wasm32"))]
1418#[derive(Default)]
1419struct KeyWriter {
1420    bits: u64,
1421    width: u32,
1422}
1423
1424#[cfg(not(target_arch = "wasm32"))]
1425impl KeyWriter {
1426    fn put(&mut self, value: u64, width: u32) {
1427        self.bits |= value << self.width;
1428        self.width += width;
1429    }
1430
1431    fn put_byte(&mut self, value: Option<u8>) {
1432        self.put(u64::from(value.is_some()), 1);
1433        self.put(u64::from(value.unwrap_or_default()), 8);
1434    }
1435}
1436
1437/// Unpacks what [`KeyWriter`] packed, low bit first.
1438struct KeyReader(u64);
1439
1440impl KeyReader {
1441    fn take(&mut self, width: u32) -> u64 {
1442        let value = self.0 & ((1 << width) - 1);
1443        self.0 >>= width;
1444        value
1445    }
1446
1447    fn take_flag(&mut self) -> bool {
1448        self.take(1) == 1
1449    }
1450
1451    fn take_byte(&mut self) -> Option<u8> {
1452        let present = self.take_flag();
1453        let value = u8::try_from(self.take(8)).ok()?;
1454        present.then_some(value)
1455    }
1456
1457    fn take_kinds(&mut self) -> Option<u8> {
1458        u8::try_from(self.take(4)).ok().filter(|kinds| *kinds != 0)
1459    }
1460}
1461
1462impl ShapePipelineKey {
1463    #[cfg(not(target_arch = "wasm32"))]
1464    pub(crate) const DISK_LAYOUT: [u8; 8] = *b"CPKEY002";
1465
1466    /// The key as a number a later launch reads back with
1467    /// [`Self::from_bits`], to build the pipeline ahead of its first frame.
1468    #[cfg(not(target_arch = "wasm32"))]
1469    pub(crate) fn to_bits(self) -> u64 {
1470        let mut bits = KeyWriter::default();
1471        bits.put(self.blend_mode as u64, 5);
1472        bits.put(self.tier as u64, 2);
1473        let variant = self.variant;
1474        bits.put(u64::from(variant.kinds), 4);
1475        bits.put_byte(variant.brush);
1476        bits.put(u64::from(variant.solid), 1);
1477        bits.put(
1478            match variant.clip {
1479                SegmentClip::Untested => 0,
1480                SegmentClip::Tested => 1,
1481                SegmentClip::Rounded => 2,
1482            },
1483            2,
1484        );
1485        bits.put(u64::from(variant.interior), 1);
1486        bits.put(u64::from(variant.dither), 1);
1487        bits.put(u64::from(variant.ablation.material), 1);
1488        bits.put(u64::from(variant.ablation.fill), 1);
1489        bits.put(
1490            match self.turns {
1491                ShapeTurns::None => 0,
1492                ShapeTurns::All => 1,
1493                ShapeTurns::Mixed => 2,
1494            },
1495            2,
1496        );
1497        bits.put(
1498            match self.depth {
1499                ShapeDepth::Off => 0,
1500                ShapeDepth::Tested => 1,
1501                ShapeDepth::Interior => 2,
1502            },
1503            2,
1504        );
1505        bits.bits
1506    }
1507
1508    /// The key [`Self::to_bits`] packed, or `None` for a number no key
1509    /// packs to.
1510    pub(crate) fn from_bits(packed: u64) -> Option<Self> {
1511        let mut bits = KeyReader(packed);
1512        let blend_mode = *BlendMode::ALL.get(usize::try_from(bits.take(5)).ok()?)?;
1513        let tier = match bits.take(2) {
1514            0 => RunTier::Store,
1515            1 => RunTier::Arena,
1516            2 => RunTier::Either,
1517            _ => return None,
1518        };
1519        let kinds = bits.take_kinds()?;
1520        let brush = bits.take_byte();
1521        let solid = bits.take_flag();
1522        let clip = match bits.take(2) {
1523            0 => SegmentClip::Untested,
1524            1 => SegmentClip::Tested,
1525            2 => SegmentClip::Rounded,
1526            _ => return None,
1527        };
1528        let interior = bits.take_flag();
1529        let dither = bits.take_flag();
1530        let ablation = ShapeAblation {
1531            material: bits.take_flag(),
1532            fill: bits.take_flag(),
1533        };
1534        let turns = match bits.take(2) {
1535            0 => ShapeTurns::None,
1536            1 => ShapeTurns::All,
1537            2 => ShapeTurns::Mixed,
1538            _ => return None,
1539        };
1540        let depth = match bits.take(2) {
1541            0 => ShapeDepth::Off,
1542            1 => ShapeDepth::Tested,
1543            2 => ShapeDepth::Interior,
1544            _ => return None,
1545        };
1546        (bits.0 == 0).then_some(Self {
1547            blend_mode,
1548            tier,
1549            variant: ShapeVariant {
1550                kinds,
1551                brush,
1552                solid,
1553                clip,
1554                interior,
1555                dither,
1556                ablation,
1557            },
1558            turns,
1559            depth,
1560        })
1561    }
1562
1563    #[cfg(test)]
1564    pub(crate) fn general_for(blend_mode: BlendMode, tier: RunTier) -> Self {
1565        Self {
1566            blend_mode,
1567            tier,
1568            variant: ShapeVariant::GENERAL,
1569            turns: ShapeTurns::None,
1570            depth: ShapeDepth::Off,
1571        }
1572    }
1573
1574    /// The pipeline that lays down the opaque interiors of this key's
1575    /// draws, if they have any: plain source-over draws. A turned record's
1576    /// layer draws in place only under a rigid turn, which keeps the
1577    /// interior's half-pixel inset from the fill's edge exact.
1578    pub(crate) fn interior(self) -> Option<Self> {
1579        (self.depth == ShapeDepth::Tested
1580            && !self.variant.rounded()
1581            && self.blend_mode == BlendMode::SrcOver
1582            && !self.variant.ablation.material
1583            && !self.variant.ablation.fill)
1584            .then_some(Self {
1585                variant: self.variant.general(),
1586                depth: ShapeDepth::Interior,
1587                ..self
1588            })
1589    }
1590
1591    pub(crate) fn general(self) -> Self {
1592        Self {
1593            tier: RunTier::Either,
1594            variant: self.variant.general(),
1595            ..self
1596        }
1597    }
1598
1599    pub(crate) fn is_general(self) -> bool {
1600        self.variant == self.variant.general()
1601    }
1602
1603    pub(crate) fn joined(self) -> Self {
1604        Self {
1605            variant: self.variant.joined(),
1606            ..self
1607        }
1608    }
1609}
1610
1611pub(crate) fn create_shape_pipeline(
1612    device: &wgpu::Device,
1613    cache: Option<&wgpu::PipelineCache>,
1614    surface_format: wgpu::TextureFormat,
1615    shader: &SharedShader,
1616    key: ShapePipelineKey,
1617    mode: RunBufferMode,
1618) -> wgpu::RenderPipeline {
1619    let ShapePipelineKey {
1620        blend_mode,
1621        tier,
1622        variant,
1623        turns,
1624        depth,
1625    } = key;
1626    let constants = [
1627        ("SHAPE_KIND_FIXED", variant.kind().map_or(-1.0, f64::from)),
1628        ("SHAPE_KINDS", f64::from(variant.kinds)),
1629        ("BRUSH_KIND_FIXED", variant.brush.map_or(-1.0, f64::from)),
1630        ("SHAPE_SOLID", f64::from(u8::from(variant.solid))),
1631        (
1632            "SHAPE_CLIPPED",
1633            f64::from(u8::from(variant.clip != SegmentClip::Untested)),
1634        ),
1635        ("SHAPE_ROUNDED_CLIP", f64::from(u8::from(variant.rounded()))),
1636        (
1637            "SHAPE_INTERIOR",
1638            f64::from(u8::from(variant.interior && !variant.rounded())),
1639        ),
1640        ("SHAPE_DITHER", f64::from(u8::from(variant.dither))),
1641        ("SHAPE_TURNS", turns.constant()),
1642        ("SHAPE_TIER", f64::from(tier as u8)),
1643        ("SHAPE_DEPTH", f64::from(u8::from(depth != ShapeDepth::Off))),
1644        ("SHAPE_BANDS", f64::from(u8::from(mode.storage))),
1645        ("SHAPE_TRIG_FILLED", f64::from(u8::from(mode.trig_fill))),
1646        ("SHAPE_FLAT", f64::from(u8::from(variant.ablation.material))),
1647        ("SHAPE_DISCARD", f64::from(u8::from(variant.ablation.fill))),
1648    ];
1649    let (vertex_entry, fragment_entry) = if depth == ShapeDepth::Interior {
1650        ("vs_record_interior", "fs_interior")
1651    } else {
1652        variant.entries(turns == ShapeTurns::None)
1653    };
1654    let blend = (depth != ShapeDepth::Interior).then(|| blend_state_for_mode(blend_mode));
1655    let instance_layout = record_vertex_layouts().map(Some);
1656    let module = shader.module();
1657
1658    create_render_pipeline_logged(
1659        device,
1660        cache,
1661        &format!(
1662            "shape blend={blend_mode:?} tier={tier:?} variant={variant:?} turns={turns:?} depth={depth:?}"
1663        ),
1664        wgpu::RenderPipelineDescriptor {
1665            label: Some("Shape Pipeline"),
1666            layout: Some(shader.layout()),
1667            vertex: wgpu::VertexState {
1668                module,
1669                entry_point: Some(vertex_entry),
1670                compilation_options: wgpu::PipelineCompilationOptions {
1671                    constants: &constants,
1672                    ..wgpu::PipelineCompilationOptions::default()
1673                },
1674                buffers: &instance_layout,
1675            },
1676            fragment: Some(wgpu::FragmentState {
1677                module,
1678                entry_point: Some(fragment_entry),
1679                compilation_options: wgpu::PipelineCompilationOptions {
1680                    constants: &constants,
1681                    ..wgpu::PipelineCompilationOptions::default()
1682                },
1683                targets: &[Some(wgpu::ColorTargetState {
1684                    format: surface_format,
1685                    blend,
1686                    write_mask: wgpu::ColorWrites::ALL,
1687                })],
1688            }),
1689            primitive: wgpu::PrimitiveState {
1690                topology: wgpu::PrimitiveTopology::TriangleList,
1691                strip_index_format: None,
1692                front_face: wgpu::FrontFace::Ccw,
1693                cull_mode: None,
1694                unclipped_depth: false,
1695                polygon_mode: wgpu::PolygonMode::Fill,
1696                conservative: false,
1697            },
1698            depth_stencil: depth.stencil_state(),
1699            multisample: wgpu::MultisampleState::default(),
1700            multiview_mask: None,
1701            cache: None,
1702        },
1703    )
1704}
1705fn create_image_pipeline(
1706    device: &wgpu::Device,
1707    cache: Option<&wgpu::PipelineCache>,
1708    surface_format: wgpu::TextureFormat,
1709    shader: &SharedShader,
1710    blend_mode: BlendMode,
1711    depth: bool,
1712    alpha_mask: bool,
1713) -> wgpu::RenderPipeline {
1714    let module = shader.module();
1715    create_render_pipeline_logged(
1716        device,
1717        cache,
1718        &format!("image blend={blend_mode:?} depth={depth} alpha_mask={alpha_mask}"),
1719        wgpu::RenderPipelineDescriptor {
1720            label: Some("Image Pipeline"),
1721            layout: Some(shader.layout()),
1722            vertex: wgpu::VertexState {
1723                module,
1724                entry_point: Some("image_vs_main"),
1725                compilation_options: wgpu::PipelineCompilationOptions::default(),
1726                buffers: &[Some(Vertex::desc())],
1727            },
1728            fragment: Some(wgpu::FragmentState {
1729                module,
1730                entry_point: Some(if alpha_mask {
1731                    "image_mask_fs_main"
1732                } else {
1733                    "image_fs_main"
1734                }),
1735                compilation_options: wgpu::PipelineCompilationOptions::default(),
1736                targets: &[Some(wgpu::ColorTargetState {
1737                    format: surface_format,
1738                    blend: Some(blend_state_for_mode(blend_mode)),
1739                    write_mask: wgpu::ColorWrites::ALL,
1740                })],
1741            }),
1742            primitive: wgpu::PrimitiveState {
1743                topology: wgpu::PrimitiveTopology::TriangleList,
1744                strip_index_format: None,
1745                front_face: wgpu::FrontFace::Ccw,
1746                cull_mode: None,
1747                unclipped_depth: false,
1748                polygon_mode: wgpu::PolygonMode::Fill,
1749                conservative: false,
1750            },
1751            depth_stencil: overlay_depth_state(depth),
1752            multisample: wgpu::MultisampleState::default(),
1753            multiview_mask: None,
1754            cache: None,
1755        },
1756    )
1757}
1758
1759fn create_glyph_atlas_pipeline(
1760    device: &wgpu::Device,
1761    cache: Option<&wgpu::PipelineCache>,
1762    surface_format: wgpu::TextureFormat,
1763    shader: &SharedShader,
1764    (depth, turned): (bool, bool),
1765) -> wgpu::RenderPipeline {
1766    let module = shader.module();
1767    create_render_pipeline_logged(
1768        device,
1769        cache,
1770        match (depth, turned) {
1771            (false, false) => "glyph-atlas",
1772            (true, false) => "glyph-atlas depth",
1773            (false, true) => "glyph-atlas turned",
1774            (true, true) => "glyph-atlas turned depth",
1775        },
1776        wgpu::RenderPipelineDescriptor {
1777            label: Some("Glyph Atlas Pipeline"),
1778            layout: Some(shader.layout()),
1779            vertex: wgpu::VertexState {
1780                module,
1781                entry_point: Some(if turned {
1782                    "glyph_atlas_turned_vs_main"
1783                } else {
1784                    "glyph_atlas_vs_main"
1785                }),
1786                compilation_options: wgpu::PipelineCompilationOptions::default(),
1787                buffers: &[Some(if turned {
1788                    TurnedGlyph::desc()
1789                } else {
1790                    GlyphInstance::desc()
1791                })],
1792            },
1793            fragment: Some(wgpu::FragmentState {
1794                module,
1795                entry_point: Some("glyph_atlas_fs_main"),
1796                compilation_options: wgpu::PipelineCompilationOptions::default(),
1797                targets: &[Some(wgpu::ColorTargetState {
1798                    format: surface_format,
1799                    blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1800                    write_mask: wgpu::ColorWrites::ALL,
1801                })],
1802            }),
1803            primitive: wgpu::PrimitiveState {
1804                topology: wgpu::PrimitiveTopology::TriangleStrip,
1805                strip_index_format: None,
1806                front_face: wgpu::FrontFace::Ccw,
1807                cull_mode: None,
1808                unclipped_depth: false,
1809                polygon_mode: wgpu::PolygonMode::Fill,
1810                conservative: false,
1811            },
1812            depth_stencil: overlay_depth_state(depth),
1813            multisample: wgpu::MultisampleState::default(),
1814            multiview_mask: None,
1815            cache: None,
1816        },
1817    )
1818}
1819
1820#[repr(C)]
1821#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1822pub(crate) struct Vertex {
1823    position: [f32; 2],
1824    color: [f32; 4],
1825    uv: [f32; 2],
1826    uv_bounds: [f32; 4],
1827}
1828
1829impl Vertex {
1830    const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
1831        0 => Float32x2,
1832        1 => Float32x4,
1833        2 => Float32x2,
1834        3 => Float32x4
1835    ];
1836
1837    fn desc() -> wgpu::VertexBufferLayout<'static> {
1838        wgpu::VertexBufferLayout {
1839            array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
1840            step_mode: wgpu::VertexStepMode::Vertex,
1841            attributes: &Self::ATTRIBS,
1842        }
1843    }
1844}
1845
1846/// The corners a glyph instance draws as a triangle strip: top-left,
1847/// top-right, bottom-left, bottom-right.
1848const GLYPH_QUAD_CORNERS: u32 = 4;
1849
1850/// One glyph quad as the glyph pipeline draws it: an instance whose four
1851/// corners the vertex stage picks from `rect` and `uv`.
1852#[repr(C)]
1853#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)]
1854pub(crate) struct GlyphInstance {
1855    rect: [f32; 4],
1856    uv: [f32; 4],
1857    uv_bounds: [f32; 4],
1858    color: [f32; 4],
1859}
1860
1861impl GlyphInstance {
1862    const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
1863        0 => Float32x4,
1864        1 => Float32x4,
1865        2 => Float32x4,
1866        3 => Float32x4
1867    ];
1868
1869    fn desc() -> wgpu::VertexBufferLayout<'static> {
1870        wgpu::VertexBufferLayout {
1871            array_stride: std::mem::size_of::<GlyphInstance>() as wgpu::BufferAddress,
1872            step_mode: wgpu::VertexStepMode::Instance,
1873            attributes: &Self::ATTRIBS,
1874        }
1875    }
1876
1877    /// The part of the quad inside `edges` (left, top, right, bottom, in the
1878    /// quad's own space), its atlas coordinates cut to match; `None` when
1879    /// none of it is inside.
1880    pub(crate) fn clipped_to(self, edges: [f32; 4]) -> Option<Self> {
1881        let [x0, y0, x1, y1] = self.rect;
1882        let rect = [
1883            x0.max(edges[0]),
1884            y0.max(edges[1]),
1885            x1.min(edges[2]),
1886            y1.min(edges[3]),
1887        ];
1888        if rect[2] <= rect[0] || rect[3] <= rect[1] {
1889            return None;
1890        }
1891        if rect == self.rect {
1892            return Some(self);
1893        }
1894        let [u0, v0, u1, v1] = self.uv;
1895        let u = |x: f32| u0 + (x - x0) / (x1 - x0) * (u1 - u0);
1896        let v = |y: f32| v0 + (y - y0) / (y1 - y0) * (v1 - v0);
1897        Some(Self {
1898            rect,
1899            uv: [u(rect[0]), v(rect[1]), u(rect[2]), v(rect[3])],
1900            ..self
1901        })
1902    }
1903}
1904
1905/// Hands `sink` each quad of a glyph run at `source_raster_rect` that the
1906/// viewport shows, as an instance cut to `cut` when the text is cut.
1907#[inline]
1908fn for_each_visible_glyph(
1909    source_raster_rect: Rect,
1910    quads: GlyphRunQuads<'_>,
1911    (clip, cut): (Option<Rect>, Option<[f32; 4]>),
1912    viewport: ViewportUniformParams,
1913    root_scale: f32,
1914    mut sink: impl FnMut(GlyphInstance),
1915) {
1916    let whole_run_visible =
1917        quads
1918            .bounds
1919            .within_viewport(source_raster_rect, clip, viewport, root_scale);
1920    for quad in quads.iter() {
1921        if !whole_run_visible
1922            && !cached_text_glyph_quad_is_visible_in_viewport(
1923                source_raster_rect,
1924                &quad,
1925                clip,
1926                viewport,
1927                root_scale,
1928            )
1929        {
1930            continue;
1931        }
1932        let Some(glyph) = cached_text_glyph_instance(source_raster_rect, &quad) else {
1933            continue;
1934        };
1935        let glyph = match cut {
1936            Some(edges) => glyph.clipped_to(edges),
1937            None => Some(glyph),
1938        };
1939        if let Some(glyph) = glyph {
1940            sink(glyph);
1941        }
1942    }
1943}
1944
1945/// A glyph quad of a layer drawn in place under a turn, with the turn that
1946/// places it: glyphs of layers turned differently then share a draw, and
1947/// every other glyph keeps [`GlyphInstance`]'s smaller layout.
1948#[repr(C)]
1949#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)]
1950pub(crate) struct TurnedGlyph {
1951    glyph: GlyphInstance,
1952    /// The turn, row-major.
1953    turn: [f32; 4],
1954    /// The turn's translation in `xy`.
1955    translation: [f32; 4],
1956}
1957
1958impl TurnedGlyph {
1959    const ATTRIBS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
1960        0 => Float32x4,
1961        1 => Float32x4,
1962        2 => Float32x4,
1963        3 => Float32x4,
1964        4 => Float32x4,
1965        5 => Float32x4
1966    ];
1967
1968    pub(crate) fn new(glyph: GlyphInstance, turn: SegmentTransform) -> Self {
1969        let (linear, [x, y], _) = turn.uniform_parts();
1970        Self {
1971            glyph,
1972            turn: linear,
1973            translation: [x, y, 0.0, 0.0],
1974        }
1975    }
1976
1977    fn desc() -> wgpu::VertexBufferLayout<'static> {
1978        wgpu::VertexBufferLayout {
1979            array_stride: std::mem::size_of::<TurnedGlyph>() as wgpu::BufferAddress,
1980            step_mode: wgpu::VertexStepMode::Instance,
1981            attributes: &Self::ATTRIBS,
1982        }
1983    }
1984}
1985
1986/// The glyph quads a pass draws from its shared buffers: the plain ones and
1987/// those of layers drawn in place under a turn.
1988#[derive(Default)]
1989pub(crate) struct GlyphInstances {
1990    pub(crate) plain: Vec<GlyphInstance>,
1991    pub(crate) turned: Vec<TurnedGlyph>,
1992}
1993
1994impl GlyphInstances {
1995    pub(crate) fn clear(&mut self) {
1996        self.plain.clear();
1997        self.turned.clear();
1998    }
1999
2000    fn lens(&self) -> (usize, usize) {
2001        (self.plain.len(), self.turned.len())
2002    }
2003
2004    /// How many turned glyphs, or plain ones, the pass holds.
2005    fn len_of(&self, turned: bool) -> usize {
2006        if turned {
2007            self.turned.len()
2008        } else {
2009            self.plain.len()
2010        }
2011    }
2012
2013    /// The scissor the glyphs at `range` need and the target pixels they
2014    /// touch: see [`shared_glyph_clip`]; turned glyphs need none.
2015    fn draw_clip(
2016        &self,
2017        (range, turned): (std::ops::Range<usize>, bool),
2018        scissor: TargetRect,
2019        viewport: ViewportUniformParams,
2020    ) -> (Option<TargetRect>, TargetRect) {
2021        if turned {
2022            (None, scissor)
2023        } else {
2024            shared_glyph_clip(&self.plain[range], scissor, viewport)
2025        }
2026    }
2027
2028    fn truncate(&mut self, (plain, turned): (usize, usize)) {
2029        self.plain.truncate(plain);
2030        self.turned.truncate(turned);
2031    }
2032}
2033
2034/// A text clip's edges in device pixels of the space its glyph quads are
2035/// laid out in, before a viewport transform moves them.
2036fn glyph_clip_edges(clip: Rect, root_scale: f32) -> [f32; 4] {
2037    [
2038        canonicalize_device_coordinate(clip.x * root_scale),
2039        canonicalize_device_coordinate(clip.y * root_scale),
2040        canonicalize_device_coordinate((clip.x + clip.width) * root_scale),
2041        canonicalize_device_coordinate((clip.y + clip.height) * root_scale),
2042    ]
2043}
2044
2045/// Where the glyph quads of a text are cut, in the quads' own space. Under a
2046/// turned viewport the cut is the text's `clip` itself, before the turn,
2047/// which no scissor can follow; the text then draws without one. Otherwise
2048/// it is the pixel edges of the text's `scissor`, its draw rect within its
2049/// clip: cut there, the quads cover exactly the pixels the scissor would
2050/// pass, with the same texels, so the text needs no scissor of its own and
2051/// draws in one call with its neighbours, even where a last line's
2052/// descenders or a glyph's padding reach past its draw rect.
2053fn glyph_cut_edges(
2054    clip: Option<Rect>,
2055    scissor: TargetRect,
2056    viewport: ViewportUniformParams,
2057    root_scale: f32,
2058) -> Option<[f32; 4]> {
2059    if !viewport.transform.is_identity() {
2060        return clip.map(|clip| glyph_clip_edges(clip, root_scale));
2061    }
2062    let (x, y, width, height) = scissor;
2063    let [offset_x, offset_y] = viewport.offset;
2064    Some([
2065        x as f32 + offset_x,
2066        y as f32 + offset_y,
2067        (x + width) as f32 + offset_x,
2068        (y + height) as f32 + offset_y,
2069    ])
2070}
2071
2072#[repr(C)]
2073#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2074struct Uniforms {
2075    viewport: [f32; 2],
2076    viewport_offset: [f32; 2],
2077    transform: [f32; 4],
2078    translation: [f32; 2],
2079    reserved: [f32; 2],
2080    inverse: [f32; 4],
2081    origin: [f32; 2],
2082    origin_reserved: [f32; 2],
2083    placement: PlacementData,
2084}
2085
2086impl Uniforms {
2087    fn of(params: ViewportUniformParams, placement: PlacementData, tier: RunTier) -> Self {
2088        let (transform, translation, inverse) = params.transform.uniform_parts();
2089        Self {
2090            viewport: [params.width as f32, params.height as f32],
2091            viewport_offset: params.offset,
2092            transform,
2093            translation,
2094            reserved: [params.depth_base, f32::from(tier as u8)],
2095            inverse,
2096            origin: params.origin,
2097            origin_reserved: [0.0; 2],
2098            placement,
2099        }
2100    }
2101}
2102
2103static SURVIVE_GPU_ERRORS: DebugToggle = DebugToggle::new("CRANPOSE_SURVIVE_GPU_ERRORS");
2104
2105fn survive_gpu_errors_enabled() -> bool {
2106    !SURVIVE_GPU_ERRORS.equals("0")
2107}
2108
2109struct CachedImageTexture {
2110    alpha_mask: bool,
2111    _texture: wgpu::Texture,
2112    _view: wgpu::TextureView,
2113    nearest_bind_group: wgpu::BindGroup,
2114    linear_bind_group: wgpu::BindGroup,
2115    bytes: usize,
2116}
2117
2118impl CachedImageTexture {
2119    fn bind_group(&self, sampling: ImageSampling) -> &wgpu::BindGroup {
2120        match sampling {
2121            ImageSampling::Nearest => &self.nearest_bind_group,
2122            ImageSampling::Linear => &self.linear_bind_group,
2123        }
2124    }
2125}
2126
2127#[derive(Clone, Copy)]
2128struct GlyphAtlasEntry {
2129    x: u16,
2130    y: u16,
2131    width: u16,
2132    height: u16,
2133}
2134
2135fn next_glyph_atlas_size(current: u32, max: u32) -> u32 {
2136    current.saturating_mul(2).clamp(1, max.max(1))
2137}
2138
2139/// The samplers the glyph atlas binds: nearest for glyphs on the pixel grid,
2140/// linear for glyphs a segment transform turns.
2141#[derive(Clone, Copy)]
2142struct GlyphSamplers<'a> {
2143    nearest: &'a wgpu::Sampler,
2144    linear: &'a wgpu::Sampler,
2145}
2146
2147/// A glyph's atlas slot: its mask corrected for the luminance class of the
2148/// text it is drawn in, so one glyph in light and dark text takes two slots.
2149#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2150struct GlyphAtlasSlotKey {
2151    glyph: SoftwareGlyphAtlasKey,
2152    luminance: TextLuminance,
2153}
2154
2155impl GlyphAtlasSlotKey {
2156    fn new(glyph: SoftwareGlyphAtlasKey, color: Color) -> Self {
2157        Self {
2158            glyph,
2159            luminance: TextLuminance::of_color(color),
2160        }
2161    }
2162}
2163
2164/// Slots of [`RunAtlasEntries`], by glyph id.
2165const RUN_ATLAS_ENTRY_SLOTS: usize = 64;
2166
2167/// The atlas entries a run's preparation has looked up, by glyph id: a run
2168/// repeats its letters, and each atlas lookup hashes the slot key and moves
2169/// the entry up the atlas's recency order, where the first lookup already
2170/// put it for this frame.
2171struct RunAtlasEntries {
2172    slots: [Option<(GlyphAtlasSlotKey, GlyphAtlasEntry)>; RUN_ATLAS_ENTRY_SLOTS],
2173}
2174
2175impl RunAtlasEntries {
2176    fn new() -> Self {
2177        Self {
2178            slots: [None; RUN_ATLAS_ENTRY_SLOTS],
2179        }
2180    }
2181
2182    /// `glyph`'s entry from an earlier glyph of the run, else from
2183    /// `look_up`, remembered for the rest of it.
2184    fn entry(
2185        &mut self,
2186        renderer: &mut GpuRenderer,
2187        glyph: &SoftwareGlyphAtlasPlacement,
2188        look_up: impl FnOnce(&mut GpuRenderer) -> Result<GlyphAtlasEntry, String>,
2189    ) -> Result<GlyphAtlasEntry, String> {
2190        let key = GlyphAtlasSlotKey::new(glyph.key, glyph.color);
2191        let slot = &mut self.slots[glyph.key.glyph_id as usize % RUN_ATLAS_ENTRY_SLOTS];
2192        if let Some((seen, entry)) = *slot
2193            && seen == key
2194        {
2195            renderer.frame_stats.record_text_glyph_atlas_hits(1);
2196            return Ok(entry);
2197        }
2198        let entry = look_up(renderer)?;
2199        *slot = Some((key, entry));
2200        Ok(entry)
2201    }
2202}
2203
2204struct TextGlyphAtlas {
2205    texture: wgpu::Texture,
2206    _view: wgpu::TextureView,
2207    texel_bind_group: Rc<wgpu::BindGroup>,
2208    filtered_bind_group: Rc<wgpu::BindGroup>,
2209    entries: BoundedLruCache<GlyphAtlasSlotKey, GlyphAtlasEntry>,
2210    generation: u64,
2211    size: u32,
2212    max_size: u32,
2213    cursor_x: u32,
2214    cursor_y: u32,
2215    row_height: u32,
2216    upload_scratch: Vec<u8>,
2217}
2218
2219impl TextGlyphAtlas {
2220    fn new(
2221        device: &wgpu::Device,
2222        image_layout: &wgpu::BindGroupLayout,
2223        samplers: GlyphSamplers<'_>,
2224        size: u32,
2225    ) -> Self {
2226        let max_size = TEXT_GLYPH_ATLAS_MAX_SIZE.min(device.limits().max_texture_dimension_2d);
2227        let size = size.clamp(TEXT_GLYPH_ATLAS_MIN_SIZE.min(max_size), max_size);
2228        let texture = Self::create_texture(device, size);
2229        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
2230        let bind = |label: &'static str, sampler: &wgpu::Sampler| {
2231            Rc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
2232                label: Some(label),
2233                layout: image_layout,
2234                entries: &[
2235                    wgpu::BindGroupEntry {
2236                        binding: 0,
2237                        resource: wgpu::BindingResource::TextureView(&view),
2238                    },
2239                    wgpu::BindGroupEntry {
2240                        binding: 1,
2241                        resource: wgpu::BindingResource::Sampler(sampler),
2242                    },
2243                ],
2244            }))
2245        };
2246        let texel_bind_group = bind("Text Glyph Atlas Bind Group", samplers.nearest);
2247        let filtered_bind_group = bind("Filtered Text Glyph Atlas Bind Group", samplers.linear);
2248        Self {
2249            texture,
2250            _view: view,
2251            texel_bind_group,
2252            filtered_bind_group,
2253            entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
2254            generation: 0,
2255            size,
2256            max_size,
2257            cursor_x: TEXT_GLYPH_ATLAS_PADDING,
2258            cursor_y: TEXT_GLYPH_ATLAS_PADDING,
2259            row_height: 0,
2260            upload_scratch: Vec::new(),
2261        }
2262    }
2263
2264    fn create_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
2265        device.create_texture(&wgpu::TextureDescriptor {
2266            label: Some("Text Glyph Atlas Texture"),
2267            size: wgpu::Extent3d {
2268                width: size,
2269                height: size,
2270                depth_or_array_layers: 1,
2271            },
2272            mip_level_count: 1,
2273            sample_count: 1,
2274            dimension: wgpu::TextureDimension::D2,
2275            format: wgpu::TextureFormat::R8Unorm,
2276            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2277            view_formats: &[],
2278        })
2279    }
2280
2281    fn reset(
2282        &mut self,
2283        device: &wgpu::Device,
2284        image_layout: &wgpu::BindGroupLayout,
2285        samplers: GlyphSamplers<'_>,
2286    ) {
2287        let generation = self.generation.wrapping_add(1);
2288        let grown = next_glyph_atlas_size(self.size, self.max_size);
2289        let mut next = Self::new(device, image_layout, samplers, grown);
2290        next.generation = generation;
2291        *self = next;
2292    }
2293
2294    /// The atlas bound for glyphs drawn under `transform`: texel for texel
2295    /// on the pixel grid, filtered once a transform turns the glyph quads
2296    /// off it.
2297    fn bind_group(&self, transform: SegmentTransform) -> Rc<wgpu::BindGroup> {
2298        Rc::clone(if transform.is_identity() {
2299            &self.texel_bind_group
2300        } else {
2301            &self.filtered_bind_group
2302        })
2303    }
2304
2305    fn generation(&self) -> u64 {
2306        self.generation
2307    }
2308
2309    fn size(&self) -> u32 {
2310        self.size
2311    }
2312
2313    fn entry(&mut self, key: &GlyphAtlasSlotKey) -> Option<GlyphAtlasEntry> {
2314        self.entries.get(key).copied()
2315    }
2316
2317    fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
2318        let max_extent = self.size.saturating_sub(TEXT_GLYPH_ATLAS_PADDING * 2);
2319        if width == 0 || height == 0 || width > max_extent || height > max_extent {
2320            return None;
2321        }
2322
2323        if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > self.size {
2324            self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
2325            self.cursor_y = self
2326                .cursor_y
2327                .saturating_add(self.row_height)
2328                .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
2329            self.row_height = 0;
2330        }
2331        if self.cursor_y > self.size - height - TEXT_GLYPH_ATLAS_PADDING {
2332            return None;
2333        }
2334
2335        let entry = GlyphAtlasEntry {
2336            x: self.cursor_x as u16,
2337            y: self.cursor_y as u16,
2338            width: width as u16,
2339            height: height as u16,
2340        };
2341        self.cursor_x = self
2342            .cursor_x
2343            .saturating_add(width)
2344            .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
2345        self.row_height = self.row_height.max(height);
2346        Some(entry)
2347    }
2348
2349    fn upload_glyph(
2350        &mut self,
2351        glyph: &SoftwareGlyphAtlasGlyph,
2352        queue: &wgpu::Queue,
2353        executor: &mut WgpuFrameGraphExecutor,
2354        frame_stats: &mut gpu_stats::FrameStats,
2355    ) -> Option<GlyphAtlasEntry> {
2356        let key = GlyphAtlasSlotKey::new(glyph.key, glyph.color);
2357        if let Some(entry) = self.entry(&key) {
2358            frame_stats.record_text_glyph_atlas_hits(1);
2359            return Some(entry);
2360        }
2361
2362        let width = u32::try_from(glyph.mask.width).ok()?;
2363        let height = u32::try_from(glyph.mask.height).ok()?;
2364        let entry = self.allocate(width, height)?;
2365        self.upload_scratch.clear();
2366        let correction = key.luminance.correction();
2367        self.upload_scratch.extend(
2368            glyph
2369                .mask
2370                .alpha
2371                .iter()
2372                .map(|&coverage| correction.apply(coverage)),
2373        );
2374
2375        let upload_stats = executor.upload_texture(
2376            queue,
2377            wgpu::TexelCopyTextureInfo {
2378                texture: &self.texture,
2379                mip_level: 0,
2380                origin: wgpu::Origin3d {
2381                    x: u32::from(entry.x),
2382                    y: u32::from(entry.y),
2383                    z: 0,
2384                },
2385                aspect: wgpu::TextureAspect::All,
2386            },
2387            &self.upload_scratch,
2388            wgpu::TexelCopyBufferLayout {
2389                offset: 0,
2390                bytes_per_row: Some(width),
2391                rows_per_image: Some(height),
2392            },
2393            wgpu::Extent3d {
2394                width,
2395                height,
2396                depth_or_array_layers: 1,
2397            },
2398        );
2399        frame_stats.record_command_stats(upload_stats);
2400        frame_stats.record_text_glyph_atlas_miss(width, height);
2401        self.entries.put(key, entry);
2402        Some(entry)
2403    }
2404}
2405
2406pub(crate) struct ImageDrawCmd {
2407    index_start: u32,
2408    scissor: (u32, u32, u32, u32),
2409    image_id: u64,
2410    sampling: ImageSampling,
2411}
2412
2413#[derive(Clone)]
2414enum GlyphDrawSource {
2415    Shared {
2416        instance_start: u32,
2417        instance_count: u32,
2418        /// Whether the instances are [`TurnedGlyph`]s.
2419        turned: bool,
2420    },
2421    Retained {
2422        run: Rc<CachedGpuTextGlyphRun>,
2423        uniform_slot: usize,
2424    },
2425}
2426
2427#[derive(Clone)]
2428pub(crate) struct GlyphDrawCmd {
2429    atlas: Rc<wgpu::BindGroup>,
2430    source: GlyphDrawSource,
2431    /// The scissor the text's own clip needs, `None` when its quads lie
2432    /// inside that clip anyway and only the batch's bound applies.
2433    scissor: Option<(u32, u32, u32, u32)>,
2434    /// Target pixels the draw can touch, for ordering it against others.
2435    bounds: (u32, u32, u32, u32),
2436}
2437
2438/// One draw of a glyph batch: a stretch of shared quads, or a retained run.
2439struct GlyphDraw<'a> {
2440    atlas: &'a Rc<wgpu::BindGroup>,
2441    scissor: Option<(u32, u32, u32, u32)>,
2442    step: GlyphDrawStep<'a>,
2443}
2444
2445enum GlyphDrawStep<'a> {
2446    /// A stretch of the shared instances: turned ones, or plain ones.
2447    Shared(std::ops::Range<u32>, bool),
2448    Retained {
2449        run: &'a CachedGpuTextGlyphRun,
2450        uniform_slot: usize,
2451    },
2452}
2453
2454/// The draws a batch's glyph commands take: consecutive shared commands with
2455/// one atlas and one scissor whose quads follow each other in the frame's
2456/// shared buffer draw as one.
2457struct GlyphDraws<'a> {
2458    cmds: &'a [GlyphDrawCmd],
2459}
2460
2461impl<'a> GlyphDraws<'a> {
2462    fn new(cmds: &'a [GlyphDrawCmd]) -> Self {
2463        Self { cmds }
2464    }
2465}
2466
2467impl<'a> Iterator for GlyphDraws<'a> {
2468    type Item = GlyphDraw<'a>;
2469
2470    fn next(&mut self) -> Option<Self::Item> {
2471        let (first, rest) = self.cmds.split_first()?;
2472        self.cmds = rest;
2473        let step = match &first.source {
2474            GlyphDrawSource::Retained { run, uniform_slot } => GlyphDrawStep::Retained {
2475                run,
2476                uniform_slot: *uniform_slot,
2477            },
2478            GlyphDrawSource::Shared {
2479                instance_start,
2480                instance_count,
2481                turned,
2482            } => {
2483                let mut end = instance_start + instance_count;
2484                while let Some((next, rest)) = self.cmds.split_first() {
2485                    match next.source {
2486                        GlyphDrawSource::Shared {
2487                            instance_start,
2488                            instance_count,
2489                            turned: next_turned,
2490                        } if instance_start == end
2491                            && next_turned == *turned
2492                            && next.scissor == first.scissor
2493                            && Rc::ptr_eq(&next.atlas, &first.atlas) =>
2494                        {
2495                            end += instance_count;
2496                            self.cmds = rest;
2497                        }
2498                        _ => break,
2499                    }
2500                }
2501                GlyphDrawStep::Shared(*instance_start..end, *turned)
2502            }
2503        };
2504        Some(GlyphDraw {
2505            atlas: &first.atlas,
2506            scissor: first.scissor,
2507            step,
2508        })
2509    }
2510}
2511
2512impl GlyphDrawCmd {
2513    fn shared(
2514        (instances, turned): (std::ops::Range<usize>, bool),
2515        scissor: Option<(u32, u32, u32, u32)>,
2516        bounds: (u32, u32, u32, u32),
2517        atlas: Rc<wgpu::BindGroup>,
2518    ) -> Self {
2519        let start = u32::try_from(instances.start).unwrap_or(u32::MAX);
2520        let end = u32::try_from(instances.end).unwrap_or(u32::MAX);
2521        Self {
2522            atlas,
2523            source: GlyphDrawSource::Shared {
2524                instance_start: start,
2525                instance_count: end.saturating_sub(start),
2526                turned,
2527            },
2528            scissor,
2529            bounds,
2530        }
2531    }
2532
2533    fn retained(
2534        run: Rc<CachedGpuTextGlyphRun>,
2535        uniform_slot: usize,
2536        scissor: (u32, u32, u32, u32),
2537        atlas: Rc<wgpu::BindGroup>,
2538    ) -> Self {
2539        Self {
2540            atlas,
2541            source: GlyphDrawSource::Retained { run, uniform_slot },
2542            scissor: Some(scissor),
2543            bounds: scissor,
2544        }
2545    }
2546
2547    /// Target pixels the draw can touch.
2548    pub(crate) fn bounds(&self) -> (u32, u32, u32, u32) {
2549        self.bounds
2550    }
2551
2552    /// Whether the draw's quads are [`TurnedGlyph`]s, or `None` for a
2553    /// retained run, which draws on its own.
2554    fn turned(&self) -> Option<bool> {
2555        match self.source {
2556            GlyphDrawSource::Shared { turned, .. } => Some(turned),
2557            GlyphDrawSource::Retained { .. } => None,
2558        }
2559    }
2560}
2561
2562/// The kind of glyph draw, turned (`true`) or plain, that can move after
2563/// every draw of the other kind in `draws` without moving past a draw it
2564/// shares a pixel with: glyphs blend, so only draws that share pixels must
2565/// keep their order. The fewer kind is tried first, which checks fewer
2566/// pairs. `None` when neither can move, a retained run is among them, or
2567/// the kinds do not change back and forth.
2568pub(crate) fn movable_glyph_kind<T>(
2569    draws: &[T],
2570    kind: impl Fn(&T) -> Option<bool>,
2571    bounds: impl Fn(&T) -> TargetRect,
2572) -> Option<bool> {
2573    let mut turned = 0;
2574    for draw in draws {
2575        turned += usize::from(kind(draw)?);
2576    }
2577    let changes = draws
2578        .windows(2)
2579        .filter(|pair| kind(&pair[0]) != kind(&pair[1]))
2580        .count();
2581    if changes < 2 {
2582        return None;
2583    }
2584    let fewer = turned * 2 < draws.len();
2585    [fewer, !fewer].into_iter().find(|&moved| {
2586        draws.iter().enumerate().all(|(index, draw)| {
2587            kind(draw) != Some(moved)
2588                || draws[index + 1..].iter().all(|later| {
2589                    kind(later) == Some(moved)
2590                        || !crate::draw_pass::target_rects_overlap(bounds(draw), bounds(later))
2591                })
2592        })
2593    })
2594}
2595
2596/// Moves a glyph batch's draws of the kind [`movable_glyph_kind`] picks
2597/// after those of the other kind, each kind in its order, so each draws
2598/// once instead of once per change between them. `moved` is scratch.
2599pub(crate) fn group_glyph_kinds(cmds: &mut [GlyphDrawCmd], moved: &mut Vec<GlyphDrawCmd>) {
2600    let Some(kind) = movable_glyph_kind(cmds, GlyphDrawCmd::turned, GlyphDrawCmd::bounds) else {
2601        return;
2602    };
2603    moved.clear();
2604    let mut kept = 0;
2605    for read in 0..cmds.len() {
2606        if cmds[read].turned() == Some(kind) {
2607            moved.push(cmds[read].clone());
2608        } else {
2609            cmds.swap(kept, read);
2610            kept += 1;
2611        }
2612    }
2613    for (slot, cmd) in cmds[kept..].iter_mut().zip(moved.drain(..)) {
2614        *slot = cmd;
2615    }
2616}
2617
2618#[derive(Clone, Copy, Debug, PartialEq)]
2619struct ImageUvRect {
2620    min: [f32; 2],
2621    max: [f32; 2],
2622    sample_bounds: [f32; 4],
2623}
2624
2625/// A growable vertex buffer and index buffer pair.
2626/// One pass's image and shared glyph quads: the frame's vertex and index
2627/// uploads they were appended to.
2628pub(crate) struct ImageSlot {
2629    vertices: BufferUpload,
2630    indices: BufferUpload,
2631}
2632
2633fn image_vertex_spec() -> UploadAllocatorSpec {
2634    UploadAllocatorSpec::vertex("Image Vertex Buffer", std::mem::size_of::<Vertex>() as u64)
2635}
2636
2637fn image_index_spec() -> UploadAllocatorSpec {
2638    UploadAllocatorSpec::index("Image Index Buffer", std::mem::size_of::<u32>() as u64)
2639}
2640
2641fn glyph_instance_spec() -> UploadAllocatorSpec {
2642    UploadAllocatorSpec::vertex(
2643        "Glyph Instance Buffer",
2644        std::mem::size_of::<GlyphInstance>() as u64,
2645    )
2646}
2647
2648fn turned_glyph_spec() -> UploadAllocatorSpec {
2649    UploadAllocatorSpec::vertex(
2650        "Turned Glyph Instance Buffer",
2651        std::mem::size_of::<TurnedGlyph>() as u64,
2652    )
2653}
2654
2655#[derive(Default)]
2656struct ViewportUniforms {
2657    uploads: FrameUploadAllocators,
2658    slots: Vec<UniformUpload>,
2659}
2660
2661impl ViewportUniforms {
2662    fn begin_frame(&mut self) {
2663        self.slots.clear();
2664        self.uploads.reset();
2665    }
2666
2667    fn claim(
2668        &mut self,
2669        device: &wgpu::Device,
2670        layout: &wgpu::BindGroupLayout,
2671        uniforms: &Uniforms,
2672    ) -> usize {
2673        let slot = self.slots.len();
2674        self.slots.push(self.uploads.upload_uniform(
2675            UploadAllocatorId::Viewport,
2676            UploadAllocatorSpec::uniform(
2677                "Viewport Uniform Buffer",
2678                "Viewport Uniform Bind Group",
2679                std::mem::size_of::<Uniforms>() as u64,
2680            ),
2681            device,
2682            layout,
2683            bytemuck::bytes_of(uniforms),
2684        ));
2685        slot
2686    }
2687
2688    fn bind(&self, pass: &mut wgpu::RenderPass<'_>, slot: usize) -> Result<(), String> {
2689        let uniform = self
2690            .slots
2691            .get(slot)
2692            .ok_or_else(|| "viewport uniform slot was never claimed this frame".to_string())?;
2693        pass.set_bind_group(0, &uniform.bind_group, &[uniform.offset]);
2694        Ok(())
2695    }
2696
2697    fn stage_pending(
2698        &mut self,
2699        device: &wgpu::Device,
2700        recorder: &mut impl FrameCommandRecorder,
2701    ) -> FrameCommandStats {
2702        self.uploads.stage_pending(|buffer, offset, bytes| {
2703            recorder.stage_frame_buffer_copy(device, buffer, offset, bytes)
2704        })
2705    }
2706}
2707
2708#[derive(Clone, Copy, Debug, PartialEq)]
2709pub(crate) struct ViewportUniformParams {
2710    pub(crate) width: u32,
2711    pub(crate) height: u32,
2712    pub(crate) offset: [f32; 2],
2713    pub(crate) transform: SegmentTransform,
2714    /// Where the drawn vertices' origin sits in the segment's device space,
2715    /// added before the transform: zero, except for a retained glyph run
2716    /// drawn under a transform, whose vertices sit at its raster origin. The
2717    /// sum is the one the shared path writes, so both draw the same pixels.
2718    pub(crate) origin: [f32; 2],
2719    /// The pass-order index of the batch's first shape record, from which
2720    /// each record's depth counts.
2721    pub(crate) depth_base: f32,
2722}
2723
2724impl ViewportUniformParams {
2725    /// The logical rect of the drawn scene that the target shows.
2726    pub(crate) fn scene_rect(self, root_scale: f32) -> Rect {
2727        segment_scene_rect(
2728            self.transform,
2729            Rect {
2730                x: self.offset[0],
2731                y: self.offset[1],
2732                width: self.width as f32,
2733                height: self.height as f32,
2734            },
2735            root_scale,
2736        )
2737    }
2738}
2739
2740/// The logical rect of a scene drawn under `transform` that the device rect
2741/// `device` of the target's scene space shows: the rect itself when the
2742/// scene is not transformed, else the bounds it maps back to.
2743pub(crate) fn segment_scene_rect(
2744    transform: SegmentTransform,
2745    device: Rect,
2746    root_scale: f32,
2747) -> Rect {
2748    let device = if transform.is_identity() {
2749        device
2750    } else {
2751        transform.segment_bounds(device)
2752    };
2753    Rect {
2754        x: device.x / root_scale,
2755        y: device.y / root_scale,
2756        width: device.width / root_scale,
2757        height: device.height / root_scale,
2758    }
2759}
2760
2761/// A stored run's draws for one pass: its tables by command, the uniform
2762/// slot holding its placement, and the pipeline and vertex range of each
2763/// segment's quads and bands.
2764pub(crate) struct StoreRunBatch {
2765    pub(crate) command: DrawCommandId,
2766    pub(crate) uniform_slot: usize,
2767    pub(crate) draws: SmallVec<[RunDrawCall; 8]>,
2768    /// The scissor an unturned run's clip puts on its paint: see
2769    /// [`clip_scissor`].
2770    pub(crate) clip: Option<TargetRect>,
2771}
2772
2773struct CompositionTarget {
2774    target: Rc<OffscreenTarget>,
2775    output_bind_group: wgpu::BindGroup,
2776}
2777
2778/// Where a frame renders: straight into the presentable image, or into the
2779/// reusable composition target that the output conversion then copies out.
2780enum FrameRoot {
2781    Surface(Rc<OffscreenTarget>),
2782    Composition(CompositionTarget),
2783}
2784
2785impl FrameRoot {
2786    fn target(&self) -> &Rc<OffscreenTarget> {
2787        match self {
2788            Self::Surface(target) => target,
2789            Self::Composition(composition) => &composition.target,
2790        }
2791    }
2792
2793    /// The output conversion's destination and source bind group; nothing
2794    /// when the frame already rendered into the presentable image.
2795    fn output<'a>(
2796        &'a self,
2797        output_view: Option<&'a wgpu::TextureView>,
2798        screenshot_bind_group: Option<&'a wgpu::BindGroup>,
2799    ) -> Option<(&'a wgpu::TextureView, &'a wgpu::BindGroup)> {
2800        match self {
2801            Self::Surface(_) => None,
2802            Self::Composition(composition) => output_view.map(|view| {
2803                (
2804                    view,
2805                    screenshot_bind_group.unwrap_or(&composition.output_bind_group),
2806                )
2807            }),
2808        }
2809    }
2810}
2811
2812const DIRECT_SURFACE_ROOT_USAGES: wgpu::TextureUsages = wgpu::TextureUsages::RENDER_ATTACHMENT
2813    .union(wgpu::TextureUsages::TEXTURE_BINDING)
2814    .union(wgpu::TextureUsages::COPY_SRC)
2815    .union(wgpu::TextureUsages::COPY_DST);
2816
2817/// The usages a presentable image needs to serve as the frame's root
2818/// target: rendering plus the capture usages the composition target has.
2819/// Callers configuring a surface ask for them when the surface offers them
2820/// all; a partial set falls back to the composition copy, so nothing is
2821/// requested in that case beyond rendering.
2822pub fn presentable_root_usages(supported: wgpu::TextureUsages) -> wgpu::TextureUsages {
2823    if supported.contains(DIRECT_SURFACE_ROOT_USAGES) {
2824        DIRECT_SURFACE_ROOT_USAGES
2825    } else {
2826        wgpu::TextureUsages::RENDER_ATTACHMENT
2827    }
2828}
2829
2830/// Whether the presented image can be the frame's root target: its bytes
2831/// are the composition format (so the 8-bit output conversion would be an
2832/// identity), it can be captured and sampled the way the composition target
2833/// is, and it is the viewport's size.
2834fn surface_is_direct_root(
2835    texture: &wgpu::Texture,
2836    composition_format: wgpu::TextureFormat,
2837    viewport: (u32, u32),
2838) -> bool {
2839    texture.format().remove_srgb_suffix() == composition_format
2840        && texture.usage().contains(DIRECT_SURFACE_ROOT_USAGES)
2841        && (texture.width(), texture.height()) == viewport
2842}
2843
2844#[derive(Clone, Copy)]
2845enum OutputMode {
2846    Display,
2847    Screenshot,
2848}
2849
2850pub struct GpuRenderer {
2851    pub(crate) device: Arc<wgpu::Device>,
2852    pub(crate) queue: Arc<wgpu::Queue>,
2853    device_errors: Arc<DeviceErrorSentry>,
2854    renderer_epoch: u64,
2855    pub(crate) composition_format: wgpu::TextureFormat,
2856    #[cfg(not(target_arch = "wasm32"))]
2857    display_format: wgpu::TextureFormat,
2858    composition_target: Option<CompositionTarget>,
2859    output_converter: OutputConverter,
2860    screenshot_converter: OutputConverter,
2861    adapter_backend: wgpu::Backend,
2862    pipeline_cache: Option<wgpu::PipelineCache>,
2863    #[cfg(not(target_arch = "wasm32"))]
2864    _pipeline_cache_watcher: Option<crate::pipeline_disk_cache::PersistWatcher>,
2865    shape_pipelines: ShapePipelines,
2866    /// Image and glyph pipelines for passes without and with a depth buffer.
2867    image_pipeline: [LazyGpuResource<wgpu::RenderPipeline>; 4],
2868    image_pipeline_dst_out: [LazyGpuResource<wgpu::RenderPipeline>; 4],
2869    /// Indexed by depth, then turned: see [`GpuRenderer::glyph_atlas_pipeline`].
2870    glyph_atlas_pipeline: [LazyGpuResource<wgpu::RenderPipeline>; 4],
2871    image_shader: SharedShader,
2872    glyph_atlas_shader: SharedShader,
2873    /// Transient depth buffers by target size, for passes that lay opaque
2874    /// interiors down first.
2875    depth_targets: Vec<((u32, u32), wgpu::TextureView)>,
2876    uniform_bind_group_layout: wgpu::BindGroupLayout,
2877    image_bind_group_layout: wgpu::BindGroupLayout,
2878    image_nearest_sampler: wgpu::Sampler,
2879    image_linear_sampler: wgpu::Sampler,
2880    text_fonts: SoftwareTextFontSet,
2881    viewport_uniforms: ViewportUniforms,
2882    run_store: RunStore,
2883    image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
2884    image_texture_cache_bytes: usize,
2885    text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
2886    text_glyph_atlas: TextGlyphAtlas,
2887    text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
2888    text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, Rc<CachedGpuTextGlyphRun>>,
2889    text_glyph_run_arena: GlyphRunArena,
2890    text_glyph_run_frame: u64,
2891    text_glyph_mask_cache: SoftwareGlyphRasterCache,
2892    text_line_index_cache: TextLineIndexCache,
2893    pub(crate) scratch_image_vertices: Vec<Vertex>,
2894    pub(crate) scratch_image_indices: Vec<u32>,
2895    pub(crate) scratch_glyph_instances: GlyphInstances,
2896    pub(crate) scratch_image_cmds: Vec<ImageDrawCmd>,
2897    pub(crate) scratch_glyph_cmds: Vec<GlyphDrawCmd>,
2898    pub(crate) scratch_glyph_moved: Vec<GlyphDrawCmd>,
2899    pub(crate) scratch_arena_draws: Vec<RunDrawCall>,
2900    scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
2901    scratch_text_glyph_entries: Vec<GlyphAtlasEntry>,
2902    run_glyph_scratch: RunGlyphScratch,
2903    frame_graph_executor: WgpuFrameGraphExecutor,
2904    deferred_offscreen_releases: Vec<OffscreenTarget>,
2905    pub(crate) effect_renderer: EffectRenderer,
2906    pub(crate) layer_cache: LayerCache,
2907    pub(crate) ablation: Ablation,
2908    pub(crate) ablation_frames: u32,
2909    pub(crate) nesting_overflow_reported: bool,
2910    pub(crate) backdrop_gates: HashMap<NodeId, AdmissionGate>,
2911    pub(crate) fill_gates: HashMap<DrawCommandId, AdmissionGate>,
2912    pub(crate) effect_gates: HashMap<NodeId, AdmissionGate>,
2913    pub(crate) source_gates: HashMap<NodeId, AdmissionGate>,
2914    transparent_sources: HashMap<(u32, u32), Rc<OffscreenTarget>>,
2915    shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
2916    shadow_surface_cache_bytes: u64,
2917    pub(crate) frame_stats: gpu_stats::FrameStats,
2918    last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
2919    pending_frame_warmup_frames: u8,
2920    frame_count: u64,
2921    /// How many requested shader warm-ups this renderer has queued.
2922    shader_warm_ups_queued: usize,
2923}
2924
2925/// What a frame clears to before it draws: nothing for a transparent
2926/// window, the framework's background for every other surface.
2927pub fn frame_clear_color(transparent: bool) -> wgpu::Color {
2928    if transparent {
2929        wgpu::Color::TRANSPARENT
2930    } else {
2931        CLEAR_COLOR
2932    }
2933}
2934
2935fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
2936    let filter = match sampling {
2937        ImageSampling::Nearest => wgpu::FilterMode::Nearest,
2938        ImageSampling::Linear => wgpu::FilterMode::Linear,
2939    };
2940    wgpu::SamplerDescriptor {
2941        label: Some(match sampling {
2942            ImageSampling::Nearest => "Nearest Image Sampler",
2943            ImageSampling::Linear => "Linear Image Sampler",
2944        }),
2945        address_mode_u: wgpu::AddressMode::ClampToEdge,
2946        address_mode_v: wgpu::AddressMode::ClampToEdge,
2947        address_mode_w: wgpu::AddressMode::ClampToEdge,
2948        mag_filter: filter,
2949        min_filter: filter,
2950        mipmap_filter: wgpu::MipmapFilterMode::Nearest,
2951        ..Default::default()
2952    }
2953}
2954
2955/// What a [`GpuRenderer`] is built from. It crosses to the present thread
2956/// whole, so it holds only owned, `Send` parts.
2957pub(crate) struct GpuRendererInit {
2958    pub(crate) device: Arc<wgpu::Device>,
2959    pub(crate) queue: Arc<wgpu::Queue>,
2960    pub(crate) surface_format: wgpu::TextureFormat,
2961    pub(crate) adapter_backend: wgpu::Backend,
2962    pub(crate) adapter_downlevel: wgpu::DownlevelFlags,
2963    pub(crate) text_fonts: SoftwareTextFontSet,
2964    pub(crate) renderer_epoch: u64,
2965    pub(crate) pipeline_compilation: PipelineCompilation,
2966}
2967
2968impl GpuRenderer {
2969    pub fn new(init: GpuRendererInit) -> Self {
2970        let GpuRendererInit {
2971            device,
2972            queue,
2973            surface_format,
2974            adapter_backend,
2975            adapter_downlevel,
2976            text_fonts,
2977            renderer_epoch,
2978            pipeline_compilation,
2979        } = init;
2980        let display_format = surface_format;
2981        let construction_started = Instant::now();
2982        let device_errors = Arc::new(DeviceErrorSentry::default());
2983        if survive_gpu_errors_enabled() {
2984            let sentry = Arc::clone(&device_errors);
2985            device.on_uncaptured_error(Arc::new(move |error| sentry.record(&error)));
2986        }
2987        let composition_format =
2988            crate::offscreen::settle_composition_format(&device, adapter_backend);
2989        device.set_device_lost_callback(|reason, message| {
2990            log::error!("[gpu-device] device lost ({reason:?}): {message}");
2991        });
2992        let mut run_store = RunStore::new(
2993            &device,
2994            RunBufferMode::for_device(&device, adapter_downlevel),
2995        );
2996        let uniform_bind_group_layout =
2997            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
2998                label: Some("Viewport Uniform Bind Group Layout"),
2999                entries: &[wgpu::BindGroupLayoutEntry {
3000                    binding: 0,
3001                    visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
3002                    ty: wgpu::BindingType::Buffer {
3003                        ty: wgpu::BufferBindingType::Uniform,
3004                        has_dynamic_offset: true,
3005                        min_binding_size: wgpu::BufferSize::new(
3006                            std::mem::size_of::<Uniforms>() as u64
3007                        ),
3008                    },
3009                    count: None,
3010                }],
3011            });
3012        let image_bind_group_layout =
3013            device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3014                label: Some("Image Texture Bind Group Layout"),
3015                entries: &[
3016                    wgpu::BindGroupLayoutEntry {
3017                        binding: 0,
3018                        visibility: wgpu::ShaderStages::FRAGMENT,
3019                        ty: wgpu::BindingType::Texture {
3020                            multisampled: false,
3021                            view_dimension: wgpu::TextureViewDimension::D2,
3022                            sample_type: wgpu::TextureSampleType::Float { filterable: true },
3023                        },
3024                        count: None,
3025                    },
3026                    wgpu::BindGroupLayoutEntry {
3027                        binding: 1,
3028                        visibility: wgpu::ShaderStages::FRAGMENT,
3029                        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3030                        count: None,
3031                    },
3032                ],
3033            });
3034        let image_nearest_sampler =
3035            device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
3036        let image_linear_sampler =
3037            device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
3038        let text_glyph_atlas = TextGlyphAtlas::new(
3039            &device,
3040            &image_bind_group_layout,
3041            GlyphSamplers {
3042                nearest: &image_nearest_sampler,
3043                linear: &image_linear_sampler,
3044            },
3045            TEXT_GLYPH_ATLAS_MIN_SIZE,
3046        );
3047        let viewport_uniforms = ViewportUniforms::default();
3048
3049        static GLASS_MATERIAL_FOLDS: DebugToggle =
3050            DebugToggle::new("CRANPOSE_GLASS_MATERIAL_FOLDS");
3051        if GLASS_MATERIAL_FOLDS.equals("1") {
3052            cranpose_ui_graphics::set_glass_material_folds(true);
3053        } else if GLASS_MATERIAL_FOLDS.equals("0") {
3054            cranpose_ui_graphics::set_glass_material_folds(false);
3055        }
3056        log::info!(
3057            "[gpu-init] liquid glass material folds {}",
3058            if cranpose_ui_graphics::glass_material_folds_enabled() {
3059                "on: a pipeline per material's feature set"
3060            } else {
3061                "off: one pipeline per blend mode"
3062            }
3063        );
3064
3065        #[cfg(not(target_arch = "wasm32"))]
3066        let crate::pipeline_disk_cache::Loaded {
3067            cache: pipeline_cache,
3068            first_screen,
3069        } = crate::pipeline_disk_cache::load(&device);
3070        #[cfg(target_arch = "wasm32")]
3071        let (pipeline_cache, first_screen): (Option<wgpu::PipelineCache>, Vec<u64>) =
3072            (None, Vec::new());
3073        #[cfg(not(target_arch = "wasm32"))]
3074        let pipeline_cache_watcher =
3075            crate::pipeline_disk_cache::spawn_persist_watcher(pipeline_cache.clone());
3076
3077        let effects_started = Instant::now();
3078        let pipeline_compiler = PipelineCompiler::for_compilation(pipeline_compilation);
3079        let effect_renderer = EffectRenderer::new(
3080            &device,
3081            pipeline_compiler.clone(),
3082            pipeline_cache.clone(),
3083            composition_format,
3084            adapter_backend,
3085        );
3086        let output_converter = OutputConverter::new(&device, adapter_backend, display_format);
3087        let screenshot_converter =
3088            OutputConverter::new(&device, adapter_backend, wgpu::TextureFormat::Rgba8Unorm);
3089        let effects_ms = instant_ms(effects_started, Instant::now());
3090        let mut frame_graph_executor = WgpuFrameGraphExecutor::new();
3091        frame_graph_executor.init_pass_timing(&device, &queue);
3092        let shape_shader = SharedShader::new(
3093            &device,
3094            adapter_backend,
3095            "Shape Shader",
3096            shape_shader_source(run_store.mode()),
3097            &[Some(&uniform_bind_group_layout), Some(run_store.layout())],
3098        );
3099        if run_store.mode().trig_fill {
3100            let fill = ArcTrigFill::new(
3101                &device,
3102                adapter_backend,
3103                pipeline_cache.clone(),
3104                shape_shader.clone(),
3105            );
3106            if first_screen.contains(&crate::arc_trig_fill::FIRST_SCREEN_KEY) {
3107                fill.warm(&pipeline_compiler);
3108            }
3109            run_store.attach_trig_fill(fill);
3110        }
3111        let shape_pipelines = ShapePipelines::new(
3112            ShapePipelineFactory {
3113                device: Arc::clone(&device),
3114                cache: pipeline_cache.clone(),
3115                format: composition_format,
3116                shader: shape_shader,
3117                mode: run_store.mode(),
3118            },
3119            adapter_backend,
3120            &pipeline_compiler,
3121            first_screen
3122                .into_iter()
3123                .filter(|&bits| bits != crate::arc_trig_fill::FIRST_SCREEN_KEY)
3124                .filter_map(ShapePipelineKey::from_bits),
3125        );
3126        let image_layouts = [
3127            Some(&uniform_bind_group_layout),
3128            Some(&image_bind_group_layout),
3129        ];
3130        let image_shader = SharedShader::new(
3131            &device,
3132            adapter_backend,
3133            "Image Shader",
3134            || shaders::IMAGE_SHADER.into(),
3135            &image_layouts,
3136        );
3137        let glyph_atlas_shader = SharedShader::new(
3138            &device,
3139            adapter_backend,
3140            "Glyph Atlas Shader",
3141            || shaders::GLYPH_ATLAS_SHADER.into(),
3142            &image_layouts,
3143        );
3144
3145        let mut renderer = Self {
3146            device,
3147            queue,
3148            device_errors,
3149            renderer_epoch,
3150            composition_format,
3151            #[cfg(not(target_arch = "wasm32"))]
3152            display_format,
3153            composition_target: None,
3154            output_converter,
3155            screenshot_converter,
3156            adapter_backend,
3157            pipeline_cache,
3158            #[cfg(not(target_arch = "wasm32"))]
3159            _pipeline_cache_watcher: pipeline_cache_watcher,
3160            shape_pipelines,
3161            image_pipeline: [
3162                LazyGpuResource::new("image/src-over"),
3163                LazyGpuResource::new("image/src-over/depth"),
3164                LazyGpuResource::new("image/src-over/alpha-mask"),
3165                LazyGpuResource::new("image/src-over/alpha-mask/depth"),
3166            ],
3167            image_pipeline_dst_out: [
3168                LazyGpuResource::new("image/dst-out"),
3169                LazyGpuResource::new("image/dst-out/depth"),
3170                LazyGpuResource::new("image/dst-out/alpha-mask"),
3171                LazyGpuResource::new("image/dst-out/alpha-mask/depth"),
3172            ],
3173            glyph_atlas_pipeline: [
3174                LazyGpuResource::new("glyph/atlas"),
3175                LazyGpuResource::new("glyph/atlas/depth"),
3176                LazyGpuResource::new("glyph/atlas/turned"),
3177                LazyGpuResource::new("glyph/atlas/turned/depth"),
3178            ],
3179            image_shader,
3180            glyph_atlas_shader,
3181            depth_targets: Vec::new(),
3182            uniform_bind_group_layout,
3183            image_bind_group_layout,
3184            image_nearest_sampler,
3185            image_linear_sampler,
3186            text_fonts,
3187            viewport_uniforms,
3188            run_store,
3189            image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
3190                MAX_TEXTURE_CACHE_ITEMS,
3191            ),
3192            image_texture_cache_bytes: 0,
3193            text_image_cache: BoundedLruCache::with_capacity_at_least_one(
3194                MAX_TEXT_IMAGE_CACHE_ITEMS,
3195            ),
3196            text_glyph_atlas,
3197            text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
3198                MAX_TEXT_GLYPH_RUN_CACHE_ITEMS,
3199            ),
3200            text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
3201                MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
3202            ),
3203            text_glyph_run_arena: GlyphRunArena::default(),
3204            text_glyph_run_frame: 0,
3205            text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
3206                MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
3207            ),
3208            text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_CACHE_ITEMS),
3209            scratch_image_vertices: Vec::new(),
3210            scratch_image_indices: Vec::new(),
3211            scratch_glyph_instances: GlyphInstances::default(),
3212            scratch_image_cmds: Vec::new(),
3213            scratch_glyph_cmds: Vec::new(),
3214            scratch_glyph_moved: Vec::new(),
3215            scratch_arena_draws: Vec::new(),
3216            scratch_text_glyph_run: Vec::new(),
3217            scratch_text_glyph_entries: Vec::new(),
3218            run_glyph_scratch: RunGlyphScratch::default(),
3219            frame_graph_executor,
3220            deferred_offscreen_releases: Vec::new(),
3221            effect_renderer,
3222            layer_cache: LayerCache::new(),
3223            ablation: Ablation::default(),
3224            ablation_frames: 0,
3225            nesting_overflow_reported: false,
3226            backdrop_gates: HashMap::default(),
3227            fill_gates: HashMap::default(),
3228            effect_gates: HashMap::default(),
3229            source_gates: HashMap::default(),
3230            transparent_sources: HashMap::default(),
3231            shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
3232                MAX_SHADOW_SURFACE_CACHE_ITEMS,
3233            ),
3234            shadow_surface_cache_bytes: 0,
3235            frame_stats: gpu_stats::FrameStats::default(),
3236            last_frame_stats: None,
3237            pending_frame_warmup_frames: 0,
3238            frame_count: 0,
3239            shader_warm_ups_queued: 0,
3240        };
3241        renderer.warm_requested_shaders();
3242        log::info!(
3243            "[gpu-init] {:?} renderer ready in {:.1} ms (effects {:.1} ms)",
3244            adapter_backend,
3245            instant_ms(construction_started, Instant::now()),
3246            effects_ms,
3247        );
3248        renderer
3249    }
3250
3251    fn ensure_shape_pipeline(&mut self, key: ShapePipelineKey, vertices: u64) {
3252        self.shape_pipelines.ensure(key, vertices);
3253    }
3254
3255    /// Queues the shader warm-ups requested since the last call, each at the
3256    /// target it draws to (`cranpose_ui_graphics::request_shader_warm_ups`).
3257    fn warm_requested_shaders(&mut self) {
3258        let requested = cranpose_ui_graphics::shader_warm_ups_after(self.shader_warm_ups_queued);
3259        if requested.is_empty() {
3260            return;
3261        }
3262        self.shader_warm_ups_queued += requested.len();
3263        self.effect_renderer.warm_shaders(&requested);
3264    }
3265
3266    fn image_pipeline_resource(
3267        &self,
3268        blend_mode: BlendMode,
3269        depth: bool,
3270        alpha_mask: bool,
3271    ) -> &LazyGpuResource<wgpu::RenderPipeline> {
3272        let pipelines = match blend_mode {
3273            BlendMode::DstOut => &self.image_pipeline_dst_out,
3274            _ => &self.image_pipeline,
3275        };
3276        &pipelines[usize::from(depth) + 2 * usize::from(alpha_mask)]
3277    }
3278
3279    fn image_pipeline_job(
3280        &self,
3281        blend_mode: BlendMode,
3282        depth: bool,
3283        alpha_mask: bool,
3284    ) -> impl FnOnce() -> wgpu::RenderPipeline + CompilerSend + 'static {
3285        let device = Arc::clone(&self.device);
3286        let cache = self.pipeline_cache.clone();
3287        let format = self.composition_format;
3288        let shader = self.image_shader.clone();
3289        move || {
3290            create_image_pipeline(
3291                &device,
3292                cache.as_ref(),
3293                format,
3294                &shader,
3295                blend_mode,
3296                depth,
3297                alpha_mask,
3298            )
3299        }
3300    }
3301
3302    pub(crate) fn image_pipeline(
3303        &self,
3304        blend_mode: BlendMode,
3305        depth: bool,
3306        alpha_mask: bool,
3307    ) -> &wgpu::RenderPipeline {
3308        self.image_pipeline_resource(blend_mode, depth, alpha_mask)
3309            .get_or_init(self.adapter_backend, || {
3310                self.image_pipeline_job(blend_mode, depth, alpha_mask)()
3311            })
3312    }
3313
3314    fn glyph_atlas_pipeline_job(
3315        &self,
3316        depth: bool,
3317        turned: bool,
3318    ) -> impl FnOnce() -> wgpu::RenderPipeline + CompilerSend + 'static {
3319        let device = Arc::clone(&self.device);
3320        let cache = self.pipeline_cache.clone();
3321        let format = self.composition_format;
3322        let shader = self.glyph_atlas_shader.clone();
3323        move || {
3324            create_glyph_atlas_pipeline(&device, cache.as_ref(), format, &shader, (depth, turned))
3325        }
3326    }
3327
3328    /// The glyph pipeline for a pass with a depth buffer or not, drawing
3329    /// [`TurnedGlyph`]s or plain [`GlyphInstance`]s.
3330    fn glyph_atlas_pipeline(&self, depth: bool, turned: bool) -> &wgpu::RenderPipeline {
3331        self.glyph_atlas_pipeline[usize::from(depth) + 2 * usize::from(turned)]
3332            .get_or_init(self.adapter_backend, || {
3333                self.glyph_atlas_pipeline_job(depth, turned)()
3334            })
3335    }
3336
3337    /// The transient depth buffer for a target of `size`, created on first
3338    /// use; a few sizes are kept, the most recent first.
3339    pub(crate) fn depth_target(&mut self, size: (u32, u32)) -> wgpu::TextureView {
3340        const KEPT_DEPTH_TARGETS: usize = 4;
3341        if let Some(index) = self
3342            .depth_targets
3343            .iter()
3344            .position(|(kept, _)| *kept == size)
3345        {
3346            let entry = self.depth_targets.remove(index);
3347            let view = entry.1.clone();
3348            self.depth_targets.insert(0, entry);
3349            return view;
3350        }
3351        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
3352            label: Some("Opaque interior depth"),
3353            size: wgpu::Extent3d {
3354                width: size.0,
3355                height: size.1,
3356                depth_or_array_layers: 1,
3357            },
3358            mip_level_count: 1,
3359            sample_count: 1,
3360            dimension: wgpu::TextureDimension::D2,
3361            format: DEPTH_FORMAT,
3362            // A browser's WebGPU may not know the transient usage; every
3363            // other backend takes it and ignores it where it saves nothing.
3364            usage: if self.adapter_backend == wgpu::Backend::BrowserWebGpu {
3365                wgpu::TextureUsages::RENDER_ATTACHMENT
3366            } else {
3367                wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TRANSIENT_ATTACHMENT
3368            },
3369            view_formats: &[],
3370        });
3371        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3372        self.depth_targets.insert(0, (size, view.clone()));
3373        self.depth_targets.truncate(KEPT_DEPTH_TARGETS);
3374        view
3375    }
3376
3377    fn ensure_image_cached(&mut self, image: &ImageBitmap) -> Result<(), String> {
3378        if self.image_texture_cache.get(&image.id()).is_some() {
3379            return Ok(());
3380        }
3381
3382        let size = wgpu::Extent3d {
3383            width: image.width(),
3384            height: image.height(),
3385            depth_or_array_layers: 1,
3386        };
3387
3388        let alpha_mask = matches!(
3389            image.format(),
3390            cranpose_ui_graphics::ImagePixelFormat::Alpha8 { .. }
3391        );
3392        let (format, bytes_per_pixel) = if alpha_mask {
3393            (wgpu::TextureFormat::R8Unorm, 1)
3394        } else {
3395            (wgpu::TextureFormat::Rgba8Unorm, 4)
3396        };
3397
3398        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
3399            label: Some("Image Texture"),
3400            size,
3401            mip_level_count: 1,
3402            sample_count: 1,
3403            dimension: wgpu::TextureDimension::D2,
3404            format,
3405            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3406            view_formats: &[],
3407        });
3408
3409        let upload_stats = self.frame_graph_executor.upload_texture(
3410            &self.queue,
3411            wgpu::TexelCopyTextureInfo {
3412                texture: &texture,
3413                mip_level: 0,
3414                origin: wgpu::Origin3d::ZERO,
3415                aspect: wgpu::TextureAspect::All,
3416            },
3417            image.pixels(),
3418            wgpu::TexelCopyBufferLayout {
3419                offset: 0,
3420                bytes_per_row: Some(bytes_per_pixel * image.width()),
3421                rows_per_image: Some(image.height()),
3422            },
3423            size,
3424        );
3425        self.frame_stats.record_command_stats(upload_stats);
3426
3427        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3428        let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
3429        let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
3430
3431        let bytes = image.pixels().len();
3432        if let Some(replaced) = self.image_texture_cache.put(
3433            image.id(),
3434            CachedImageTexture {
3435                alpha_mask,
3436                _texture: texture,
3437                _view: view,
3438                nearest_bind_group,
3439                linear_bind_group,
3440                bytes,
3441            },
3442        ) {
3443            self.image_texture_cache_bytes = self
3444                .image_texture_cache_bytes
3445                .saturating_sub(replaced.bytes);
3446        }
3447        self.image_texture_cache_bytes += bytes;
3448        while self.image_texture_cache_bytes > MAX_IMAGE_TEXTURE_CACHE_BYTES
3449            && self.image_texture_cache.len() > 1
3450        {
3451            let Some((_, evicted)) = self.image_texture_cache.pop_lru() else {
3452                break;
3453            };
3454            self.image_texture_cache_bytes =
3455                self.image_texture_cache_bytes.saturating_sub(evicted.bytes);
3456        }
3457        Ok(())
3458    }
3459
3460    fn image_bind_group(
3461        &self,
3462        view: &wgpu::TextureView,
3463        sampler: &wgpu::Sampler,
3464    ) -> wgpu::BindGroup {
3465        self.device.create_bind_group(&wgpu::BindGroupDescriptor {
3466            label: Some("Image Texture Bind Group"),
3467            layout: &self.image_bind_group_layout,
3468            entries: &[
3469                wgpu::BindGroupEntry {
3470                    binding: 0,
3471                    resource: wgpu::BindingResource::TextureView(view),
3472                },
3473                wgpu::BindGroupEntry {
3474                    binding: 1,
3475                    resource: wgpu::BindingResource::Sampler(sampler),
3476                },
3477            ],
3478        })
3479    }
3480
3481    pub(crate) fn max_texture_dim(&self) -> u32 {
3482        self.effect_renderer.max_texture_dim()
3483    }
3484
3485    /// A pooled texture that outlives the frame: layer cache entries and
3486    /// cached shadow surfaces.
3487    pub(crate) fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
3488        self.effect_renderer
3489            .acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
3490    }
3491
3492    fn frame_root(
3493        &mut self,
3494        output_mode: OutputMode,
3495        output_view: Option<&wgpu::TextureView>,
3496        output_texture: Option<&wgpu::Texture>,
3497        viewport: (u32, u32),
3498    ) -> FrameRoot {
3499        if let (OutputMode::Display, Some(view), Some(texture)) =
3500            (output_mode, output_view, output_texture)
3501            && surface_is_direct_root(texture, self.composition_format, viewport)
3502        {
3503            return FrameRoot::Surface(Rc::new(OffscreenTarget::from_surface(
3504                texture.clone(),
3505                view.clone(),
3506            )));
3507        }
3508        FrameRoot::Composition(self.take_composition_target(viewport.0.max(1), viewport.1.max(1)))
3509    }
3510
3511    fn take_composition_target(&mut self, width: u32, height: u32) -> CompositionTarget {
3512        if let Some(target) = self.composition_target.take()
3513            && target.target.width == width
3514            && target.target.height == height
3515        {
3516            return target;
3517        }
3518        let target = Rc::new(OffscreenTarget::new(
3519            &self.device,
3520            self.composition_format,
3521            width,
3522            height,
3523        ));
3524        let output_bind_group = self.output_converter.bind_group(&self.device, &target.view);
3525        CompositionTarget {
3526            target,
3527            output_bind_group,
3528        }
3529    }
3530
3531    fn transient_offscreen_descriptor(
3532        &self,
3533        label: &'static str,
3534        width: u32,
3535        height: u32,
3536    ) -> FrameTextureDescriptor {
3537        let max_texture_dim = self.max_texture_dim();
3538        FrameTextureDescriptor::render_attachment(
3539            label,
3540            width.min(max_texture_dim),
3541            height.min(max_texture_dim),
3542            self.composition_format,
3543        )
3544    }
3545
3546    /// A texture of the given size that stays transparent: the input of a
3547    /// runtime shader whose layer draws nothing itself, so the shader needs
3548    /// no surface pass and reads the same empty content every frame.
3549    pub(crate) fn transparent_source<C: FrameCommandRecorder>(
3550        &mut self,
3551        recorder: &mut C,
3552        width: u32,
3553        height: u32,
3554    ) -> Rc<OffscreenTarget> {
3555        if let Some(source) = self.transparent_sources.get(&(width, height)) {
3556            return Rc::clone(source);
3557        }
3558        if self.transparent_sources.len() >= MAX_TRANSPARENT_SOURCES {
3559            for (_, source) in self.transparent_sources.drain() {
3560                if let Ok(target) = Rc::try_unwrap(source) {
3561                    self.deferred_offscreen_releases.push(target);
3562                }
3563            }
3564        }
3565        let source = Rc::new(self.acquire_retained_surface(width, height));
3566        self.clear_target(
3567            recorder,
3568            &source.view,
3569            wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3570        );
3571        self.transparent_sources
3572            .insert((width, height), Rc::clone(&source));
3573        source
3574    }
3575
3576    fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
3577        self.deferred_offscreen_releases.push(target);
3578    }
3579
3580    fn flush_deferred_offscreen_releases(&mut self) {
3581        let layer_cache = &mut self.layer_cache;
3582        let mut retire = |gate: &mut AdmissionGate| {
3583            let seen = gate.end_frame();
3584            if !seen && let Some(dead) = gate.dead_entry() {
3585                layer_cache.remove(&dead);
3586            }
3587            seen
3588        };
3589        self.backdrop_gates.retain(|_, gate| retire(gate));
3590        self.fill_gates.retain(|_, gate| retire(gate));
3591        self.effect_gates.retain(|_, gate| retire(gate));
3592        self.source_gates.retain(|_, gate| retire(gate));
3593        for target in self.deferred_offscreen_releases.drain(..) {
3594            self.effect_renderer.release_offscreen(target);
3595        }
3596        self.layer_cache.end_frame();
3597        for (transient, target) in self.layer_cache.take_released() {
3598            match transient {
3599                Some(descriptor) => self
3600                    .frame_graph_executor
3601                    .return_cached_transient(descriptor, target),
3602                None => self.effect_renderer.release_offscreen(target),
3603            }
3604        }
3605        self.effect_renderer.end_offscreen_frame();
3606        self.frame_graph_executor.end_transient_frame();
3607    }
3608
3609    fn insert_cached_shadow_surface(
3610        &mut self,
3611        key: ShadowSurfaceCacheKey,
3612        target: Rc<OffscreenTarget>,
3613    ) {
3614        let byte_size = offscreen_byte_size(target.width, target.height);
3615        while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
3616            let Some((_, evicted)) = self.shadow_surface_cache.pop_lru() else {
3617                break;
3618            };
3619            self.shadow_surface_cache_bytes = self
3620                .shadow_surface_cache_bytes
3621                .saturating_sub(evicted.byte_size);
3622        }
3623        let cached = CachedShadowSurface { target, byte_size };
3624        if let Some((_, replaced)) = self.shadow_surface_cache.push(key, cached) {
3625            self.shadow_surface_cache_bytes = self
3626                .shadow_surface_cache_bytes
3627                .saturating_sub(replaced.byte_size);
3628        }
3629        self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
3630    }
3631}
3632fn frame_stats_need_warmup_frame(snapshot: &gpu_stats::FrameStatsSnapshot) -> bool {
3633    snapshot.layer_cache_misses > 0
3634        || snapshot.shadow_shape_cache_misses > 0
3635        || snapshot.text_image_cache_misses > 0
3636        || snapshot.text_glyph_atlas_misses > 0
3637}
3638
3639fn update_frame_warmup_budget(pending_frames: &mut u8, snapshot: &gpu_stats::FrameStatsSnapshot) {
3640    if *pending_frames > 0 {
3641        *pending_frames = pending_frames.saturating_sub(1);
3642    } else if frame_stats_need_warmup_frame(snapshot) {
3643        *pending_frames = CACHE_MISS_WARMUP_FRAMES;
3644    }
3645}
3646
3647impl GpuRenderer {
3648    #[expect(clippy::too_many_arguments)]
3649    pub fn render(
3650        &mut self,
3651        texture: &wgpu::Texture,
3652        view: &wgpu::TextureView,
3653        width: u32,
3654        height: u32,
3655        packet: FramePacket,
3656        surface_epoch: u64,
3657        returns: &mut RenderReturns,
3658    ) -> Result<(), String> {
3659        self.render_internal(
3660            width,
3661            height,
3662            packet,
3663            surface_epoch,
3664            returns,
3665            OutputMode::Display,
3666            Some(view),
3667            Some(texture),
3668        )
3669    }
3670
3671    #[expect(clippy::too_many_arguments)]
3672    fn render_internal(
3673        &mut self,
3674        width: u32,
3675        height: u32,
3676        packet: FramePacket,
3677        surface_epoch: u64,
3678        returns: &mut RenderReturns,
3679        output_mode: OutputMode,
3680        output_view: Option<&wgpu::TextureView>,
3681        output_texture: Option<&wgpu::Texture>,
3682    ) -> Result<(), String> {
3683        let cancel_reason = if packet.renderer_epoch != self.renderer_epoch {
3684            Some(CancelReason::RendererEpoch)
3685        } else if packet.surface_epoch != surface_epoch {
3686            Some(CancelReason::SurfaceEpoch)
3687        } else if packet.viewport != (width, height) {
3688            Some(CancelReason::Viewport)
3689        } else {
3690            None
3691        };
3692        if let Some(reason) = cancel_reason {
3693            return Self::cancel_packet(packet, reason, returns);
3694        }
3695        if self.device_errors.take_poison() {
3696            return Self::cancel_packet(packet, CancelReason::DeviceError, returns);
3697        }
3698        returns.frame_id = packet.frame_id;
3699        let render_start = Instant::now();
3700        self.warm_requested_shaders();
3701        self.shape_pipelines.begin_frame();
3702        self.viewport_uniforms.begin_frame();
3703        self.run_store.begin_frame(gpu_stats_enabled());
3704        self.begin_text_glyph_run_frame();
3705
3706        let text_cache_len = packet.text_cache_len;
3707        let frame_root = self.frame_root(output_mode, output_view, output_texture, (width, height));
3708        let root = frame_root.target();
3709        let screenshot_bind_group = output_view.and_then(|_| {
3710            matches!(output_mode, OutputMode::Screenshot).then(|| {
3711                self.screenshot_converter
3712                    .bind_group(&self.device, &root.view)
3713            })
3714        });
3715        let output = frame_root.output(output_view, screenshot_bind_group.as_ref());
3716        let result = self.render_graph(root, packet, returns, output_mode, output);
3717        if let FrameRoot::Composition(composition) = frame_root {
3718            self.composition_target = Some(composition);
3719        }
3720        let after_graph = Instant::now();
3721        self.flush_deferred_offscreen_releases();
3722
3723        self.frame_stats
3724            .layer_cache_size
3725            .set(self.layer_cache.len() as u32);
3726        self.frame_stats
3727            .layer_cache_bytes
3728            .set(self.layer_cache.bytes());
3729        self.frame_stats.offscreen_pool_size.set(
3730            self.effect_renderer
3731                .retained_offscreen_count()
3732                .saturating_add(self.frame_graph_executor.retained_texture_count())
3733                .saturating_add(usize::from(self.composition_target.is_some())) as u32,
3734        );
3735        self.frame_stats.offscreen_pool_bytes.set(
3736            (self.effect_renderer.retained_offscreen_bytes() as u64)
3737                .saturating_add(self.frame_graph_executor.retained_texture_bytes())
3738                .saturating_add(self.composition_target.as_ref().map_or(0, |target| {
3739                    u64::from(target.target.width)
3740                        .saturating_mul(u64::from(target.target.height))
3741                        .saturating_mul(composition_bytes_per_pixel())
3742                })),
3743        );
3744        self.frame_stats
3745            .text_pool_size
3746            .set(self.text_image_cache.len() as u32);
3747        self.frame_stats
3748            .image_cache_size
3749            .set(self.image_texture_cache.len() as u32);
3750        self.frame_stats.text_cache_size.set(text_cache_len as u32);
3751        self.effect_renderer
3752            .merge_and_reset_debug_counters(&self.frame_stats);
3753        self.frame_graph_executor.reset_upload_allocators();
3754        let snapshot = self.frame_stats.snapshot();
3755        if crate::frame_graph::frame_graph_pass_telemetry_threshold_ms().is_some() {
3756            log::warn!(
3757                "[wgpu-render-stage:frame-stats] layer_hit={} layer_miss={} miss_px={} \
3758                 offscreen_acq={} offscreen_new={} isolated={} draws={}",
3759                snapshot.layer_cache_hits,
3760                snapshot.layer_cache_misses,
3761                snapshot.layer_cache_miss_pixels,
3762                snapshot.offscreen_acquires,
3763                snapshot.offscreen_news,
3764                snapshot.isolated_layer_renders,
3765                snapshot.draw_calls,
3766            );
3767        }
3768        self.last_frame_stats = Some(snapshot);
3769        PRESENTED_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3770        update_frame_warmup_budget(&mut self.pending_frame_warmup_frames, &snapshot);
3771        let gpu_stats_on = gpu_stats_enabled();
3772        self.frame_stats
3773            .maybe_print_snapshot(snapshot, &mut self.frame_count, gpu_stats_on);
3774        if gpu_stats_on && self.frame_count.is_multiple_of(60) {
3775            gpu_stats::print_gpu_memory_report(&self.device, self.frame_count);
3776        }
3777        self.frame_graph_executor
3778            .end_pass_timing_frame(&self.device, &self.queue);
3779        self.frame_stats.reset();
3780        let after_stats = Instant::now();
3781        if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
3782            log::warn!(
3783                "[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
3784                instant_ms(render_start, after_graph),
3785                instant_ms(after_graph, after_stats),
3786            );
3787        }
3788        if result.is_ok() {
3789            returns.outcome = PresentOutcome::Presented;
3790        }
3791        result
3792    }
3793
3794    /// Returns a packet unrendered, handing its scene back for recycling.
3795    pub(crate) fn cancel_packet(
3796        packet: FramePacket,
3797        reason: CancelReason,
3798        returns: &mut RenderReturns,
3799    ) -> Result<(), String> {
3800        returns.scene = Some(FrameSceneStorage {
3801            root: packet.root,
3802            overlay: packet.overlay,
3803        });
3804        returns.frame_id = packet.frame_id;
3805        returns.outcome = PresentOutcome::Cancelled(reason);
3806        Ok(())
3807    }
3808
3809    pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
3810        self.last_frame_stats
3811    }
3812
3813    pub fn gpu_pass_timings(&self) -> crate::pass_timing::GpuPassTimingReport {
3814        self.frame_graph_executor.pass_timing_report()
3815    }
3816
3817    pub fn needs_frame_warmup(&self) -> bool {
3818        self.pending_frame_warmup_frames > 0
3819    }
3820
3821    pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
3822        DebugCpuAllocationStats {
3823            scene_graph_node_count: 0,
3824            scene_graph_heap_bytes: 0,
3825            scene_hits_len: 0,
3826            scene_hits_cap: 0,
3827            scene_node_index_len: 0,
3828            scene_node_index_cap: 0,
3829            text_renderer_pool_len: self.text_image_cache.len(),
3830            text_renderer_pool_cap: self.text_image_cache.cap().get(),
3831            image_texture_cache_len: self.image_texture_cache.len(),
3832            image_texture_cache_cap: self.image_texture_cache.cap().get(),
3833            run_arena_staging_bytes: self.run_store.arena_staging_bytes(),
3834            run_store_bytes: self.run_store.stored_bytes(),
3835            run_store_runs: self.run_store.stored_count(),
3836            scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
3837            scratch_image_indices_cap: self.scratch_image_indices.capacity(),
3838            scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
3839            scratch_glyph_instances_cap: self.scratch_glyph_instances.plain.capacity()
3840                + self.scratch_glyph_instances.turned.capacity(),
3841            layer_cache_len: self.layer_cache.len(),
3842            layer_cache_bytes: self.layer_cache.bytes(),
3843        }
3844    }
3845    pub fn render_to_rgba_pixels(
3846        &mut self,
3847        width: u32,
3848        height: u32,
3849        packet: FramePacket,
3850        surface_epoch: u64,
3851        returns: &mut RenderReturns,
3852    ) -> Result<Vec<u8>, String> {
3853        if width == 0 || height == 0 {
3854            return Err("Screenshot size must be non-zero".to_string());
3855        }
3856        let errors_before = self.device_errors.error_count();
3857
3858        let output_texture = crate::offscreen::create_2d_texture(
3859            &self.device,
3860            wgpu::TextureFormat::Rgba8Unorm,
3861            width,
3862            height,
3863            wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
3864            Some("Screenshot Output Texture"),
3865        );
3866        let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
3867        self.render_internal(
3868            width,
3869            height,
3870            packet,
3871            surface_epoch,
3872            returns,
3873            OutputMode::Screenshot,
3874            Some(&output_view),
3875            None,
3876        )?;
3877        if returns.outcome != PresentOutcome::Presented {
3878            return Err(format!(
3879                "Screenshot rendering did not complete: {:?}",
3880                returns.outcome
3881            ));
3882        }
3883        self.read_screenshot_pixels(&output_texture, errors_before)
3884    }
3885
3886    fn read_screenshot_pixels(
3887        &mut self,
3888        output_texture: &wgpu::Texture,
3889        errors_before: u64,
3890    ) -> Result<Vec<u8>, String> {
3891        let width = output_texture.width();
3892        let height = output_texture.height();
3893        let bytes_per_pixel = 4u32;
3894        let unpadded_bytes_per_row = width
3895            .checked_mul(bytes_per_pixel)
3896            .ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
3897        let padded_bytes_per_row =
3898            align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
3899        let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
3900
3901        let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
3902            label: Some("Screenshot Readback Buffer"),
3903            size: output_buffer_size,
3904            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
3905            mapped_at_creation: false,
3906        });
3907
3908        let device = self.device.clone();
3909        let queue = self.queue.clone();
3910        let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
3911        let source = graph.import_surface("screenshot-copy-source");
3912        graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
3913            context.encoder.copy_texture_to_buffer(
3914                wgpu::TexelCopyTextureInfo {
3915                    texture: output_texture,
3916                    mip_level: 0,
3917                    origin: wgpu::Origin3d::ZERO,
3918                    aspect: wgpu::TextureAspect::All,
3919                },
3920                wgpu::TexelCopyBufferInfo {
3921                    buffer: &output_buffer,
3922                    layout: wgpu::TexelCopyBufferLayout {
3923                        offset: 0,
3924                        bytes_per_row: Some(padded_bytes_per_row),
3925                        rows_per_image: Some(height),
3926                    },
3927                },
3928                wgpu::Extent3d {
3929                    width,
3930                    height,
3931                    depth_or_array_layers: 1,
3932                },
3933            );
3934            Ok(())
3935        });
3936        let mut executor = std::mem::take(&mut self.frame_graph_executor);
3937        let execution = executor.execute_recorded_graph(&device, &queue, graph);
3938        self.frame_graph_executor = executor;
3939        let execution = execution.map_err(|error| error.to_string())?;
3940        let submission_index = execution.submission;
3941        let copy_stats = execution.stats;
3942        self.last_frame_stats = self
3943            .last_frame_stats
3944            .map(|snapshot| snapshot.with_command_stats_added(copy_stats));
3945
3946        let buffer_slice = output_buffer.slice(..);
3947        let (tx, rx) = mpsc::channel();
3948        buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
3949            let _ = tx.send(result);
3950        });
3951        self.device
3952            .poll(wgpu::PollType::Wait {
3953                submission_index: Some(submission_index),
3954                timeout: None,
3955            })
3956            .map_err(|error| format!("Screenshot GPU wait failed: {error}"))?;
3957
3958        match rx.recv_timeout(Duration::from_secs(3)) {
3959            Ok(Ok(())) => {}
3960            Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
3961            Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
3962        }
3963        if self.device_errors.error_count() != errors_before {
3964            output_buffer.unmap();
3965            return Err("GPU errors occurred while capturing the screenshot".to_string());
3966        }
3967
3968        let mapped = buffer_slice
3969            .get_mapped_range()
3970            .map_err(|err| format!("Screenshot readback could not be read: {err}"))?;
3971        let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
3972
3973        let src_row_len = padded_bytes_per_row as usize;
3974        let dst_row_len = unpadded_bytes_per_row as usize;
3975        for row in 0..height as usize {
3976            let src_offset = row * src_row_len;
3977            let dst_offset = row * dst_row_len;
3978            pixels[dst_offset..dst_offset + dst_row_len]
3979                .copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
3980        }
3981        drop(mapped);
3982        output_buffer.unmap();
3983
3984        self.convert_surface_pixels_to_rgba(&pixels)
3985    }
3986
3987    fn render_graph(
3988        &mut self,
3989        root_target: &Rc<OffscreenTarget>,
3990        packet: FramePacket,
3991        returns: &mut RenderReturns,
3992        output_mode: OutputMode,
3993        output: Option<(&wgpu::TextureView, &wgpu::BindGroup)>,
3994    ) -> Result<(), String> {
3995        let device = self.device.clone();
3996        let queue = self.queue.clone();
3997        let graph_start = Instant::now();
3998        let FramePacket {
3999            root,
4000            overlay,
4001            root_scale,
4002            clear,
4003            ..
4004        } = packet;
4005        let page = Rc::clone(root_target);
4006
4007        #[cfg(not(target_arch = "wasm32"))]
4008        let (result, submitted) = {
4009            let mut executor = std::mem::take(&mut self.frame_graph_executor);
4010            let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
4011            let surface = frame_graph.import_surface("renderer-surface");
4012            frame_graph.add_fallible_recorded_command_pass(
4013                Some("Renderer Frame Pass"),
4014                &[],
4015                &[surface],
4016                |frame_encoder| {
4017                    self.encode_frame(
4018                        frame_encoder,
4019                        &root,
4020                        overlay.as_ref(),
4021                        Rc::clone(&page),
4022                        root_scale,
4023                        clear,
4024                        output_mode,
4025                        output,
4026                    )
4027                },
4028            );
4029            let after_build = Instant::now();
4030            let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
4031            let after_execute = Instant::now();
4032            self.frame_graph_executor = executor;
4033            if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
4034                log::warn!(
4035                    "[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
4036                    instant_ms(graph_start, after_build),
4037                    instant_ms(after_build, after_execute),
4038                );
4039            }
4040            match execution {
4041                Ok(execution) => {
4042                    if execution.stats.pass_count > 0 {
4043                        self.frame_stats.record_command_stats(execution.stats);
4044                    }
4045                    (Ok(()), true)
4046                }
4047                Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => (Ok(()), false),
4048                Err(error) => (Err(error.to_string()), false),
4049            }
4050        };
4051
4052        #[cfg(target_arch = "wasm32")]
4053        let (result, submitted) = {
4054            let mut executor = std::mem::take(&mut self.frame_graph_executor);
4055            let (result, execution) = {
4056                let mut frame_encoder =
4057                    executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
4058                let initial_pass_count = frame_encoder.recorded_pass_count();
4059                let result = self.encode_frame(
4060                    &mut frame_encoder,
4061                    &root,
4062                    overlay.as_ref(),
4063                    Rc::clone(&page),
4064                    root_scale,
4065                    clear,
4066                    output_mode,
4067                    output,
4068                );
4069                let execution =
4070                    if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
4071                        Some(frame_encoder.finish())
4072                    } else {
4073                        None
4074                    };
4075                (result, execution)
4076            };
4077            let after_execute = Instant::now();
4078            self.frame_graph_executor = executor;
4079            if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
4080                log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
4081            }
4082            let submitted = execution.is_some();
4083            if let Some(execution) = execution {
4084                self.frame_stats.record_command_stats(execution.stats);
4085            }
4086            (result, submitted)
4087        };
4088        #[cfg(not(target_arch = "wasm32"))]
4089        if submitted {
4090            crate::pipeline_disk_cache::note_frame_drawn();
4091        }
4092        if !submitted {
4093            self.run_store.invalidate_uploads();
4094            self.text_glyph_gpu_run_cache.clear();
4095            self.text_glyph_run_arena = GlyphRunArena::default();
4096        }
4097        returns.scene = Some(FrameSceneStorage { root, overlay });
4098        result
4099    }
4100
4101    /// Records the frame: the root and overlay layer scenes into the frame's
4102    /// target, the output conversion when the target is not the presented
4103    /// image, and the viewport uniforms the recorded passes claimed.
4104    #[expect(clippy::too_many_arguments)]
4105    fn encode_frame<C: FrameCommandRecorder>(
4106        &mut self,
4107        recorder: &mut C,
4108        root: &LayerScene,
4109        overlay: Option<&LayerScene>,
4110        page: Rc<OffscreenTarget>,
4111        root_scale: f32,
4112        clear: wgpu::Color,
4113        output_mode: OutputMode,
4114        output: Option<(&wgpu::TextureView, &wgpu::BindGroup)>,
4115    ) -> Result<(), String> {
4116        FrameExecutor::new(self, recorder).render_frame(
4117            root,
4118            overlay,
4119            page,
4120            root_scale,
4121            wgpu::LoadOp::Clear(clear),
4122        )?;
4123        if let Some((output_view, bind_group)) = output {
4124            match output_mode {
4125                OutputMode::Display => &self.output_converter,
4126                OutputMode::Screenshot => &self.screenshot_converter,
4127            }
4128            .encode(
4129                &self.device,
4130                recorder,
4131                output_view,
4132                bind_group,
4133                self.adapter_backend,
4134            );
4135            recorder.record_pass();
4136        }
4137        self.stage_frame_uploads(recorder);
4138        self.shape_pipelines.request_wanted();
4139        self.viewport_uniforms.uploads.finish_frame();
4140        self.run_store.finish_frame();
4141        Ok(())
4142    }
4143
4144    pub(crate) fn stage_frame_uploads(&mut self, recorder: &mut impl FrameCommandRecorder) {
4145        let mut upload = self.viewport_uniforms.stage_pending(&self.device, recorder);
4146        upload += self.run_store.stage_pending(&self.device, recorder);
4147        upload += self
4148            .text_glyph_run_arena
4149            .stage_pending(&self.device, recorder);
4150        self.run_store.fill_arena_trig(&self.device, recorder);
4151        self.frame_stats.record_command_stats(upload);
4152    }
4153    /// Claims this frame's next viewport uniform slot for `params`.
4154    pub(crate) fn claim_uniform_slot(&mut self, params: ViewportUniformParams) -> usize {
4155        let uniforms = Uniforms::of(params, PlacementData::zeroed(), RunTier::Arena);
4156        self.viewport_uniforms
4157            .claim(&self.device, &self.uniform_bind_group_layout, &uniforms)
4158    }
4159
4160    /// Resolves a blurred shadow at `z` into a texture and queues its
4161    /// composites. The shadow's shapes and texts render into a source the
4162    /// size of their blur footprint, blur in place and take the post-blur
4163    /// cutouts; the source is then blitted in bands around the occluder.
4164    /// Shape-only shadows live in the shadow cache, keyed by their content
4165    /// and device placement, so a scrolling card re-blits its cached blur.
4166    /// The blurred shadow texture and whether the cache held it: a
4167    /// shape-only shadow is cached by content and placement, a shadow with
4168    /// text renders every frame.
4169    fn blurred_shadow_source<C: FrameCommandRecorder>(
4170        &mut self,
4171        recorder: &mut C,
4172        shadow: &ShadowDraw,
4173        source_device: DevicePixelBounds,
4174        pixel_radius: f32,
4175        root_scale: f32,
4176        transients: &mut Vec<(FrameTextureDescriptor, Rc<OffscreenTarget>)>,
4177    ) -> Option<(Rc<OffscreenTarget>, bool, SourceContent)> {
4178        let shape_only = shadow.texts.is_empty();
4179        let key = if shape_only {
4180            shape_shadow_surface_cache_key(shadow, source_device, pixel_radius, root_scale)
4181        } else {
4182            None
4183        };
4184        let content = key.map_or(SourceContent::Transient, |key| {
4185            SourceContent::retained(&key)
4186        });
4187        if let Some(entry) = key.and_then(|key| self.shadow_surface_cache.get(&key)) {
4188            return Some((Rc::clone(&entry.target), true, content));
4189        }
4190        if !shape_only {
4191            self.frame_stats.record_shadow_text_blur_fallback();
4192        }
4193        let source = self.render_shadow_source(
4194            recorder,
4195            shadow,
4196            source_device,
4197            pixel_radius,
4198            root_scale,
4199            key.is_some(),
4200            transients,
4201        )?;
4202        if let Some(key) = key {
4203            self.frame_stats
4204                .record_shadow_shape_cache_miss(source_device.width, source_device.height);
4205            self.frame_stats.maybe_print_shadow_shape_cache_miss(
4206                source_device.width,
4207                source_device.height,
4208                key.content_hash,
4209                pixel_radius,
4210                [source_device.x, source_device.y],
4211                shadow.shapes.as_ref().map_or(0, RunDraw::record_count) as usize,
4212                shadow.clip,
4213            );
4214            self.insert_cached_shadow_surface(key, Rc::clone(&source));
4215        }
4216        Some((source, false, content))
4217    }
4218
4219    #[expect(clippy::too_many_arguments)]
4220    pub(crate) fn resolve_blurred_shadow<C: FrameCommandRecorder>(
4221        &mut self,
4222        recorder: &mut C,
4223        shadow: &ShadowDraw,
4224        z: usize,
4225        root_scale: f32,
4226        target_rect: DeviceRect4,
4227        transients: &mut Vec<(FrameTextureDescriptor, Rc<OffscreenTarget>)>,
4228        resolved: &mut Vec<ResolvedComposite>,
4229    ) {
4230        if !shadow.requires_surface()
4231            || skip_shadow_draws()
4232            || !root_scale.is_finite()
4233            || root_scale <= 0.0
4234        {
4235            return;
4236        }
4237        let Some(bounds) = shadow_draw_bounds(shadow) else {
4238            return;
4239        };
4240        let margin = blur_reach(shadow.blur_radius, root_scale);
4241        let source_bounds = expand_rect(bounds, margin, margin);
4242        let mut visible = source_bounds;
4243        if let Some(clip) = shadow.clip {
4244            let Some(clipped) = visible.intersect(expand_rect(clip, margin, margin)) else {
4245                return;
4246            };
4247            visible = clipped;
4248        }
4249        let target_logical = Rect {
4250            x: target_rect.0 / root_scale,
4251            y: target_rect.1 / root_scale,
4252            width: target_rect.2 / root_scale,
4253            height: target_rect.3 / root_scale,
4254        };
4255        let Some(visible) = visible.intersect(expand_rect(target_logical, margin, margin)) else {
4256            return;
4257        };
4258        let max_texture_dim = self.max_texture_dim();
4259        let shape_only = shadow.texts.is_empty();
4260        let anchor = shadow
4261            .shapes
4262            .as_ref()
4263            .and_then(|run| run.placement.snap_anchor);
4264        let source_device = shape_only
4265            .then(|| {
4266                translation_stable_anchored_device_pixel_bounds(
4267                    source_bounds,
4268                    anchor,
4269                    root_scale,
4270                    max_texture_dim,
4271                )
4272            })
4273            .flatten()
4274            .or_else(|| device_pixel_bounds(visible, root_scale, max_texture_dim));
4275        let Some(source_device) = source_device else {
4276            return;
4277        };
4278        let pixel_radius = shadow.blur_radius * root_scale;
4279        let Some((source, hit, content)) = self.blurred_shadow_source(
4280            recorder,
4281            shadow,
4282            source_device,
4283            pixel_radius,
4284            root_scale,
4285            transients,
4286        ) else {
4287            return;
4288        };
4289        let dest = (
4290            source_device.x,
4291            source_device.y,
4292            source_device.width as f32,
4293            source_device.height as f32,
4294        );
4295        let mut coverage = intersect_device_rects(dest, target_rect);
4296        if let Some(clip) = shadow.clip {
4297            coverage = coverage.and_then(|coverage| {
4298                intersect_device_rects(coverage, anchored_rect_to_device(clip, anchor, root_scale))
4299            });
4300        }
4301        let Some(coverage) = coverage else {
4302            return;
4303        };
4304        let bands = shadow_bands(
4305            coverage,
4306            shadow
4307                .occluder
4308                .map(|occluder| anchored_rect_to_device(occluder, anchor, root_scale)),
4309        );
4310        if bands.is_empty() {
4311            self.frame_stats.record_shadow_fully_occluded();
4312            return;
4313        }
4314        if hit {
4315            self.frame_stats
4316                .record_shadow_shape_cache_hit(banded_pixels(&bands));
4317        }
4318        let rounded_mask = shadow_composite_mask(shadow, anchor, root_scale);
4319        let downscaled =
4320            (source.width, source.height) != (source_device.width, source_device.height);
4321        let (sample_mode, source_viewport) = if downscaled {
4322            (
4323                CompositeSampleMode::Linear,
4324                Some((0.0, 0.0, source.width as f32, source.height as f32)),
4325            )
4326        } else {
4327            (CompositeSampleMode::Nearest, None)
4328        };
4329        for band in bands {
4330            resolved.push(ResolvedComposite {
4331                z_index: z,
4332                source: Rc::clone(&source),
4333                content,
4334                dest,
4335                scissor: Some(band),
4336                kind: ResolvedCompositeKind::Blit {
4337                    alpha: 1.0,
4338                    blend_mode: BlendMode::SrcOver,
4339                    rounded_mask,
4340                    sample_mode,
4341                    source_viewport,
4342                },
4343            });
4344        }
4345    }
4346
4347    /// Draws a shadow's shapes and texts into a surface covering `bounds`
4348    /// and blurs it. A wide blur runs at its scratch size and its result
4349    /// stays there, read bilinearly by the composite; a post-blur cutout
4350    /// needs the surface's full size, so the blurred result is interpolated
4351    /// back into it first and the cutout drawn at that size. A retained
4352    /// result feeds the shadow cache; a transient one is registered with
4353    /// the frame's transients and released with them. `None` when the
4354    /// shadow draws nothing.
4355    #[expect(clippy::too_many_arguments)]
4356    fn render_shadow_source<C: FrameCommandRecorder>(
4357        &mut self,
4358        recorder: &mut C,
4359        shadow: &ShadowDraw,
4360        bounds: DevicePixelBounds,
4361        pixel_radius: f32,
4362        root_scale: f32,
4363        retained: bool,
4364        transients: &mut Vec<(FrameTextureDescriptor, Rc<OffscreenTarget>)>,
4365    ) -> Option<Rc<OffscreenTarget>> {
4366        let (width, height) = (bounds.width, bounds.height);
4367        let device = self.device.clone();
4368        let (scratch_width, scratch_height) =
4369            crate::effect_renderer::blur_scratch_size(pixel_radius, pixel_radius, width, height);
4370        let full_size_result = shadow.post_blur_cutouts.is_some()
4371            || (scratch_width, scratch_height) == (width, height);
4372        let (result_width, result_height) = if full_size_result {
4373            (width, height)
4374        } else {
4375            (scratch_width, scratch_height)
4376        };
4377        let result = if retained {
4378            Rc::new(self.acquire_retained_surface(result_width, result_height))
4379        } else {
4380            self.shadow_transient(
4381                recorder,
4382                transients,
4383                "Shadow Result",
4384                result_width,
4385                result_height,
4386            )
4387        };
4388        let source = if full_size_result {
4389            Rc::clone(&result)
4390        } else {
4391            self.shadow_transient(recorder, transients, "Shadow Source", width, height)
4392        };
4393        let offset = [bounds.x, bounds.y];
4394        let target = PassTarget {
4395            view: &source.view,
4396            width,
4397            height,
4398        };
4399        let scene = shadow_scene(
4400            shadow.shapes.as_ref(),
4401            &shadow.texts,
4402            blur_reach(shadow.blur_radius, root_scale),
4403        );
4404        let segment = PassSegment {
4405            scene: &scene,
4406            ops: &scene.draw_ops,
4407            composites: &[],
4408            offset,
4409            scissor: None,
4410            first_run_window: None,
4411            transform: SegmentTransform::IDENTITY,
4412            scale: root_scale,
4413        };
4414        let drew = self.encode_pass(
4415            recorder,
4416            target,
4417            std::slice::from_ref(&segment),
4418            wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
4419            "Shadow Source Pass",
4420        );
4421        match drew {
4422            Ok(true) => {}
4423            Ok(false) => {
4424                drop(source);
4425                if retained && let Ok(target) = Rc::try_unwrap(result) {
4426                    self.defer_offscreen_release(target);
4427                }
4428                return None;
4429            }
4430            Err(error) => {
4431                log::error!("shadow source pass failed: {error}");
4432                return None;
4433            }
4434        }
4435        if pixel_radius > 0.0 {
4436            let scratch_descriptor = self.transient_offscreen_descriptor(
4437                "Shadow Blur Scratch",
4438                scratch_width,
4439                scratch_height,
4440            );
4441            let scratch = recorder.acquire_transient_offscreen(&device, scratch_descriptor);
4442            let blurred = if full_size_result && (scratch_width, scratch_height) != (width, height)
4443            {
4444                Some(self.shadow_transient(
4445                    recorder,
4446                    transients,
4447                    "Shadow Blur Result",
4448                    scratch_width,
4449                    scratch_height,
4450                ))
4451            } else {
4452                None
4453            };
4454            let blur_dest = match &blurred {
4455                Some(blurred) => (&blurred.view, (scratch_width, scratch_height)),
4456                None => (&result.view, (result_width, result_height)),
4457            };
4458            let passes = self.effect_renderer.encode_blur_scissored_ping_pong_passes(
4459                recorder,
4460                &device,
4461                &source,
4462                &scratch,
4463                blur_dest,
4464                pixel_radius,
4465                pixel_radius,
4466                TileMode::Decal,
4467                None,
4468            );
4469            recorder.record_passes(passes);
4470            self.effect_renderer.record_blur_pass();
4471            recorder.release_transient_offscreen(scratch_descriptor, scratch);
4472            if let Some(blurred) = &blurred {
4473                self.effect_renderer
4474                    .encode_upscale_pass(recorder, &device, blurred, &result.view);
4475                recorder.record_pass();
4476            }
4477        }
4478        if let Some(cutout_run) = &shadow.post_blur_cutouts {
4479            let cutouts = shadow_scene(Some(cutout_run), &[], 0.0);
4480            let segment = PassSegment {
4481                scene: &cutouts,
4482                ops: &cutouts.draw_ops,
4483                composites: &[],
4484                offset,
4485                scissor: None,
4486                first_run_window: None,
4487                transform: SegmentTransform::IDENTITY,
4488                scale: root_scale,
4489            };
4490            if let Err(error) = self.encode_pass(
4491                recorder,
4492                target,
4493                std::slice::from_ref(&segment),
4494                wgpu::LoadOp::Load,
4495                "Shadow Cutout Pass",
4496            ) {
4497                log::error!("shadow cutout pass failed: {error}");
4498            }
4499        }
4500        Some(result)
4501    }
4502
4503    /// A transient surface of a shadow's frame, released with the frame's
4504    /// transients.
4505    fn shadow_transient<C: FrameCommandRecorder>(
4506        &self,
4507        recorder: &mut C,
4508        transients: &mut Vec<(FrameTextureDescriptor, Rc<OffscreenTarget>)>,
4509        label: &'static str,
4510        width: u32,
4511        height: u32,
4512    ) -> Rc<OffscreenTarget> {
4513        let descriptor = self.transient_offscreen_descriptor(label, width, height);
4514        let target = Rc::new(recorder.acquire_transient_offscreen(&self.device, descriptor));
4515        transients.push((descriptor, Rc::clone(&target)));
4516        target
4517    }
4518
4519    /// Whether `run` draws from retained buffers keyed by its command.
4520    pub(crate) fn run_is_stored(&self, run: &RunDraw) -> bool {
4521        self.run_store.is_stored(run)
4522    }
4523
4524    /// The pipeline `segment` of a run at `placement` draws with. `clipped`
4525    /// says the variant tests the run's clip: a turned run's, since an
4526    /// unturned one's clip is its paint's scissor (see [`clip_scissor`]).
4527    fn run_pipeline_key(
4528        segment: &RecordSegment,
4529        placement: &crate::scene::Placement,
4530        tier: RunTier,
4531        ablation: ShapeAblation,
4532        turns: ShapeTurns,
4533        (depth, clip): (bool, SegmentClip),
4534        viewport: ViewportUniformParams,
4535    ) -> ShapePipelineKey {
4536        let blend_mode = supported_blend_mode(segment.blend);
4537        let laid = depth
4538            && blend_mode == BlendMode::SrcOver
4539            && !placement.paints()
4540            && clip != SegmentClip::Rounded
4541            && !ablation.material
4542            && !ablation.fill;
4543        ShapePipelineKey {
4544            blend_mode,
4545            tier,
4546            variant: ShapeVariant::of_segment(
4547                segment,
4548                clip,
4549                ablation,
4550                laid,
4551                turns == ShapeTurns::None
4552                    && placement.snap_anchor.is_some()
4553                    && viewport.offset.iter().all(|value| value.fract() == 0.0),
4554            ),
4555            turns,
4556            depth: if depth {
4557                ShapeDepth::Tested
4558            } else {
4559                ShapeDepth::Off
4560            },
4561        }
4562    }
4563
4564    /// Prepares `key`'s pipeline and, in a pass with a depth buffer, the
4565    /// one laying down its opaque interiors.
4566    fn ensure_run_pipelines(&mut self, key: ShapePipelineKey, vertices: u64) {
4567        self.ensure_shape_pipeline(key, vertices);
4568        if let Some(interior) = key.interior() {
4569            self.ensure_shape_pipeline(interior, vertices);
4570        }
4571    }
4572
4573    /// Brings a stored run's tables up to date and records its draws under
4574    /// a placement uniform of its own.
4575    pub(crate) fn prepare_store_run<C: FrameCommandRecorder>(
4576        &mut self,
4577        recorder: &mut C,
4578        run: &RunDraw,
4579        viewport: ViewportUniformParams,
4580        root_scale: f32,
4581        window: &std::ops::Range<u32>,
4582        depth: bool,
4583    ) -> StoreRunBatch {
4584        let command = run.command.expect("a stored run has a command");
4585        let placement = &run.placement;
4586        let ablation = self.ablation.shape;
4587        let turns = ShapeTurns::of(viewport.transform, false);
4588        let scissor = clip_scissor(placement, root_scale, viewport);
4589        let clip = SegmentClip::of(placement, scissor.is_some());
4590        let mut draws = SmallVec::new();
4591        self.run_store.stored_run_draws(
4592            &self.device,
4593            run,
4594            &mut |segment| {
4595                Self::run_pipeline_key(
4596                    segment,
4597                    placement,
4598                    RunTier::Store,
4599                    ablation,
4600                    turns,
4601                    (depth, clip),
4602                    viewport,
4603                )
4604            },
4605            &mut draws,
4606        );
4607        window_draws(&mut draws, window);
4608        let upload_start = Instant::now();
4609        let (upload, fill) =
4610            self.run_store
4611                .upload_stored(&self.device, recorder, run, root_scale, window, &draws);
4612        if let Some(total_ms) = should_log_wgpu_render_stage(upload_start, Instant::now()) {
4613            log::warn!(
4614                "[wgpu-render-stage:run-upload] total_ms={total_ms:.2} bytes={} records={}",
4615                upload.upload_bytes,
4616                run.tables().shapes.len()
4617            );
4618        }
4619        self.frame_stats.record_command_stats(upload);
4620        if let Some(fill) = fill {
4621            self.frame_stats.add_shape_fill(fill);
4622        }
4623        let uniforms = Uniforms::of(
4624            viewport,
4625            PlacementData::of(&run.placement, root_scale, viewport.transform),
4626            RunTier::Store,
4627        );
4628        let uniform_slot =
4629            self.viewport_uniforms
4630                .claim(&self.device, &self.uniform_bind_group_layout, &uniforms);
4631        for draw in &draws {
4632            self.ensure_run_pipelines(
4633                draw.key,
4634                crate::run_store::draw_vertices(
4635                    draw.records.end - draw.records.start,
4636                    draw.band_class,
4637                ),
4638            );
4639        }
4640        StoreRunBatch {
4641            command,
4642            uniform_slot,
4643            draws,
4644            clip: scissor,
4645        }
4646    }
4647
4648    pub(crate) fn open_arena(&mut self) -> usize {
4649        self.run_store.open_arena()
4650    }
4651
4652    /// The records the open arena chunk holds so far.
4653    pub(crate) fn open_arena_records(&self) -> u32 {
4654        self.run_store.open_arena_records()
4655    }
4656
4657    pub(crate) fn arena_accepts(&self, chunk: usize, run: &RunDraw) -> bool {
4658        self.run_store.arena_accepts(chunk, run)
4659    }
4660
4661    /// Appends `window` of `run`'s records to the open arena chunk, their
4662    /// placement under `turn`, keyed for a pass whose flat and turned
4663    /// records alternate often when `mixed_turns`.
4664    #[expect(clippy::too_many_arguments)]
4665    /// Appends `run`'s records at `window` to the open arena chunk. `clip`
4666    /// says how its variant tests its clip: an unturned run leaves a rect
4667    /// clip to its paint's scissor, never a rounded one.
4668    pub(crate) fn append_arena_run(
4669        &mut self,
4670        chunk: usize,
4671        run: &RunDraw,
4672        window: std::ops::Range<u32>,
4673        root_scale: f32,
4674        viewport: ViewportUniformParams,
4675        mixed_turns: bool,
4676        (depth, clip): (bool, SegmentClip),
4677    ) -> u32 {
4678        let placement = &run.placement;
4679        let ablation = self.ablation.shape;
4680        let turn = viewport.transform;
4681        let turns = ShapeTurns::of(turn, mixed_turns);
4682        let mut keys: SmallVec<[(ShapePipelineKey, u64); 4]> = SmallVec::new();
4683        let taken = self.run_store.append_arena(
4684            chunk,
4685            run,
4686            window,
4687            (root_scale, turn),
4688            &mut |segment| {
4689                Self::run_pipeline_key(
4690                    segment,
4691                    placement,
4692                    RunTier::Arena,
4693                    ablation,
4694                    turns,
4695                    (depth, clip),
4696                    viewport,
4697                )
4698            },
4699            &mut keys,
4700        );
4701        for (key, vertices) in keys {
4702            self.ensure_run_pipelines(key, vertices);
4703        }
4704        taken
4705    }
4706
4707    /// Moves the open chunk's draws since its last cut onto `out`, keeping
4708    /// it open.
4709    pub(crate) fn cut_arena(&mut self, chunk: usize, out: &mut Vec<RunDrawCall>) {
4710        self.run_store.cut_arena(&self.device, chunk, out);
4711    }
4712
4713    /// Uploads the open chunk and moves its draws since its last cut onto
4714    /// `out`.
4715    pub(crate) fn close_arena(&mut self, chunk: usize, out: &mut Vec<RunDrawCall>) {
4716        if let Some(fill) = self.run_store.close_arena(&self.device, chunk, out) {
4717            self.frame_stats.add_shape_fill(fill);
4718        }
4719    }
4720
4721    pub(crate) fn draw_run_calls(
4722        &self,
4723        pass: &mut wgpu::RenderPass<'_>,
4724        tables: ArenaBinding<'_>,
4725        uniform_slot: usize,
4726        draws: &[RunDrawCall],
4727        scissor: (u32, u32, u32, u32),
4728        stage: RunStage,
4729    ) -> Result<(), String> {
4730        match stage {
4731            RunStage::Paint => {
4732                if draws.is_empty() {
4733                    return Ok(());
4734                }
4735                self.frame_stats.bump_shapes();
4736                self.record_run_draws(
4737                    pass,
4738                    tables,
4739                    uniform_slot,
4740                    scissor,
4741                    draws.iter().map(PlannedRunDraw::paint),
4742                )
4743            }
4744            RunStage::Interiors => {
4745                let planned = interior_run_draws(draws);
4746                if planned.is_empty() {
4747                    return Ok(());
4748                }
4749                self.frame_stats
4750                    .shape_interior_draws
4751                    .set(self.frame_stats.shape_interior_draws.get() + planned.len() as u32);
4752                self.record_run_draws(pass, tables, uniform_slot, scissor, planned.into_iter())
4753            }
4754        }
4755    }
4756
4757    /// Records `planned` against one batch's tables, uniform and scissor.
4758    fn record_run_draws(
4759        &self,
4760        pass: &mut wgpu::RenderPass<'_>,
4761        tables: ArenaBinding<'_>,
4762        uniform_slot: usize,
4763        scissor: (u32, u32, u32, u32),
4764        planned: impl ExactSizeIterator<Item = PlannedRunDraw>,
4765    ) -> Result<(), String> {
4766        self.frame_stats.add_draw_calls(planned.len() as u32);
4767        let (x, y, width, height) = scissor;
4768        pass.set_scissor_rect(x, y, width, height);
4769        self.viewport_uniforms.bind(pass, uniform_slot)?;
4770        pass.set_bind_group(1, tables.bind_group, &tables.offsets[2..]);
4771        for (slot, buffer) in tables.records.into_iter().enumerate() {
4772            pass.set_vertex_buffer(slot as u32, buffer.slice(u64::from(tables.offsets[slot])..));
4773        }
4774        let mut bound_class = None;
4775        for draw in planned {
4776            let key = draw.key;
4777            let (pipeline, fallback) = self
4778                .shape_pipelines
4779                .get(key)
4780                .ok_or_else(|| format!("shape pipeline {key:?} was not prepared"))?;
4781            if fallback {
4782                self.frame_stats
4783                    .shape_pipeline_fallback_draws
4784                    .set(self.frame_stats.shape_pipeline_fallback_draws.get() + 1);
4785            } else if !key.is_general() {
4786                self.frame_stats
4787                    .shape_specialized_draws
4788                    .set(self.frame_stats.shape_specialized_draws.get() + 1);
4789            }
4790            if bound_class != Some(draw.band_class) {
4791                pass.set_index_buffer(
4792                    self.run_store.strip_index_buffer(draw.band_class).slice(..),
4793                    wgpu::IndexFormat::Uint32,
4794                );
4795                bound_class = Some(draw.band_class);
4796            }
4797            pass.set_pipeline(pipeline);
4798            pass.draw_indexed(draw.indices, 0, draw.records);
4799        }
4800        Ok(())
4801    }
4802
4803    pub(crate) fn draw_store_run(
4804        &self,
4805        pass: &mut wgpu::RenderPass<'_>,
4806        batch: &StoreRunBatch,
4807        scissor: (u32, u32, u32, u32),
4808        stage: RunStage,
4809    ) -> Result<(), String> {
4810        let stored = self
4811            .run_store
4812            .stored(&batch.command)
4813            .ok_or_else(|| "a stored run left the store before its draw".to_string())?;
4814        self.draw_run_calls(
4815            pass,
4816            stored.buffers.binding(),
4817            batch.uniform_slot,
4818            &batch.draws,
4819            scissor,
4820            stage,
4821        )
4822    }
4823
4824    pub(crate) fn draw_arena(
4825        &self,
4826        pass: &mut wgpu::RenderPass<'_>,
4827        chunk: usize,
4828        uniform_slot: usize,
4829        draws: &[RunDrawCall],
4830        scissor: (u32, u32, u32, u32),
4831        stage: RunStage,
4832    ) -> Result<(), String> {
4833        self.draw_run_calls(
4834            pass,
4835            self.run_store.arena_binding(chunk),
4836            uniform_slot,
4837            draws,
4838            scissor,
4839            stage,
4840        )
4841    }
4842    #[cfg(not(target_arch = "wasm32"))]
4843    pub(crate) fn surface_format(&self) -> wgpu::TextureFormat {
4844        self.display_format
4845    }
4846
4847    pub fn device_error_count(&self) -> u64 {
4848        self.device_errors.error_count()
4849    }
4850
4851    /// A pass that only applies `load_op` to the target, for a scene with
4852    /// nothing to draw that still needs its clear.
4853    pub(crate) fn clear_target<C: FrameCommandRecorder>(
4854        &self,
4855        recorder: &mut C,
4856        view: &wgpu::TextureView,
4857        load_op: wgpu::LoadOp<wgpu::Color>,
4858    ) {
4859        self.empty_pass(recorder, "Clear Pass", view, load_op);
4860    }
4861
4862    pub(crate) fn empty_pass<C: FrameCommandRecorder>(
4863        &self,
4864        recorder: &mut C,
4865        label: &'static str,
4866        view: &wgpu::TextureView,
4867        load_op: wgpu::LoadOp<wgpu::Color>,
4868    ) {
4869        let pass = recorder.begin_color_pass(label, view, load_op);
4870        drop(pass);
4871        recorder.record_pass();
4872    }
4873    pub(crate) fn draw_image_cmds(
4874        &self,
4875        pass: &mut wgpu::RenderPass<'_>,
4876        image_slot: &ImageSlot,
4877        uniform_slot: usize,
4878        cmds: &[ImageDrawCmd],
4879        (blend_mode, depth): (BlendMode, bool),
4880        bound: Option<(u32, u32, u32, u32)>,
4881    ) -> Result<(), String> {
4882        if cmds.is_empty() {
4883            return Ok(());
4884        }
4885        self.frame_stats.bump_images();
4886        self.frame_stats.add_draw_calls(cmds.len() as u32);
4887        let mut bound_pipeline = None;
4888        self.viewport_uniforms.bind(pass, uniform_slot)?;
4889        pass.set_index_buffer(image_slot.indices.slice(), wgpu::IndexFormat::Uint32);
4890        pass.set_vertex_buffer(0, image_slot.vertices.slice());
4891        for cmd in cmds {
4892            let Some((x, y, width, height)) = bounded_scissor(cmd.scissor, bound) else {
4893                continue;
4894            };
4895            pass.set_scissor_rect(x, y, width, height);
4896            let cached = self
4897                .image_texture_cache
4898                .peek(&cmd.image_id)
4899                .ok_or_else(|| "image texture missing from cache".to_string())?;
4900            if bound_pipeline != Some(cached.alpha_mask) {
4901                pass.set_pipeline(self.image_pipeline(blend_mode, depth, cached.alpha_mask));
4902                bound_pipeline = Some(cached.alpha_mask);
4903            }
4904            pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
4905            pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
4906        }
4907        Ok(())
4908    }
4909
4910    pub(crate) fn draw_glyph_cmds(
4911        &self,
4912        pass: &mut wgpu::RenderPass<'_>,
4913        (glyph_slot, turned_slot): (Option<&BufferUpload>, Option<&BufferUpload>),
4914        uniform_slot: usize,
4915        cmds: &[GlyphDrawCmd],
4916        bound: Option<(u32, u32, u32, u32)>,
4917        frame: PassFrame,
4918    ) -> Result<(), String> {
4919        if cmds.is_empty() {
4920            return Ok(());
4921        }
4922        let whole_target = frame.scissor(bound);
4923        self.frame_stats.bump_text();
4924        let mut bound_atlas = None;
4925        // Which shared instances are bound, turned or plain, with their
4926        // pipeline; retained runs draw with the plain one.
4927        let mut shared_bound: Option<bool> = None;
4928        let mut bound_pipeline: Option<bool> = None;
4929        let mut bound_run_instances: Option<&wgpu::Buffer> = None;
4930        let mut draws = 0u32;
4931        for draw in GlyphDraws::new(cmds) {
4932            let scissor = match draw.scissor {
4933                Some(scissor) => bounded_scissor(scissor, bound),
4934                None => Some(whole_target),
4935            };
4936            let Some((x, y, width, height)) = scissor else {
4937                continue;
4938            };
4939            pass.set_scissor_rect(x, y, width, height);
4940            if !bound_atlas.is_some_and(|atlas| Rc::ptr_eq(atlas, draw.atlas)) {
4941                pass.set_bind_group(1, draw.atlas.as_ref(), &[]);
4942                bound_atlas = Some(draw.atlas);
4943            }
4944            draws += 1;
4945            match draw.step {
4946                GlyphDrawStep::Shared(instances, turned) => {
4947                    if bound_pipeline != Some(turned) {
4948                        pass.set_pipeline(self.glyph_atlas_pipeline(frame.depth, turned));
4949                        bound_pipeline = Some(turned);
4950                    }
4951                    if shared_bound != Some(turned) {
4952                        let slot =
4953                            if turned { turned_slot } else { glyph_slot }.ok_or_else(|| {
4954                                "shared glyph draw without glyph instances".to_string()
4955                            })?;
4956                        self.viewport_uniforms.bind(pass, uniform_slot)?;
4957                        pass.set_vertex_buffer(0, slot.slice());
4958                        shared_bound = Some(turned);
4959                        bound_run_instances = None;
4960                    }
4961                    pass.draw(0..GLYPH_QUAD_CORNERS, instances);
4962                }
4963                GlyphDrawStep::Retained {
4964                    run,
4965                    uniform_slot: retained_slot,
4966                } => {
4967                    if bound_pipeline != Some(false) {
4968                        pass.set_pipeline(self.glyph_atlas_pipeline(frame.depth, false));
4969                        bound_pipeline = Some(false);
4970                    }
4971                    shared_bound = None;
4972                    self.viewport_uniforms.bind(pass, retained_slot)?;
4973                    let instances = run.span.instance_buffer();
4974                    if bound_run_instances != Some(instances) {
4975                        pass.set_vertex_buffer(0, instances.slice(..));
4976                        bound_run_instances = Some(instances);
4977                    }
4978                    pass.draw(0..GLYPH_QUAD_CORNERS, run.span.instances());
4979                }
4980            }
4981        }
4982        self.frame_stats.add_draw_calls(draws);
4983        Ok(())
4984    }
4985    pub(crate) fn append_image_draw_cmd(
4986        &mut self,
4987        image_draw: &ImageDraw,
4988        viewport: ViewportUniformParams,
4989        root_scale: f32,
4990        image_vertices: &mut Vec<Vertex>,
4991        image_indices: &mut Vec<u32>,
4992        image_cmds: &mut Vec<ImageDrawCmd>,
4993    ) -> Result<(), String> {
4994        let snap_delta = image_draw
4995            .snap_anchor
4996            .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
4997            .unwrap_or_default();
4998        let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
4999        if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
5000            return Ok(());
5001        }
5002
5003        let (mut tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
5004        if tint[3] <= 0.0 {
5005            return Ok(());
5006        }
5007
5008        let filtered_image = cpu_filter
5009            .map(|filter| apply_filter_to_bitmap(&image_draw.image, filter))
5010            .transpose()?;
5011        let prepared_image = filtered_image.as_ref().unwrap_or(&image_draw.image);
5012        self.ensure_image_cached(prepared_image)?;
5013        if let cranpose_ui_graphics::ImagePixelFormat::Alpha8 { color } = prepared_image.format() {
5014            for (channel, value) in tint[..3].iter_mut().zip(color) {
5015                *channel *= value as f32 / 255.0;
5016            }
5017        }
5018
5019        let mut adjusted_image = ImageDraw {
5020            rect,
5021            local_rect: image_draw.local_rect.translate(snap_delta.x, snap_delta.y),
5022            quad: translate_quad(image_draw.quad, snap_delta),
5023            snap_anchor: image_draw.snap_anchor,
5024            image: image_draw.image.clone(),
5025            alpha: image_draw.alpha,
5026            color_filter: image_draw.color_filter,
5027            sampling: image_draw.sampling,
5028            z_index: image_draw.z_index,
5029            clip: image_draw.clip,
5030            blend_mode: image_draw.blend_mode,
5031            src_rect: image_draw.src_rect,
5032            motion_context_animated: image_draw.motion_context_animated,
5033        };
5034        snap_nearest_image_to_device_pixels(&mut adjusted_image, root_scale);
5035        let Some(scissor) = scissor_rect_for_image(&adjusted_image, root_scale, viewport) else {
5036            return Ok(());
5037        };
5038
5039        let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
5040            return Ok(());
5041        };
5042        let device_quad =
5043            nearest_image_device_quad(&adjusted_image, root_scale).unwrap_or_else(|| {
5044                if adjusted_image.snap_anchor.is_some() {
5045                    canonicalized_scaled_quad(adjusted_image.quad, root_scale)
5046                } else {
5047                    scaled_quad(adjusted_image.quad, root_scale)
5048                }
5049            });
5050
5051        let base_vertex = image_vertices.len() as u32;
5052        let index_start = image_indices.len() as u32;
5053        image_indices.extend_from_slice(&[
5054            base_vertex,
5055            base_vertex + 1,
5056            base_vertex + 2,
5057            base_vertex + 2,
5058            base_vertex + 1,
5059            base_vertex + 3,
5060        ]);
5061        image_vertices.extend_from_slice(&[
5062            Vertex {
5063                position: device_quad[0],
5064                color: tint,
5065                uv: [uv_rect.min[0], uv_rect.min[1]],
5066                uv_bounds: uv_rect.sample_bounds,
5067            },
5068            Vertex {
5069                position: device_quad[1],
5070                color: tint,
5071                uv: [uv_rect.max[0], uv_rect.min[1]],
5072                uv_bounds: uv_rect.sample_bounds,
5073            },
5074            Vertex {
5075                position: device_quad[2],
5076                color: tint,
5077                uv: [uv_rect.min[0], uv_rect.max[1]],
5078                uv_bounds: uv_rect.sample_bounds,
5079            },
5080            Vertex {
5081                position: device_quad[3],
5082                color: tint,
5083                uv: [uv_rect.max[0], uv_rect.max[1]],
5084                uv_bounds: uv_rect.sample_bounds,
5085            },
5086        ]);
5087
5088        image_cmds.push(ImageDrawCmd {
5089            index_start,
5090            scissor,
5091            image_id: prepared_image.id(),
5092            sampling: adjusted_image.sampling,
5093        });
5094        Ok(())
5095    }
5096
5097    /// Uploads a pass's image and glyph quads into the frame's buffers.
5098    pub(crate) fn upload_image_slot<C: FrameCommandRecorder>(
5099        &self,
5100        recorder: &mut C,
5101        vertices: &[Vertex],
5102        indices: &[u32],
5103    ) -> ImageSlot {
5104        ImageSlot {
5105            vertices: recorder.upload_buffer(
5106                image_vertex_spec(),
5107                &self.device,
5108                bytemuck::cast_slice(vertices),
5109            ),
5110            indices: recorder.upload_buffer(
5111                image_index_spec(),
5112                &self.device,
5113                bytemuck::cast_slice(indices),
5114            ),
5115        }
5116    }
5117
5118    pub(crate) fn upload_glyph_instances<C: FrameCommandRecorder>(
5119        &self,
5120        recorder: &mut C,
5121        instances: &[GlyphInstance],
5122    ) -> BufferUpload {
5123        recorder.upload_buffer(
5124            glyph_instance_spec(),
5125            &self.device,
5126            bytemuck::cast_slice(instances),
5127        )
5128    }
5129
5130    pub(crate) fn upload_turned_glyphs<C: FrameCommandRecorder>(
5131        &self,
5132        recorder: &mut C,
5133        glyphs: &[TurnedGlyph],
5134    ) -> BufferUpload {
5135        recorder.upload_buffer(
5136            turned_glyph_spec(),
5137            &self.device,
5138            bytemuck::cast_slice(glyphs),
5139        )
5140    }
5141
5142    fn glyph_atlas_entry_for(
5143        &mut self,
5144        glyph: &SoftwareGlyphAtlasGlyph,
5145    ) -> Result<GlyphAtlasEntry, String> {
5146        if let Some(entry) = self.text_glyph_atlas.upload_glyph(
5147            glyph,
5148            &self.queue,
5149            &mut self.frame_graph_executor,
5150            &mut self.frame_stats,
5151        ) {
5152            return Ok(entry);
5153        }
5154
5155        self.text_glyph_atlas.reset(
5156            &self.device,
5157            &self.image_bind_group_layout,
5158            GlyphSamplers {
5159                nearest: &self.image_nearest_sampler,
5160                linear: &self.image_linear_sampler,
5161            },
5162        );
5163        Err("text glyph atlas filled and was reset".to_string())
5164    }
5165
5166    fn glyph_atlas_entry_for_cached(
5167        &mut self,
5168        glyph: &SoftwareGlyphAtlasPlacement,
5169    ) -> Option<GlyphAtlasEntry> {
5170        let entry = self
5171            .text_glyph_atlas
5172            .entry(&GlyphAtlasSlotKey::new(glyph.key, glyph.color))?;
5173        self.frame_stats.record_text_glyph_atlas_hits(1);
5174        Some(entry)
5175    }
5176
5177    fn glyph_atlas_entry_for_placement(
5178        &mut self,
5179        glyph: &SoftwareGlyphAtlasPlacement,
5180    ) -> Result<GlyphAtlasEntry, String> {
5181        if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
5182            return Ok(entry);
5183        }
5184
5185        let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
5186            return Err("text glyph placement has no retained raster mask".to_string());
5187        };
5188        self.glyph_atlas_entry_for(&upload_glyph)
5189    }
5190
5191    /// The atlas entries of a run's drawing glyphs: of `cached_glyph_run`,
5192    /// which holds only those, or of the drawing glyphs of `collected_run`,
5193    /// a run just collected, whose new glyphs are uploaded here.
5194    fn prepare_text_glyph_entries(
5195        &mut self,
5196        run_key: TextGlyphRunCacheKey,
5197        atlas_generation: u64,
5198        cached_glyph_run: Option<&RunGlyphs>,
5199        collected_run: &[SoftwareGlyphAtlasRunGlyph],
5200        entries: &mut Vec<GlyphAtlasEntry>,
5201    ) -> Result<Rc<[GlyphAtlasEntry]>, String> {
5202        entries.clear();
5203        let mut seen = RunAtlasEntries::new();
5204        if let Some(glyph_run) = cached_glyph_run {
5205            for glyph in glyph_run.iter() {
5206                entries.push(seen.entry(self, &glyph, |renderer| {
5207                    renderer.glyph_atlas_entry_for_placement(&glyph)
5208                })?);
5209            }
5210        } else {
5211            for run_glyph in collected_run {
5212                entries.push(seen.entry(
5213                    self,
5214                    &run_glyph.placement(),
5215                    |renderer| match run_glyph {
5216                        SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
5217                            renderer.glyph_atlas_entry_for_placement(placement)
5218                        }
5219                        SoftwareGlyphAtlasRunGlyph::New(glyph) => {
5220                            renderer.glyph_atlas_entry_for(glyph)
5221                        }
5222                    },
5223                )?);
5224            }
5225        }
5226
5227        let entries: Rc<[GlyphAtlasEntry]> = Rc::from(entries.as_slice());
5228        if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
5229            cached.atlas_entries = Some(Rc::clone(&entries));
5230            cached.atlas_generation = atlas_generation;
5231        }
5232        Ok(entries)
5233    }
5234
5235    #[expect(clippy::too_many_arguments)]
5236    fn append_text_glyph_quad_run(
5237        &mut self,
5238        source_raster_rect: Rect,
5239        quads: GlyphRunQuads<'_>,
5240        (clip, cut): (Option<Rect>, Option<[f32; 4]>),
5241        viewport: ViewportUniformParams,
5242        root_scale: f32,
5243        glyph_instances: &mut GlyphInstances,
5244        record_cached_hits: bool,
5245    ) {
5246        let start = glyph_instances.lens();
5247        if viewport.transform.is_identity() {
5248            let plain = &mut glyph_instances.plain;
5249            for_each_visible_glyph(
5250                source_raster_rect,
5251                quads,
5252                (clip, cut),
5253                viewport,
5254                root_scale,
5255                |glyph| plain.push(glyph),
5256            );
5257        } else {
5258            let turn = viewport.transform;
5259            let turned = &mut glyph_instances.turned;
5260            for_each_visible_glyph(
5261                source_raster_rect,
5262                quads,
5263                (clip, cut),
5264                viewport,
5265                root_scale,
5266                |glyph| turned.push(TurnedGlyph::new(glyph, turn)),
5267            );
5268        }
5269        let end = glyph_instances.lens();
5270        let appended = (end.0 - start.0) + (end.1 - start.1);
5271        if record_cached_hits {
5272            self.frame_stats
5273                .record_text_glyph_atlas_hits(u32::try_from(appended).unwrap_or(u32::MAX));
5274        }
5275    }
5276
5277    /// The viewport a retained glyph run draws under: its vertices sit at
5278    /// its raster rect's origin, so an untransformed target moves its offset
5279    /// back by that origin, and a transformed one adds the origin to each
5280    /// vertex before its transform, as the shared path's vertices hold it.
5281    fn retained_glyph_viewport(
5282        viewport: ViewportUniformParams,
5283        source_raster_rect: Rect,
5284    ) -> ViewportUniformParams {
5285        if !viewport.transform.is_identity() {
5286            return ViewportUniformParams {
5287                origin: [source_raster_rect.x, source_raster_rect.y],
5288                ..viewport
5289            };
5290        }
5291        ViewportUniformParams {
5292            offset: [
5293                viewport.offset[0] - source_raster_rect.x,
5294                viewport.offset[1] - source_raster_rect.y,
5295            ],
5296            ..viewport
5297        }
5298    }
5299
5300    fn retained_text_glyph_run(
5301        &mut self,
5302        cache_key: TextGlyphRunCacheKey,
5303    ) -> Option<Rc<CachedGpuTextGlyphRun>> {
5304        let atlas_generation = self.text_glyph_atlas.generation();
5305        let frame = self.text_glyph_run_frame;
5306        self.text_glyph_gpu_run_cache
5307            .get(&cache_key)
5308            .filter(|cached| cached.atlas_generation == atlas_generation)
5309            .inspect(|cached| cached.last_frame.set(frame))
5310            .cloned()
5311    }
5312
5313    /// Opens a frame for text runs: runs no frame drew for
5314    /// [`TEXT_GLYPH_RUN_IDLE_FRAMES`] leave both caches, and the arena takes
5315    /// back the quads dropped retained runs held.
5316    fn begin_text_glyph_run_frame(&mut self) {
5317        self.text_glyph_run_frame += 1;
5318        let frame = self.text_glyph_run_frame;
5319        evict_idle(&mut self.text_glyph_gpu_run_cache, frame, |run| {
5320            run.last_frame.get()
5321        });
5322        evict_idle(&mut self.text_glyph_run_cache, frame, |run| {
5323            run.last_frame.get()
5324        });
5325        self.text_glyph_run_arena.begin_frame();
5326    }
5327
5328    fn emit_retained_text_glyph_run_if_ready(
5329        &mut self,
5330        cache_key: TextGlyphRunCacheKey,
5331        quads: GlyphRunQuads<'_>,
5332        viewport: ViewportUniformParams,
5333        source_raster_rect: Rect,
5334        scissor: (u32, u32, u32, u32),
5335        glyph_cmds: &mut Vec<GlyphDrawCmd>,
5336    ) -> bool {
5337        let Some(run) = self.retained_text_glyph_run(cache_key).or_else(|| {
5338            if self.ensure_retained_text_glyph_run(cache_key, quads) {
5339                self.retained_text_glyph_run(cache_key)
5340            } else {
5341                None
5342            }
5343        }) else {
5344            return false;
5345        };
5346        let uniform_slot =
5347            self.claim_uniform_slot(Self::retained_glyph_viewport(viewport, source_raster_rect));
5348        self.frame_stats
5349            .record_text_glyph_atlas_hits(u32::try_from(quads.len()).unwrap_or(u32::MAX));
5350        glyph_cmds.push(GlyphDrawCmd::retained(
5351            run,
5352            uniform_slot,
5353            scissor,
5354            self.text_glyph_atlas.bind_group(viewport.transform),
5355        ));
5356        true
5357    }
5358
5359    fn ensure_retained_text_glyph_run(
5360        &mut self,
5361        cache_key: TextGlyphRunCacheKey,
5362        quads: GlyphRunQuads<'_>,
5363    ) -> bool {
5364        let atlas_generation = self.text_glyph_atlas.generation();
5365        if self
5366            .text_glyph_gpu_run_cache
5367            .peek(&cache_key)
5368            .is_some_and(|cached| cached.atlas_generation == atlas_generation)
5369        {
5370            return true;
5371        }
5372
5373        let origin = Rect {
5374            x: 0.0,
5375            y: 0.0,
5376            width: 0.0,
5377            height: 0.0,
5378        };
5379        let Some(span) = self.text_glyph_run_arena.insert(
5380            &self.device,
5381            quads
5382                .iter()
5383                .filter_map(|quad| cached_text_glyph_instance(origin, &quad)),
5384        ) else {
5385            return false;
5386        };
5387        self.text_glyph_gpu_run_cache.put(
5388            cache_key,
5389            Rc::new(CachedGpuTextGlyphRun {
5390                span,
5391                atlas_generation,
5392                last_frame: Cell::new(self.text_glyph_run_frame),
5393            }),
5394        );
5395        true
5396    }
5397    /// Appends the glyph atlas draws of `text_draw` if `viewport` shows it.
5398    /// `false` when the text cannot draw from the atlas (animated motion, or
5399    /// a run the atlas cannot hold): nothing was appended, and the caller
5400    /// draws the text as a rasterized image instead.
5401    pub(crate) fn append_text_glyph_draws(
5402        &mut self,
5403        text_draw: &TextDraw,
5404        viewport: ViewportUniformParams,
5405        root_scale: f32,
5406        glyph_instances: &mut GlyphInstances,
5407        glyph_cmds: &mut Vec<GlyphDrawCmd>,
5408    ) -> bool {
5409        let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
5410            self.text_raster_geometry(text_draw, root_scale)
5411        else {
5412            return true;
5413        };
5414        if !static_text_motion {
5415            return false;
5416        }
5417        if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
5418            return true;
5419        }
5420        let run_key =
5421            Self::text_glyph_run_cache_key(text_draw, raster_rect, text_scale, static_text_motion);
5422        let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
5423        let mut generated_entries = std::mem::take(&mut self.scratch_text_glyph_entries);
5424        let initial_instance_len = glyph_instances.lens();
5425        let initial_cmd_len = glyph_cmds.len();
5426        let drew = self
5427            .text_glyph_run(
5428                text_draw,
5429                raster_rect,
5430                text_scale,
5431                run_key,
5432                &mut collected_run,
5433            )
5434            .is_some_and(|run| {
5435                self.append_text_glyph_run(
5436                    TextGlyphRunDraw {
5437                        text_draw,
5438                        raster_rect,
5439                        run_key,
5440                        run,
5441                    },
5442                    (viewport, root_scale),
5443                    (&collected_run, &mut generated_entries),
5444                    glyph_instances,
5445                    glyph_cmds,
5446                )
5447            });
5448        self.scratch_text_glyph_run = collected_run;
5449        self.scratch_text_glyph_entries = generated_entries;
5450        if !drew {
5451            glyph_instances.truncate(initial_instance_len);
5452            glyph_cmds.truncate(initial_cmd_len);
5453        }
5454        drew
5455    }
5456
5457    /// The run of `text_draw` from the cache, or collected into
5458    /// `collected_run` and cached; `None` when the text cannot draw from the
5459    /// atlas.
5460    fn text_glyph_run(
5461        &mut self,
5462        text_draw: &TextDraw,
5463        raster_rect: Rect,
5464        text_scale: f32,
5465        run_key: TextGlyphRunCacheKey,
5466        collected_run: &mut Vec<SoftwareGlyphAtlasRunGlyph>,
5467    ) -> Option<TextGlyphRunLookup> {
5468        let atlas_generation = self.text_glyph_atlas.generation();
5469        let frame = self.text_glyph_run_frame;
5470        if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
5471            cached.last_frame.set(frame);
5472            return Some(TextGlyphRunLookup {
5473                glyphs: cached.glyphs.clone(),
5474                bounds: cached.bounds,
5475                cached: true,
5476                entries: (cached.atlas_generation == atlas_generation)
5477                    .then(|| cached.atlas_entries.as_ref().map(Rc::clone))
5478                    .flatten(),
5479            });
5480        }
5481        collected_run.clear();
5482        if collect_solid_text_atlas_run(
5483            text_draw.text.as_ref(),
5484            raster_rect,
5485            &text_draw.text_style,
5486            text_draw.color,
5487            text_draw.font_size,
5488            text_scale,
5489            &self.text_fonts,
5490            &mut self.text_glyph_mask_cache,
5491            collected_run,
5492        )
5493        .is_none()
5494        {
5495            log_text_atlas_fallback(text_draw);
5496            return None;
5497        }
5498        let Some(glyphs) = RunGlyphs::of(
5499            collected_run
5500                .iter()
5501                .map(SoftwareGlyphAtlasRunGlyph::placement),
5502            &mut self.run_glyph_scratch,
5503        ) else {
5504            log_text_atlas_fallback(text_draw);
5505            return None;
5506        };
5507        let bounds = GlyphRunBounds::of(glyphs.iter());
5508        self.text_glyph_run_cache.put(
5509            run_key,
5510            CachedTextGlyphRun {
5511                glyphs: glyphs.clone(),
5512                bounds,
5513                atlas_entries: None,
5514                atlas_generation: 0,
5515                last_frame: Cell::new(frame),
5516            },
5517        );
5518        Some(TextGlyphRunLookup {
5519            glyphs,
5520            bounds,
5521            cached: false,
5522            entries: None,
5523        })
5524    }
5525
5526    /// Appends the draws of a text's run: its retained quads when it has
5527    /// them, else its visible quads into the frame's shared glyphs. `false`
5528    /// when the atlas cannot hold its glyphs.
5529    fn append_text_glyph_run(
5530        &mut self,
5531        draw: TextGlyphRunDraw<'_>,
5532        (viewport, root_scale): (ViewportUniformParams, f32),
5533        (collected_run, generated_entries): (
5534            &[SoftwareGlyphAtlasRunGlyph],
5535            &mut Vec<GlyphAtlasEntry>,
5536        ),
5537        glyph_instances: &mut GlyphInstances,
5538        glyph_cmds: &mut Vec<GlyphDrawCmd>,
5539    ) -> bool {
5540        let TextGlyphRunDraw {
5541            text_draw,
5542            raster_rect,
5543            run_key,
5544            run,
5545        } = draw;
5546        let draw_rect = Rect {
5547            x: raster_rect.x / root_scale,
5548            y: raster_rect.y / root_scale,
5549            width: raster_rect.width / root_scale,
5550            height: raster_rect.height / root_scale,
5551        };
5552        let Some(scissor) = scissor_rect_for_layer(draw_rect, text_draw.clip, root_scale, viewport)
5553        else {
5554            return true;
5555        };
5556        let atlas_size = self.text_glyph_atlas.size();
5557
5558        // A retained run's quads are uploaded whole: under a turn only the
5559        // shared path cuts them to a clip.
5560        let clip_needs_cut = !viewport.transform.is_identity() && text_draw.clip.is_some();
5561        if let Some(entries) = run.entries.as_ref()
5562            && entries.len() >= RETAINED_TEXT_GLYPH_RUN_MIN_QUADS
5563            && !clip_needs_cut
5564            && self.emit_retained_text_glyph_run_if_ready(
5565                run_key,
5566                GlyphRunQuads {
5567                    glyphs: &run.glyphs,
5568                    entries,
5569                    atlas_size,
5570                    bounds: run.bounds,
5571                },
5572                viewport,
5573                raster_rect,
5574                scissor,
5575                glyph_cmds,
5576            )
5577        {
5578            return true;
5579        }
5580
5581        let turned = !viewport.transform.is_identity();
5582        let instance_start = glyph_instances.len_of(turned);
5583        let (entries, entries_cached) = match run.entries {
5584            Some(entries) => (entries, true),
5585            None => {
5586                let Ok(entries) = self.prepare_text_glyph_entries(
5587                    run_key,
5588                    self.text_glyph_atlas.generation(),
5589                    run.cached.then_some(&run.glyphs),
5590                    collected_run,
5591                    generated_entries,
5592                ) else {
5593                    return false;
5594                };
5595                (entries, false)
5596            }
5597        };
5598        let quads = GlyphRunQuads {
5599            glyphs: &run.glyphs,
5600            entries: &entries,
5601            atlas_size: self.text_glyph_atlas.size(),
5602            bounds: run.bounds,
5603        };
5604        let cut = glyph_cut_edges(text_draw.clip, scissor, viewport, root_scale);
5605        self.append_text_glyph_quad_run(
5606            raster_rect,
5607            quads,
5608            (text_draw.clip, cut),
5609            viewport,
5610            root_scale,
5611            glyph_instances,
5612            entries_cached,
5613        );
5614        let instance_end = glyph_instances.len_of(turned);
5615        if instance_end > instance_start {
5616            let (clip, bounds) = glyph_instances.draw_clip(
5617                (instance_start..instance_end, turned),
5618                scissor,
5619                viewport,
5620            );
5621            glyph_cmds.push(GlyphDrawCmd::shared(
5622                (instance_start..instance_end, turned),
5623                clip,
5624                bounds,
5625                self.text_glyph_atlas.bind_group(viewport.transform),
5626            ));
5627        }
5628        true
5629    }
5630
5631    #[expect(clippy::too_many_arguments)]
5632    fn append_image_bitmap_draw_cmd(
5633        &mut self,
5634        image: &ImageBitmap,
5635        rect: Rect,
5636        clip: Option<Rect>,
5637        sampling: ImageSampling,
5638        viewport: ViewportUniformParams,
5639        root_scale: f32,
5640        image_vertices: &mut Vec<Vertex>,
5641        image_indices: &mut Vec<u32>,
5642        image_cmds: &mut Vec<ImageDrawCmd>,
5643    ) -> Result<(), String> {
5644        if rect.width <= 0.0 || rect.height <= 0.0 {
5645            return Ok(());
5646        }
5647
5648        self.ensure_image_cached(image)?;
5649
5650        let (device_quad, scissor_rect) =
5651            if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
5652                let left_px = (rect.x * root_scale).round();
5653                let top_px = (rect.y * root_scale).round();
5654                let width_px = (rect.width * root_scale).round().max(1.0);
5655                let height_px = (rect.height * root_scale).round().max(1.0);
5656                let snapped_rect = Rect {
5657                    x: left_px / root_scale,
5658                    y: top_px / root_scale,
5659                    width: width_px / root_scale,
5660                    height: height_px / root_scale,
5661                };
5662                let right_px = left_px + width_px;
5663                let bottom_px = top_px + height_px;
5664                (
5665                    [
5666                        [left_px, top_px],
5667                        [right_px, top_px],
5668                        [left_px, bottom_px],
5669                        [right_px, bottom_px],
5670                    ],
5671                    snapped_rect,
5672                )
5673            } else {
5674                (
5675                    rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
5676                    rect,
5677                )
5678            };
5679
5680        let Some(scissor) = scissor_rect_for_layer(scissor_rect, clip, root_scale, viewport) else {
5681            return Ok(());
5682        };
5683        let Some(uv_rect) = image_uv_rect(image, None) else {
5684            return Ok(());
5685        };
5686
5687        let base_vertex = image_vertices.len() as u32;
5688        let index_start = image_indices.len() as u32;
5689        image_indices.extend_from_slice(&[
5690            base_vertex,
5691            base_vertex + 1,
5692            base_vertex + 2,
5693            base_vertex + 2,
5694            base_vertex + 1,
5695            base_vertex + 3,
5696        ]);
5697        let color = [1.0, 1.0, 1.0, 1.0];
5698        image_vertices.extend_from_slice(&[
5699            Vertex {
5700                position: device_quad[0],
5701                color,
5702                uv: [uv_rect.min[0], uv_rect.min[1]],
5703                uv_bounds: uv_rect.sample_bounds,
5704            },
5705            Vertex {
5706                position: device_quad[1],
5707                color,
5708                uv: [uv_rect.max[0], uv_rect.min[1]],
5709                uv_bounds: uv_rect.sample_bounds,
5710            },
5711            Vertex {
5712                position: device_quad[2],
5713                color,
5714                uv: [uv_rect.min[0], uv_rect.max[1]],
5715                uv_bounds: uv_rect.sample_bounds,
5716            },
5717            Vertex {
5718                position: device_quad[3],
5719                color,
5720                uv: [uv_rect.max[0], uv_rect.max[1]],
5721                uv_bounds: uv_rect.sample_bounds,
5722            },
5723        ]);
5724        image_cmds.push(ImageDrawCmd {
5725            index_start,
5726            scissor,
5727            image_id: image.id(),
5728            sampling,
5729        });
5730        Ok(())
5731    }
5732
5733    /// Appends `text_draw` as a rasterized image if `viewport` shows it.
5734    pub(crate) fn append_text_image_draw_cmds(
5735        &mut self,
5736        text_draw: &TextDraw,
5737        viewport: ViewportUniformParams,
5738        root_scale: f32,
5739        image_vertices: &mut Vec<Vertex>,
5740        image_indices: &mut Vec<u32>,
5741        image_cmds: &mut Vec<ImageDrawCmd>,
5742    ) -> Result<(), String> {
5743        let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
5744            self.text_raster_geometry(text_draw, root_scale)
5745        else {
5746            return Ok(());
5747        };
5748        if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
5749            return Ok(());
5750        }
5751
5752        let raster_source = self.text_image_raster_source(
5753            text_draw,
5754            logical_rect,
5755            raster_rect,
5756            clip,
5757            root_scale,
5758            static_text_motion,
5759        );
5760        let source_draw = raster_source.draw.as_ref();
5761        let source_raster_rect = raster_source.raster_rect;
5762        let source_origin = if source_draw.text.span_styles().is_empty() {
5763            Point::new(source_raster_rect.x, source_raster_rect.y)
5764        } else {
5765            Point::new(raster_rect.x, raster_rect.y)
5766        };
5767
5768        let mut cache_key = Self::text_image_cache_key(
5769            source_draw,
5770            source_raster_rect,
5771            text_scale,
5772            static_text_motion,
5773        );
5774        if !source_draw.text.span_styles().is_empty() {
5775            let mut state = default_hash::new();
5776            cache_key.0.hash(&mut state);
5777            (source_origin.x - source_raster_rect.x)
5778                .to_bits()
5779                .hash(&mut state);
5780            (source_origin.y - source_raster_rect.y)
5781                .to_bits()
5782                .hash(&mut state);
5783            cache_key = TextImageCacheKey(state.finish());
5784        }
5785        let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
5786            self.frame_stats
5787                .record_text_image_cache_hit(cached.image.width(), cached.image.height());
5788            cached.image.clone()
5789        } else {
5790            let Some(image) = self.rasterize_text_draw_to_image(
5791                source_draw,
5792                source_raster_rect,
5793                source_origin,
5794                text_scale,
5795            ) else {
5796                return Ok(());
5797            };
5798            self.frame_stats
5799                .record_text_image_cache_miss(image.width(), image.height());
5800            self.text_image_cache.put(
5801                cache_key,
5802                CachedTextImage {
5803                    image: image.clone(),
5804                },
5805            );
5806            image
5807        };
5808
5809        let draw_origin = if static_text_motion {
5810            Point::new(
5811                source_raster_rect.x / root_scale,
5812                source_raster_rect.y / root_scale,
5813            )
5814        } else {
5815            Point::new(logical_rect.x, logical_rect.y)
5816        };
5817        let draw_rect = Rect {
5818            x: draw_origin.x,
5819            y: draw_origin.y,
5820            width: image.width() as f32 / root_scale,
5821            height: image.height() as f32 / root_scale,
5822        };
5823        self.append_image_bitmap_draw_cmd(
5824            &image,
5825            draw_rect,
5826            clip,
5827            sampling_under(ImageSampling::Nearest, viewport.transform),
5828            viewport,
5829            root_scale,
5830            image_vertices,
5831            image_indices,
5832            image_cmds,
5833        )
5834    }
5835
5836    fn text_image_raster_source<'a>(
5837        &mut self,
5838        text_draw: &'a TextDraw,
5839        logical_rect: Rect,
5840        raster_rect: Rect,
5841        clip: Option<Rect>,
5842        root_scale: f32,
5843        static_text_motion: bool,
5844    ) -> TextRasterSource<'a> {
5845        let Some(clip) = clip else {
5846            return TextRasterSource {
5847                draw: Cow::Borrowed(text_draw),
5848                raster_rect,
5849            };
5850        };
5851        if !static_text_motion || text_draw.text.text().find('\n').is_none() {
5852            return TextRasterSource {
5853                draw: Cow::Borrowed(text_draw),
5854                raster_rect,
5855            };
5856        }
5857
5858        if !text_draw.text.span_styles().is_empty() {
5859            let device_clip = Rect {
5860                x: (clip.x * root_scale).floor(),
5861                y: (clip.y * root_scale).floor(),
5862                width: ((clip.x + clip.width) * root_scale).ceil() - (clip.x * root_scale).floor(),
5863                height: ((clip.y + clip.height) * root_scale).ceil()
5864                    - (clip.y * root_scale).floor(),
5865            };
5866            return TextRasterSource {
5867                draw: Cow::Borrowed(text_draw),
5868                raster_rect: raster_rect.intersect(device_clip).unwrap_or(raster_rect),
5869            };
5870        }
5871
5872        let line_starts = self.text_line_index_cache.line_starts(&text_draw.text);
5873        clipped_text_raster_source_with_line_starts(
5874            text_draw,
5875            logical_rect,
5876            raster_rect,
5877            clip,
5878            root_scale,
5879            line_starts.as_ref(),
5880        )
5881    }
5882
5883    fn text_raster_geometry(
5884        &self,
5885        text_draw: &TextDraw,
5886        root_scale: f32,
5887    ) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
5888        text_raster_geometry_for_draw(text_draw, root_scale)
5889    }
5890
5891    fn text_image_cache_key(
5892        text_draw: &TextDraw,
5893        raster_rect: Rect,
5894        text_scale: f32,
5895        static_text_motion: bool,
5896    ) -> TextImageCacheKey {
5897        let mut state = default_hash::new();
5898        text_draw.text.render_hash().hash(&mut state);
5899        text_draw.text_style.render_hash().hash(&mut state);
5900        text_draw.color.render_hash().hash(&mut state);
5901        hash_text_raster_geometry_for_cache(raster_rect, static_text_motion, &mut state);
5902        if std::iter::once(&text_draw.text_style.span_style)
5903            .chain(text_draw.text.span_styles().iter().map(|span| &span.item))
5904            .any(|span| {
5905                span.brush
5906                    .as_ref()
5907                    .is_some_and(|brush| !matches!(brush, cranpose_ui_graphics::Brush::Solid(_)))
5908            })
5909        {
5910            for coordinate in [raster_rect.x, raster_rect.y] {
5911                let phase = if coordinate >= 0.0 {
5912                    coordinate.rem_euclid(4.0)
5913                } else {
5914                    coordinate
5915                };
5916                phase.to_bits().hash(&mut state);
5917            }
5918        }
5919        text_draw.font_size.to_bits().hash(&mut state);
5920        text_scale.to_bits().hash(&mut state);
5921        text_draw.layout_options.hash(&mut state);
5922        TextImageCacheKey(state.finish())
5923    }
5924
5925    fn text_glyph_run_cache_key(
5926        text_draw: &TextDraw,
5927        raster_rect: Rect,
5928        text_scale: f32,
5929        static_text_motion: bool,
5930    ) -> TextGlyphRunCacheKey {
5931        TextGlyphRunCacheKey(
5932            Self::text_image_cache_key(text_draw, raster_rect, text_scale, static_text_motion).0,
5933        )
5934    }
5935
5936    fn rasterize_text_draw_to_image(
5937        &mut self,
5938        text_draw: &TextDraw,
5939        raster_rect: Rect,
5940        source_origin: Point,
5941        text_scale: f32,
5942    ) -> Option<ImageBitmap> {
5943        if text_draw.text.span_styles().is_empty() {
5944            let font = self.text_fonts.resolve(&text_draw.text_style)?;
5945            return rasterize_text_to_image_with_glyph_cache(
5946                text_draw.text.text(),
5947                raster_rect,
5948                &text_draw.text_style,
5949                text_draw.color,
5950                text_draw.font_size,
5951                text_scale,
5952                font,
5953                &mut self.text_glyph_mask_cache,
5954            );
5955        }
5956
5957        rasterize_annotated_text_region(
5958            text_draw.text.as_ref(),
5959            raster_rect,
5960            source_origin,
5961            &text_draw.text_style,
5962            text_draw.color,
5963            text_draw.font_size,
5964            text_scale,
5965            &self.text_fonts,
5966            Some(&mut self.text_glyph_mask_cache),
5967        )
5968    }
5969}
5970
5971struct TextRasterSource<'a> {
5972    draw: Cow<'a, TextDraw>,
5973    raster_rect: Rect,
5974}
5975
5976fn clipped_text_raster_source_with_line_starts<'a>(
5977    text_draw: &'a TextDraw,
5978    logical_rect: Rect,
5979    raster_rect: Rect,
5980    clip: Rect,
5981    root_scale: f32,
5982    line_starts: &[usize],
5983) -> TextRasterSource<'a> {
5984    if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
5985        return TextRasterSource {
5986            draw: Cow::Borrowed(text_draw),
5987            raster_rect,
5988        };
5989    }
5990
5991    let Some(visible_rect) = logical_rect.intersect(clip) else {
5992        return TextRasterSource {
5993            draw: Cow::Borrowed(text_draw),
5994            raster_rect,
5995        };
5996    };
5997
5998    let line_count = line_starts.len().max(1);
5999    let line_height = logical_rect.height / line_count as f32;
6000    if !line_height.is_finite() || line_height <= 0.0 {
6001        return TextRasterSource {
6002            draw: Cow::Borrowed(text_draw),
6003            raster_rect,
6004        };
6005    }
6006
6007    let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
6008    let visible_bottom =
6009        ((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
6010    let start_line = visible_top.saturating_sub(1).max(0) as usize;
6011    let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
6012    let end_line = end_line.min(line_count);
6013    if start_line == 0 && end_line >= line_count {
6014        return TextRasterSource {
6015            draw: Cow::Borrowed(text_draw),
6016            raster_rect,
6017        };
6018    }
6019
6020    let byte_start = line_starts[start_line];
6021    let byte_end = line_end_offset(text_draw.text.text(), line_starts, end_line - 1);
6022    if byte_start >= byte_end {
6023        return TextRasterSource {
6024            draw: Cow::Borrowed(text_draw),
6025            raster_rect,
6026        };
6027    }
6028
6029    let slice_y = logical_rect.y + start_line as f32 * line_height;
6030    let slice_height = (end_line - start_line) as f32 * line_height;
6031    let mut slice_raster_rect = Rect {
6032        x: logical_rect.x * root_scale,
6033        y: slice_y * root_scale,
6034        width: logical_rect.width * root_scale,
6035        height: slice_height * root_scale,
6036    };
6037    slice_raster_rect.x = slice_raster_rect.x.round();
6038    slice_raster_rect.y = slice_raster_rect.y.round();
6039    slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
6040    slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
6041
6042    let mut sliced_draw = text_draw.clone();
6043    sliced_draw.rect = Rect {
6044        x: logical_rect.x,
6045        y: slice_y,
6046        width: logical_rect.width,
6047        height: slice_height,
6048    };
6049    sliced_draw.text = Arc::new(text_draw.text.subsequence(byte_start..byte_end));
6050
6051    TextRasterSource {
6052        draw: Cow::Owned(sliced_draw),
6053        raster_rect: slice_raster_rect,
6054    }
6055}
6056
6057fn line_start_offsets(text: &str) -> Vec<usize> {
6058    let mut starts =
6059        Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
6060    starts.push(0);
6061    starts.extend(
6062        text.char_indices()
6063            .filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
6064    );
6065    starts
6066}
6067
6068fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
6069    line_starts.get(line + 1).copied().unwrap_or(text.len())
6070}
6071
6072fn align_to(value: u32, alignment: u32) -> u32 {
6073    debug_assert!(alignment > 0);
6074    value.div_ceil(alignment) * alignment
6075}
6076
6077impl GpuRenderer {
6078    fn convert_surface_pixels_to_rgba(&self, pixels: &[u8]) -> Result<Vec<u8>, String> {
6079        if !pixels.len().is_multiple_of(4) {
6080            return Err("Screenshot readback has an incomplete pixel".to_string());
6081        }
6082        Ok(pixels.to_vec())
6083    }
6084}
6085
6086/// The scissor of a logical rect in a target whose origin sits at
6087/// `viewport.offset` of the scene's device space, clamped to the target.
6088/// `None` when nothing of the rect lands in the target.
6089pub(crate) fn scissor_rect_for_rect(
6090    rect: Rect,
6091    root_scale: f32,
6092    viewport: ViewportUniformParams,
6093) -> Option<(u32, u32, u32, u32)> {
6094    let width = viewport.width as f32;
6095    let height = viewport.height as f32;
6096    let mut left = canonicalize_device_coordinate(rect.x * root_scale);
6097    let mut top = canonicalize_device_coordinate(rect.y * root_scale);
6098    let mut right = canonicalize_device_coordinate((rect.x + rect.width) * root_scale);
6099    let mut bottom = canonicalize_device_coordinate((rect.y + rect.height) * root_scale);
6100    if !viewport.transform.is_identity() {
6101        let bounds = viewport.transform.target_bounds(Rect {
6102            x: left,
6103            y: top,
6104            width: right - left,
6105            height: bottom - top,
6106        });
6107        (left, top) = (bounds.x, bounds.y);
6108        (right, bottom) = (bounds.x + bounds.width, bounds.y + bounds.height);
6109    }
6110    let left = (left - viewport.offset[0]).clamp(0.0, width).floor();
6111    let top = (top - viewport.offset[1]).clamp(0.0, height).floor();
6112    let right = (right - viewport.offset[0]).clamp(0.0, width).ceil();
6113    let bottom = (bottom - viewport.offset[1]).clamp(0.0, height).ceil();
6114    if right <= left || bottom <= top {
6115        return None;
6116    }
6117    Some((
6118        left as u32,
6119        top as u32,
6120        (right - left) as u32,
6121        (bottom - top) as u32,
6122    ))
6123}
6124
6125fn scissor_rect_for_layer(
6126    rect: Rect,
6127    clip: Option<Rect>,
6128    root_scale: f32,
6129    viewport: ViewportUniformParams,
6130) -> Option<(u32, u32, u32, u32)> {
6131    let clipped_rect = match clip {
6132        Some(clip_rect) => rect.intersect(clip_rect)?,
6133        None => rect,
6134    };
6135    scissor_rect_for_rect(clipped_rect, root_scale, viewport)
6136}
6137fn tint_for_image(
6138    color_filter: Option<ColorFilter>,
6139    alpha: f32,
6140) -> ([f32; 4], Option<ColorFilter>) {
6141    let alpha = alpha.clamp(0.0, 1.0);
6142    match color_filter {
6143        Some(filter) if filter.supports_gpu_vertex_modulation() => {
6144            let Some(tint) = filter.gpu_vertex_tint() else {
6145                return ([1.0, 1.0, 1.0, alpha], Some(filter));
6146            };
6147            (
6148                [
6149                    tint[0].clamp(0.0, 1.0),
6150                    tint[1].clamp(0.0, 1.0),
6151                    tint[2].clamp(0.0, 1.0),
6152                    (tint[3] * alpha).clamp(0.0, 1.0),
6153                ],
6154                None,
6155            )
6156        }
6157        Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
6158        None => ([1.0, 1.0, 1.0, alpha], None),
6159    }
6160}
6161
6162fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
6163    let Some(src) = src_rect else {
6164        return Some(ImageUvRect {
6165            min: [0.0, 0.0],
6166            max: [1.0, 1.0],
6167            sample_bounds: [0.0, 0.0, 1.0, 1.0],
6168        });
6169    };
6170
6171    let (u_min, u_max, u_bound_min, u_bound_max) =
6172        source_axis_uv(src.x, src.width, image.width() as f32)?;
6173    let (v_min, v_max, v_bound_min, v_bound_max) =
6174        source_axis_uv(src.y, src.height, image.height() as f32)?;
6175
6176    Some(ImageUvRect {
6177        min: [u_min, v_min],
6178        max: [u_max, v_max],
6179        sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
6180    })
6181}
6182
6183fn glyph_atlas_uv_rect(entry: GlyphAtlasEntry, atlas_size: u32) -> ImageUvRect {
6184    let atlas_width = atlas_size as f32;
6185    let atlas_height = atlas_size as f32;
6186    let min = [entry.x as f32 / atlas_width, entry.y as f32 / atlas_height];
6187    let max = [
6188        (entry.x + entry.width) as f32 / atlas_width,
6189        (entry.y + entry.height) as f32 / atlas_height,
6190    ];
6191    let center_min = [
6192        (entry.x as f32 + 0.5) / atlas_width,
6193        (entry.y as f32 + 0.5) / atlas_height,
6194    ];
6195    let center_max = [
6196        (entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) / atlas_width,
6197        (entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) / atlas_height,
6198    ];
6199    ImageUvRect {
6200        min,
6201        max,
6202        sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
6203    }
6204}
6205
6206fn snap_nearest_image_to_device_pixels(image: &mut ImageDraw, root_scale: f32) {
6207    if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
6208        return;
6209    }
6210
6211    let Some(rect) = axis_aligned_quad_rect(image.quad) else {
6212        return;
6213    };
6214
6215    let left_px = (rect.x * root_scale).round();
6216    let top_px = (rect.y * root_scale).round();
6217    let width_px = (rect.width * root_scale).round().max(1.0);
6218    let height_px = (rect.height * root_scale).round().max(1.0);
6219    let snapped = Rect {
6220        x: left_px / root_scale,
6221        y: top_px / root_scale,
6222        width: width_px / root_scale,
6223        height: height_px / root_scale,
6224    };
6225
6226    image.rect = snapped;
6227    image.local_rect = Rect {
6228        x: image.local_rect.x + snapped.x - rect.x,
6229        y: image.local_rect.y + snapped.y - rect.y,
6230        width: snapped.width,
6231        height: snapped.height,
6232    };
6233    image.quad = crate::rect_to_quad(snapped);
6234}
6235
6236fn nearest_image_device_quad(image: &ImageDraw, root_scale: f32) -> Option<[[f32; 2]; 4]> {
6237    if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
6238        return None;
6239    }
6240
6241    let rect = axis_aligned_quad_rect(image.quad)?;
6242    let left_px = (rect.x * root_scale).round();
6243    let top_px = (rect.y * root_scale).round();
6244    let width_px = (rect.width * root_scale).round().max(1.0);
6245    let height_px = (rect.height * root_scale).round().max(1.0);
6246    let right_px = left_px + width_px;
6247    let bottom_px = top_px + height_px;
6248    Some([
6249        [left_px, top_px],
6250        [right_px, top_px],
6251        [left_px, bottom_px],
6252        [right_px, bottom_px],
6253    ])
6254}
6255
6256fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
6257    if !start.is_finite()
6258        || !extent.is_finite()
6259        || !image_extent.is_finite()
6260        || extent == 0.0
6261        || image_extent <= 0.0
6262    {
6263        return None;
6264    }
6265
6266    let end = start + extent;
6267    let edge_min = start.min(end).clamp(0.0, image_extent);
6268    let edge_max = start.max(end).clamp(0.0, image_extent);
6269    if edge_max <= edge_min {
6270        return None;
6271    }
6272
6273    let center_min = edge_min + 0.5;
6274    let center_max = edge_max - 0.5;
6275    let (bound_min, bound_max) = if center_min <= center_max {
6276        (center_min, center_max)
6277    } else {
6278        let center = (edge_min + edge_max) * 0.5;
6279        (center, center)
6280    };
6281
6282    Some((
6283        edge_min / image_extent,
6284        edge_max / image_extent,
6285        bound_min / image_extent,
6286        bound_max / image_extent,
6287    ))
6288}
6289
6290fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
6291    let pixel_count = image.width() as usize * image.height() as usize;
6292    let mut filtered = Vec::with_capacity(pixel_count * 4);
6293    for index in 0..pixel_count {
6294        let pixel = image.rgba8_pixel(index);
6295        let rgba = [
6296            pixel[0] as f32 / 255.0,
6297            pixel[1] as f32 / 255.0,
6298            pixel[2] as f32 / 255.0,
6299            pixel[3] as f32 / 255.0,
6300        ];
6301        let out = filter.apply_rgba(rgba);
6302        filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
6303        filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
6304        filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
6305        filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
6306    }
6307    ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
6308        .map_err(|error| format!("failed to build filtered bitmap: {error}"))
6309}
6310
6311/// How a text raster samples its texels under a segment transform: as it
6312/// asks on the pixel grid, and filtered once a transform turns it off the
6313/// grid, where no texel lands on a pixel for nearest sampling to keep.
6314fn sampling_under(sampling: ImageSampling, transform: SegmentTransform) -> ImageSampling {
6315    if transform.is_identity() {
6316        sampling
6317    } else {
6318        ImageSampling::Linear
6319    }
6320}
6321
6322fn scissor_rect_for_image(
6323    image: &ImageDraw,
6324    root_scale: f32,
6325    viewport: ViewportUniformParams,
6326) -> Option<(u32, u32, u32, u32)> {
6327    scissor_rect_for_layer(image.rect, image.clip, root_scale, viewport)
6328}
6329
6330/// The rounded mask a shadow's composite applies, in the target's pixels: an
6331/// inner shadow masks itself to its fill shape, and a shadow lowered out of a
6332/// clipped layer masks itself to that layer's rounded clip.
6333/// The rounded mask a shadow's composite applies, in the scene's device
6334/// pixels: an inner shadow masks itself to its fill shape, and a shadow
6335/// lowered out of a clipped layer masks itself to that layer's rounded clip.
6336fn shadow_composite_mask(
6337    shadow: &ShadowDraw,
6338    snap_anchor: Option<SnapAnchor>,
6339    root_scale: f32,
6340) -> Option<RoundedCompositeMask> {
6341    inner_shadow_composite_mask(shadow, root_scale).or_else(|| {
6342        shadow.rounded_clip.map(|clip| RoundedCompositeMask {
6343            rect: mask_rect(anchored_device_rect(clip.rect, snap_anchor, root_scale)),
6344            radii: clip.radii.map(|radius| radius * root_scale),
6345        })
6346    })
6347}
6348
6349/// A scene holding just a shadow's own draws, in the order they arrive, so
6350/// the shadow source renders through the same pass encoder as everything
6351/// else.
6352fn shadow_scene(
6353    shapes: Option<&RunDraw>,
6354    texts: &[TextDraw],
6355    input_margin: f32,
6356) -> CompositorScene {
6357    let mut scene = CompositorScene::new();
6358    if let Some(run) = shapes {
6359        scene.push_run(run.clone());
6360    }
6361    for text in texts {
6362        let z_index = scene.next_z();
6363        scene.draw_ops.push(DrawOp {
6364            z_index,
6365            kind: DrawOpKind::Text(scene.texts.len()),
6366        });
6367        let mut source_text = text.clone();
6368        source_text.clip = source_text
6369            .clip
6370            .map(|clip| expand_rect(clip, input_margin, input_margin));
6371        scene.texts.push(source_text);
6372        scene.next_z += 1;
6373    }
6374    scene
6375}
6376
6377/// The whole device pixels a logical rect covers, or `None` when it covers
6378/// none or more than a texture can hold.
6379fn device_pixel_bounds(
6380    rect: Rect,
6381    root_scale: f32,
6382    max_texture_dim: u32,
6383) -> Option<DevicePixelBounds> {
6384    let x = (rect.x * root_scale).floor();
6385    let y = (rect.y * root_scale).floor();
6386    let right = ((rect.x + rect.width) * root_scale).ceil();
6387    let bottom = ((rect.y + rect.height) * root_scale).ceil();
6388    let width = (right - x).max(0.0) as u32;
6389    let height = (bottom - y).max(0.0) as u32;
6390    if width == 0 || height == 0 || width > max_texture_dim || height > max_texture_dim {
6391        return None;
6392    }
6393    Some(DevicePixelBounds {
6394        x,
6395        y,
6396        width,
6397        height,
6398    })
6399}
6400fn inner_shadow_composite_mask(
6401    shadow: &ShadowDraw,
6402    root_scale: f32,
6403) -> Option<RoundedCompositeMask> {
6404    let run = shadow.shapes.as_ref()?;
6405    if !run
6406        .tables()
6407        .shapes
6408        .iter()
6409        .any(|record| record.blend_mode() == BlendMode::DstOut)
6410    {
6411        return None;
6412    }
6413    let fill = run.tables().shapes.get(0)?;
6414    let rect = run.placement.translated_bounds(fill.stored_rect());
6415    if rect.width <= 0.0 || rect.height <= 0.0 {
6416        return None;
6417    }
6418    let resolved =
6419        cranpose_ui_graphics::RoundedCornerShape::with_radii(cranpose_ui_graphics::CornerRadii {
6420            top_left: fill.radii[0],
6421            top_right: fill.radii[1],
6422            bottom_right: fill.radii[2],
6423            bottom_left: fill.radii[3],
6424        })
6425        .resolve(rect.width, rect.height);
6426    let radii = [
6427        resolved.top_left * root_scale,
6428        resolved.top_right * root_scale,
6429        resolved.bottom_left * root_scale,
6430        resolved.bottom_right * root_scale,
6431    ];
6432
6433    Some(RoundedCompositeMask {
6434        rect: mask_rect(anchored_device_rect(
6435            rect,
6436            run.placement.snap_anchor,
6437            root_scale,
6438        )),
6439        radii,
6440    })
6441}
6442
6443/// One draw call of a run batch's stage: its pipeline, the strip index
6444/// buffer it binds and the indices and records it instances.
6445#[derive(Clone, Debug, PartialEq, Eq)]
6446pub(crate) struct PlannedRunDraw {
6447    pub(crate) key: ShapePipelineKey,
6448    pub(crate) band_class: u8,
6449    pub(crate) indices: std::ops::Range<u32>,
6450    pub(crate) records: std::ops::Range<u32>,
6451}
6452
6453impl PlannedRunDraw {
6454    /// A draw of the paint, as it was recorded.
6455    fn paint(draw: &RunDrawCall) -> Self {
6456        Self {
6457            key: draw.key,
6458            band_class: draw.band_class,
6459            indices: draw.indices(),
6460            records: draw.records.clone(),
6461        }
6462    }
6463}
6464
6465/// The draw calls laying a batch's opaque interiors down: last first, so
6466/// they go down front to back, one call over each stretch of neighbouring
6467/// draws that share the interior pipeline, cut to the span from the stretch's
6468/// first draw holding an occluder to its last. The draws between ride along:
6469/// the pipeline lays any record's exact interior down and skips the rest, so
6470/// taking them costs a few vertices rather than a call, and it shades only a
6471/// record's quad, the first two triangles of every band class's pattern.
6472pub(crate) fn interior_run_draws(draws: &[RunDrawCall]) -> SmallVec<[PlannedRunDraw; 4]> {
6473    struct Stretch {
6474        key: ShapePipelineKey,
6475        start: u32,
6476        span: Option<PlannedRunDraw>,
6477    }
6478    let mut planned: SmallVec<[PlannedRunDraw; 4]> = SmallVec::new();
6479    let mut stretch: Option<Stretch> = None;
6480    for draw in draws.iter().rev() {
6481        let Some(key) = draw.key.interior() else {
6482            planned.extend(stretch.take().and_then(|open| open.span));
6483            continue;
6484        };
6485        if stretch
6486            .as_ref()
6487            .is_some_and(|open| open.key != key || open.start != draw.records.end)
6488        {
6489            planned.extend(stretch.take().and_then(|open| open.span));
6490        }
6491        let open = stretch.get_or_insert(Stretch {
6492            key,
6493            start: draw.records.end,
6494            span: None,
6495        });
6496        open.start = draw.records.start;
6497        if !draw.occluders {
6498            continue;
6499        }
6500        match &mut open.span {
6501            Some(span) => span.records.start = draw.records.start,
6502            None => {
6503                open.span = Some(PlannedRunDraw {
6504                    key,
6505                    band_class: draw.band_class,
6506                    indices: 0..cranpose_ui_graphics::strip_indices(1),
6507                    records: draw.records.clone(),
6508                });
6509            }
6510        }
6511    }
6512    planned.extend(stretch.and_then(|open| open.span));
6513    planned
6514}
6515
6516/// Drops the entries of `cache` no frame drew for more than
6517/// [`TEXT_GLYPH_RUN_IDLE_FRAMES`] before `frame`: the least recently used
6518/// first, which is the order they were last drawn in.
6519fn evict_idle<K: Clone + Eq + std::hash::Hash, V>(
6520    cache: &mut BoundedLruCache<K, V>,
6521    frame: u64,
6522    last_frame: impl Fn(&V) -> u64,
6523) {
6524    while cache.peek_lru().is_some_and(|(_, value)| {
6525        frame.saturating_sub(last_frame(value)) > TEXT_GLYPH_RUN_IDLE_FRAMES
6526    }) {
6527        cache.pop_lru();
6528    }
6529}
6530
6531fn window_draws(draws: &mut SmallVec<[RunDrawCall; 8]>, window: &std::ops::Range<u32>) {
6532    let mut relative = 0u32;
6533    draws.retain(|draw| {
6534        let count = draw.records.end - draw.records.start;
6535        let first = relative;
6536        relative += count;
6537        let keep_start = window.start.max(first).min(first + count);
6538        let keep_end = window.end.min(first + count).max(keep_start);
6539        draw.records =
6540            draw.records.start + (keep_start - first)..draw.records.start + (keep_end - first);
6541        draw.records.start < draw.records.end
6542    });
6543}
6544
6545#[cfg(test)]
6546#[path = "tests/render_text_bounds_tests.rs"]
6547mod text_bounds_tests;
6548
6549#[cfg(test)]
6550#[path = "tests/render_retained_glyph_tests.rs"]
6551mod retained_glyph_tests;
6552
6553#[cfg(test)]
6554#[path = "tests/frame_clear_tests.rs"]
6555mod frame_clear_tests;
6556
6557#[cfg(test)]
6558#[path = "tests/render_clip_scissor_tests.rs"]
6559mod clip_scissor_tests;
6560
6561#[cfg(test)]
6562#[path = "tests/glyph_kind_tests.rs"]
6563mod glyph_kind_tests;
6564
6565#[cfg(test)]
6566#[path = "tests/glyph_run_bounds_tests.rs"]
6567mod glyph_run_bounds_tests;
6568
6569#[cfg(test)]
6570#[path = "tests/shape_variant_tests.rs"]
6571mod shape_variant_tests;