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