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