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