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