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