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