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