Skip to main content

cranpose_render_wgpu/
render.rs

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