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