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