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