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