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