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