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