Skip to main content

cranpose_render_wgpu/
render.rs

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