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