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