Skip to main content

cranpose_render_wgpu/
render.rs

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