Skip to main content

cranpose_render_wgpu/
render.rs

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