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