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