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