Skip to main content

cranpose_render_wgpu/
render.rs

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