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