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