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