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