Skip to main content

cranpose_render_wgpu/
render.rs

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