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