Skip to main content

cranpose_render_wgpu/
render.rs

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