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