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