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