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