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