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