Skip to main content

cranpose_render_wgpu/
render.rs

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