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