1use std::{
2 borrow::Cow,
3 cell::Cell,
4 hash::{Hash, Hasher},
5 rc::Rc,
6 sync::{Arc, mpsc},
7 time::Duration,
8};
9
10use bytemuck::{Pod, Zeroable};
11use cranpose_core::{
12 NodeId,
13 collections::{bounded_lru::BoundedLruCache, map::HashMap},
14 hash::default as default_hash,
15};
16use cranpose_render_common::{
17 geometry::blur_reach,
18 graph::{DrawCommandId, quad_bounds},
19 software_text_raster::{
20 SoftwareGlyphAtlasGlyph, SoftwareGlyphAtlasKey, SoftwareGlyphAtlasPlacement,
21 SoftwareGlyphAtlasRunGlyph, SoftwareGlyphRasterCache, SoftwareTextFontSet,
22 collect_solid_text_atlas_run, rasterize_annotated_text_region,
23 },
24 text_mask_gamma::TextLuminance,
25};
26use cranpose_ui_graphics::{
27 BlendMode, Color, ColorFilter, FRAGMENT_KIND_ARC, FRAGMENT_KIND_FILL, FRAGMENT_KIND_LINE,
28 FRAGMENT_KIND_STROKE, FRAGMENT_KIND_TRAPEZOID, FxHasher, ImageBitmap, ImageSampling, Point,
29 RecordLane, RecordSegment, Rect, RenderHash, TileMode,
30};
31use smallvec::SmallVec;
32use web_time::Instant;
33
34use crate::{
35 DebugCpuAllocationStats,
36 ablation::{Ablation, ShapeAblation},
37 arc_trig_fill::ArcTrigFill,
38 collect::LayerScene,
39 debug_toggles::DebugToggle,
40 draw_pass::{
41 PassSegment, PassTarget, ResolvedComposite, ResolvedCompositeKind, SourceContent,
42 TrackedPass,
43 },
44 effect_renderer::{CompositeSampleMode, EffectRenderer, RoundedCompositeMask},
45 fixed_pipeline::FixedPipeline,
46 frame::{AdmissionGate, FrameExecutor, StageSideScratch},
47 frame_graph::{
48 BufferUpload, FrameCommandRecorder, FrameCommandStats, FrameTextureDescriptor,
49 FrameUploadAllocators, UniformUpload, UploadAllocatorId, UploadAllocatorSpec, UploadMode,
50 WgpuFrameGraph, WgpuFrameGraphExecutor,
51 },
52 frame_packet::{CancelReason, FramePacket, FrameSceneStorage, PresentOutcome, RenderReturns},
53 geometry::{
54 DevicePixelBounds, SegmentTransform, anchored_device_rect, axis_aligned_quad_rect,
55 canonicalize_device_coordinate, canonicalized_scaled_quad, offscreen_byte_size,
56 scaled_quad, snap_delta_for_anchor, snapped_anchor_device_origin, translate_quad,
57 translation_stable_anchored_device_pixel_bounds,
58 },
59 glyph_run::{RunGlyphScratch, RunGlyphs},
60 glyph_run_arena::{GlyphRunArena, GlyphRunSpan},
61 gpu_stats::{self, gpu_stats_enabled},
62 idle_pool::FrameScratch,
63 layer_cache::LayerCache,
64 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#[cfg(not(target_arch = "wasm32"))]
1539#[derive(Default)]
1540struct KeyWriter {
1541 bits: u64,
1542 width: u32,
1543}
1544
1545#[cfg(not(target_arch = "wasm32"))]
1546impl KeyWriter {
1547 fn put(&mut self, value: u64, width: u32) {
1548 self.bits |= value << self.width;
1549 self.width += width;
1550 }
1551
1552 fn put_byte(&mut self, value: Option<u8>) {
1553 self.put(u64::from(value.is_some()), 1);
1554 self.put(u64::from(value.unwrap_or_default()), 8);
1555 }
1556}
1557
1558const KIND_BITS: u32 = FRAGMENT_KIND_TRAPEZOID + 1;
1561
1562struct KeyReader(u64);
1563
1564impl KeyReader {
1565 fn take(&mut self, width: u32) -> u64 {
1566 let value = self.0 & ((1 << width) - 1);
1567 self.0 >>= width;
1568 value
1569 }
1570
1571 fn take_flag(&mut self) -> bool {
1572 self.take(1) == 1
1573 }
1574
1575 fn take_byte(&mut self) -> Option<u8> {
1576 let present = self.take_flag();
1577 let value = u8::try_from(self.take(8)).ok()?;
1578 present.then_some(value)
1579 }
1580
1581 fn take_kinds(&mut self) -> Option<u8> {
1582 u8::try_from(self.take(KIND_BITS))
1583 .ok()
1584 .filter(|kinds| *kinds != 0)
1585 }
1586}
1587
1588impl ShapePipelineKey {
1589 #[cfg(not(target_arch = "wasm32"))]
1590 pub(crate) const DISK_LAYOUT: [u8; 8] = *b"CPKEY003";
1591
1592 #[cfg(not(target_arch = "wasm32"))]
1596 const SOLID_RECT: Self = Self {
1597 blend_mode: BlendMode::SrcOver,
1598 tier: RunTier::Arena,
1599 variant: ShapeVariant {
1600 kinds: 1 << FRAGMENT_KIND_FILL,
1601 brush: None,
1602 solid: true,
1603 clip: SegmentClip::Untested,
1604 interior: false,
1605 dither: false,
1606 ablation: ShapeAblation {
1607 material: false,
1608 fill: false,
1609 },
1610 },
1611 turns: ShapeTurns::None,
1612 depth: ShapeDepth::Off,
1613 };
1614
1615 #[cfg(not(target_arch = "wasm32"))]
1618 pub(crate) fn to_bits(self) -> u64 {
1619 let mut bits = KeyWriter::default();
1620 bits.put(self.blend_mode as u64, 5);
1621 bits.put(self.tier as u64, 2);
1622 let variant = self.variant;
1623 bits.put(u64::from(variant.kinds), KIND_BITS);
1624 bits.put_byte(variant.brush);
1625 bits.put(u64::from(variant.solid), 1);
1626 bits.put(
1627 match variant.clip {
1628 SegmentClip::Untested => 0,
1629 SegmentClip::Tested => 1,
1630 SegmentClip::Rounded => 2,
1631 },
1632 2,
1633 );
1634 bits.put(u64::from(variant.interior), 1);
1635 bits.put(u64::from(variant.dither), 1);
1636 bits.put(u64::from(variant.ablation.material), 1);
1637 bits.put(u64::from(variant.ablation.fill), 1);
1638 bits.put(
1639 match self.turns {
1640 ShapeTurns::None => 0,
1641 ShapeTurns::All => 1,
1642 ShapeTurns::Mixed => 2,
1643 },
1644 2,
1645 );
1646 bits.put(
1647 match self.depth {
1648 ShapeDepth::Off => 0,
1649 ShapeDepth::Tested => 1,
1650 ShapeDepth::Interior => 2,
1651 },
1652 2,
1653 );
1654 bits.bits
1655 }
1656
1657 pub(crate) fn from_bits(packed: u64) -> Option<Self> {
1660 let mut bits = KeyReader(packed);
1661 let blend_mode = *BlendMode::ALL.get(usize::try_from(bits.take(5)).ok()?)?;
1662 let tier = match bits.take(2) {
1663 0 => RunTier::Store,
1664 1 => RunTier::Arena,
1665 2 => RunTier::Either,
1666 _ => return None,
1667 };
1668 let kinds = bits.take_kinds()?;
1669 let brush = bits.take_byte();
1670 let solid = bits.take_flag();
1671 let clip = match bits.take(2) {
1672 0 => SegmentClip::Untested,
1673 1 => SegmentClip::Tested,
1674 2 => SegmentClip::Rounded,
1675 _ => return None,
1676 };
1677 let interior = bits.take_flag();
1678 let dither = bits.take_flag();
1679 let ablation = ShapeAblation {
1680 material: bits.take_flag(),
1681 fill: bits.take_flag(),
1682 };
1683 let turns = match bits.take(2) {
1684 0 => ShapeTurns::None,
1685 1 => ShapeTurns::All,
1686 2 => ShapeTurns::Mixed,
1687 _ => return None,
1688 };
1689 let depth = match bits.take(2) {
1690 0 => ShapeDepth::Off,
1691 1 => ShapeDepth::Tested,
1692 2 => ShapeDepth::Interior,
1693 _ => return None,
1694 };
1695 (bits.0 == 0).then_some(Self {
1696 blend_mode,
1697 tier,
1698 variant: ShapeVariant {
1699 kinds,
1700 brush,
1701 solid,
1702 clip,
1703 interior,
1704 dither,
1705 ablation,
1706 },
1707 turns,
1708 depth,
1709 })
1710 }
1711
1712 #[cfg(test)]
1713 pub(crate) fn general_for(blend_mode: BlendMode, tier: RunTier) -> Self {
1714 Self {
1715 blend_mode,
1716 tier,
1717 variant: ShapeVariant::GENERAL,
1718 turns: ShapeTurns::None,
1719 depth: ShapeDepth::Off,
1720 }
1721 }
1722
1723 pub(crate) fn interior(self) -> Option<Self> {
1728 (self.depth == ShapeDepth::Tested
1729 && !self.variant.rounded()
1730 && self.blend_mode == BlendMode::SrcOver
1731 && !self.variant.ablation.material
1732 && !self.variant.ablation.fill)
1733 .then_some(Self {
1734 variant: self.variant.general(),
1735 depth: ShapeDepth::Interior,
1736 ..self
1737 })
1738 }
1739
1740 pub(crate) fn general(self) -> Self {
1741 Self {
1742 tier: RunTier::Either,
1743 variant: self.variant.general(),
1744 ..self
1745 }
1746 }
1747
1748 pub(crate) fn is_general(self) -> bool {
1749 self.variant == self.variant.general()
1750 }
1751
1752 pub(crate) fn joined(self) -> Self {
1753 Self {
1754 variant: self.variant.joined(),
1755 ..self
1756 }
1757 }
1758}
1759
1760pub(crate) struct ShapeProgram {
1762 pub(crate) vertex_entry: &'static str,
1763 pub(crate) fragment_entry: &'static str,
1764 pub(crate) constants: [(&'static str, f64); 15],
1765}
1766
1767impl ShapePipelineKey {
1768 pub(crate) fn program(self, mode: RunBufferMode) -> ShapeProgram {
1769 let Self {
1770 tier,
1771 variant,
1772 turns,
1773 depth,
1774 ..
1775 } = self;
1776 let constants = [
1777 ("SHAPE_KIND_FIXED", variant.kind().map_or(-1.0, f64::from)),
1778 ("SHAPE_KINDS", f64::from(variant.kinds)),
1779 ("BRUSH_KIND_FIXED", variant.brush.map_or(-1.0, f64::from)),
1780 ("SHAPE_SOLID", f64::from(u8::from(variant.solid))),
1781 (
1782 "SHAPE_CLIPPED",
1783 f64::from(u8::from(variant.clip != SegmentClip::Untested)),
1784 ),
1785 ("SHAPE_ROUNDED_CLIP", f64::from(u8::from(variant.rounded()))),
1786 (
1787 "SHAPE_INTERIOR",
1788 f64::from(u8::from(variant.interior && !variant.rounded())),
1789 ),
1790 ("SHAPE_DITHER", f64::from(u8::from(variant.dither))),
1791 ("SHAPE_TURNS", turns.constant()),
1792 ("SHAPE_TIER", f64::from(tier as u8)),
1793 ("SHAPE_DEPTH", f64::from(u8::from(depth != ShapeDepth::Off))),
1794 ("SHAPE_BANDS", f64::from(u8::from(mode.storage))),
1795 ("SHAPE_TRIG_FILLED", f64::from(u8::from(mode.trig_fill))),
1796 ("SHAPE_FLAT", f64::from(u8::from(variant.ablation.material))),
1797 ("SHAPE_DISCARD", f64::from(u8::from(variant.ablation.fill))),
1798 ];
1799 let (vertex_entry, fragment_entry) = if depth == ShapeDepth::Interior {
1800 ("vs_record_interior", "fs_interior")
1801 } else {
1802 variant.entries(turns == ShapeTurns::None)
1803 };
1804 ShapeProgram {
1805 vertex_entry,
1806 fragment_entry,
1807 constants,
1808 }
1809 }
1810}
1811
1812pub(crate) fn create_shape_pipeline(
1813 device: &wgpu::Device,
1814 cache: Option<&wgpu::PipelineCache>,
1815 surface_format: wgpu::TextureFormat,
1816 shader: &SharedShader,
1817 key: ShapePipelineKey,
1818 mode: RunBufferMode,
1819) -> wgpu::RenderPipeline {
1820 let ShapePipelineKey {
1821 blend_mode,
1822 tier,
1823 variant,
1824 turns,
1825 depth,
1826 } = key;
1827 let ShapeProgram {
1828 vertex_entry,
1829 fragment_entry,
1830 constants,
1831 } = key.program(mode);
1832 let blend = (depth != ShapeDepth::Interior).then(|| blend_state_for_mode(blend_mode));
1833 let instance_layout = record_vertex_layouts().map(Some);
1834 let stages = shader.stages((vertex_entry, fragment_entry), &constants);
1835
1836 create_render_pipeline_logged(
1837 device,
1838 cache,
1839 &format!(
1840 "shape blend={blend_mode:?} tier={tier:?} variant={variant:?} turns={turns:?} depth={depth:?}"
1841 ),
1842 wgpu::RenderPipelineDescriptor {
1843 label: Some("Shape Pipeline"),
1844 layout: Some(shader.layout()),
1845 vertex: wgpu::VertexState {
1846 module: &stages.vertex,
1847 entry_point: Some(vertex_entry),
1848 compilation_options: wgpu::PipelineCompilationOptions {
1849 constants: stages.constants,
1850 ..wgpu::PipelineCompilationOptions::default()
1851 },
1852 buffers: &instance_layout,
1853 },
1854 fragment: Some(wgpu::FragmentState {
1855 module: &stages.fragment,
1856 entry_point: Some(fragment_entry),
1857 compilation_options: wgpu::PipelineCompilationOptions {
1858 constants: stages.constants,
1859 ..wgpu::PipelineCompilationOptions::default()
1860 },
1861 targets: &[Some(wgpu::ColorTargetState {
1862 format: surface_format,
1863 blend,
1864 write_mask: wgpu::ColorWrites::ALL,
1865 })],
1866 }),
1867 primitive: wgpu::PrimitiveState {
1868 topology: wgpu::PrimitiveTopology::TriangleList,
1869 strip_index_format: None,
1870 front_face: wgpu::FrontFace::Ccw,
1871 cull_mode: None,
1872 unclipped_depth: false,
1873 polygon_mode: wgpu::PolygonMode::Fill,
1874 conservative: false,
1875 },
1876 depth_stencil: depth.stencil_state(),
1877 multisample: wgpu::MultisampleState::default(),
1878 multiview_mask: None,
1879 cache: None,
1880 },
1881 )
1882}
1883fn create_image_pipeline(
1884 device: &wgpu::Device,
1885 cache: Option<&wgpu::PipelineCache>,
1886 surface_format: wgpu::TextureFormat,
1887 shader: &SharedShader,
1888 blend_mode: BlendMode,
1889 variant: ImageVariant,
1890) -> wgpu::RenderPipeline {
1891 let module = shader.module();
1892 let ImageVariant {
1893 depth,
1894 alpha_mask,
1895 rounded,
1896 } = variant;
1897 let (vertex_entry, fragment_entry) = match (alpha_mask, rounded) {
1898 (false, false) => ("image_vs_main", "image_fs_main"),
1899 (true, false) => ("image_vs_main", "image_mask_fs_main"),
1900 (false, true) => ("image_rounded_vs_main", "image_rounded_fs_main"),
1901 (true, true) => ("image_rounded_vs_main", "image_mask_rounded_fs_main"),
1902 };
1903 let buffers = [Some(Vertex::desc()), Some(DeviceRoundedClip::desc())];
1905 create_render_pipeline_logged(
1906 device,
1907 cache,
1908 &format!(
1909 "image blend={blend_mode:?} depth={depth} alpha_mask={alpha_mask} rounded={rounded}"
1910 ),
1911 wgpu::RenderPipelineDescriptor {
1912 label: Some("Image Pipeline"),
1913 layout: Some(shader.layout()),
1914 vertex: wgpu::VertexState {
1915 module,
1916 entry_point: Some(vertex_entry),
1917 compilation_options: wgpu::PipelineCompilationOptions::default(),
1918 buffers: if rounded { &buffers } else { &buffers[..1] },
1919 },
1920 fragment: Some(wgpu::FragmentState {
1921 module,
1922 entry_point: Some(fragment_entry),
1923 compilation_options: wgpu::PipelineCompilationOptions::default(),
1924 targets: &[Some(wgpu::ColorTargetState {
1925 format: surface_format,
1926 blend: Some(blend_state_for_mode(blend_mode)),
1927 write_mask: wgpu::ColorWrites::ALL,
1928 })],
1929 }),
1930 primitive: wgpu::PrimitiveState {
1931 topology: wgpu::PrimitiveTopology::TriangleList,
1932 strip_index_format: None,
1933 front_face: wgpu::FrontFace::Ccw,
1934 cull_mode: None,
1935 unclipped_depth: false,
1936 polygon_mode: wgpu::PolygonMode::Fill,
1937 conservative: false,
1938 },
1939 depth_stencil: overlay_depth_state(depth),
1940 multisample: wgpu::MultisampleState::default(),
1941 multiview_mask: None,
1942 cache: None,
1943 },
1944 )
1945}
1946
1947fn create_glyph_atlas_pipeline(
1948 device: &wgpu::Device,
1949 cache: Option<&wgpu::PipelineCache>,
1950 surface_format: wgpu::TextureFormat,
1951 shader: &SharedShader,
1952 (depth, quads): (bool, GlyphQuads),
1953) -> wgpu::RenderPipeline {
1954 let module = shader.module();
1955 create_render_pipeline_logged(
1956 device,
1957 cache,
1958 match (depth, quads) {
1959 (false, GlyphQuads::Plain) => "glyph-atlas",
1960 (true, GlyphQuads::Plain) => "glyph-atlas depth",
1961 (false, GlyphQuads::Aligned) => "glyph-atlas aligned",
1962 (true, GlyphQuads::Aligned) => "glyph-atlas aligned depth",
1963 (false, GlyphQuads::Turned) => "glyph-atlas turned",
1964 (true, GlyphQuads::Turned) => "glyph-atlas turned depth",
1965 },
1966 wgpu::RenderPipelineDescriptor {
1967 label: Some("Glyph Atlas Pipeline"),
1968 layout: Some(shader.layout()),
1969 vertex: wgpu::VertexState {
1970 module,
1971 entry_point: Some(match quads {
1972 GlyphQuads::Plain => "glyph_atlas_vs_main",
1973 GlyphQuads::Aligned => "glyph_atlas_aligned_vs_main",
1974 GlyphQuads::Turned => "glyph_atlas_turned_vs_main",
1975 }),
1976 compilation_options: wgpu::PipelineCompilationOptions::default(),
1977 buffers: &[Some(match quads {
1978 GlyphQuads::Turned => TurnedGlyph::desc(),
1979 GlyphQuads::Plain | GlyphQuads::Aligned => GlyphInstance::desc(),
1980 })],
1981 },
1982 fragment: Some(wgpu::FragmentState {
1983 module,
1984 entry_point: Some(match quads {
1985 GlyphQuads::Aligned => "glyph_atlas_aligned_fs_main",
1986 GlyphQuads::Plain | GlyphQuads::Turned => "glyph_atlas_fs_main",
1987 }),
1988 compilation_options: wgpu::PipelineCompilationOptions::default(),
1989 targets: &[Some(wgpu::ColorTargetState {
1990 format: surface_format,
1991 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1992 write_mask: wgpu::ColorWrites::ALL,
1993 })],
1994 }),
1995 primitive: wgpu::PrimitiveState {
1996 topology: wgpu::PrimitiveTopology::TriangleStrip,
1997 strip_index_format: None,
1998 front_face: wgpu::FrontFace::Ccw,
1999 cull_mode: None,
2000 unclipped_depth: false,
2001 polygon_mode: wgpu::PolygonMode::Fill,
2002 conservative: false,
2003 },
2004 depth_stencil: overlay_depth_state(depth),
2005 multisample: wgpu::MultisampleState::default(),
2006 multiview_mask: None,
2007 cache: None,
2008 },
2009 )
2010}
2011
2012#[repr(C)]
2013#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2014pub(crate) struct Vertex {
2015 position: [f32; 2],
2016 color: [f32; 4],
2017 uv: [f32; 2],
2018 uv_bounds: [f32; 4],
2019}
2020
2021impl Vertex {
2022 const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
2023 0 => Float32x2,
2024 1 => Float32x4,
2025 2 => Float32x2,
2026 3 => Float32x4,
2027 ];
2028
2029 fn desc() -> wgpu::VertexBufferLayout<'static> {
2030 wgpu::VertexBufferLayout {
2031 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
2032 step_mode: wgpu::VertexStepMode::Vertex,
2033 attributes: &Self::ATTRIBS,
2034 }
2035 }
2036}
2037
2038const GLYPH_QUAD_CORNERS: u32 = 4;
2041
2042#[repr(C)]
2045#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)]
2046pub(crate) struct GlyphInstance {
2047 rect: [f32; 4],
2048 uv: [f32; 4],
2049 uv_bounds: [f32; 4],
2050 color: [f32; 4],
2051}
2052
2053impl GlyphInstance {
2054 const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
2055 0 => Float32x4,
2056 1 => Float32x4,
2057 2 => Float32x4,
2058 3 => Float32x4
2059 ];
2060
2061 fn desc() -> wgpu::VertexBufferLayout<'static> {
2062 wgpu::VertexBufferLayout {
2063 array_stride: std::mem::size_of::<GlyphInstance>() as wgpu::BufferAddress,
2064 step_mode: wgpu::VertexStepMode::Instance,
2065 attributes: &Self::ATTRIBS,
2066 }
2067 }
2068
2069 pub(crate) fn clipped_to(self, edges: [f32; 4]) -> Option<Self> {
2073 let [x0, y0, x1, y1] = self.rect;
2074 let rect = [
2075 x0.max(edges[0]),
2076 y0.max(edges[1]),
2077 x1.min(edges[2]),
2078 y1.min(edges[3]),
2079 ];
2080 if rect[2] <= rect[0] || rect[3] <= rect[1] {
2081 return None;
2082 }
2083 if rect == self.rect {
2084 return Some(self);
2085 }
2086 let [u0, v0, u1, v1] = self.uv;
2087 let u = |x: f32| u0 + (x - x0) / (x1 - x0) * (u1 - u0);
2088 let v = |y: f32| v0 + (y - y0) / (y1 - y0) * (v1 - v0);
2089 Some(Self {
2090 rect,
2091 uv: [u(rect[0]), v(rect[1]), u(rect[2]), v(rect[3])],
2092 ..self
2093 })
2094 }
2095}
2096
2097#[inline]
2100fn for_each_visible_glyph(
2101 source_raster_rect: Rect,
2102 quads: GlyphRunQuads<'_>,
2103 (clip, cut): (Option<Rect>, Option<[f32; 4]>),
2104 viewport: ViewportUniformParams,
2105 root_scale: f32,
2106 mut sink: impl FnMut(GlyphInstance),
2107) {
2108 let whole_run_visible =
2109 quads
2110 .bounds
2111 .within_viewport(source_raster_rect, clip, viewport, root_scale);
2112 for quad in quads.iter() {
2113 if !whole_run_visible
2114 && !cached_text_glyph_quad_is_visible_in_viewport(
2115 source_raster_rect,
2116 &quad,
2117 clip,
2118 viewport,
2119 root_scale,
2120 )
2121 {
2122 continue;
2123 }
2124 let Some(glyph) = cached_text_glyph_instance(source_raster_rect, &quad) else {
2125 continue;
2126 };
2127 let glyph = match cut {
2128 Some(edges) => glyph.clipped_to(edges),
2129 None => Some(glyph),
2130 };
2131 if let Some(glyph) = glyph {
2132 sink(glyph);
2133 }
2134 }
2135}
2136
2137#[repr(C)]
2141#[derive(Copy, Clone, Debug, PartialEq, Pod, Zeroable)]
2142pub(crate) struct TurnedGlyph {
2143 glyph: GlyphInstance,
2144 turn: [f32; 4],
2146 translation: [f32; 4],
2148}
2149
2150impl TurnedGlyph {
2151 const ATTRIBS: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
2152 0 => Float32x4,
2153 1 => Float32x4,
2154 2 => Float32x4,
2155 3 => Float32x4,
2156 4 => Float32x4,
2157 5 => Float32x4
2158 ];
2159
2160 pub(crate) fn new(glyph: GlyphInstance, turn: SegmentTransform) -> Self {
2161 let (linear, [x, y], _) = turn.uniform_parts();
2162 Self {
2163 glyph,
2164 turn: linear,
2165 translation: [x, y, 0.0, 0.0],
2166 }
2167 }
2168
2169 fn desc() -> wgpu::VertexBufferLayout<'static> {
2170 wgpu::VertexBufferLayout {
2171 array_stride: std::mem::size_of::<TurnedGlyph>() as wgpu::BufferAddress,
2172 step_mode: wgpu::VertexStepMode::Instance,
2173 attributes: &Self::ATTRIBS,
2174 }
2175 }
2176}
2177
2178#[derive(Default)]
2181pub(crate) struct GlyphInstances {
2182 pub(crate) plain: FrameScratch<GlyphInstance>,
2183 pub(crate) turned: FrameScratch<TurnedGlyph>,
2184}
2185
2186impl GlyphInstances {
2187 pub(crate) fn clear(&mut self) {
2188 self.plain.clear();
2189 self.turned.clear();
2190 }
2191
2192 pub(crate) fn end_frame(&mut self) {
2195 self.plain.end_frame();
2196 self.turned.end_frame();
2197 }
2198
2199 fn lens(&self) -> (usize, usize) {
2200 (self.plain.len(), self.turned.len())
2201 }
2202
2203 fn len_of(&self, turned: bool) -> usize {
2205 if turned {
2206 self.turned.len()
2207 } else {
2208 self.plain.len()
2209 }
2210 }
2211
2212 fn truncate(&mut self, (plain, turned): (usize, usize)) {
2213 self.plain.truncate(plain);
2214 self.turned.truncate(turned);
2215 }
2216}
2217
2218fn glyph_clip_edges(clip: Rect, root_scale: f32) -> [f32; 4] {
2221 [
2222 canonicalize_device_coordinate(clip.x * root_scale),
2223 canonicalize_device_coordinate(clip.y * root_scale),
2224 canonicalize_device_coordinate((clip.x + clip.width) * root_scale),
2225 canonicalize_device_coordinate((clip.y + clip.height) * root_scale),
2226 ]
2227}
2228
2229fn glyph_cut_edges(
2238 clip: Option<Rect>,
2239 scissor: TargetRect,
2240 viewport: ViewportUniformParams,
2241 root_scale: f32,
2242) -> Option<[f32; 4]> {
2243 if !viewport.transform.is_identity() {
2244 return clip.map(|clip| glyph_clip_edges(clip, root_scale));
2245 }
2246 let (x, y, width, height) = scissor;
2247 let [offset_x, offset_y] = viewport.offset;
2248 Some([
2249 x as f32 + offset_x,
2250 y as f32 + offset_y,
2251 (x + width) as f32 + offset_x,
2252 (y + height) as f32 + offset_y,
2253 ])
2254}
2255
2256#[repr(C)]
2257#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2258struct Uniforms {
2259 viewport: [f32; 2],
2260 viewport_offset: [f32; 2],
2261 transform: [f32; 4],
2262 translation: [f32; 2],
2263 reserved: [f32; 2],
2264 inverse: [f32; 4],
2265 origin: [f32; 2],
2266 origin_reserved: [f32; 2],
2267 placement: PlacementData,
2268}
2269
2270impl Uniforms {
2271 fn of(params: ViewportUniformParams, placement: PlacementData, tier: RunTier) -> Self {
2272 let (transform, translation, inverse) = params.transform.uniform_parts();
2273 Self {
2274 viewport: [params.width as f32, params.height as f32],
2275 viewport_offset: params.offset,
2276 transform,
2277 translation,
2278 reserved: [params.depth_base, f32::from(tier as u8)],
2279 inverse,
2280 origin: params.origin,
2281 origin_reserved: [0.0; 2],
2282 placement,
2283 }
2284 }
2285}
2286
2287static SURVIVE_GPU_ERRORS: DebugToggle = DebugToggle::new("CRANPOSE_SURVIVE_GPU_ERRORS");
2288
2289fn survive_gpu_errors_enabled() -> bool {
2290 !SURVIVE_GPU_ERRORS.equals("0")
2291}
2292
2293struct CachedImageTexture {
2294 alpha_mask: bool,
2295 _texture: wgpu::Texture,
2296 _view: wgpu::TextureView,
2297 nearest_bind_group: wgpu::BindGroup,
2298 linear_bind_group: wgpu::BindGroup,
2299 bytes: usize,
2300}
2301
2302impl CachedImageTexture {
2303 fn binding(&self, sampling: ImageSampling) -> ImageBinding {
2304 ImageBinding {
2305 bind_group: match sampling {
2306 ImageSampling::Nearest => self.nearest_bind_group.clone(),
2307 ImageSampling::Linear => self.linear_bind_group.clone(),
2308 },
2309 alpha_mask: self.alpha_mask,
2310 }
2311 }
2312}
2313
2314pub(crate) struct ImageBinding {
2318 bind_group: wgpu::BindGroup,
2319 alpha_mask: bool,
2320}
2321
2322#[derive(Clone, Copy)]
2323struct GlyphAtlasEntry {
2324 x: u16,
2325 y: u16,
2326 width: u16,
2327 height: u16,
2328}
2329
2330fn next_glyph_atlas_size(current: u32, max: u32) -> u32 {
2331 current.saturating_mul(2).clamp(1, max.max(1))
2332}
2333
2334#[derive(Clone, Copy)]
2337struct GlyphSamplers<'a> {
2338 nearest: &'a wgpu::Sampler,
2339 linear: &'a wgpu::Sampler,
2340}
2341
2342#[derive(Clone, Copy, PartialEq, Eq, Hash)]
2345struct GlyphAtlasSlotKey {
2346 glyph: SoftwareGlyphAtlasKey,
2347 luminance: TextLuminance,
2348}
2349
2350impl GlyphAtlasSlotKey {
2351 fn new(glyph: SoftwareGlyphAtlasKey, color: Color) -> Self {
2352 Self {
2353 glyph,
2354 luminance: TextLuminance::of_color(color),
2355 }
2356 }
2357}
2358
2359const RUN_ATLAS_ENTRY_SLOTS: usize = 64;
2361
2362struct RunAtlasEntries {
2367 slots: [Option<(GlyphAtlasSlotKey, GlyphAtlasEntry)>; RUN_ATLAS_ENTRY_SLOTS],
2368}
2369
2370impl RunAtlasEntries {
2371 fn new() -> Self {
2372 Self {
2373 slots: [None; RUN_ATLAS_ENTRY_SLOTS],
2374 }
2375 }
2376
2377 fn entry(
2380 &mut self,
2381 renderer: &mut GpuRenderer,
2382 glyph: &SoftwareGlyphAtlasPlacement,
2383 look_up: impl FnOnce(&mut GpuRenderer) -> Result<GlyphAtlasEntry, String>,
2384 ) -> Result<GlyphAtlasEntry, String> {
2385 let key = GlyphAtlasSlotKey::new(glyph.key, glyph.color);
2386 let slot = &mut self.slots[glyph.key.glyph_id as usize % RUN_ATLAS_ENTRY_SLOTS];
2387 if let Some((seen, entry)) = *slot
2388 && seen == key
2389 {
2390 renderer.frame_stats.record_text_glyph_atlas_hits(1);
2391 return Ok(entry);
2392 }
2393 let entry = look_up(renderer)?;
2394 *slot = Some((key, entry));
2395 Ok(entry)
2396 }
2397}
2398
2399struct TextGlyphAtlas {
2400 texture: wgpu::Texture,
2401 _view: wgpu::TextureView,
2402 texel_bind_group: Rc<wgpu::BindGroup>,
2403 filtered_bind_group: Rc<wgpu::BindGroup>,
2404 entries: BoundedLruCache<GlyphAtlasSlotKey, GlyphAtlasEntry>,
2405 generation: u64,
2406 size: u32,
2407 max_size: u32,
2408 cursor_x: u32,
2409 cursor_y: u32,
2410 row_height: u32,
2411 upload_scratch: Vec<u8>,
2412}
2413
2414impl TextGlyphAtlas {
2415 fn new(
2416 device: &wgpu::Device,
2417 image_layout: &wgpu::BindGroupLayout,
2418 samplers: GlyphSamplers<'_>,
2419 size: u32,
2420 ) -> Self {
2421 let device_max = device.limits().max_texture_dimension_2d.max(1);
2424 let max_size = TEXT_GLYPH_ATLAS_MAX_SIZE.min(1 << device_max.ilog2());
2425 let size = size
2426 .next_power_of_two()
2427 .clamp(TEXT_GLYPH_ATLAS_MIN_SIZE.min(max_size), max_size);
2428 let texture = Self::create_texture(device, size);
2429 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
2430 let bind = |label: &'static str, sampler: &wgpu::Sampler| {
2431 Rc::new(device.create_bind_group(&wgpu::BindGroupDescriptor {
2432 label: Some(label),
2433 layout: image_layout,
2434 entries: &[
2435 wgpu::BindGroupEntry {
2436 binding: 0,
2437 resource: wgpu::BindingResource::TextureView(&view),
2438 },
2439 wgpu::BindGroupEntry {
2440 binding: 1,
2441 resource: wgpu::BindingResource::Sampler(sampler),
2442 },
2443 ],
2444 }))
2445 };
2446 let texel_bind_group = bind("Text Glyph Atlas Bind Group", samplers.nearest);
2447 let filtered_bind_group = bind("Filtered Text Glyph Atlas Bind Group", samplers.linear);
2448 Self {
2449 texture,
2450 _view: view,
2451 texel_bind_group,
2452 filtered_bind_group,
2453 entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
2454 generation: 0,
2455 size,
2456 max_size,
2457 cursor_x: TEXT_GLYPH_ATLAS_PADDING,
2458 cursor_y: TEXT_GLYPH_ATLAS_PADDING,
2459 row_height: 0,
2460 upload_scratch: Vec::new(),
2461 }
2462 }
2463
2464 fn create_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
2465 device.create_texture(&wgpu::TextureDescriptor {
2466 label: Some("Text Glyph Atlas Texture"),
2467 size: wgpu::Extent3d {
2468 width: size,
2469 height: size,
2470 depth_or_array_layers: 1,
2471 },
2472 mip_level_count: 1,
2473 sample_count: 1,
2474 dimension: wgpu::TextureDimension::D2,
2475 format: wgpu::TextureFormat::R8Unorm,
2476 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
2477 view_formats: &[],
2478 })
2479 }
2480
2481 fn reset(
2482 &mut self,
2483 device: &wgpu::Device,
2484 image_layout: &wgpu::BindGroupLayout,
2485 samplers: GlyphSamplers<'_>,
2486 ) {
2487 let generation = self.generation.wrapping_add(1);
2488 let grown = next_glyph_atlas_size(self.size, self.max_size);
2489 let mut next = Self::new(device, image_layout, samplers, grown);
2490 next.generation = generation;
2491 *self = next;
2492 }
2493
2494 fn bind_group(&self, transform: SegmentTransform) -> Rc<wgpu::BindGroup> {
2498 Rc::clone(if transform.is_identity() {
2499 &self.texel_bind_group
2500 } else {
2501 &self.filtered_bind_group
2502 })
2503 }
2504
2505 fn generation(&self) -> u64 {
2506 self.generation
2507 }
2508
2509 fn size(&self) -> u32 {
2510 self.size
2511 }
2512
2513 fn texel(&self) -> f32 {
2517 (self.size() as f32).recip()
2518 }
2519
2520 fn entry(&mut self, key: &GlyphAtlasSlotKey) -> Option<GlyphAtlasEntry> {
2521 self.entries.get(key).copied()
2522 }
2523
2524 fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
2525 let max_extent = self.size.saturating_sub(TEXT_GLYPH_ATLAS_PADDING * 2);
2526 if width == 0 || height == 0 || width > max_extent || height > max_extent {
2527 return None;
2528 }
2529
2530 if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > self.size {
2531 self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
2532 self.cursor_y = self
2533 .cursor_y
2534 .saturating_add(self.row_height)
2535 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
2536 self.row_height = 0;
2537 }
2538 if self.cursor_y > self.size - height - TEXT_GLYPH_ATLAS_PADDING {
2539 return None;
2540 }
2541
2542 let entry = GlyphAtlasEntry {
2543 x: self.cursor_x as u16,
2544 y: self.cursor_y as u16,
2545 width: width as u16,
2546 height: height as u16,
2547 };
2548 self.cursor_x = self
2549 .cursor_x
2550 .saturating_add(width)
2551 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
2552 self.row_height = self.row_height.max(height);
2553 Some(entry)
2554 }
2555
2556 fn upload_glyph(
2557 &mut self,
2558 glyph: &SoftwareGlyphAtlasGlyph,
2559 queue: &wgpu::Queue,
2560 executor: &mut WgpuFrameGraphExecutor,
2561 frame_stats: &mut gpu_stats::FrameStats,
2562 ) -> Option<GlyphAtlasEntry> {
2563 let key = GlyphAtlasSlotKey::new(glyph.key, glyph.color);
2564 if let Some(entry) = self.entry(&key) {
2565 frame_stats.record_text_glyph_atlas_hits(1);
2566 return Some(entry);
2567 }
2568
2569 let width = u32::try_from(glyph.mask.width).ok()?;
2570 let height = u32::try_from(glyph.mask.height).ok()?;
2571 let entry = self.allocate(width, height)?;
2572 self.upload_scratch.clear();
2573 let correction = key.luminance.correction();
2574 self.upload_scratch.extend(
2575 glyph
2576 .mask
2577 .alpha
2578 .iter()
2579 .map(|&coverage| correction.apply(coverage)),
2580 );
2581
2582 let upload_stats = executor.upload_texture(
2583 queue,
2584 wgpu::TexelCopyTextureInfo {
2585 texture: &self.texture,
2586 mip_level: 0,
2587 origin: wgpu::Origin3d {
2588 x: u32::from(entry.x),
2589 y: u32::from(entry.y),
2590 z: 0,
2591 },
2592 aspect: wgpu::TextureAspect::All,
2593 },
2594 &self.upload_scratch,
2595 wgpu::TexelCopyBufferLayout {
2596 offset: 0,
2597 bytes_per_row: Some(width),
2598 rows_per_image: Some(height),
2599 },
2600 wgpu::Extent3d {
2601 width,
2602 height,
2603 depth_or_array_layers: 1,
2604 },
2605 );
2606 frame_stats.record_command_stats(upload_stats);
2607 frame_stats.record_text_glyph_atlas_miss(width, height);
2608 self.entries.put(key, entry);
2609 Some(entry)
2610 }
2611}
2612
2613#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2616pub(crate) struct ImageVariant {
2617 depth: bool,
2618 alpha_mask: bool,
2619 rounded: bool,
2620}
2621
2622pub(crate) struct ImageDrawCmd {
2623 index_start: u32,
2624 scissor: (u32, u32, u32, u32),
2625 image: ImageBinding,
2626 clip: Option<u32>,
2629}
2630
2631impl ImageVariant {
2632 fn of(cmd: &ImageDrawCmd, depth: bool) -> Self {
2634 Self {
2635 depth,
2636 alpha_mask: cmd.image.alpha_mask,
2637 rounded: cmd.clip.is_some(),
2638 }
2639 }
2640}
2641
2642impl ImageDrawCmd {
2643 pub(crate) fn bounds(&self) -> TargetRect {
2645 self.scissor
2646 }
2647}
2648
2649#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2654pub(crate) enum GlyphQuads {
2655 Plain,
2656 Aligned,
2657 Turned,
2658}
2659
2660impl GlyphQuads {
2661 fn turned(self) -> bool {
2662 self == Self::Turned
2663 }
2664
2665 fn plain(aligned: bool) -> Self {
2667 if aligned { Self::Aligned } else { Self::Plain }
2668 }
2669}
2670
2671fn glyph_rect_aligned(rect: [f32; 4], offset: [f32; 2]) -> bool {
2674 let whole = |value: f32| value.fract() == 0.0;
2675 whole(rect[0] - offset[0])
2676 && whole(rect[1] - offset[1])
2677 && whole(rect[2] - offset[0])
2678 && whole(rect[3] - offset[1])
2679}
2680
2681#[derive(Clone)]
2682enum GlyphDrawSource {
2683 Shared {
2684 instance_start: u32,
2685 instance_count: u32,
2686 quads: GlyphQuads,
2687 },
2688 Retained {
2689 run: Rc<CachedGpuTextGlyphRun>,
2690 uniform_slot: usize,
2691 aligned: bool,
2692 },
2693}
2694
2695#[derive(Clone)]
2696pub(crate) struct GlyphDrawCmd {
2697 atlas: Rc<wgpu::BindGroup>,
2698 source: GlyphDrawSource,
2699 scissor: Option<(u32, u32, u32, u32)>,
2702 bounds: (u32, u32, u32, u32),
2704}
2705
2706struct GlyphDraw<'a> {
2708 atlas: &'a Rc<wgpu::BindGroup>,
2709 scissor: Option<(u32, u32, u32, u32)>,
2710 step: GlyphDrawStep<'a>,
2711}
2712
2713enum GlyphDrawStep<'a> {
2714 Shared(std::ops::Range<u32>, GlyphQuads),
2716 Retained {
2717 run: &'a CachedGpuTextGlyphRun,
2718 uniform_slot: usize,
2719 aligned: bool,
2720 },
2721}
2722
2723struct GlyphDraws<'a> {
2727 cmds: &'a [GlyphDrawCmd],
2728}
2729
2730impl<'a> GlyphDraws<'a> {
2731 fn new(cmds: &'a [GlyphDrawCmd]) -> Self {
2732 Self { cmds }
2733 }
2734}
2735
2736impl<'a> Iterator for GlyphDraws<'a> {
2737 type Item = GlyphDraw<'a>;
2738
2739 fn next(&mut self) -> Option<Self::Item> {
2740 let (first, rest) = self.cmds.split_first()?;
2741 self.cmds = rest;
2742 let step = match &first.source {
2743 GlyphDrawSource::Retained {
2744 run,
2745 uniform_slot,
2746 aligned,
2747 } => GlyphDrawStep::Retained {
2748 run,
2749 uniform_slot: *uniform_slot,
2750 aligned: *aligned,
2751 },
2752 GlyphDrawSource::Shared {
2753 instance_start,
2754 instance_count,
2755 quads,
2756 } => {
2757 let mut end = instance_start + instance_count;
2758 while let Some((next, rest)) = self.cmds.split_first() {
2759 match next.source {
2760 GlyphDrawSource::Shared {
2761 instance_start,
2762 instance_count,
2763 quads: next_quads,
2764 } if instance_start == end
2765 && next_quads == *quads
2766 && next.scissor == first.scissor
2767 && Rc::ptr_eq(&next.atlas, &first.atlas) =>
2768 {
2769 end += instance_count;
2770 self.cmds = rest;
2771 }
2772 _ => break,
2773 }
2774 }
2775 GlyphDrawStep::Shared(*instance_start..end, *quads)
2776 }
2777 };
2778 Some(GlyphDraw {
2779 atlas: &first.atlas,
2780 scissor: first.scissor,
2781 step,
2782 })
2783 }
2784}
2785
2786impl GlyphDrawCmd {
2787 fn quads(&self) -> GlyphQuads {
2789 match &self.source {
2790 GlyphDrawSource::Shared { quads, .. } => *quads,
2791 GlyphDrawSource::Retained { aligned, .. } => GlyphQuads::plain(*aligned),
2792 }
2793 }
2794
2795 fn shared(
2796 (instances, quads): (std::ops::Range<usize>, GlyphQuads),
2797 scissor: Option<(u32, u32, u32, u32)>,
2798 bounds: (u32, u32, u32, u32),
2799 atlas: Rc<wgpu::BindGroup>,
2800 ) -> Self {
2801 let start = u32::try_from(instances.start).unwrap_or(u32::MAX);
2802 let end = u32::try_from(instances.end).unwrap_or(u32::MAX);
2803 Self {
2804 atlas,
2805 source: GlyphDrawSource::Shared {
2806 instance_start: start,
2807 instance_count: end.saturating_sub(start),
2808 quads,
2809 },
2810 scissor,
2811 bounds,
2812 }
2813 }
2814
2815 fn retained(
2816 (run, uniform_slot, aligned): (Rc<CachedGpuTextGlyphRun>, usize, bool),
2817 scissor: (u32, u32, u32, u32),
2818 atlas: Rc<wgpu::BindGroup>,
2819 ) -> Self {
2820 Self {
2821 atlas,
2822 source: GlyphDrawSource::Retained {
2823 run,
2824 uniform_slot,
2825 aligned,
2826 },
2827 scissor: Some(scissor),
2828 bounds: scissor,
2829 }
2830 }
2831
2832 pub(crate) fn bounds(&self) -> (u32, u32, u32, u32) {
2834 self.bounds
2835 }
2836
2837 fn turned(&self) -> Option<bool> {
2840 match self.source {
2841 GlyphDrawSource::Shared { quads, .. } => Some(quads.turned()),
2842 GlyphDrawSource::Retained { .. } => None,
2843 }
2844 }
2845}
2846
2847pub(crate) fn movable_glyph_kind<T>(
2854 draws: &[T],
2855 kind: impl Fn(&T) -> Option<bool>,
2856 bounds: impl Fn(&T) -> TargetRect,
2857) -> Option<bool> {
2858 let mut turned = 0;
2859 for draw in draws {
2860 turned += usize::from(kind(draw)?);
2861 }
2862 let changes = draws
2863 .windows(2)
2864 .filter(|pair| kind(&pair[0]) != kind(&pair[1]))
2865 .count();
2866 if changes < 2 {
2867 return None;
2868 }
2869 let fewer = turned * 2 < draws.len();
2870 [fewer, !fewer].into_iter().find(|&moved| {
2871 draws.iter().enumerate().all(|(index, draw)| {
2872 kind(draw) != Some(moved)
2873 || draws[index + 1..].iter().all(|later| {
2874 kind(later) == Some(moved)
2875 || !crate::draw_pass::target_rects_overlap(bounds(draw), bounds(later))
2876 })
2877 })
2878 })
2879}
2880
2881pub(crate) fn defer_retained_glyph_runs(
2889 cmds: &mut [GlyphDrawCmd],
2890 moved: &mut Vec<GlyphDrawCmd>,
2891) -> usize {
2892 let retained = |cmd: &GlyphDrawCmd| cmd.turned().is_none();
2893 let shared = cmds.iter().filter(|cmd| !retained(cmd)).count();
2894 if cmds[..shared].iter().all(|cmd| !retained(cmd)) {
2895 return shared;
2896 }
2897 let deferrable = cmds.iter().enumerate().all(|(index, cmd)| {
2898 !retained(cmd)
2899 || cmds[index + 1..].iter().all(|later| {
2900 retained(later)
2901 || !crate::draw_pass::target_rects_overlap(cmd.bounds(), later.bounds())
2902 })
2903 });
2904 if !deferrable {
2905 return 0;
2906 }
2907 moved.clear();
2908 let mut kept = 0;
2909 for read in 0..cmds.len() {
2910 if retained(&cmds[read]) {
2911 moved.push(cmds[read].clone());
2912 } else {
2913 cmds.swap(kept, read);
2914 kept += 1;
2915 }
2916 }
2917 for (slot, cmd) in cmds[kept..].iter_mut().zip(moved.drain(..)) {
2918 *slot = cmd;
2919 }
2920 shared
2921}
2922
2923pub(crate) fn group_glyph_kinds(cmds: &mut [GlyphDrawCmd], moved: &mut Vec<GlyphDrawCmd>) {
2927 let Some(kind) = movable_glyph_kind(cmds, GlyphDrawCmd::turned, GlyphDrawCmd::bounds) else {
2928 return;
2929 };
2930 moved.clear();
2931 let mut kept = 0;
2932 for read in 0..cmds.len() {
2933 if cmds[read].turned() == Some(kind) {
2934 moved.push(cmds[read].clone());
2935 } else {
2936 cmds.swap(kept, read);
2937 kept += 1;
2938 }
2939 }
2940 for (slot, cmd) in cmds[kept..].iter_mut().zip(moved.drain(..)) {
2941 *slot = cmd;
2942 }
2943}
2944
2945#[derive(Clone, Copy, Debug, PartialEq)]
2946struct ImageUvRect {
2947 min: [f32; 2],
2948 max: [f32; 2],
2949 sample_bounds: [f32; 4],
2950}
2951
2952pub(crate) struct ImageSlot {
2956 vertices: BufferUpload,
2957 indices: BufferUpload,
2958 clips: Option<BufferUpload>,
2960}
2961
2962fn image_vertex_spec() -> UploadAllocatorSpec {
2963 UploadAllocatorSpec::vertex("Image Vertex Buffer", std::mem::size_of::<Vertex>() as u64)
2964}
2965
2966fn image_index_spec() -> UploadAllocatorSpec {
2967 UploadAllocatorSpec::index("Image Index Buffer", std::mem::size_of::<u32>() as u64)
2968}
2969
2970fn glyph_instance_spec() -> UploadAllocatorSpec {
2971 UploadAllocatorSpec::vertex(
2972 "Glyph Instance Buffer",
2973 std::mem::size_of::<GlyphInstance>() as u64,
2974 )
2975}
2976
2977fn turned_glyph_spec() -> UploadAllocatorSpec {
2978 UploadAllocatorSpec::vertex(
2979 "Turned Glyph Instance Buffer",
2980 std::mem::size_of::<TurnedGlyph>() as u64,
2981 )
2982}
2983
2984#[derive(Default)]
2985struct ViewportUniforms {
2986 uploads: FrameUploadAllocators,
2987 slots: Vec<UniformUpload>,
2988}
2989
2990impl ViewportUniforms {
2991 fn begin_frame(&mut self) {
2992 self.slots.clear();
2993 self.uploads.reset();
2994 }
2995
2996 fn claim(
2997 &mut self,
2998 device: &wgpu::Device,
2999 layout: &wgpu::BindGroupLayout,
3000 uniforms: &Uniforms,
3001 ) -> usize {
3002 let slot = self.slots.len();
3003 self.slots.push(self.uploads.upload_uniform(
3004 UploadAllocatorId::Viewport,
3005 UploadAllocatorSpec::uniform(
3006 "Viewport Uniform Buffer",
3007 "Viewport Uniform Bind Group",
3008 std::mem::size_of::<Uniforms>() as u64,
3009 ),
3010 device,
3011 layout,
3012 bytemuck::bytes_of(uniforms),
3013 ));
3014 slot
3015 }
3016
3017 fn bind(&self, pass: &mut TrackedPass<'_, '_>, slot: usize) -> Result<(), String> {
3018 let uniform = self
3019 .slots
3020 .get(slot)
3021 .ok_or_else(|| "viewport uniform slot was never claimed this frame".to_string())?;
3022 pass.set_bind_group(0, &uniform.bind_group, &[uniform.offset]);
3023 Ok(())
3024 }
3025
3026 fn stage_pending(
3027 &mut self,
3028 device: &wgpu::Device,
3029 recorder: &mut impl FrameCommandRecorder,
3030 ) -> FrameCommandStats {
3031 self.uploads.stage_pending(|buffer, offset, bytes| {
3032 recorder.stage_frame_buffer_copy(device, buffer, offset, bytes)
3033 })
3034 }
3035}
3036
3037#[derive(Clone, Copy, Debug, PartialEq)]
3038pub(crate) struct ViewportUniformParams {
3039 pub(crate) width: u32,
3040 pub(crate) height: u32,
3041 pub(crate) offset: [f32; 2],
3042 pub(crate) transform: SegmentTransform,
3043 pub(crate) origin: [f32; 2],
3048 pub(crate) depth_base: f32,
3051}
3052
3053impl ViewportUniformParams {
3054 pub(crate) fn scene_rect(self, root_scale: f32) -> Rect {
3056 segment_scene_rect(
3057 self.transform,
3058 Rect {
3059 x: self.offset[0],
3060 y: self.offset[1],
3061 width: self.width as f32,
3062 height: self.height as f32,
3063 },
3064 root_scale,
3065 )
3066 }
3067}
3068
3069pub(crate) fn segment_scene_rect(
3073 transform: SegmentTransform,
3074 device: Rect,
3075 root_scale: f32,
3076) -> Rect {
3077 let device = if transform.is_identity() {
3078 device
3079 } else {
3080 transform.segment_bounds(device)
3081 };
3082 Rect {
3083 x: device.x / root_scale,
3084 y: device.y / root_scale,
3085 width: device.width / root_scale,
3086 height: device.height / root_scale,
3087 }
3088}
3089
3090pub(crate) struct StoreRunBatch {
3095 pub(crate) tables: StoredTables,
3096 pub(crate) uniform_slot: usize,
3097 pub(crate) draws: std::ops::Range<usize>,
3098 pub(crate) clip: Option<TargetRect>,
3101}
3102
3103struct CompositionTarget {
3104 target: Rc<OffscreenTarget>,
3105 output_bind_group: wgpu::BindGroup,
3106}
3107
3108enum FrameRoot {
3111 Surface(Rc<OffscreenTarget>),
3112 Composition(CompositionTarget),
3113}
3114
3115impl FrameRoot {
3116 fn target(&self) -> &Rc<OffscreenTarget> {
3117 match self {
3118 Self::Surface(target) => target,
3119 Self::Composition(composition) => &composition.target,
3120 }
3121 }
3122
3123 fn output<'a>(
3126 &'a self,
3127 output_view: Option<&'a wgpu::TextureView>,
3128 screenshot_bind_group: Option<&'a wgpu::BindGroup>,
3129 ) -> Option<(&'a wgpu::TextureView, &'a wgpu::BindGroup)> {
3130 match self {
3131 Self::Surface(_) => None,
3132 Self::Composition(composition) => output_view.map(|view| {
3133 (
3134 view,
3135 screenshot_bind_group.unwrap_or(&composition.output_bind_group),
3136 )
3137 }),
3138 }
3139 }
3140}
3141
3142const DIRECT_SURFACE_ROOT_USAGES: wgpu::TextureUsages = wgpu::TextureUsages::RENDER_ATTACHMENT
3146 .union(wgpu::TextureUsages::COPY_SRC)
3147 .union(wgpu::TextureUsages::COPY_DST);
3148
3149pub fn presentable_root_usages(supported: wgpu::TextureUsages) -> wgpu::TextureUsages {
3153 wgpu::TextureUsages::RENDER_ATTACHMENT
3154 | (supported & (DIRECT_SURFACE_ROOT_USAGES | wgpu::TextureUsages::TEXTURE_BINDING))
3155}
3156
3157fn surface_is_direct_root(
3164 texture: &wgpu::Texture,
3165 composition_format: wgpu::TextureFormat,
3166 viewport: (u32, u32),
3167 reads_page: bool,
3168) -> bool {
3169 let usage = texture.usage();
3170 texture.format().remove_srgb_suffix() == composition_format
3171 && usage.contains(DIRECT_SURFACE_ROOT_USAGES)
3172 && (!reads_page || usage.contains(wgpu::TextureUsages::TEXTURE_BINDING))
3173 && (texture.width(), texture.height()) == viewport
3174}
3175
3176#[derive(Clone, Copy)]
3177enum OutputMode {
3178 Display,
3179 Screenshot,
3180}
3181
3182pub struct GpuRenderer {
3183 pub(crate) device: Arc<wgpu::Device>,
3184 pub(crate) queue: Arc<wgpu::Queue>,
3185 device_errors: Arc<DeviceErrorSentry>,
3186 renderer_epoch: u64,
3187 pub(crate) composition_format: wgpu::TextureFormat,
3188 #[cfg(not(target_arch = "wasm32"))]
3189 display_format: wgpu::TextureFormat,
3190 composition_target: Option<CompositionTarget>,
3191 output_converter: OutputConverter,
3192 screenshot_converter: OutputConverter,
3193 adapter_backend: wgpu::Backend,
3194 pipeline_cache: Option<wgpu::PipelineCache>,
3195 #[cfg(not(target_arch = "wasm32"))]
3196 _pipeline_cache_watcher: Option<crate::pipeline_disk_cache::PersistWatcher>,
3197 #[cfg(not(target_arch = "wasm32"))]
3200 recent_after_update: Option<Arc<crate::pipeline_records::PipelineRecords>>,
3201 recorder: crate::pipeline_recorder::PipelineRecorder,
3203 pipeline_compiler: PipelineCompiler,
3206 #[cfg(not(target_arch = "wasm32"))]
3209 landing: Option<Arc<crate::pipeline_compiler::Landing>>,
3210 #[cfg(not(target_arch = "wasm32"))]
3213 built_at_frame_start: u64,
3214 pub(crate) drew_placeholder: bool,
3217 shape_pipelines: ShapePipelines,
3218 image_pipeline: [FixedPipeline; 8],
3220 image_pipeline_dst_out: [FixedPipeline; 8],
3221 glyph_atlas_pipeline: [FixedPipeline; 6],
3223 rrect_shadow_pipeline: [FixedPipeline; 2],
3225 image_shader: SharedShader,
3226 glyph_atlas_shader: SharedShader,
3227 rrect_shadow_shader: SharedShader,
3228 depth_targets: crate::idle_pool::IdlePool<((u32, u32), wgpu::TextureView)>,
3232 pub(crate) depth_gate: crate::depth_gate::DepthGate,
3233 pub(crate) surface_atlas_sizes: crate::frame::SurfaceAtlasSizes,
3235 uniform_bind_group_layout: wgpu::BindGroupLayout,
3236 image_bind_group_layout: wgpu::BindGroupLayout,
3237 image_nearest_sampler: wgpu::Sampler,
3238 image_linear_sampler: wgpu::Sampler,
3239 text_fonts: SoftwareTextFontSet,
3240 viewport_uniforms: ViewportUniforms,
3241 run_store: RunStore,
3242 image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
3243 image_texture_cache_bytes: usize,
3244 text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
3245 text_glyph_atlas: TextGlyphAtlas,
3246 text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
3247 text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, Rc<CachedGpuTextGlyphRun>>,
3248 text_glyph_run_arena: GlyphRunArena,
3249 pub(crate) copy_free: UploadMode,
3252 text_glyph_run_frame: u64,
3253 text_glyph_runs_drawn: usize,
3255 text_glyph_mask_cache: SoftwareGlyphRasterCache,
3256 text_line_index_cache: TextLineIndexCache,
3257 pub(crate) scratch_image_vertices: Vec<Vertex>,
3258 pub(crate) scratch_image_indices: Vec<u32>,
3259 pub(crate) scratch_glyph_instances: GlyphInstances,
3260 pub(crate) scratch_image_cmds: Vec<ImageDrawCmd>,
3261 pub(crate) scratch_image_clips: Vec<DeviceRoundedClip>,
3262 pub(crate) scratch_glyph_cmds: Vec<GlyphDrawCmd>,
3263 pub(crate) scratch_glyph_moved: Vec<GlyphDrawCmd>,
3264 pub(crate) scratch_shadow_instances: Vec<ShadowInstance>,
3265 pub(crate) scratch_arena_draws: Vec<RunDrawCall>,
3266 pub(crate) scratch_pipelines: PipelineDemand,
3267 pub(crate) scratch_held: crate::draw_pass::HeldDraws,
3268 scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
3269 scratch_text_glyph_entries: Vec<GlyphAtlasEntry>,
3270 run_glyph_scratch: RunGlyphScratch,
3271 frame_graph_executor: WgpuFrameGraphExecutor,
3272 pub(crate) stage_side_scratch: StageSideScratch,
3273 deferred_offscreen_releases: Vec<OffscreenTarget>,
3274 pub(crate) effect_renderer: EffectRenderer,
3275 pub(crate) layer_cache: LayerCache,
3276 pub(crate) ablation: Ablation,
3277 pub(crate) ablation_frames: u32,
3278 pub(crate) nesting_overflow_reported: bool,
3279 pub(crate) backdrop_gates: HashMap<NodeId, AdmissionGate>,
3280 pub(crate) fill_gates: HashMap<DrawCommandId, AdmissionGate>,
3281 pub(crate) effect_gates: HashMap<NodeId, AdmissionGate>,
3282 pub(crate) source_gates: HashMap<NodeId, AdmissionGate>,
3283 transparent_sources: HashMap<(u32, u32), Rc<OffscreenTarget>>,
3284 shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
3285 shadow_surface_cache_bytes: u64,
3286 pub(crate) frame_stats: gpu_stats::FrameStats,
3287 last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
3288 pending_frame_warmup_frames: u8,
3289 frame_count: u64,
3290 shader_warm_ups_queued: usize,
3292}
3293
3294pub fn frame_clear_color(transparent: bool) -> wgpu::Color {
3297 if transparent {
3298 wgpu::Color::TRANSPARENT
3299 } else {
3300 CLEAR_COLOR
3301 }
3302}
3303
3304fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
3305 let filter = match sampling {
3306 ImageSampling::Nearest => wgpu::FilterMode::Nearest,
3307 ImageSampling::Linear => wgpu::FilterMode::Linear,
3308 };
3309 wgpu::SamplerDescriptor {
3310 label: Some(match sampling {
3311 ImageSampling::Nearest => "Nearest Image Sampler",
3312 ImageSampling::Linear => "Linear Image Sampler",
3313 }),
3314 address_mode_u: wgpu::AddressMode::ClampToEdge,
3315 address_mode_v: wgpu::AddressMode::ClampToEdge,
3316 address_mode_w: wgpu::AddressMode::ClampToEdge,
3317 mag_filter: filter,
3318 min_filter: filter,
3319 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
3320 ..Default::default()
3321 }
3322}
3323
3324pub(crate) struct GpuRendererInit {
3327 pub(crate) device: Arc<wgpu::Device>,
3328 pub(crate) queue: Arc<wgpu::Queue>,
3329 pub(crate) surface_format: wgpu::TextureFormat,
3330 pub(crate) adapter_backend: wgpu::Backend,
3331 pub(crate) adapter_downlevel: wgpu::DownlevelFlags,
3332 pub(crate) text_fonts: SoftwareTextFontSet,
3333 pub(crate) renderer_epoch: u64,
3334 pub(crate) pipeline_compilation: PipelineCompilation,
3335}
3336
3337impl GpuRenderer {
3338 pub fn new(init: GpuRendererInit) -> Self {
3339 let GpuRendererInit {
3340 device,
3341 queue,
3342 surface_format,
3343 adapter_backend,
3344 adapter_downlevel,
3345 text_fonts,
3346 renderer_epoch,
3347 pipeline_compilation,
3348 } = init;
3349 let display_format = surface_format;
3350 let construction_started = Instant::now();
3351 let device_errors = Arc::new(DeviceErrorSentry::default());
3352 if survive_gpu_errors_enabled() {
3353 let sentry = Arc::clone(&device_errors);
3354 device.on_uncaptured_error(Arc::new(move |error| sentry.record(&error)));
3355 }
3356 let composition_format = crate::offscreen::composition_format(display_format);
3357 device.set_device_lost_callback(|reason, message| {
3358 log::error!("[gpu-device] device lost ({reason:?}): {message}");
3359 });
3360 let copy_free = UploadMode::copy_free(&device, adapter_backend);
3361 let mut run_store = RunStore::new(
3362 &device,
3363 RunBufferMode::for_device(&device, adapter_downlevel),
3364 copy_free,
3365 );
3366 let uniform_bind_group_layout =
3367 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3368 label: Some("Viewport Uniform Bind Group Layout"),
3369 entries: &[wgpu::BindGroupLayoutEntry {
3370 binding: 0,
3371 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
3372 ty: wgpu::BindingType::Buffer {
3373 ty: wgpu::BufferBindingType::Uniform,
3374 has_dynamic_offset: true,
3375 min_binding_size: wgpu::BufferSize::new(
3376 std::mem::size_of::<Uniforms>() as u64
3377 ),
3378 },
3379 count: None,
3380 }],
3381 });
3382 let image_bind_group_layout =
3383 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
3384 label: Some("Image Texture Bind Group Layout"),
3385 entries: &[
3386 wgpu::BindGroupLayoutEntry {
3387 binding: 0,
3388 visibility: wgpu::ShaderStages::FRAGMENT,
3389 ty: wgpu::BindingType::Texture {
3390 multisampled: false,
3391 view_dimension: wgpu::TextureViewDimension::D2,
3392 sample_type: wgpu::TextureSampleType::Float { filterable: true },
3393 },
3394 count: None,
3395 },
3396 wgpu::BindGroupLayoutEntry {
3397 binding: 1,
3398 visibility: wgpu::ShaderStages::FRAGMENT,
3399 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
3400 count: None,
3401 },
3402 ],
3403 });
3404 let image_nearest_sampler =
3405 device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
3406 let image_linear_sampler =
3407 device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
3408 let text_glyph_atlas = TextGlyphAtlas::new(
3409 &device,
3410 &image_bind_group_layout,
3411 GlyphSamplers {
3412 nearest: &image_nearest_sampler,
3413 linear: &image_linear_sampler,
3414 },
3415 TEXT_GLYPH_ATLAS_MIN_SIZE,
3416 );
3417 let viewport_uniforms = ViewportUniforms::default();
3418
3419 static GLASS_MATERIAL_FOLDS: DebugToggle =
3420 DebugToggle::new("CRANPOSE_GLASS_MATERIAL_FOLDS");
3421 if GLASS_MATERIAL_FOLDS.equals("1") {
3422 cranpose_ui_graphics::set_glass_material_folds(true);
3423 } else if GLASS_MATERIAL_FOLDS.equals("0") {
3424 cranpose_ui_graphics::set_glass_material_folds(false);
3425 }
3426 log::info!(
3427 "[gpu-init] liquid glass material folds {}",
3428 if cranpose_ui_graphics::glass_material_folds_enabled() {
3429 "on: a pipeline per material's feature set"
3430 } else {
3431 "off: one pipeline per blend mode"
3432 }
3433 );
3434
3435 #[cfg(not(target_arch = "wasm32"))]
3436 let crate::pipeline_disk_cache::Loaded {
3437 cache: pipeline_cache,
3438 records,
3439 updated,
3440 holds_recorded,
3441 } = crate::pipeline_disk_cache::load(&device);
3442 #[cfg(not(target_arch = "wasm32"))]
3443 let first_screen: Vec<u64> = records
3444 .shapes
3445 .iter()
3446 .filter(|shape| shape.first_screen)
3447 .map(|shape| shape.entry)
3448 .collect();
3449 #[cfg(target_arch = "wasm32")]
3450 let (pipeline_cache, first_screen): (Option<wgpu::PipelineCache>, Vec<u64>) =
3451 (None, Vec::new());
3452 let recorder = crate::pipeline_recorder::PipelineRecorder::default();
3453 #[cfg(not(target_arch = "wasm32"))]
3454 let pipeline_cache_watcher = crate::pipeline_disk_cache::spawn_persist_watcher(
3455 pipeline_cache.clone(),
3456 Arc::clone(&records),
3457 recorder.clone(),
3458 );
3459
3460 let effects_started = Instant::now();
3461 let pipeline_compiler = PipelineCompiler::for_compilation(pipeline_compilation);
3462 let depth_gate = crate::depth_gate::DepthGate::new(
3463 crate::draw_pass::lays_interiors_first(adapter_backend),
3464 pipeline_compiler.is_active(),
3465 );
3466 let effect_renderer = EffectRenderer::new(
3467 &device,
3468 pipeline_compiler.clone(),
3469 pipeline_cache.clone(),
3470 composition_format,
3471 adapter_backend,
3472 recorder.clone(),
3473 );
3474 let output_converter = OutputConverter::new(&device, adapter_backend, display_format);
3475 let screenshot_converter =
3476 OutputConverter::new(&device, adapter_backend, wgpu::TextureFormat::Rgba8Unorm);
3477 let effects_ms = instant_ms(effects_started, Instant::now());
3478 let mut frame_graph_executor = WgpuFrameGraphExecutor::new();
3479 frame_graph_executor.init_pass_timing(&device, &queue);
3480 #[cfg(target_arch = "wasm32")]
3481 frame_graph_executor.batch_queue_writes(adapter_backend == wgpu::Backend::BrowserWebGpu);
3482 let shape_shader = SharedShader::new(
3483 &device,
3484 adapter_backend,
3485 "Shape Shader",
3486 shape_shader_code(run_store.mode()),
3487 &[Some(&uniform_bind_group_layout), Some(run_store.layout())],
3488 );
3489 if run_store.mode().trig_fill {
3490 let fill = ArcTrigFill::new(
3491 &device,
3492 adapter_backend,
3493 pipeline_cache.clone(),
3494 shape_shader.clone(),
3495 recorder.clone(),
3496 );
3497 if first_screen.contains(&crate::arc_trig_fill::FIRST_SCREEN_KEY) {
3498 fill.warm(&pipeline_compiler);
3499 }
3500 run_store.attach_trig_fill(fill);
3501 }
3502 let shape_pipelines = ShapePipelines::new(
3503 ShapePipelineFactory {
3504 device: Arc::clone(&device),
3505 cache: pipeline_cache.clone(),
3506 format: composition_format,
3507 shader: shape_shader,
3508 mode: run_store.mode(),
3509 },
3510 adapter_backend,
3511 &pipeline_compiler,
3512 first_screen
3513 .into_iter()
3514 .filter(|&bits| bits != crate::arc_trig_fill::FIRST_SCREEN_KEY)
3515 .filter_map(ShapePipelineKey::from_bits),
3516 recorder.clone(),
3517 );
3518 let image_layouts = [
3519 Some(&uniform_bind_group_layout),
3520 Some(&image_bind_group_layout),
3521 ];
3522 let image_shader = SharedShader::new(
3523 &device,
3524 adapter_backend,
3525 "Image Shader",
3526 shaders::IMAGE,
3527 &image_layouts,
3528 );
3529 let glyph_atlas_shader = SharedShader::new(
3530 &device,
3531 adapter_backend,
3532 "Glyph Atlas Shader",
3533 shaders::GLYPH_ATLAS,
3534 &image_layouts,
3535 );
3536 let rrect_shadow_shader = SharedShader::new(
3537 &device,
3538 adapter_backend,
3539 "Round Rect Shadow Shader",
3540 shaders::RRECT_SHADOW,
3541 &[Some(&uniform_bind_group_layout)],
3542 );
3543
3544 let mut renderer = Self {
3545 device,
3546 queue,
3547 device_errors,
3548 renderer_epoch,
3549 composition_format,
3550 #[cfg(not(target_arch = "wasm32"))]
3551 display_format,
3552 composition_target: None,
3553 output_converter,
3554 screenshot_converter,
3555 adapter_backend,
3556 pipeline_cache,
3557 #[cfg(not(target_arch = "wasm32"))]
3558 _pipeline_cache_watcher: pipeline_cache_watcher,
3559 #[cfg(not(target_arch = "wasm32"))]
3560 recent_after_update: updated.then(|| Arc::clone(&records)),
3561 recorder,
3562 #[cfg(not(target_arch = "wasm32"))]
3563 landing: pipeline_compiler.landing(),
3564 pipeline_compiler,
3565 #[cfg(not(target_arch = "wasm32"))]
3566 built_at_frame_start: 0,
3567 drew_placeholder: false,
3568 shape_pipelines,
3569 image_pipeline: [
3570 FixedPipeline::new("image/src-over"),
3571 FixedPipeline::new("image/src-over/depth"),
3572 FixedPipeline::new("image/src-over/alpha-mask"),
3573 FixedPipeline::new("image/src-over/alpha-mask/depth"),
3574 FixedPipeline::new("image/src-over/rounded"),
3575 FixedPipeline::new("image/src-over/rounded/depth"),
3576 FixedPipeline::new("image/src-over/alpha-mask/rounded"),
3577 FixedPipeline::new("image/src-over/alpha-mask/rounded/depth"),
3578 ],
3579 image_pipeline_dst_out: [
3580 FixedPipeline::new("image/dst-out"),
3581 FixedPipeline::new("image/dst-out/depth"),
3582 FixedPipeline::new("image/dst-out/alpha-mask"),
3583 FixedPipeline::new("image/dst-out/alpha-mask/depth"),
3584 FixedPipeline::new("image/dst-out/rounded"),
3585 FixedPipeline::new("image/dst-out/rounded/depth"),
3586 FixedPipeline::new("image/dst-out/alpha-mask/rounded"),
3587 FixedPipeline::new("image/dst-out/alpha-mask/rounded/depth"),
3588 ],
3589 glyph_atlas_pipeline: [
3590 FixedPipeline::new("glyph/atlas"),
3591 FixedPipeline::new("glyph/atlas/depth"),
3592 FixedPipeline::new("glyph/atlas/aligned"),
3593 FixedPipeline::new("glyph/atlas/aligned/depth"),
3594 FixedPipeline::new("glyph/atlas/turned"),
3595 FixedPipeline::new("glyph/atlas/turned/depth"),
3596 ],
3597 rrect_shadow_pipeline: [
3598 FixedPipeline::new("shadow/rrect"),
3599 FixedPipeline::new("shadow/rrect/depth"),
3600 ],
3601 image_shader,
3602 glyph_atlas_shader,
3603 rrect_shadow_shader,
3604 depth_targets: crate::idle_pool::IdlePool::default(),
3605 depth_gate,
3606 surface_atlas_sizes: crate::frame::SurfaceAtlasSizes::for_backend(adapter_backend),
3607 uniform_bind_group_layout,
3608 image_bind_group_layout,
3609 image_nearest_sampler,
3610 image_linear_sampler,
3611 text_fonts,
3612 viewport_uniforms,
3613 run_store,
3614 image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
3615 MAX_TEXTURE_CACHE_ITEMS,
3616 ),
3617 image_texture_cache_bytes: 0,
3618 text_image_cache: BoundedLruCache::with_capacity_at_least_one(
3619 MAX_TEXT_IMAGE_CACHE_ITEMS,
3620 ),
3621 text_glyph_atlas,
3622 text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
3623 MIN_TEXT_GLYPH_RUN_CACHE_ITEMS,
3624 ),
3625 text_glyph_runs_drawn: 0,
3626 text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
3627 MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
3628 ),
3629 text_glyph_run_arena: GlyphRunArena::new(copy_free),
3630 copy_free,
3631 text_glyph_run_frame: 0,
3632 text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
3633 MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
3634 ),
3635 text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_CACHE_ITEMS),
3636 scratch_image_vertices: Vec::new(),
3637 scratch_image_indices: Vec::new(),
3638 scratch_glyph_instances: GlyphInstances::default(),
3639 scratch_image_cmds: Vec::new(),
3640 scratch_image_clips: Vec::new(),
3641 scratch_glyph_cmds: Vec::new(),
3642 scratch_glyph_moved: Vec::new(),
3643 scratch_shadow_instances: Vec::new(),
3644 scratch_arena_draws: Vec::new(),
3645 scratch_pipelines: PipelineDemand::default(),
3646 scratch_held: crate::draw_pass::HeldDraws::default(),
3647 scratch_text_glyph_run: Vec::new(),
3648 scratch_text_glyph_entries: Vec::new(),
3649 run_glyph_scratch: RunGlyphScratch::default(),
3650 frame_graph_executor,
3651 stage_side_scratch: StageSideScratch::default(),
3652 deferred_offscreen_releases: Vec::new(),
3653 effect_renderer,
3654 layer_cache: LayerCache::new(),
3655 ablation: Ablation::default(),
3656 ablation_frames: 0,
3657 nesting_overflow_reported: false,
3658 backdrop_gates: HashMap::default(),
3659 fill_gates: HashMap::default(),
3660 effect_gates: HashMap::default(),
3661 source_gates: HashMap::default(),
3662 transparent_sources: HashMap::default(),
3663 shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
3664 MAX_SHADOW_SURFACE_CACHE_ITEMS,
3665 ),
3666 shadow_surface_cache_bytes: 0,
3667 frame_stats: gpu_stats::FrameStats::default(),
3668 last_frame_stats: None,
3669 pending_frame_warmup_frames: 0,
3670 frame_count: 0,
3671 shader_warm_ups_queued: 0,
3672 };
3673 #[cfg(not(target_arch = "wasm32"))]
3674 renderer.warm_first_screen(&records, holds_recorded);
3675 renderer.warm_requested_shaders();
3676 log::info!(
3677 "[gpu-init] {:?} renderer ready in {:.1} ms (effects {:.1} ms)",
3678 adapter_backend,
3679 instant_ms(construction_started, Instant::now()),
3680 effects_ms,
3681 );
3682 renderer
3683 }
3684
3685 #[cfg(not(target_arch = "wasm32"))]
3692 fn warm_first_screen(
3693 &mut self,
3694 records: &crate::pipeline_records::PipelineRecords,
3695 holds_recorded: bool,
3696 ) {
3697 self.warm_fixed_pipelines(&|label| {
3698 records
3699 .fixed
3700 .iter()
3701 .any(|fixed| fixed.first_screen && fixed.entry == label)
3702 });
3703 if holds_recorded {
3704 self.effect_renderer.trust_cached(records);
3705 }
3706 self.effect_renderer.warm_first_screen_shaders(
3707 records
3708 .shaders
3709 .iter()
3710 .filter(|shader| shader.first_screen)
3711 .map(|shader| &shader.entry),
3712 );
3713 if !records.shapes.iter().any(|shape| shape.first_screen) {
3714 self.warm_every_screen();
3715 }
3716 }
3717
3718 #[cfg(not(target_arch = "wasm32"))]
3725 fn warm_every_screen(&mut self) {
3726 self.shape_pipelines.warm(ShapePipelineKey::SOLID_RECT);
3727 self.glyph_atlas_pipeline_resource(false, GlyphQuads::Aligned)
3728 .prefetch(
3729 CompileLane::WarmUp,
3730 &self.pipeline_compiler,
3731 self.adapter_backend,
3732 || self.glyph_atlas_pipeline_job(false, GlyphQuads::Aligned),
3733 );
3734 }
3735
3736 fn ensure_shape_pipeline(&mut self, key: ShapePipelineKey, vertices: u64) {
3737 self.shape_pipelines.ensure(key, vertices);
3738 }
3739
3740 fn warm_requested_shaders(&mut self) {
3743 let requested = cranpose_ui_graphics::shader_warm_ups_after(self.shader_warm_ups_queued);
3744 if requested.is_empty() {
3745 return;
3746 }
3747 self.shader_warm_ups_queued += requested.len();
3748 self.effect_renderer.warm_shaders(&requested);
3749 }
3750
3751 #[cfg(not(target_arch = "wasm32"))]
3755 fn warm_recent(&mut self, records: &crate::pipeline_records::PipelineRecords) {
3756 self.warm_fixed_pipelines(&|label| records.fixed.iter().any(|fixed| fixed.entry == label));
3757 let shapes = records
3758 .shapes
3759 .iter()
3760 .map(|shape| shape.entry)
3761 .filter(|&bits| bits != crate::arc_trig_fill::FIRST_SCREEN_KEY)
3762 .filter_map(ShapePipelineKey::from_bits);
3763 for key in shapes {
3764 self.shape_pipelines.warm(key);
3765 }
3766 self.effect_renderer
3767 .warm_recorded_shaders(records.shaders.iter().map(|shader| &shader.entry));
3768 }
3769
3770 #[cfg(not(target_arch = "wasm32"))]
3772 fn warm_fixed_pipelines(&self, wanted: &dyn Fn(&str) -> bool) {
3773 self.effect_renderer.warm_fixed(&self.device, wanted);
3774 let warm =
3775 |pipeline: &FixedPipeline,
3776 job: &dyn Fn() -> Box<dyn FnOnce() -> wgpu::RenderPipeline + Send>| {
3777 if wanted(pipeline.label()) {
3778 pipeline.queue(
3779 self.effect_renderer.compiler(),
3780 crate::pipeline_compiler::CompileLane::WarmUp,
3781 self.adapter_backend,
3782 job(),
3783 );
3784 }
3785 };
3786 for depth in [false, true] {
3787 for (alpha_mask, rounded) in
3788 [(false, false), (true, false), (false, true), (true, true)]
3789 {
3790 let variant = ImageVariant {
3791 depth,
3792 alpha_mask,
3793 rounded,
3794 };
3795 for blend_mode in [BlendMode::SrcOver, BlendMode::DstOut] {
3796 warm(self.image_pipeline_resource(blend_mode, variant), &|| {
3797 Box::new(self.image_pipeline_job(blend_mode, variant))
3798 });
3799 }
3800 }
3801 for quads in [GlyphQuads::Plain, GlyphQuads::Aligned, GlyphQuads::Turned] {
3802 warm(self.glyph_atlas_pipeline_resource(depth, quads), &|| {
3803 Box::new(self.glyph_atlas_pipeline_job(depth, quads))
3804 });
3805 }
3806 warm(&self.rrect_shadow_pipeline[usize::from(depth)], &|| {
3807 Box::new(self.rrect_shadow_pipeline_job(depth))
3808 });
3809 }
3810 }
3811
3812 fn image_pipeline_resource(
3813 &self,
3814 blend_mode: BlendMode,
3815 variant: ImageVariant,
3816 ) -> &FixedPipeline {
3817 let pipelines = match blend_mode {
3818 BlendMode::DstOut => &self.image_pipeline_dst_out,
3819 _ => &self.image_pipeline,
3820 };
3821 &pipelines[usize::from(variant.depth)
3822 + 2 * usize::from(variant.alpha_mask)
3823 + 4 * usize::from(variant.rounded)]
3824 }
3825
3826 fn image_pipeline_job(
3827 &self,
3828 blend_mode: BlendMode,
3829 variant: ImageVariant,
3830 ) -> impl FnOnce() -> wgpu::RenderPipeline + CompilerSend + 'static {
3831 let device = Arc::clone(&self.device);
3832 let cache = self.pipeline_cache.clone();
3833 let format = self.composition_format;
3834 let shader = self.image_shader.clone();
3835 move || {
3836 create_image_pipeline(
3837 &device,
3838 cache.as_ref(),
3839 format,
3840 &shader,
3841 blend_mode,
3842 variant,
3843 )
3844 }
3845 }
3846
3847 pub(crate) fn prefetch_image_pipelines(
3851 &self,
3852 cmds: &[ImageDrawCmd],
3853 blend_mode: BlendMode,
3854 (depth, lane): (bool, CompileLane),
3855 awaited: &mut impl FnMut(Pending<wgpu::RenderPipeline>),
3856 ) {
3857 let mut last = None;
3858 for cmd in cmds {
3859 let variant = ImageVariant::of(cmd, depth);
3860 if last.replace(variant) == Some(variant) {
3861 continue;
3862 }
3863 if let Some(pending) = self.image_pipeline_resource(blend_mode, variant).prefetch(
3864 lane,
3865 &self.pipeline_compiler,
3866 self.adapter_backend,
3867 || self.image_pipeline_job(blend_mode, variant),
3868 ) {
3869 awaited(pending);
3870 }
3871 }
3872 }
3873
3874 pub(crate) fn prefetch_glyph_pipelines(
3878 &self,
3879 cmds: &[GlyphDrawCmd],
3880 (depth, lane): (bool, CompileLane),
3881 awaited: &mut impl FnMut(Pending<wgpu::RenderPipeline>),
3882 ) {
3883 let mut last = None;
3884 for cmd in cmds {
3885 let quads = cmd.quads();
3886 if last.replace(quads) == Some(quads) {
3887 continue;
3888 }
3889 if let Some(pending) = self.glyph_atlas_pipeline_resource(depth, quads).prefetch(
3890 lane,
3891 &self.pipeline_compiler,
3892 self.adapter_backend,
3893 || self.glyph_atlas_pipeline_job(depth, quads),
3894 ) {
3895 awaited(pending);
3896 }
3897 }
3898 }
3899
3900 pub(crate) fn prefetch_shadow_pipeline(
3904 &self,
3905 (depth, lane): (bool, CompileLane),
3906 awaited: &mut impl FnMut(Pending<wgpu::RenderPipeline>),
3907 ) {
3908 if let Some(pending) = self.rrect_shadow_pipeline[usize::from(depth)].prefetch(
3909 lane,
3910 &self.pipeline_compiler,
3911 self.adapter_backend,
3912 || self.rrect_shadow_pipeline_job(depth),
3913 ) {
3914 awaited(pending);
3915 }
3916 }
3917
3918 pub(crate) fn image_pipeline(
3919 &self,
3920 blend_mode: BlendMode,
3921 variant: ImageVariant,
3922 ) -> &wgpu::RenderPipeline {
3923 self.image_pipeline_resource(blend_mode, variant).for_draw(
3924 &self.recorder,
3925 self.adapter_backend,
3926 || self.image_pipeline_job(blend_mode, variant)(),
3927 )
3928 }
3929
3930 fn glyph_atlas_pipeline_job(
3931 &self,
3932 depth: bool,
3933 quads: GlyphQuads,
3934 ) -> impl FnOnce() -> wgpu::RenderPipeline + CompilerSend + 'static {
3935 let device = Arc::clone(&self.device);
3936 let cache = self.pipeline_cache.clone();
3937 let format = self.composition_format;
3938 let shader = self.glyph_atlas_shader.clone();
3939 move || {
3940 create_glyph_atlas_pipeline(&device, cache.as_ref(), format, &shader, (depth, quads))
3941 }
3942 }
3943
3944 fn glyph_atlas_pipeline(&self, depth: bool, quads: GlyphQuads) -> &wgpu::RenderPipeline {
3947 self.glyph_atlas_pipeline_resource(depth, quads).for_draw(
3948 &self.recorder,
3949 self.adapter_backend,
3950 || self.glyph_atlas_pipeline_job(depth, quads)(),
3951 )
3952 }
3953
3954 fn rrect_shadow_pipeline_job(
3955 &self,
3956 depth: bool,
3957 ) -> impl FnOnce() -> wgpu::RenderPipeline + CompilerSend + 'static {
3958 let device = Arc::clone(&self.device);
3959 let cache = self.pipeline_cache.clone();
3960 let format = self.composition_format;
3961 let shader = self.rrect_shadow_shader.clone();
3962 move || create_rrect_shadow_pipeline(&device, cache.as_ref(), format, &shader, depth)
3963 }
3964
3965 pub(crate) fn draw_shadow_instances(
3968 &self,
3969 pass: &mut TrackedPass<'_, '_>,
3970 buffer: &BufferUpload,
3971 uniform_slot: usize,
3972 instances: std::ops::Range<u32>,
3973 scissor: Option<(u32, u32, u32, u32)>,
3974 frame: PassFrame,
3975 ) -> Result<(), String> {
3976 let depth = frame.depth;
3977 let pipeline = self.rrect_shadow_pipeline[usize::from(depth)].for_draw(
3978 &self.recorder,
3979 self.adapter_backend,
3980 || self.rrect_shadow_pipeline_job(depth)(),
3981 );
3982 let (x, y, width, height) = frame.scissor(scissor);
3983 pass.set_scissor_rect(x, y, width, height);
3984 pass.set_pipeline(pipeline);
3985 self.viewport_uniforms.bind(pass, uniform_slot)?;
3986 pass.set_vertex_buffer(0, buffer.slice());
3987 pass.draw(0..SHADOW_QUAD_CORNERS, instances);
3988 self.frame_stats.add_draw_calls(1);
3989 Ok(())
3990 }
3991
3992 pub(crate) fn upload_shadow_instances<C: FrameCommandRecorder>(
3993 &self,
3994 recorder: &mut C,
3995 instances: &[ShadowInstance],
3996 ) -> BufferUpload {
3997 recorder.upload_buffer(
3998 UploadAllocatorSpec::vertex(
3999 "Shadow Instance Buffer",
4000 std::mem::size_of::<ShadowInstance>() as u64,
4001 ),
4002 &self.device,
4003 bytemuck::cast_slice(instances),
4004 )
4005 }
4006
4007 fn glyph_atlas_pipeline_resource(&self, depth: bool, quads: GlyphQuads) -> &FixedPipeline {
4008 let kind = match quads {
4009 GlyphQuads::Plain => 0,
4010 GlyphQuads::Aligned => 1,
4011 GlyphQuads::Turned => 2,
4012 };
4013 &self.glyph_atlas_pipeline[usize::from(depth) + 2 * kind]
4014 }
4015
4016 pub(crate) fn depth_target(&mut self, size: (u32, u32)) -> wgpu::TextureView {
4019 const KEPT_DEPTH_TARGETS: usize = 4;
4020 let view = match self.depth_targets.take(|(kept, _)| *kept == size) {
4021 Some((_, view)) => view,
4022 None => self.create_depth_target(size),
4023 };
4024 self.depth_targets
4025 .put((size, view.clone()), KEPT_DEPTH_TARGETS, u64::MAX, |_| 0);
4026 view
4027 }
4028
4029 fn create_depth_target(&self, size: (u32, u32)) -> wgpu::TextureView {
4030 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
4031 label: Some("Opaque interior depth"),
4032 size: wgpu::Extent3d {
4033 width: size.0,
4034 height: size.1,
4035 depth_or_array_layers: 1,
4036 },
4037 mip_level_count: 1,
4038 sample_count: 1,
4039 dimension: wgpu::TextureDimension::D2,
4040 format: DEPTH_FORMAT,
4041 usage: wgpu::TextureUsages::RENDER_ATTACHMENT
4042 | wgpu::TextureUsages::TRANSIENT_ATTACHMENT,
4043 view_formats: &[],
4044 });
4045 texture.create_view(&wgpu::TextureViewDescriptor::default())
4046 }
4047
4048 fn image_binding(
4051 &mut self,
4052 image: &ImageBitmap,
4053 sampling: ImageSampling,
4054 ) -> Result<ImageBinding, String> {
4055 if let Some(cached) = self.image_texture_cache.get(&image.id()) {
4056 return Ok(cached.binding(sampling));
4057 }
4058
4059 let size = wgpu::Extent3d {
4060 width: image.width(),
4061 height: image.height(),
4062 depth_or_array_layers: 1,
4063 };
4064
4065 let alpha_mask = matches!(
4066 image.format(),
4067 cranpose_ui_graphics::ImagePixelFormat::Alpha8 { .. }
4068 );
4069 let (format, bytes_per_pixel) = if alpha_mask {
4070 (wgpu::TextureFormat::R8Unorm, 1)
4071 } else {
4072 (wgpu::TextureFormat::Rgba8Unorm, 4)
4073 };
4074
4075 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
4076 label: Some("Image Texture"),
4077 size,
4078 mip_level_count: 1,
4079 sample_count: 1,
4080 dimension: wgpu::TextureDimension::D2,
4081 format,
4082 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4083 view_formats: &[],
4084 });
4085
4086 let upload_stats = self.frame_graph_executor.upload_texture(
4087 &self.queue,
4088 wgpu::TexelCopyTextureInfo {
4089 texture: &texture,
4090 mip_level: 0,
4091 origin: wgpu::Origin3d::ZERO,
4092 aspect: wgpu::TextureAspect::All,
4093 },
4094 image.pixels(),
4095 wgpu::TexelCopyBufferLayout {
4096 offset: 0,
4097 bytes_per_row: Some(bytes_per_pixel * image.width()),
4098 rows_per_image: Some(image.height()),
4099 },
4100 size,
4101 );
4102 self.frame_stats.record_command_stats(upload_stats);
4103
4104 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4105 let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
4106 let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
4107
4108 let bytes = image.pixels().len();
4109 let cached = CachedImageTexture {
4110 alpha_mask,
4111 _texture: texture,
4112 _view: view,
4113 nearest_bind_group,
4114 linear_bind_group,
4115 bytes,
4116 };
4117 let binding = cached.binding(sampling);
4118 if let Some(replaced) = self.image_texture_cache.put(image.id(), cached) {
4119 self.image_texture_cache_bytes = self
4120 .image_texture_cache_bytes
4121 .saturating_sub(replaced.bytes);
4122 }
4123 self.image_texture_cache_bytes += bytes;
4124 while self.image_texture_cache_bytes > MAX_IMAGE_TEXTURE_CACHE_BYTES
4125 && self.image_texture_cache.len() > 1
4126 {
4127 let Some((_, evicted)) = self.image_texture_cache.pop_lru() else {
4128 break;
4129 };
4130 self.image_texture_cache_bytes =
4131 self.image_texture_cache_bytes.saturating_sub(evicted.bytes);
4132 }
4133 Ok(binding)
4134 }
4135
4136 fn image_bind_group(
4137 &self,
4138 view: &wgpu::TextureView,
4139 sampler: &wgpu::Sampler,
4140 ) -> wgpu::BindGroup {
4141 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4142 label: Some("Image Texture Bind Group"),
4143 layout: &self.image_bind_group_layout,
4144 entries: &[
4145 wgpu::BindGroupEntry {
4146 binding: 0,
4147 resource: wgpu::BindingResource::TextureView(view),
4148 },
4149 wgpu::BindGroupEntry {
4150 binding: 1,
4151 resource: wgpu::BindingResource::Sampler(sampler),
4152 },
4153 ],
4154 })
4155 }
4156
4157 pub(crate) fn max_texture_dim(&self) -> u32 {
4158 self.effect_renderer.max_texture_dim()
4159 }
4160
4161 pub(crate) fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
4164 self.effect_renderer
4165 .acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
4166 }
4167
4168 fn frame_root(
4169 &mut self,
4170 output_mode: OutputMode,
4171 output_view: Option<&wgpu::TextureView>,
4172 output_texture: Option<&wgpu::Texture>,
4173 viewport: (u32, u32),
4174 reads_page: bool,
4175 ) -> FrameRoot {
4176 if let (OutputMode::Display, Some(view), Some(texture)) =
4177 (output_mode, output_view, output_texture)
4178 && surface_is_direct_root(texture, self.composition_format, viewport, reads_page)
4179 {
4180 return FrameRoot::Surface(Rc::new(OffscreenTarget::from_surface(
4181 texture.clone(),
4182 view.clone(),
4183 )));
4184 }
4185 FrameRoot::Composition(self.take_composition_target(viewport.0.max(1), viewport.1.max(1)))
4186 }
4187
4188 fn take_composition_target(&mut self, width: u32, height: u32) -> CompositionTarget {
4189 if let Some(target) = self.composition_target.take()
4190 && target.target.width == width
4191 && target.target.height == height
4192 {
4193 return target;
4194 }
4195 let target = Rc::new(OffscreenTarget::new(
4196 &self.device,
4197 self.composition_format,
4198 width,
4199 height,
4200 ));
4201 let output_bind_group = self.output_converter.bind_group(&self.device, &target.view);
4202 CompositionTarget {
4203 target,
4204 output_bind_group,
4205 }
4206 }
4207
4208 fn transient_offscreen_descriptor(
4209 &self,
4210 label: &'static str,
4211 width: u32,
4212 height: u32,
4213 ) -> FrameTextureDescriptor {
4214 let max_texture_dim = self.max_texture_dim();
4215 FrameTextureDescriptor::render_attachment(
4216 label,
4217 width.min(max_texture_dim),
4218 height.min(max_texture_dim),
4219 self.composition_format,
4220 )
4221 }
4222
4223 pub(crate) fn transparent_source<C: FrameCommandRecorder>(
4227 &mut self,
4228 recorder: &mut C,
4229 width: u32,
4230 height: u32,
4231 ) -> Rc<OffscreenTarget> {
4232 if let Some(source) = self.transparent_sources.get(&(width, height)) {
4233 return Rc::clone(source);
4234 }
4235 if self.transparent_sources.len() >= MAX_TRANSPARENT_SOURCES {
4236 for (_, source) in self.transparent_sources.drain() {
4237 if let Ok(target) = Rc::try_unwrap(source) {
4238 self.deferred_offscreen_releases.push(target);
4239 }
4240 }
4241 }
4242 let source = Rc::new(self.acquire_retained_surface(width, height));
4243 self.clear_target(
4244 recorder,
4245 &source.view,
4246 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
4247 );
4248 self.transparent_sources
4249 .insert((width, height), Rc::clone(&source));
4250 source
4251 }
4252
4253 fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
4254 self.deferred_offscreen_releases.push(target);
4255 }
4256
4257 fn flush_deferred_offscreen_releases(&mut self) {
4258 let layer_cache = &mut self.layer_cache;
4259 let mut retire = |gate: &mut AdmissionGate| {
4260 let seen = gate.end_frame();
4261 if !seen && let Some(dead) = gate.dead_entry() {
4262 layer_cache.remove(&dead);
4263 }
4264 seen
4265 };
4266 self.backdrop_gates.retain(|_, gate| retire(gate));
4267 self.fill_gates.retain(|_, gate| retire(gate));
4268 self.effect_gates.retain(|_, gate| retire(gate));
4269 self.source_gates.retain(|_, gate| retire(gate));
4270 for target in self.deferred_offscreen_releases.drain(..) {
4271 self.effect_renderer.release_offscreen(target);
4272 }
4273 self.layer_cache.end_frame();
4274 for (transient, target) in self.layer_cache.take_released() {
4275 match transient {
4276 Some(descriptor) => self
4277 .frame_graph_executor
4278 .return_cached_transient(descriptor, target),
4279 None => self.effect_renderer.release_offscreen(target),
4280 }
4281 }
4282 self.effect_renderer.end_offscreen_frame();
4283 self.frame_graph_executor.end_transient_frame();
4284 self.depth_targets.end_frame();
4285 self.surface_atlas_sizes.end_frame();
4286 }
4287
4288 fn insert_cached_shadow_surface(
4289 &mut self,
4290 key: ShadowSurfaceCacheKey,
4291 target: Rc<OffscreenTarget>,
4292 ) {
4293 let byte_size = offscreen_byte_size(target.width, target.height);
4294 while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
4295 let Some((_, evicted)) = self.shadow_surface_cache.pop_lru() else {
4296 break;
4297 };
4298 self.shadow_surface_cache_bytes = self
4299 .shadow_surface_cache_bytes
4300 .saturating_sub(evicted.byte_size);
4301 }
4302 let cached = CachedShadowSurface { target, byte_size };
4303 if let Some((_, replaced)) = self.shadow_surface_cache.push(key, cached) {
4304 self.shadow_surface_cache_bytes = self
4305 .shadow_surface_cache_bytes
4306 .saturating_sub(replaced.byte_size);
4307 }
4308 self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
4309 }
4310}
4311fn frame_stats_need_warmup_frame(snapshot: &gpu_stats::FrameStatsSnapshot) -> bool {
4312 snapshot.layer_cache_misses > 0
4313 || snapshot.shadow_shape_cache_misses > 0
4314 || snapshot.text_image_cache_misses > 0
4315 || snapshot.text_glyph_atlas_misses > 0
4316}
4317
4318fn update_frame_warmup_budget(pending_frames: &mut u8, snapshot: &gpu_stats::FrameStatsSnapshot) {
4319 if *pending_frames > 0 {
4320 *pending_frames = pending_frames.saturating_sub(1);
4321 } else if frame_stats_need_warmup_frame(snapshot) {
4322 *pending_frames = CACHE_MISS_WARMUP_FRAMES;
4323 }
4324}
4325
4326impl GpuRenderer {
4327 #[expect(clippy::too_many_arguments)]
4328 pub fn render(
4329 &mut self,
4330 texture: &wgpu::Texture,
4331 view: &wgpu::TextureView,
4332 width: u32,
4333 height: u32,
4334 packet: FramePacket,
4335 surface_epoch: u64,
4336 returns: &mut RenderReturns,
4337 ) -> Result<(), String> {
4338 self.render_internal(
4339 width,
4340 height,
4341 packet,
4342 surface_epoch,
4343 returns,
4344 OutputMode::Display,
4345 Some(view),
4346 Some(texture),
4347 )
4348 }
4349
4350 #[expect(clippy::too_many_arguments)]
4351 fn render_internal(
4352 &mut self,
4353 width: u32,
4354 height: u32,
4355 packet: FramePacket,
4356 surface_epoch: u64,
4357 returns: &mut RenderReturns,
4358 output_mode: OutputMode,
4359 output_view: Option<&wgpu::TextureView>,
4360 output_texture: Option<&wgpu::Texture>,
4361 ) -> Result<(), String> {
4362 let cancel_reason = if packet.renderer_epoch != self.renderer_epoch {
4363 Some(CancelReason::RendererEpoch)
4364 } else if packet.surface_epoch != surface_epoch {
4365 Some(CancelReason::SurfaceEpoch)
4366 } else if packet.viewport != (width, height) {
4367 Some(CancelReason::Viewport)
4368 } else {
4369 None
4370 };
4371 if let Some(reason) = cancel_reason {
4372 return Self::cancel_packet(packet, reason, returns);
4373 }
4374 if self.device_errors.take_poison() {
4375 return Self::cancel_packet(packet, CancelReason::DeviceError, returns);
4376 }
4377 returns.frame_id = packet.frame_id;
4378 let render_start = Instant::now();
4379 #[cfg(not(target_arch = "wasm32"))]
4380 let builds_before = thread_builds();
4381 self.warm_requested_shaders();
4382 self.shape_pipelines.begin_frame();
4383 self.recorder.begin_frame();
4384 self.begin_placeholder_frame();
4385 self.viewport_uniforms.begin_frame();
4386 self.run_store.begin_frame(gpu_stats_enabled());
4387 self.begin_text_glyph_run_frame();
4388
4389 let text_cache_len = packet.text_cache_len;
4390 let reads_page = packet.root.contains_backdrop()
4391 || packet
4392 .overlay
4393 .as_ref()
4394 .is_some_and(LayerScene::contains_backdrop);
4395 let frame_root = self.frame_root(
4396 output_mode,
4397 output_view,
4398 output_texture,
4399 (width, height),
4400 reads_page,
4401 );
4402 let root = frame_root.target();
4403 let screenshot_bind_group = output_view.and_then(|_| {
4404 matches!(output_mode, OutputMode::Screenshot).then(|| {
4405 self.screenshot_converter
4406 .bind_group(&self.device, &root.view)
4407 })
4408 });
4409 let output = frame_root.output(output_view, screenshot_bind_group.as_ref());
4410 let result = self.render_graph(root, packet, returns, output_mode, output);
4411 #[cfg(not(target_arch = "wasm32"))]
4412 self.effect_renderer
4413 .compiler()
4414 .spread_demand(thread_builds() == builds_before);
4415 if let FrameRoot::Composition(composition) = frame_root {
4416 self.composition_target = Some(composition);
4417 }
4418 let after_graph = Instant::now();
4419 self.flush_deferred_offscreen_releases();
4420 self.scratch_glyph_instances.end_frame();
4421
4422 self.frame_stats
4423 .layer_cache_size
4424 .set(self.layer_cache.len() as u32);
4425 self.frame_stats
4426 .layer_cache_bytes
4427 .set(self.layer_cache.bytes());
4428 self.frame_stats.offscreen_pool_size.set(
4429 self.effect_renderer
4430 .retained_offscreen_count()
4431 .saturating_add(self.frame_graph_executor.retained_texture_count())
4432 .saturating_add(usize::from(self.composition_target.is_some())) as u32,
4433 );
4434 self.frame_stats.offscreen_pool_bytes.set(
4435 (self.effect_renderer.retained_offscreen_bytes() as u64)
4436 .saturating_add(self.frame_graph_executor.retained_texture_bytes())
4437 .saturating_add(self.composition_target.as_ref().map_or(0, |target| {
4438 u64::from(target.target.width)
4439 .saturating_mul(u64::from(target.target.height))
4440 .saturating_mul(COMPOSITION_BYTES_PER_PIXEL)
4441 })),
4442 );
4443 self.frame_stats
4444 .text_pool_size
4445 .set(self.text_image_cache.len() as u32);
4446 self.frame_stats
4447 .image_cache_size
4448 .set(self.image_texture_cache.len() as u32);
4449 self.frame_stats.text_cache_size.set(text_cache_len as u32);
4450 self.effect_renderer
4451 .merge_and_reset_debug_counters(&self.frame_stats);
4452 self.frame_graph_executor.reset_upload_allocators();
4453 let snapshot = self.frame_stats.snapshot();
4454 if crate::frame_graph::frame_graph_pass_telemetry_threshold_ms().is_some() {
4455 log::warn!(
4456 "[wgpu-render-stage:frame-stats] layer_hit={} layer_miss={} miss_px={} \
4457 offscreen_acq={} offscreen_new={} isolated={} draws={}",
4458 snapshot.layer_cache_hits,
4459 snapshot.layer_cache_misses,
4460 snapshot.layer_cache_miss_pixels,
4461 snapshot.offscreen_acquires,
4462 snapshot.offscreen_news,
4463 snapshot.isolated_layer_renders,
4464 snapshot.draw_calls,
4465 );
4466 }
4467 self.last_frame_stats = Some(snapshot);
4468 PRESENTED_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4469 update_frame_warmup_budget(&mut self.pending_frame_warmup_frames, &snapshot);
4470 let gpu_stats_on = gpu_stats_enabled();
4471 self.frame_stats
4472 .maybe_print_snapshot(snapshot, &mut self.frame_count, gpu_stats_on);
4473 if gpu_stats_on && self.frame_count.is_multiple_of(60) {
4474 gpu_stats::print_gpu_memory_report(&self.device, self.frame_count);
4475 }
4476 self.frame_graph_executor
4477 .end_pass_timing_frame(&self.device, &self.queue);
4478 self.frame_stats.reset();
4479 let after_stats = Instant::now();
4480 if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
4481 log::warn!(
4482 "[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
4483 instant_ms(render_start, after_graph),
4484 instant_ms(after_graph, after_stats),
4485 );
4486 }
4487 if result.is_ok() {
4488 returns.outcome = PresentOutcome::Presented;
4489 }
4490 result
4491 }
4492
4493 pub(crate) fn cancel_packet(
4495 packet: FramePacket,
4496 reason: CancelReason,
4497 returns: &mut RenderReturns,
4498 ) -> Result<(), String> {
4499 returns.scene = Some(FrameSceneStorage {
4500 root: packet.root,
4501 overlay: packet.overlay,
4502 });
4503 returns.frame_id = packet.frame_id;
4504 returns.outcome = PresentOutcome::Cancelled(reason);
4505 Ok(())
4506 }
4507
4508 pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
4509 self.last_frame_stats
4510 }
4511
4512 pub fn gpu_pass_timings(&self) -> crate::pass_timing::GpuPassTimingReport {
4513 self.frame_graph_executor.pass_timing_report()
4514 }
4515
4516 pub fn needs_frame_warmup(&self) -> bool {
4517 self.pending_frame_warmup_frames > 0 || self.placeholder_awaits_frame()
4518 }
4519
4520 #[cfg(target_arch = "wasm32")]
4521 pub(crate) fn pipeline_compiler(&self) -> &PipelineCompiler {
4522 &self.pipeline_compiler
4523 }
4524
4525 fn placeholder_awaits_frame(&self) -> bool {
4529 #[cfg(not(target_arch = "wasm32"))]
4530 if let Some(landing) = &self.landing {
4531 return landing.landed() || (self.drew_placeholder && !landing.has_wake());
4532 }
4533 self.drew_placeholder
4534 }
4535
4536 fn begin_placeholder_frame(&mut self) {
4539 self.drew_placeholder = false;
4540 #[cfg(not(target_arch = "wasm32"))]
4541 if let Some(landing) = &self.landing {
4542 self.built_at_frame_start = landing.built();
4543 if landing.take_landed() {
4544 self.layer_cache.clear();
4545 }
4546 }
4547 }
4548
4549 pub(crate) fn note_placeholder(&mut self) {
4552 self.drew_placeholder = true;
4553 self.recorder.hold_first_screen();
4554 self.frame_stats
4555 .placeholder_draws
4556 .set(self.frame_stats.placeholder_draws.get() + 1);
4557 #[cfg(not(target_arch = "wasm32"))]
4558 if let Some(landing) = &self.landing {
4559 landing.await_since(self.built_at_frame_start);
4560 }
4561 }
4562
4563 #[cfg(not(target_arch = "wasm32"))]
4567 pub(crate) fn prepare_recorded(&mut self) -> bool {
4568 self.effect_renderer.warm_after_first_frame();
4569 if let Some(records) = self.recent_after_update.take() {
4570 self.warm_recent(&records);
4571 }
4572 self.compiling()
4573 }
4574
4575 #[cfg(not(target_arch = "wasm32"))]
4577 pub(crate) fn compiling(&self) -> bool {
4578 self.landing
4579 .as_ref()
4580 .is_some_and(|landing| landing.in_flight())
4581 }
4582
4583 pub(crate) fn awaits_pipelines(&self) -> bool {
4586 #[cfg(not(target_arch = "wasm32"))]
4587 if let Some(landing) = &self.landing {
4588 return self.drew_placeholder && landing.in_flight();
4589 }
4590 self.drew_placeholder
4591 }
4592
4593 #[cfg(not(target_arch = "wasm32"))]
4596 pub(crate) fn landing(&self) -> Option<Arc<crate::pipeline_compiler::Landing>> {
4597 self.landing.clone()
4598 }
4599
4600 #[cfg(not(target_arch = "wasm32"))]
4603 pub(crate) fn wake_on_landing(&self, wake: Box<dyn Fn() + Send + Sync>) {
4604 if let Some(landing) = &self.landing {
4605 landing.wake_with(wake);
4606 }
4607 }
4608
4609 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
4610 DebugCpuAllocationStats {
4611 scene_graph_node_count: 0,
4612 scene_graph_heap_bytes: 0,
4613 scene_hits_len: 0,
4614 scene_hits_cap: 0,
4615 scene_node_index_len: 0,
4616 scene_node_index_cap: 0,
4617 text_renderer_pool_len: self.text_image_cache.len(),
4618 text_renderer_pool_cap: self.text_image_cache.cap().get(),
4619 image_texture_cache_len: self.image_texture_cache.len(),
4620 image_texture_cache_cap: self.image_texture_cache.cap().get(),
4621 run_arena_staging_bytes: self.run_store.arena_staging_bytes(),
4622 run_store_bytes: self.run_store.stored_bytes(),
4623 run_store_runs: self.run_store.stored_count(),
4624 scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
4625 scratch_image_indices_cap: self.scratch_image_indices.capacity(),
4626 scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
4627 scratch_glyph_instances_cap: self.scratch_glyph_instances.plain.capacity()
4628 + self.scratch_glyph_instances.turned.capacity(),
4629 layer_cache_len: self.layer_cache.len(),
4630 layer_cache_bytes: self.layer_cache.bytes(),
4631 }
4632 }
4633 pub fn render_to_rgba_pixels(
4634 &mut self,
4635 width: u32,
4636 height: u32,
4637 packet: FramePacket,
4638 surface_epoch: u64,
4639 returns: &mut RenderReturns,
4640 ) -> Result<Vec<u8>, String> {
4641 if width == 0 || height == 0 {
4642 return Err("Screenshot size must be non-zero".to_string());
4643 }
4644 let errors_before = self.device_errors.error_count();
4645
4646 let output_texture = crate::offscreen::create_2d_texture(
4647 &self.device,
4648 wgpu::TextureFormat::Rgba8Unorm,
4649 width,
4650 height,
4651 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
4652 Some("Screenshot Output Texture"),
4653 );
4654 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
4655 self.render_internal(
4656 width,
4657 height,
4658 packet,
4659 surface_epoch,
4660 returns,
4661 OutputMode::Screenshot,
4662 Some(&output_view),
4663 None,
4664 )?;
4665 if returns.outcome != PresentOutcome::Presented {
4666 return Err(format!(
4667 "Screenshot rendering did not complete: {:?}",
4668 returns.outcome
4669 ));
4670 }
4671 self.read_screenshot_pixels(&output_texture, errors_before)
4672 }
4673
4674 fn read_screenshot_pixels(
4675 &mut self,
4676 output_texture: &wgpu::Texture,
4677 errors_before: u64,
4678 ) -> Result<Vec<u8>, String> {
4679 let width = output_texture.width();
4680 let height = output_texture.height();
4681 let bytes_per_pixel = 4u32;
4682 let unpadded_bytes_per_row = width
4683 .checked_mul(bytes_per_pixel)
4684 .ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
4685 let padded_bytes_per_row =
4686 align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
4687 let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
4688
4689 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
4690 label: Some("Screenshot Readback Buffer"),
4691 size: output_buffer_size,
4692 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
4693 mapped_at_creation: false,
4694 });
4695
4696 let device = self.device.clone();
4697 let queue = self.queue.clone();
4698 let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
4699 let source = graph.import_surface("screenshot-copy-source");
4700 graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
4701 context.encoder.copy_texture_to_buffer(
4702 wgpu::TexelCopyTextureInfo {
4703 texture: output_texture,
4704 mip_level: 0,
4705 origin: wgpu::Origin3d::ZERO,
4706 aspect: wgpu::TextureAspect::All,
4707 },
4708 wgpu::TexelCopyBufferInfo {
4709 buffer: &output_buffer,
4710 layout: wgpu::TexelCopyBufferLayout {
4711 offset: 0,
4712 bytes_per_row: Some(padded_bytes_per_row),
4713 rows_per_image: Some(height),
4714 },
4715 },
4716 wgpu::Extent3d {
4717 width,
4718 height,
4719 depth_or_array_layers: 1,
4720 },
4721 );
4722 Ok(())
4723 });
4724 let mut executor = std::mem::take(&mut self.frame_graph_executor);
4725 let execution = executor.execute_recorded_graph(&device, &queue, graph);
4726 self.frame_graph_executor = executor;
4727 self.recall_frame_uploads();
4728 let execution = execution.map_err(|error| error.to_string())?;
4729 let submission_index = execution.submission;
4730 let copy_stats = execution.stats;
4731 self.last_frame_stats = self
4732 .last_frame_stats
4733 .map(|snapshot| snapshot.with_command_stats_added(copy_stats));
4734
4735 let buffer_slice = output_buffer.slice(..);
4736 let (tx, rx) = mpsc::channel();
4737 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
4738 let _ = tx.send(result);
4739 });
4740 self.device
4741 .poll(wgpu::PollType::Wait {
4742 submission_index: Some(submission_index),
4743 timeout: None,
4744 })
4745 .map_err(|error| format!("Screenshot GPU wait failed: {error}"))?;
4746
4747 match rx.recv_timeout(Duration::from_secs(3)) {
4748 Ok(Ok(())) => {}
4749 Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
4750 Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
4751 }
4752 if self.device_errors.error_count() != errors_before {
4753 output_buffer.unmap();
4754 return Err("GPU errors occurred while capturing the screenshot".to_string());
4755 }
4756
4757 let mapped = buffer_slice
4758 .get_mapped_range()
4759 .map_err(|err| format!("Screenshot readback could not be read: {err}"))?;
4760 let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
4761
4762 let src_row_len = padded_bytes_per_row as usize;
4763 let dst_row_len = unpadded_bytes_per_row as usize;
4764 for row in 0..height as usize {
4765 let src_offset = row * src_row_len;
4766 let dst_offset = row * dst_row_len;
4767 pixels[dst_offset..dst_offset + dst_row_len]
4768 .copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
4769 }
4770 drop(mapped);
4771 output_buffer.unmap();
4772
4773 self.convert_surface_pixels_to_rgba(&pixels)
4774 }
4775
4776 fn recall_frame_uploads(&mut self) {
4778 self.viewport_uniforms.uploads.recall();
4779 self.run_store.recall();
4780 }
4781
4782 fn render_graph(
4783 &mut self,
4784 root_target: &Rc<OffscreenTarget>,
4785 packet: FramePacket,
4786 returns: &mut RenderReturns,
4787 output_mode: OutputMode,
4788 output: Option<(&wgpu::TextureView, &wgpu::BindGroup)>,
4789 ) -> Result<(), String> {
4790 let device = self.device.clone();
4791 let queue = self.queue.clone();
4792 let graph_start = Instant::now();
4793 let FramePacket {
4794 root,
4795 overlay,
4796 root_scale,
4797 clear,
4798 ..
4799 } = packet;
4800 let page = Rc::clone(root_target);
4801
4802 #[cfg(not(target_arch = "wasm32"))]
4803 let (result, submitted) = {
4804 let mut executor = std::mem::take(&mut self.frame_graph_executor);
4805 let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
4806 let surface = frame_graph.import_surface("renderer-surface");
4807 frame_graph.add_fallible_recorded_command_pass(
4808 Some("Renderer Frame Pass"),
4809 &[],
4810 &[surface],
4811 |frame_encoder| {
4812 self.encode_frame(
4813 frame_encoder,
4814 &root,
4815 overlay.as_ref(),
4816 Rc::clone(&page),
4817 root_scale,
4818 clear,
4819 output_mode,
4820 output,
4821 )
4822 },
4823 );
4824 let after_build = Instant::now();
4825 let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
4826 let after_execute = Instant::now();
4827 self.frame_graph_executor = executor;
4828 self.recall_frame_uploads();
4829 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
4830 log::warn!(
4831 "[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
4832 instant_ms(graph_start, after_build),
4833 instant_ms(after_build, after_execute),
4834 );
4835 }
4836 match execution {
4837 Ok(execution) => {
4838 if execution.stats.pass_count > 0 {
4839 self.frame_stats.record_command_stats(execution.stats);
4840 }
4841 (Ok(()), true)
4842 }
4843 Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => (Ok(()), false),
4844 Err(error) => (Err(error.to_string()), false),
4845 }
4846 };
4847
4848 #[cfg(target_arch = "wasm32")]
4849 let (result, submitted) = {
4850 let mut executor = std::mem::take(&mut self.frame_graph_executor);
4851 let (result, execution) = {
4852 let mut frame_encoder =
4853 executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
4854 let initial_pass_count = frame_encoder.recorded_pass_count();
4855 let result = self.encode_frame(
4856 &mut frame_encoder,
4857 &root,
4858 overlay.as_ref(),
4859 Rc::clone(&page),
4860 root_scale,
4861 clear,
4862 output_mode,
4863 output,
4864 );
4865 let execution =
4866 if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
4867 Some(frame_encoder.finish())
4868 } else {
4869 None
4870 };
4871 (result, execution)
4872 };
4873 let after_execute = Instant::now();
4874 self.frame_graph_executor = executor;
4875 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
4876 log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
4877 }
4878 let submitted = execution.is_some();
4879 if let Some(execution) = execution {
4880 self.frame_stats.record_command_stats(execution.stats);
4881 }
4882 (result, submitted)
4883 };
4884 #[cfg(not(target_arch = "wasm32"))]
4885 if submitted {
4886 self.recorder.note_frame_drawn();
4887 self.effect_renderer.warm_after_first_frame();
4888 if let Some(records) = self.recent_after_update.take() {
4889 self.warm_recent(&records);
4890 }
4891 }
4892 if !submitted {
4893 self.run_store.invalidate_uploads();
4894 self.text_glyph_gpu_run_cache.clear();
4895 self.text_glyph_run_arena = GlyphRunArena::new(self.copy_free);
4896 }
4897 returns.scene = Some(FrameSceneStorage { root, overlay });
4898 result
4899 }
4900
4901 #[expect(clippy::too_many_arguments)]
4905 fn encode_frame<C: FrameCommandRecorder>(
4906 &mut self,
4907 recorder: &mut C,
4908 root: &LayerScene,
4909 overlay: Option<&LayerScene>,
4910 page: Rc<OffscreenTarget>,
4911 root_scale: f32,
4912 clear: wgpu::Color,
4913 output_mode: OutputMode,
4914 output: Option<(&wgpu::TextureView, &wgpu::BindGroup)>,
4915 ) -> Result<(), String> {
4916 FrameExecutor::new(self, recorder).render_frame(
4917 root,
4918 overlay,
4919 page,
4920 root_scale,
4921 wgpu::LoadOp::Clear(clear),
4922 )?;
4923 if let Some((output_view, bind_group)) = output {
4924 match output_mode {
4925 OutputMode::Display => &self.output_converter,
4926 OutputMode::Screenshot => &self.screenshot_converter,
4927 }
4928 .encode(
4929 &self.device,
4930 recorder,
4931 output_view,
4932 bind_group,
4933 self.adapter_backend,
4934 &self.recorder,
4935 );
4936 recorder.record_pass();
4937 }
4938 self.stage_frame_uploads(recorder);
4939 self.shape_pipelines.request_wanted();
4940 self.depth_gate.end_frame();
4941 self.pipeline_compiler.end_frame();
4942 self.viewport_uniforms.uploads.finish_frame();
4943 self.run_store.finish_frame();
4944 Ok(())
4945 }
4946
4947 pub(crate) fn stage_frame_uploads(&mut self, recorder: &mut impl FrameCommandRecorder) {
4948 let mut upload = self.viewport_uniforms.stage_pending(&self.device, recorder);
4949 upload += self.run_store.stage_pending(&self.device, recorder);
4950 upload += self
4951 .text_glyph_run_arena
4952 .stage_pending(&self.device, recorder);
4953 self.run_store.fill_arena_trig(&self.device, recorder);
4954 self.frame_stats.record_command_stats(upload);
4955 }
4956 pub(crate) fn claim_uniform_slot(&mut self, params: ViewportUniformParams) -> usize {
4958 let uniforms = Uniforms::of(params, PlacementData::zeroed(), RunTier::Arena);
4959 self.viewport_uniforms
4960 .claim(&self.device, &self.uniform_bind_group_layout, &uniforms)
4961 }
4962
4963 fn blurred_shadow_source<C: FrameCommandRecorder>(
4973 &mut self,
4974 recorder: &mut C,
4975 shadow: &ShadowDraw,
4976 source_device: DevicePixelBounds,
4977 pixel_radius: f32,
4978 root_scale: f32,
4979 transients: &mut Vec<(FrameTextureDescriptor, Rc<OffscreenTarget>)>,
4980 ) -> Option<(Rc<OffscreenTarget>, bool, SourceContent)> {
4981 let shape_only = shadow.texts.is_empty();
4982 let key = if shape_only {
4983 shape_shadow_surface_cache_key(shadow, source_device, pixel_radius, root_scale)
4984 } else {
4985 None
4986 };
4987 let content = key.map_or(SourceContent::Transient, |key| {
4988 SourceContent::retained(&key)
4989 });
4990 if let Some(entry) = key.and_then(|key| self.shadow_surface_cache.get(&key)) {
4991 return Some((Rc::clone(&entry.target), true, content));
4992 }
4993 if !self.effect_renderer.blur_draws_now(
4995 &self.device,
4996 TileMode::Decal,
4997 (source_device.width, source_device.height),
4998 pixel_radius,
4999 pixel_radius,
5000 ) {
5001 self.note_placeholder();
5002 return None;
5003 }
5004 if !shape_only {
5005 self.frame_stats.record_shadow_text_blur_fallback();
5006 }
5007 let source = self.render_shadow_source(
5008 recorder,
5009 shadow,
5010 source_device,
5011 pixel_radius,
5012 root_scale,
5013 key.is_some(),
5014 transients,
5015 )?;
5016 if let Some(key) = key {
5017 self.frame_stats
5018 .record_shadow_shape_cache_miss(source_device.width, source_device.height);
5019 self.frame_stats.maybe_print_shadow_shape_cache_miss(
5020 source_device.width,
5021 source_device.height,
5022 key.content_hash,
5023 pixel_radius,
5024 [source_device.x, source_device.y],
5025 shadow.shapes.as_ref().map_or(0, RunDraw::record_count) as usize,
5026 shadow.clip,
5027 );
5028 self.insert_cached_shadow_surface(key, Rc::clone(&source));
5029 }
5030 Some((source, false, content))
5031 }
5032
5033 #[expect(clippy::too_many_arguments)]
5034 pub(crate) fn resolve_blurred_shadow<C: FrameCommandRecorder>(
5035 &mut self,
5036 recorder: &mut C,
5037 shadow: &ShadowDraw,
5038 z: usize,
5039 root_scale: f32,
5040 target_rect: DeviceRect4,
5041 transients: &mut Vec<(FrameTextureDescriptor, Rc<OffscreenTarget>)>,
5042 resolved: &mut Vec<ResolvedComposite>,
5043 ) {
5044 if !shadow.requires_surface()
5045 || skip_shadow_draws()
5046 || !root_scale.is_finite()
5047 || root_scale <= 0.0
5048 {
5049 return;
5050 }
5051 let Some(bounds) = shadow_draw_bounds(shadow) else {
5052 return;
5053 };
5054 let margin = blur_reach(shadow.blur_radius, root_scale);
5055 let source_bounds = expand_rect(bounds, margin, margin);
5056 let mut visible = source_bounds;
5057 if let Some(clip) = shadow.clip {
5058 let Some(clipped) = visible.intersect(expand_rect(clip, margin, margin)) else {
5059 return;
5060 };
5061 visible = clipped;
5062 }
5063 let target_logical = Rect {
5064 x: target_rect.0 / root_scale,
5065 y: target_rect.1 / root_scale,
5066 width: target_rect.2 / root_scale,
5067 height: target_rect.3 / root_scale,
5068 };
5069 let Some(visible) = visible.intersect(expand_rect(target_logical, margin, margin)) else {
5070 return;
5071 };
5072 let max_texture_dim = self.max_texture_dim();
5073 let shape_only = shadow.texts.is_empty();
5074 let anchor = shadow
5075 .shapes
5076 .as_ref()
5077 .and_then(|run| run.placement.snap_anchor);
5078 let source_device = shape_only
5079 .then(|| {
5080 translation_stable_anchored_device_pixel_bounds(
5081 source_bounds,
5082 anchor,
5083 root_scale,
5084 max_texture_dim,
5085 )
5086 })
5087 .flatten()
5088 .or_else(|| device_pixel_bounds(visible, root_scale, max_texture_dim));
5089 let Some(source_device) = source_device else {
5090 return;
5091 };
5092 let pixel_radius = shadow.blur_radius * root_scale;
5093 let Some((source, hit, content)) = self.blurred_shadow_source(
5094 recorder,
5095 shadow,
5096 source_device,
5097 pixel_radius,
5098 root_scale,
5099 transients,
5100 ) else {
5101 return;
5102 };
5103 let dest = (
5104 source_device.x,
5105 source_device.y,
5106 source_device.width as f32,
5107 source_device.height as f32,
5108 );
5109 let mut coverage = intersect_device_rects(dest, target_rect);
5110 if let Some(clip) = shadow.clip {
5111 coverage = coverage.and_then(|coverage| {
5112 intersect_device_rects(coverage, anchored_rect_to_device(clip, anchor, root_scale))
5113 });
5114 }
5115 let Some(coverage) = coverage else {
5116 return;
5117 };
5118 if hit {
5119 self.frame_stats.record_shadow_shape_cache_hit(
5120 (coverage.2 as u64).saturating_mul(coverage.3 as u64),
5121 );
5122 }
5123 let rounded_mask = shadow_composite_mask(shadow, anchor, root_scale);
5124 let downscaled =
5125 (source.width, source.height) != (source_device.width, source_device.height);
5126 let (sample_mode, source_viewport) = if downscaled {
5127 (
5128 CompositeSampleMode::Linear,
5129 Some((0.0, 0.0, source.width as f32, source.height as f32)),
5130 )
5131 } else {
5132 (CompositeSampleMode::Nearest, None)
5133 };
5134 resolved.push(ResolvedComposite {
5135 z_index: z,
5136 source,
5137 content,
5138 dest,
5139 scissor: Some(coverage),
5140 kind: ResolvedCompositeKind::Blit {
5141 alpha: 1.0,
5142 blend_mode: BlendMode::SrcOver,
5143 rounded_mask,
5144 sample_mode,
5145 source_viewport,
5146 },
5147 });
5148 }
5149
5150 #[expect(clippy::too_many_arguments)]
5159 fn render_shadow_source<C: FrameCommandRecorder>(
5160 &mut self,
5161 recorder: &mut C,
5162 shadow: &ShadowDraw,
5163 bounds: DevicePixelBounds,
5164 pixel_radius: f32,
5165 root_scale: f32,
5166 retained: bool,
5167 transients: &mut Vec<(FrameTextureDescriptor, Rc<OffscreenTarget>)>,
5168 ) -> Option<Rc<OffscreenTarget>> {
5169 let (width, height) = (bounds.width, bounds.height);
5170 let device = self.device.clone();
5171 let (scratch_width, scratch_height) =
5172 crate::effect_renderer::blur_scratch_size(pixel_radius, pixel_radius, width, height);
5173 let full_size_result = shadow.post_blur_cutouts.is_some()
5174 || (scratch_width, scratch_height) == (width, height);
5175 let (result_width, result_height) = if full_size_result {
5176 (width, height)
5177 } else {
5178 (scratch_width, scratch_height)
5179 };
5180 let result = if retained {
5181 Rc::new(self.acquire_retained_surface(result_width, result_height))
5182 } else {
5183 self.shadow_transient(
5184 recorder,
5185 transients,
5186 "Shadow Result",
5187 result_width,
5188 result_height,
5189 )
5190 };
5191 let source = if full_size_result {
5192 Rc::clone(&result)
5193 } else {
5194 self.shadow_transient(recorder, transients, "Shadow Source", width, height)
5195 };
5196 let offset = [bounds.x, bounds.y];
5197 let target = PassTarget {
5198 view: &source.view,
5199 width,
5200 height,
5201 };
5202 let scene = shadow_scene(
5203 shadow.shapes.as_ref(),
5204 &shadow.texts,
5205 blur_reach(shadow.blur_radius, root_scale),
5206 );
5207 let segment = PassSegment {
5208 scene: &scene,
5209 ops: &scene.draw_ops,
5210 composites: &[],
5211 offset,
5212 scissor: None,
5213 first_run_window: None,
5214 transform: SegmentTransform::IDENTITY,
5215 scale: root_scale,
5216 };
5217 let drew = self.encode_pass(
5218 recorder,
5219 target,
5220 std::slice::from_ref(&segment),
5221 wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
5222 "Shadow Source Pass",
5223 );
5224 match drew {
5225 Ok(true) => {}
5226 Ok(false) => {
5227 drop(source);
5228 if retained && let Ok(target) = Rc::try_unwrap(result) {
5229 self.defer_offscreen_release(target);
5230 }
5231 return None;
5232 }
5233 Err(error) => {
5234 log::error!("shadow source pass failed: {error}");
5235 return None;
5236 }
5237 }
5238 if pixel_radius > 0.0 {
5239 let scratch_descriptor = self.transient_offscreen_descriptor(
5240 "Shadow Blur Scratch",
5241 scratch_width,
5242 scratch_height,
5243 );
5244 let scratch = recorder.acquire_transient_offscreen(&device, scratch_descriptor);
5245 let blurred = if full_size_result && (scratch_width, scratch_height) != (width, height)
5246 {
5247 Some(self.shadow_transient(
5248 recorder,
5249 transients,
5250 "Shadow Blur Result",
5251 scratch_width,
5252 scratch_height,
5253 ))
5254 } else {
5255 None
5256 };
5257 let blur_dest = match &blurred {
5258 Some(blurred) => (&blurred.view, (scratch_width, scratch_height)),
5259 None => (&result.view, (result_width, result_height)),
5260 };
5261 let passes = self.effect_renderer.encode_blur_scissored_ping_pong_passes(
5262 recorder,
5263 &device,
5264 &source,
5265 &scratch,
5266 blur_dest,
5267 pixel_radius,
5268 pixel_radius,
5269 TileMode::Decal,
5270 None,
5271 );
5272 recorder.record_passes(passes);
5273 self.effect_renderer.record_blur_pass();
5274 recorder.release_transient_offscreen(scratch_descriptor, scratch);
5275 if let Some(blurred) = &blurred {
5276 self.effect_renderer
5277 .encode_upscale_pass(recorder, &device, blurred, &result.view);
5278 recorder.record_pass();
5279 }
5280 }
5281 if let Some(cutout_run) = &shadow.post_blur_cutouts {
5282 let cutouts = shadow_scene(Some(cutout_run), &[], 0.0);
5283 let segment = PassSegment {
5284 scene: &cutouts,
5285 ops: &cutouts.draw_ops,
5286 composites: &[],
5287 offset,
5288 scissor: None,
5289 first_run_window: None,
5290 transform: SegmentTransform::IDENTITY,
5291 scale: root_scale,
5292 };
5293 if let Err(error) = self.encode_pass(
5294 recorder,
5295 target,
5296 std::slice::from_ref(&segment),
5297 wgpu::LoadOp::Load,
5298 "Shadow Cutout Pass",
5299 ) {
5300 log::error!("shadow cutout pass failed: {error}");
5301 }
5302 }
5303 Some(result)
5304 }
5305
5306 fn shadow_transient<C: FrameCommandRecorder>(
5309 &self,
5310 recorder: &mut C,
5311 transients: &mut Vec<(FrameTextureDescriptor, Rc<OffscreenTarget>)>,
5312 label: &'static str,
5313 width: u32,
5314 height: u32,
5315 ) -> Rc<OffscreenTarget> {
5316 let descriptor = self.transient_offscreen_descriptor(label, width, height);
5317 let target = Rc::new(recorder.acquire_transient_offscreen(&self.device, descriptor));
5318 transients.push((descriptor, Rc::clone(&target)));
5319 target
5320 }
5321
5322 pub(crate) fn run_is_stored(&self, run: &RunDraw) -> bool {
5324 self.run_store.is_stored(run)
5325 }
5326
5327 fn run_pipeline_key(
5331 segment: &RecordSegment,
5332 placement: &crate::scene::Placement,
5333 tier: RunTier,
5334 ablation: ShapeAblation,
5335 turns: ShapeTurns,
5336 (depth, clip): (bool, SegmentClip),
5337 viewport: ViewportUniformParams,
5338 ) -> ShapePipelineKey {
5339 let blend_mode = supported_blend_mode(segment.blend);
5340 let laid = depth
5341 && blend_mode == BlendMode::SrcOver
5342 && !placement.paints()
5343 && clip != SegmentClip::Rounded
5344 && !ablation.material
5345 && !ablation.fill;
5346 ShapePipelineKey {
5347 blend_mode,
5348 tier,
5349 variant: ShapeVariant::of_segment(
5350 segment,
5351 clip,
5352 ablation,
5353 laid,
5354 (
5355 turns == ShapeTurns::None,
5356 turns == ShapeTurns::None
5357 && placement.snap_anchor.is_some()
5358 && viewport.offset.iter().all(|value| value.fract() == 0.0),
5359 ),
5360 ),
5361 turns,
5362 depth: if depth {
5363 ShapeDepth::Tested
5364 } else {
5365 ShapeDepth::Off
5366 },
5367 }
5368 }
5369
5370 pub(crate) fn warm_shapes_later(
5373 &mut self,
5374 keys: &PipelineDemand,
5375 awaited: &mut impl FnMut(Pending<wgpu::RenderPipeline>),
5376 ) {
5377 for key in keys.keys() {
5378 self.shape_pipelines.warm_later(key, awaited);
5379 }
5380 }
5381
5382 pub(crate) fn ensure_run_pipelines(&mut self, pipelines: &mut PipelineDemand) {
5387 self.shape_pipelines.prefetch(pipelines);
5388 for (key, vertices) in pipelines.drain() {
5389 self.ensure_shape_pipeline(key, vertices);
5390 if let Some(interior) = key.interior() {
5391 self.ensure_shape_pipeline(interior, vertices);
5392 }
5393 }
5394 }
5395
5396 #[expect(clippy::too_many_arguments)]
5400 pub(crate) fn prepare_store_run<C: FrameCommandRecorder>(
5401 &mut self,
5402 recorder: &mut C,
5403 run: &RunDraw,
5404 viewport: ViewportUniformParams,
5405 root_scale: f32,
5406 window: &std::ops::Range<u32>,
5407 (depth, mut depth_keys): (bool, Option<&mut PipelineDemand>),
5408 (draws, pipelines): (&mut Vec<RunDrawCall>, &mut PipelineDemand),
5409 ) -> StoreRunBatch {
5410 let placement = &run.placement;
5411 let ablation = self.ablation.shape;
5412 let turns = ShapeTurns::of(viewport.transform, false);
5413 let scissor = clip_scissor(placement, root_scale, viewport);
5414 let clip = SegmentClip::of(placement, scissor.is_some());
5415 let start = draws.len();
5416 self.run_store.stored_run_draws(
5417 &self.device,
5418 run,
5419 &mut |segment| {
5420 let key = |depth| {
5421 Self::run_pipeline_key(
5422 segment,
5423 placement,
5424 RunTier::Store,
5425 ablation,
5426 turns,
5427 (depth, clip),
5428 viewport,
5429 )
5430 };
5431 if let Some(keys) = depth_keys.as_deref_mut() {
5432 keys.add(key(true), 0);
5433 }
5434 key(depth)
5435 },
5436 draws,
5437 );
5438 window_draws(draws, start, window);
5439 let run_draws = &draws[start..];
5440 let upload_start = Instant::now();
5441 let (upload, fill, tables) = self.run_store.upload_stored(
5442 &self.device,
5443 recorder,
5444 run,
5445 root_scale,
5446 window,
5447 run_draws,
5448 );
5449 if let Some(total_ms) = should_log_wgpu_render_stage(upload_start, Instant::now()) {
5450 log::warn!(
5451 "[wgpu-render-stage:run-upload] total_ms={total_ms:.2} bytes={} records={}",
5452 upload.upload_bytes,
5453 run.tables().shapes.len()
5454 );
5455 }
5456 self.frame_stats.record_command_stats(upload);
5457 if let Some(fill) = fill {
5458 self.frame_stats.add_shape_fill(fill);
5459 }
5460 let uniforms = Uniforms::of(
5461 viewport,
5462 PlacementData::of(&run.placement, root_scale, viewport.transform),
5463 RunTier::Store,
5464 );
5465 let uniform_slot =
5466 self.viewport_uniforms
5467 .claim(&self.device, &self.uniform_bind_group_layout, &uniforms);
5468 for draw in run_draws {
5469 pipelines.add(
5470 draw.key,
5471 crate::run_store::draw_vertices(
5472 draw.records.end - draw.records.start,
5473 draw.band_class,
5474 ),
5475 );
5476 }
5477 StoreRunBatch {
5478 tables,
5479 uniform_slot,
5480 draws: start..draws.len(),
5481 clip: scissor,
5482 }
5483 }
5484
5485 pub(crate) fn open_arena(&mut self) -> usize {
5486 self.run_store.open_arena()
5487 }
5488
5489 pub(crate) fn open_arena_records(&self) -> u32 {
5491 self.run_store.open_arena_records()
5492 }
5493
5494 pub(crate) fn arena_accepts(&self, chunk: usize, run: &RunDraw) -> bool {
5495 self.run_store.arena_accepts(chunk, run)
5496 }
5497
5498 #[expect(clippy::too_many_arguments)]
5505 pub(crate) fn append_arena_run(
5506 &mut self,
5507 chunk: usize,
5508 run: &RunDraw,
5509 window: std::ops::Range<u32>,
5510 root_scale: f32,
5511 viewport: ViewportUniformParams,
5512 mixed_turns: bool,
5513 (depth, clip): (bool, SegmentClip),
5514 (pipelines, mut depth_keys): (&mut PipelineDemand, Option<&mut PipelineDemand>),
5515 ) -> u32 {
5516 let placement = &run.placement;
5517 let ablation = self.ablation.shape;
5518 let turn = viewport.transform;
5519 let turns = ShapeTurns::of(turn, mixed_turns);
5520 self.run_store.append_arena(
5521 chunk,
5522 run,
5523 window,
5524 (root_scale, turn),
5525 &mut |segment| {
5526 let key = |depth| {
5527 Self::run_pipeline_key(
5528 segment,
5529 placement,
5530 RunTier::Arena,
5531 ablation,
5532 turns,
5533 (depth, clip),
5534 viewport,
5535 )
5536 };
5537 if let Some(keys) = depth_keys.as_deref_mut() {
5538 keys.add(key(true), 0);
5539 }
5540 key(depth)
5541 },
5542 pipelines,
5543 )
5544 }
5545
5546 pub(crate) fn cut_arena(&mut self, chunk: usize, out: &mut Vec<RunDrawCall>) {
5549 self.run_store.cut_arena(&self.device, chunk, out);
5550 }
5551
5552 pub(crate) fn close_arena(&mut self, chunk: usize, out: &mut Vec<RunDrawCall>) {
5555 if let Some(fill) = self.run_store.close_arena(&self.device, chunk, out) {
5556 self.frame_stats.add_shape_fill(fill);
5557 }
5558 }
5559
5560 pub(crate) fn draw_run_calls(
5561 &self,
5562 pass: &mut TrackedPass<'_, '_>,
5563 tables: ArenaBinding<'_>,
5564 uniform_slot: usize,
5565 draws: &[RunDrawCall],
5566 scissor: (u32, u32, u32, u32),
5567 stage: RunStage,
5568 ) -> Result<(), String> {
5569 match stage {
5570 RunStage::Paint => {
5571 if draws.is_empty() {
5572 return Ok(());
5573 }
5574 self.frame_stats.bump_shapes();
5575 self.record_run_draws(
5576 pass,
5577 tables,
5578 uniform_slot,
5579 scissor,
5580 draws.iter().map(PlannedRunDraw::paint),
5581 )
5582 }
5583 RunStage::Interiors => {
5584 let planned = interior_run_draws(draws);
5585 if planned.is_empty() {
5586 return Ok(());
5587 }
5588 self.frame_stats
5589 .shape_interior_draws
5590 .set(self.frame_stats.shape_interior_draws.get() + planned.len() as u32);
5591 self.record_run_draws(pass, tables, uniform_slot, scissor, planned.into_iter())
5592 }
5593 }
5594 }
5595
5596 fn record_run_draws(
5598 &self,
5599 pass: &mut TrackedPass<'_, '_>,
5600 tables: ArenaBinding<'_>,
5601 uniform_slot: usize,
5602 scissor: (u32, u32, u32, u32),
5603 planned: impl ExactSizeIterator<Item = PlannedRunDraw>,
5604 ) -> Result<(), String> {
5605 self.frame_stats.add_draw_calls(planned.len() as u32);
5606 let (x, y, width, height) = scissor;
5607 pass.set_scissor_rect(x, y, width, height);
5608 self.viewport_uniforms.bind(pass, uniform_slot)?;
5609 pass.set_bind_group(1, tables.bind_group, &tables.offsets[2..]);
5610 for (slot, buffer) in tables.records.into_iter().enumerate() {
5611 pass.set_vertex_buffer(slot as u32, buffer.slice(u64::from(tables.offsets[slot])..));
5612 }
5613 let mut bound_class = None;
5614 for draw in planned {
5615 let key = draw.key;
5616 let (pipeline, fallback) = self
5617 .shape_pipelines
5618 .get(key)
5619 .ok_or_else(|| format!("shape pipeline {key:?} was not prepared"))?;
5620 if fallback {
5621 self.frame_stats
5622 .shape_pipeline_fallback_draws
5623 .set(self.frame_stats.shape_pipeline_fallback_draws.get() + 1);
5624 } else if !key.is_general() {
5625 self.frame_stats
5626 .shape_specialized_draws
5627 .set(self.frame_stats.shape_specialized_draws.get() + 1);
5628 }
5629 if bound_class != Some(draw.band_class) {
5630 pass.set_index_buffer(self.run_store.strip_index_buffer(draw.band_class).slice(..));
5631 bound_class = Some(draw.band_class);
5632 }
5633 pass.set_pipeline(pipeline);
5634 pass.draw_indexed(draw.indices, 0, draw.records);
5635 }
5636 Ok(())
5637 }
5638
5639 pub(crate) fn draw_store_run(
5641 &self,
5642 pass: &mut TrackedPass<'_, '_>,
5643 batch: &StoreRunBatch,
5644 draws: &[RunDrawCall],
5645 scissor: (u32, u32, u32, u32),
5646 stage: RunStage,
5647 ) -> Result<(), String> {
5648 self.draw_run_calls(
5649 pass,
5650 batch.tables.binding(),
5651 batch.uniform_slot,
5652 &draws[batch.draws.clone()],
5653 scissor,
5654 stage,
5655 )
5656 }
5657
5658 pub(crate) fn draw_arena(
5659 &self,
5660 pass: &mut TrackedPass<'_, '_>,
5661 chunk: usize,
5662 uniform_slot: usize,
5663 draws: &[RunDrawCall],
5664 scissor: (u32, u32, u32, u32),
5665 stage: RunStage,
5666 ) -> Result<(), String> {
5667 self.draw_run_calls(
5668 pass,
5669 self.run_store.arena_binding(chunk),
5670 uniform_slot,
5671 draws,
5672 scissor,
5673 stage,
5674 )
5675 }
5676 #[cfg(not(target_arch = "wasm32"))]
5677 pub(crate) fn surface_format(&self) -> wgpu::TextureFormat {
5678 self.display_format
5679 }
5680
5681 pub fn device_error_count(&self) -> u64 {
5682 self.device_errors.error_count()
5683 }
5684
5685 pub(crate) fn clear_target<C: FrameCommandRecorder>(
5688 &self,
5689 recorder: &mut C,
5690 view: &wgpu::TextureView,
5691 load_op: wgpu::LoadOp<wgpu::Color>,
5692 ) {
5693 self.empty_pass(recorder, "Clear Pass", view, load_op);
5694 }
5695
5696 pub(crate) fn empty_pass<C: FrameCommandRecorder>(
5697 &self,
5698 recorder: &mut C,
5699 label: &'static str,
5700 view: &wgpu::TextureView,
5701 load_op: wgpu::LoadOp<wgpu::Color>,
5702 ) {
5703 let pass = recorder.begin_color_pass(label, view, load_op);
5704 drop(pass);
5705 recorder.record_pass();
5706 }
5707 pub(crate) fn draw_image_cmds(
5708 &self,
5709 pass: &mut TrackedPass<'_, '_>,
5710 image_slot: &ImageSlot,
5711 uniform_slot: usize,
5712 cmds: &[ImageDrawCmd],
5713 (blend_mode, depth): (BlendMode, bool),
5714 bound: Option<(u32, u32, u32, u32)>,
5715 ) -> Result<(), String> {
5716 if cmds.is_empty() {
5717 return Ok(());
5718 }
5719 self.frame_stats.bump_images();
5720 self.frame_stats.add_draw_calls(cmds.len() as u32);
5721 let mut bound_pipeline = None;
5722 self.viewport_uniforms.bind(pass, uniform_slot)?;
5723 pass.set_index_buffer(image_slot.indices.slice());
5724 pass.set_vertex_buffer(0, image_slot.vertices.slice());
5725 let mut clips_bound = false;
5726 for cmd in cmds {
5727 let Some((x, y, width, height)) = bounded_scissor(cmd.scissor, bound) else {
5728 continue;
5729 };
5730 pass.set_scissor_rect(x, y, width, height);
5731 let variant = ImageVariant::of(cmd, depth);
5732 if bound_pipeline != Some(variant) {
5733 pass.set_pipeline(self.image_pipeline(blend_mode, variant));
5734 bound_pipeline = Some(variant);
5735 }
5736 pass.set_bind_group(1, &cmd.image.bind_group, &[]);
5737 let instance = match cmd.clip {
5738 Some(clip) => {
5739 if !clips_bound {
5740 let clips = image_slot
5741 .clips
5742 .as_ref()
5743 .ok_or_else(|| "rounded image without clip instances".to_string())?;
5744 pass.set_vertex_buffer(1, clips.slice());
5745 clips_bound = true;
5746 }
5747 clip
5748 }
5749 None => 0,
5750 };
5751 pass.draw_indexed(
5752 cmd.index_start..(cmd.index_start + 6),
5753 0,
5754 instance..instance + 1,
5755 );
5756 }
5757 Ok(())
5758 }
5759
5760 pub(crate) fn draw_glyph_cmds(
5761 &self,
5762 pass: &mut TrackedPass<'_, '_>,
5763 (glyph_slot, turned_slot): (Option<&BufferUpload>, Option<&BufferUpload>),
5764 uniform_slot: usize,
5765 cmds: &[GlyphDrawCmd],
5766 bound: Option<(u32, u32, u32, u32)>,
5767 frame: PassFrame,
5768 ) -> Result<(), String> {
5769 if cmds.is_empty() {
5770 return Ok(());
5771 }
5772 let whole_target = frame.scissor(bound);
5773 self.frame_stats.bump_text();
5774 let mut bound_atlas = None;
5775 let mut shared_bound: Option<bool> = None;
5778 let mut bound_pipeline: Option<GlyphQuads> = None;
5779 let mut bound_run_instances: Option<&wgpu::Buffer> = None;
5780 let mut draws = 0u32;
5781 for draw in GlyphDraws::new(cmds) {
5782 let scissor = match draw.scissor {
5783 Some(scissor) => bounded_scissor(scissor, bound),
5784 None => Some(whole_target),
5785 };
5786 let Some((x, y, width, height)) = scissor else {
5787 continue;
5788 };
5789 pass.set_scissor_rect(x, y, width, height);
5790 if !bound_atlas.is_some_and(|atlas| Rc::ptr_eq(atlas, draw.atlas)) {
5791 pass.set_bind_group(1, draw.atlas.as_ref(), &[]);
5792 bound_atlas = Some(draw.atlas);
5793 }
5794 draws += 1;
5795 match draw.step {
5796 GlyphDrawStep::Shared(instances, quads) => {
5797 if bound_pipeline != Some(quads) {
5798 pass.set_pipeline(self.glyph_atlas_pipeline(frame.depth, quads));
5799 bound_pipeline = Some(quads);
5800 }
5801 let turned = quads.turned();
5802 if shared_bound != Some(turned) {
5803 let slot =
5804 if turned { turned_slot } else { glyph_slot }.ok_or_else(|| {
5805 "shared glyph draw without glyph instances".to_string()
5806 })?;
5807 self.viewport_uniforms.bind(pass, uniform_slot)?;
5808 pass.set_vertex_buffer(0, slot.slice());
5809 shared_bound = Some(turned);
5810 bound_run_instances = None;
5811 }
5812 pass.draw(0..GLYPH_QUAD_CORNERS, instances);
5813 }
5814 GlyphDrawStep::Retained {
5815 run,
5816 uniform_slot: retained_slot,
5817 aligned,
5818 } => {
5819 let quads = GlyphQuads::plain(aligned);
5820 if bound_pipeline != Some(quads) {
5821 pass.set_pipeline(self.glyph_atlas_pipeline(frame.depth, quads));
5822 bound_pipeline = Some(quads);
5823 }
5824 shared_bound = None;
5825 self.viewport_uniforms.bind(pass, retained_slot)?;
5826 let instances = run.span.instance_buffer();
5827 if bound_run_instances != Some(instances) {
5828 pass.set_vertex_buffer(0, instances.slice(..));
5829 bound_run_instances = Some(instances);
5830 }
5831 pass.draw(0..GLYPH_QUAD_CORNERS, run.span.instances());
5832 }
5833 }
5834 }
5835 self.frame_stats.add_draw_calls(draws);
5836 Ok(())
5837 }
5838 pub(crate) fn append_image_draw_cmd(
5839 &mut self,
5840 image_draw: &ImageDraw,
5841 viewport: ViewportUniformParams,
5842 root_scale: f32,
5843 (image_vertices, image_indices): (&mut Vec<Vertex>, &mut Vec<u32>),
5844 image_clips: &mut Vec<DeviceRoundedClip>,
5845 image_cmds: &mut Vec<ImageDrawCmd>,
5846 ) -> Result<(), String> {
5847 let snap_delta = image_draw
5848 .snap_anchor
5849 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
5850 .unwrap_or_default();
5851 let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
5852 if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
5853 return Ok(());
5854 }
5855
5856 let (mut tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
5857 if tint[3] <= 0.0 {
5858 return Ok(());
5859 }
5860
5861 let filtered_image = cpu_filter
5862 .map(|filter| apply_filter_to_bitmap(&image_draw.image, filter))
5863 .transpose()?;
5864 let prepared_image = filtered_image.as_ref().unwrap_or(&image_draw.image);
5865 if let cranpose_ui_graphics::ImagePixelFormat::Alpha8 { color } = prepared_image.format() {
5866 for (channel, value) in tint[..3].iter_mut().zip(color) {
5867 *channel *= value as f32 / 255.0;
5868 }
5869 }
5870
5871 let geometry = resolve_image_geometry_with_delta(image_draw, root_scale, snap_delta);
5872 let Some(scissor) =
5873 scissor_rect_for_layer(geometry.rect, geometry.clip, root_scale, viewport)
5874 else {
5875 return Ok(());
5876 };
5877
5878 let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
5879 return Ok(());
5880 };
5881 let device_quad = geometry.device_quad(root_scale);
5882 let index_start =
5883 push_image_quad((image_vertices, image_indices), device_quad, tint, &uv_rect);
5884 let clip = DeviceRoundedClip::of(image_draw, root_scale).map(|clip| {
5885 image_clips.push(clip);
5886 image_clips.len() as u32 - 1
5887 });
5888 image_cmds.push(ImageDrawCmd {
5889 index_start,
5890 scissor,
5891 image: self.image_binding(prepared_image, geometry.sampling)?,
5892 clip,
5893 });
5894 Ok(())
5895 }
5896
5897 pub(crate) fn upload_image_slot<C: FrameCommandRecorder>(
5899 &self,
5900 recorder: &mut C,
5901 vertices: &[Vertex],
5902 indices: &[u32],
5903 clips: &[DeviceRoundedClip],
5904 ) -> ImageSlot {
5905 ImageSlot {
5906 vertices: recorder.upload_buffer(
5907 image_vertex_spec(),
5908 &self.device,
5909 bytemuck::cast_slice(vertices),
5910 ),
5911 indices: recorder.upload_buffer(
5912 image_index_spec(),
5913 &self.device,
5914 bytemuck::cast_slice(indices),
5915 ),
5916 clips: (!clips.is_empty()).then(|| {
5917 recorder.upload_buffer(
5918 UploadAllocatorSpec::vertex(
5919 "Image Clip Buffer",
5920 std::mem::size_of::<DeviceRoundedClip>() as u64,
5921 ),
5922 &self.device,
5923 bytemuck::cast_slice(clips),
5924 )
5925 }),
5926 }
5927 }
5928
5929 pub(crate) fn upload_glyph_instances<C: FrameCommandRecorder>(
5930 &self,
5931 recorder: &mut C,
5932 instances: &[GlyphInstance],
5933 ) -> BufferUpload {
5934 recorder.upload_buffer(
5935 glyph_instance_spec(),
5936 &self.device,
5937 bytemuck::cast_slice(instances),
5938 )
5939 }
5940
5941 pub(crate) fn upload_turned_glyphs<C: FrameCommandRecorder>(
5942 &self,
5943 recorder: &mut C,
5944 glyphs: &[TurnedGlyph],
5945 ) -> BufferUpload {
5946 recorder.upload_buffer(
5947 turned_glyph_spec(),
5948 &self.device,
5949 bytemuck::cast_slice(glyphs),
5950 )
5951 }
5952
5953 fn glyph_atlas_entry_for(
5954 &mut self,
5955 glyph: &SoftwareGlyphAtlasGlyph,
5956 ) -> Result<GlyphAtlasEntry, String> {
5957 if let Some(entry) = self.text_glyph_atlas.upload_glyph(
5958 glyph,
5959 &self.queue,
5960 &mut self.frame_graph_executor,
5961 &mut self.frame_stats,
5962 ) {
5963 return Ok(entry);
5964 }
5965
5966 self.text_glyph_atlas.reset(
5967 &self.device,
5968 &self.image_bind_group_layout,
5969 GlyphSamplers {
5970 nearest: &self.image_nearest_sampler,
5971 linear: &self.image_linear_sampler,
5972 },
5973 );
5974 Err("text glyph atlas filled and was reset".to_string())
5975 }
5976
5977 fn glyph_atlas_entry_for_cached(
5978 &mut self,
5979 glyph: &SoftwareGlyphAtlasPlacement,
5980 ) -> Option<GlyphAtlasEntry> {
5981 let entry = self
5982 .text_glyph_atlas
5983 .entry(&GlyphAtlasSlotKey::new(glyph.key, glyph.color))?;
5984 self.frame_stats.record_text_glyph_atlas_hits(1);
5985 Some(entry)
5986 }
5987
5988 fn glyph_atlas_entry_for_placement(
5989 &mut self,
5990 glyph: &SoftwareGlyphAtlasPlacement,
5991 ) -> Result<GlyphAtlasEntry, String> {
5992 if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
5993 return Ok(entry);
5994 }
5995
5996 let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
5997 return Err("text glyph placement has no retained raster mask".to_string());
5998 };
5999 self.glyph_atlas_entry_for(&upload_glyph)
6000 }
6001
6002 fn prepare_text_glyph_entries(
6006 &mut self,
6007 run_key: TextGlyphRunCacheKey,
6008 atlas_generation: u64,
6009 cached_glyph_run: Option<&RunGlyphs>,
6010 collected_run: &[SoftwareGlyphAtlasRunGlyph],
6011 entries: &mut Vec<GlyphAtlasEntry>,
6012 ) -> Result<Rc<[GlyphAtlasEntry]>, String> {
6013 entries.clear();
6014 let mut seen = RunAtlasEntries::new();
6015 if let Some(glyph_run) = cached_glyph_run {
6016 for glyph in glyph_run.iter() {
6017 entries.push(seen.entry(self, &glyph, |renderer| {
6018 renderer.glyph_atlas_entry_for_placement(&glyph)
6019 })?);
6020 }
6021 } else {
6022 for run_glyph in collected_run {
6023 entries.push(seen.entry(
6024 self,
6025 &run_glyph.placement(),
6026 |renderer| match run_glyph {
6027 SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
6028 renderer.glyph_atlas_entry_for_placement(placement)
6029 }
6030 SoftwareGlyphAtlasRunGlyph::New(glyph) => {
6031 renderer.glyph_atlas_entry_for(glyph)
6032 }
6033 },
6034 )?);
6035 }
6036 }
6037
6038 let entries: Rc<[GlyphAtlasEntry]> = Rc::from(entries.as_slice());
6039 if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
6040 cached.atlas_entries = Some(Rc::clone(&entries));
6041 cached.atlas_generation = atlas_generation;
6042 }
6043 Ok(entries)
6044 }
6045
6046 #[expect(clippy::too_many_arguments)]
6047 fn append_text_glyph_quad_run(
6048 &mut self,
6049 source_raster_rect: Rect,
6050 quads: GlyphRunQuads<'_>,
6051 (clip, cut): (Option<Rect>, Option<[f32; 4]>),
6052 viewport: ViewportUniformParams,
6053 root_scale: f32,
6054 glyph_instances: &mut GlyphInstances,
6055 record_cached_hits: bool,
6056 ) -> (GlyphQuads, GlyphExtent) {
6057 let start = glyph_instances.lens();
6058 let mut extent = GlyphExtent::EMPTY;
6059 let kind = if viewport.transform.is_identity() {
6060 let plain = &mut glyph_instances.plain;
6061 let mut aligned = true;
6062 for_each_visible_glyph(
6063 source_raster_rect,
6064 quads,
6065 (clip, cut),
6066 viewport,
6067 root_scale,
6068 |glyph| {
6069 aligned &= glyph_rect_aligned(glyph.rect, viewport.offset);
6070 extent.add(glyph.rect);
6071 plain.push(glyph);
6072 },
6073 );
6074 GlyphQuads::plain(aligned)
6075 } else {
6076 let turn = viewport.transform;
6077 let turned = &mut glyph_instances.turned;
6078 for_each_visible_glyph(
6079 source_raster_rect,
6080 quads,
6081 (clip, cut),
6082 viewport,
6083 root_scale,
6084 |glyph| turned.push(TurnedGlyph::new(glyph, turn)),
6085 );
6086 GlyphQuads::Turned
6087 };
6088 let end = glyph_instances.lens();
6089 let appended = (end.0 - start.0) + (end.1 - start.1);
6090 if record_cached_hits {
6091 self.frame_stats
6092 .record_text_glyph_atlas_hits(u32::try_from(appended).unwrap_or(u32::MAX));
6093 }
6094 (kind, extent)
6095 }
6096
6097 fn retained_glyph_viewport(
6102 viewport: ViewportUniformParams,
6103 source_raster_rect: Rect,
6104 ) -> ViewportUniformParams {
6105 if !viewport.transform.is_identity() {
6106 return ViewportUniformParams {
6107 origin: [source_raster_rect.x, source_raster_rect.y],
6108 ..viewport
6109 };
6110 }
6111 ViewportUniformParams {
6112 offset: [
6113 viewport.offset[0] - source_raster_rect.x,
6114 viewport.offset[1] - source_raster_rect.y,
6115 ],
6116 ..viewport
6117 }
6118 }
6119
6120 fn retained_text_glyph_run(
6121 &mut self,
6122 cache_key: TextGlyphRunCacheKey,
6123 ) -> Option<Rc<CachedGpuTextGlyphRun>> {
6124 let atlas_generation = self.text_glyph_atlas.generation();
6125 let frame = self.text_glyph_run_frame;
6126 self.text_glyph_gpu_run_cache
6127 .get(&cache_key)
6128 .filter(|cached| cached.atlas_generation == atlas_generation)
6129 .inspect(|cached| cached.last_frame.set(frame))
6130 .cloned()
6131 }
6132
6133 fn begin_text_glyph_run_frame(&mut self) {
6138 self.text_glyph_run_frame += 1;
6139 let frame = self.text_glyph_run_frame;
6140 let demand = std::mem::take(&mut self.text_glyph_runs_drawn).saturating_mul(2);
6141 self.text_glyph_run_cache.set_cap(
6142 std::num::NonZeroUsize::new(demand.max(MIN_TEXT_GLYPH_RUN_CACHE_ITEMS))
6143 .unwrap_or(std::num::NonZeroUsize::MIN),
6144 );
6145 evict_idle(
6146 &mut self.text_glyph_gpu_run_cache,
6147 frame,
6148 TEXT_GLYPH_GPU_RUN_IDLE_FRAMES,
6149 |run| run.last_frame.get(),
6150 );
6151 evict_idle(
6152 &mut self.text_glyph_run_cache,
6153 frame,
6154 TEXT_GLYPH_RUN_IDLE_FRAMES,
6155 |run| run.last_frame.get(),
6156 );
6157 self.text_glyph_run_arena.begin_frame();
6158 let evacuating = self.text_glyph_run_arena.evacuating();
6159 if !evacuating.is_empty() {
6160 let moving: Vec<TextGlyphRunCacheKey> = self
6161 .text_glyph_gpu_run_cache
6162 .iter()
6163 .filter(|(_, run)| evacuating.contains(&run.span.chunk()))
6164 .map(|(key, _)| *key)
6165 .collect();
6166 for key in &moving {
6167 self.text_glyph_gpu_run_cache.pop(key);
6168 }
6169 }
6170 }
6171
6172 fn emit_retained_text_glyph_run_if_ready(
6173 &mut self,
6174 cache_key: TextGlyphRunCacheKey,
6175 quads: GlyphRunQuads<'_>,
6176 viewport: ViewportUniformParams,
6177 source_raster_rect: Rect,
6178 scissor: (u32, u32, u32, u32),
6179 glyph_cmds: &mut Vec<GlyphDrawCmd>,
6180 ) -> bool {
6181 let Some(run) = self.retained_text_glyph_run(cache_key).or_else(|| {
6182 if self.ensure_retained_text_glyph_run(cache_key, quads) {
6183 self.retained_text_glyph_run(cache_key)
6184 } else {
6185 None
6186 }
6187 }) else {
6188 return false;
6189 };
6190 let uniform_slot =
6191 self.claim_uniform_slot(Self::retained_glyph_viewport(viewport, source_raster_rect));
6192 self.frame_stats
6193 .record_text_glyph_atlas_hits(u32::try_from(quads.len()).unwrap_or(u32::MAX));
6194 let aligned = viewport.transform.is_identity()
6196 && glyph_rect_aligned(
6197 [source_raster_rect.x, source_raster_rect.y, 0.0, 0.0],
6198 [viewport.offset[0], viewport.offset[1]],
6199 );
6200 glyph_cmds.push(GlyphDrawCmd::retained(
6201 (run, uniform_slot, aligned),
6202 scissor,
6203 self.text_glyph_atlas.bind_group(viewport.transform),
6204 ));
6205 true
6206 }
6207
6208 fn ensure_retained_text_glyph_run(
6209 &mut self,
6210 cache_key: TextGlyphRunCacheKey,
6211 quads: GlyphRunQuads<'_>,
6212 ) -> bool {
6213 let atlas_generation = self.text_glyph_atlas.generation();
6214 if self
6215 .text_glyph_gpu_run_cache
6216 .peek(&cache_key)
6217 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
6218 {
6219 return true;
6220 }
6221
6222 let origin = Rect {
6223 x: 0.0,
6224 y: 0.0,
6225 width: 0.0,
6226 height: 0.0,
6227 };
6228 let Some(span) = self.text_glyph_run_arena.insert(
6229 &self.device,
6230 quads
6231 .iter()
6232 .filter_map(|quad| cached_text_glyph_instance(origin, &quad)),
6233 ) else {
6234 return false;
6235 };
6236 self.text_glyph_gpu_run_cache.put(
6237 cache_key,
6238 Rc::new(CachedGpuTextGlyphRun {
6239 span,
6240 atlas_generation,
6241 last_frame: Cell::new(self.text_glyph_run_frame),
6242 }),
6243 );
6244 true
6245 }
6246 pub(crate) fn append_text_glyph_draws(
6252 &mut self,
6253 (text_draw, logical): (&TextDraw, (Rect, f32)),
6254 viewport: ViewportUniformParams,
6255 root_scale: f32,
6256 glyph_instances: &mut GlyphInstances,
6257 glyph_cmds: &mut Vec<GlyphDrawCmd>,
6258 ) -> bool {
6259 let (_, raster_rect, _, text_scale, static_text_motion) =
6260 text_raster_geometry_at(text_draw, logical, root_scale);
6261 if !static_text_motion {
6262 return false;
6263 }
6264 let run_key = Self::text_glyph_run_cache_key(text_draw, raster_rect, text_scale);
6265 let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
6266 let mut generated_entries = std::mem::take(&mut self.scratch_text_glyph_entries);
6267 let initial_instance_len = glyph_instances.lens();
6268 let initial_cmd_len = glyph_cmds.len();
6269 let drew = self
6270 .text_glyph_run(
6271 text_draw,
6272 raster_rect,
6273 text_scale,
6274 run_key,
6275 &mut collected_run,
6276 )
6277 .is_some_and(|run| {
6278 self.append_text_glyph_run(
6279 TextGlyphRunDraw {
6280 text_draw,
6281 raster_rect,
6282 run_key,
6283 run,
6284 },
6285 (viewport, root_scale),
6286 (&collected_run, &mut generated_entries),
6287 glyph_instances,
6288 glyph_cmds,
6289 )
6290 });
6291 self.scratch_text_glyph_run = collected_run;
6292 self.scratch_text_glyph_entries = generated_entries;
6293 if !drew {
6294 glyph_instances.truncate(initial_instance_len);
6295 glyph_cmds.truncate(initial_cmd_len);
6296 }
6297 drew
6298 }
6299
6300 fn text_glyph_run(
6304 &mut self,
6305 text_draw: &TextDraw,
6306 raster_rect: Rect,
6307 text_scale: f32,
6308 run_key: TextGlyphRunCacheKey,
6309 collected_run: &mut Vec<SoftwareGlyphAtlasRunGlyph>,
6310 ) -> Option<TextGlyphRunLookup> {
6311 let atlas_generation = self.text_glyph_atlas.generation();
6312 let frame = self.text_glyph_run_frame;
6313 self.text_glyph_runs_drawn += 1;
6314 if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
6315 cached.last_frame.set(frame);
6316 return Some(TextGlyphRunLookup {
6317 glyphs: cached.glyphs.clone(),
6318 bounds: cached.bounds,
6319 cached: true,
6320 entries: (cached.atlas_generation == atlas_generation)
6321 .then(|| cached.atlas_entries.as_ref().map(Rc::clone))
6322 .flatten(),
6323 });
6324 }
6325 collected_run.clear();
6326 if collect_solid_text_atlas_run(
6327 text_draw.text.string().as_ref(),
6328 raster_rect,
6329 &text_draw.text_style,
6330 text_draw.color,
6331 text_draw.font_size,
6332 text_scale,
6333 &self.text_fonts,
6334 &mut self.text_glyph_mask_cache,
6335 collected_run,
6336 )
6337 .is_none()
6338 {
6339 log_text_atlas_fallback(text_draw);
6340 return None;
6341 }
6342 let Some(glyphs) = RunGlyphs::of(
6343 collected_run
6344 .iter()
6345 .map(SoftwareGlyphAtlasRunGlyph::placement),
6346 &mut self.run_glyph_scratch,
6347 ) else {
6348 log_text_atlas_fallback(text_draw);
6349 return None;
6350 };
6351 let bounds = GlyphRunBounds::of(glyphs.iter());
6352 self.frame_stats.record_text_glyph_run_collected();
6353 let cache = &mut self.text_glyph_run_cache;
6355 if cache.len() == cache.cap().get()
6356 && cache
6357 .peek_lru()
6358 .is_some_and(|(_, run)| run.last_frame.get() == frame)
6359 {
6360 cache.set_cap(cache.cap().saturating_add(cache.len()));
6361 }
6362 self.text_glyph_run_cache.put(
6363 run_key,
6364 CachedTextGlyphRun {
6365 glyphs: glyphs.clone(),
6366 bounds,
6367 atlas_entries: None,
6368 atlas_generation: 0,
6369 last_frame: Cell::new(frame),
6370 },
6371 );
6372 Some(TextGlyphRunLookup {
6373 glyphs,
6374 bounds,
6375 cached: false,
6376 entries: None,
6377 })
6378 }
6379
6380 fn append_text_glyph_run(
6384 &mut self,
6385 draw: TextGlyphRunDraw<'_>,
6386 (viewport, root_scale): (ViewportUniformParams, f32),
6387 (collected_run, generated_entries): (
6388 &[SoftwareGlyphAtlasRunGlyph],
6389 &mut Vec<GlyphAtlasEntry>,
6390 ),
6391 glyph_instances: &mut GlyphInstances,
6392 glyph_cmds: &mut Vec<GlyphDrawCmd>,
6393 ) -> bool {
6394 let TextGlyphRunDraw {
6395 text_draw,
6396 raster_rect,
6397 run_key,
6398 run,
6399 } = draw;
6400 let draw_rect = Rect {
6401 x: raster_rect.x / root_scale,
6402 y: raster_rect.y / root_scale,
6403 width: raster_rect.width / root_scale,
6404 height: raster_rect.height / root_scale,
6405 };
6406 let Some(scissor) = scissor_rect_for_layer(draw_rect, text_draw.clip, root_scale, viewport)
6407 else {
6408 return true;
6409 };
6410 let texel = self.text_glyph_atlas.texel();
6411
6412 let clip_needs_cut = !viewport.transform.is_identity() && text_draw.clip.is_some();
6415 if let Some(entries) = run.entries.as_ref()
6416 && entries.len() >= RETAINED_TEXT_GLYPH_RUN_MIN_QUADS
6417 && !clip_needs_cut
6418 && self.emit_retained_text_glyph_run_if_ready(
6419 run_key,
6420 GlyphRunQuads {
6421 glyphs: &run.glyphs,
6422 entries,
6423 texel,
6424 bounds: run.bounds,
6425 },
6426 viewport,
6427 raster_rect,
6428 scissor,
6429 glyph_cmds,
6430 )
6431 {
6432 return true;
6433 }
6434
6435 let turned = !viewport.transform.is_identity();
6436 let instance_start = glyph_instances.len_of(turned);
6437 let (entries, entries_cached) = match run.entries {
6438 Some(entries) => (entries, true),
6439 None => {
6440 let Ok(entries) = self.prepare_text_glyph_entries(
6441 run_key,
6442 self.text_glyph_atlas.generation(),
6443 run.cached.then_some(&run.glyphs),
6444 collected_run,
6445 generated_entries,
6446 ) else {
6447 return false;
6448 };
6449 (entries, false)
6450 }
6451 };
6452 let quads = GlyphRunQuads {
6453 glyphs: &run.glyphs,
6454 entries: &entries,
6455 texel: self.text_glyph_atlas.texel(),
6456 bounds: run.bounds,
6457 };
6458 let cut = glyph_cut_edges(text_draw.clip, scissor, viewport, root_scale);
6459 let (kind, extent) = self.append_text_glyph_quad_run(
6460 raster_rect,
6461 quads,
6462 (text_draw.clip, cut),
6463 viewport,
6464 root_scale,
6465 glyph_instances,
6466 entries_cached,
6467 );
6468 let instance_end = glyph_instances.len_of(turned);
6469 if instance_end > instance_start {
6470 let (clip, bounds) = if turned {
6471 (None, scissor)
6472 } else {
6473 shared_glyph_clip(extent, scissor, viewport)
6474 };
6475 glyph_cmds.push(GlyphDrawCmd::shared(
6476 (instance_start..instance_end, kind),
6477 clip,
6478 bounds,
6479 self.text_glyph_atlas.bind_group(viewport.transform),
6480 ));
6481 }
6482 true
6483 }
6484
6485 #[expect(clippy::too_many_arguments)]
6486 fn append_image_bitmap_draw_cmd(
6487 &mut self,
6488 image: &ImageBitmap,
6489 rect: Rect,
6490 clip: Option<Rect>,
6491 sampling: ImageSampling,
6492 viewport: ViewportUniformParams,
6493 root_scale: f32,
6494 image_vertices: &mut Vec<Vertex>,
6495 image_indices: &mut Vec<u32>,
6496 image_cmds: &mut Vec<ImageDrawCmd>,
6497 ) -> Result<(), String> {
6498 if rect.width <= 0.0 || rect.height <= 0.0 {
6499 return Ok(());
6500 }
6501
6502 let (device_quad, scissor_rect) =
6503 if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
6504 let left_px = (rect.x * root_scale).round();
6505 let top_px = (rect.y * root_scale).round();
6506 let width_px = (rect.width * root_scale).round().max(1.0);
6507 let height_px = (rect.height * root_scale).round().max(1.0);
6508 let snapped_rect = Rect {
6509 x: left_px / root_scale,
6510 y: top_px / root_scale,
6511 width: width_px / root_scale,
6512 height: height_px / root_scale,
6513 };
6514 let right_px = left_px + width_px;
6515 let bottom_px = top_px + height_px;
6516 (
6517 [
6518 [left_px, top_px],
6519 [right_px, top_px],
6520 [left_px, bottom_px],
6521 [right_px, bottom_px],
6522 ],
6523 snapped_rect,
6524 )
6525 } else {
6526 (
6527 rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
6528 rect,
6529 )
6530 };
6531
6532 let Some(scissor) = scissor_rect_for_layer(scissor_rect, clip, root_scale, viewport) else {
6533 return Ok(());
6534 };
6535 let Some(uv_rect) = image_uv_rect(image, None) else {
6536 return Ok(());
6537 };
6538
6539 let index_start = push_image_quad(
6540 (image_vertices, image_indices),
6541 device_quad,
6542 [1.0, 1.0, 1.0, 1.0],
6543 &uv_rect,
6544 );
6545 image_cmds.push(ImageDrawCmd {
6546 index_start,
6547 scissor,
6548 image: self.image_binding(image, sampling)?,
6549 clip: None,
6550 });
6551 Ok(())
6552 }
6553
6554 pub(crate) fn append_text_image_draw_cmds(
6556 &mut self,
6557 text_draw: &TextDraw,
6558 viewport: ViewportUniformParams,
6559 root_scale: f32,
6560 image_vertices: &mut Vec<Vertex>,
6561 image_indices: &mut Vec<u32>,
6562 image_cmds: &mut Vec<ImageDrawCmd>,
6563 ) -> Result<(), String> {
6564 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
6565 self.text_raster_geometry(text_draw, root_scale)
6566 else {
6567 return Ok(());
6568 };
6569 if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
6570 return Ok(());
6571 }
6572
6573 let raster_source = self.text_image_raster_source(
6574 text_draw,
6575 logical_rect,
6576 raster_rect,
6577 clip,
6578 root_scale,
6579 static_text_motion,
6580 );
6581 let source_draw = raster_source.draw.as_ref();
6582 let source_raster_rect = raster_source.raster_rect;
6583 let source_origin = if source_draw.text.string().span_styles().is_empty() {
6584 Point::new(source_raster_rect.x, source_raster_rect.y)
6585 } else {
6586 Point::new(raster_rect.x, raster_rect.y)
6587 };
6588
6589 let mut cache_key = Self::text_image_cache_key(
6590 source_draw,
6591 source_raster_rect,
6592 text_scale,
6593 static_text_motion,
6594 );
6595 if !source_draw.text.string().span_styles().is_empty() {
6596 let mut state = default_hash::new();
6597 cache_key.0.hash(&mut state);
6598 (source_origin.x - source_raster_rect.x)
6599 .to_bits()
6600 .hash(&mut state);
6601 (source_origin.y - source_raster_rect.y)
6602 .to_bits()
6603 .hash(&mut state);
6604 cache_key = TextImageCacheKey(state.finish());
6605 }
6606 let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
6607 self.frame_stats
6608 .record_text_image_cache_hit(cached.image.width(), cached.image.height());
6609 cached.image.clone()
6610 } else {
6611 let Some(image) = self.rasterize_text_draw_to_image(
6612 source_draw,
6613 source_raster_rect,
6614 source_origin,
6615 text_scale,
6616 ) else {
6617 return Ok(());
6618 };
6619 self.frame_stats
6620 .record_text_image_cache_miss(image.width(), image.height());
6621 self.text_image_cache.put(
6622 cache_key,
6623 CachedTextImage {
6624 image: image.clone(),
6625 },
6626 );
6627 image
6628 };
6629
6630 let draw_origin = if static_text_motion {
6631 Point::new(
6632 source_raster_rect.x / root_scale,
6633 source_raster_rect.y / root_scale,
6634 )
6635 } else {
6636 Point::new(logical_rect.x, logical_rect.y)
6637 };
6638 let draw_rect = Rect {
6639 x: draw_origin.x,
6640 y: draw_origin.y,
6641 width: image.width() as f32 / root_scale,
6642 height: image.height() as f32 / root_scale,
6643 };
6644 self.append_image_bitmap_draw_cmd(
6645 &image,
6646 draw_rect,
6647 clip,
6648 sampling_under(ImageSampling::Nearest, viewport.transform),
6649 viewport,
6650 root_scale,
6651 image_vertices,
6652 image_indices,
6653 image_cmds,
6654 )
6655 }
6656
6657 fn text_image_raster_source<'a>(
6658 &mut self,
6659 text_draw: &'a TextDraw,
6660 logical_rect: Rect,
6661 raster_rect: Rect,
6662 clip: Option<Rect>,
6663 root_scale: f32,
6664 static_text_motion: bool,
6665 ) -> TextRasterSource<'a> {
6666 let Some(clip) = clip else {
6667 return TextRasterSource {
6668 draw: Cow::Borrowed(text_draw),
6669 raster_rect,
6670 };
6671 };
6672 if !static_text_motion || text_draw.text.string().text().find('\n').is_none() {
6673 return TextRasterSource {
6674 draw: Cow::Borrowed(text_draw),
6675 raster_rect,
6676 };
6677 }
6678
6679 if !text_draw.text.string().span_styles().is_empty() {
6680 let device_clip = Rect {
6681 x: (clip.x * root_scale).floor(),
6682 y: (clip.y * root_scale).floor(),
6683 width: ((clip.x + clip.width) * root_scale).ceil() - (clip.x * root_scale).floor(),
6684 height: ((clip.y + clip.height) * root_scale).ceil()
6685 - (clip.y * root_scale).floor(),
6686 };
6687 return TextRasterSource {
6688 draw: Cow::Borrowed(text_draw),
6689 raster_rect: raster_rect.intersect(device_clip).unwrap_or(raster_rect),
6690 };
6691 }
6692
6693 let line_starts = self
6694 .text_line_index_cache
6695 .line_starts(text_draw.text.string());
6696 clipped_text_raster_source_with_line_starts(
6697 text_draw,
6698 logical_rect,
6699 raster_rect,
6700 clip,
6701 root_scale,
6702 line_starts.as_ref(),
6703 )
6704 }
6705
6706 fn text_raster_geometry(
6707 &self,
6708 text_draw: &TextDraw,
6709 root_scale: f32,
6710 ) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
6711 text_raster_geometry_for_draw(text_draw, root_scale)
6712 }
6713
6714 fn text_image_cache_key(
6715 text_draw: &TextDraw,
6716 raster_rect: Rect,
6717 text_scale: f32,
6718 static_text_motion: bool,
6719 ) -> TextImageCacheKey {
6720 let mut state = default_hash::new();
6721 hash_text_draw_for_cache(text_draw, raster_rect, text_scale, &mut state);
6722 hash_text_raster_geometry_for_cache(raster_rect, static_text_motion, &mut state);
6723 TextImageCacheKey(state.finish())
6724 }
6725
6726 fn text_glyph_run_cache_key(
6731 text_draw: &TextDraw,
6732 raster_rect: Rect,
6733 text_scale: f32,
6734 ) -> TextGlyphRunCacheKey {
6735 let mut state = default_hash::new();
6736 hash_text_draw_for_cache(text_draw, raster_rect, text_scale, &mut state);
6737 (raster_rect.width <= 0.0 || raster_rect.height <= 0.0).hash(&mut state);
6738 TextGlyphRunCacheKey(state.finish())
6739 }
6740
6741 fn rasterize_text_draw_to_image(
6742 &mut self,
6743 text_draw: &TextDraw,
6744 raster_rect: Rect,
6745 source_origin: Point,
6746 text_scale: f32,
6747 ) -> Option<ImageBitmap> {
6748 rasterize_annotated_text_region(
6749 text_draw.text.string().as_ref(),
6750 raster_rect,
6751 if text_draw.text.string().span_styles().is_empty() {
6752 Point::new(raster_rect.x, raster_rect.y)
6753 } else {
6754 source_origin
6755 },
6756 &text_draw.text_style,
6757 text_draw.color,
6758 text_draw.font_size,
6759 text_scale,
6760 &self.text_fonts,
6761 Some(&mut self.text_glyph_mask_cache),
6762 )
6763 }
6764}
6765
6766struct TextRasterSource<'a> {
6767 draw: Cow<'a, TextDraw>,
6768 raster_rect: Rect,
6769}
6770
6771fn clipped_text_raster_source_with_line_starts<'a>(
6772 text_draw: &'a TextDraw,
6773 logical_rect: Rect,
6774 raster_rect: Rect,
6775 clip: Rect,
6776 root_scale: f32,
6777 line_starts: &[usize],
6778) -> TextRasterSource<'a> {
6779 if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
6780 return TextRasterSource {
6781 draw: Cow::Borrowed(text_draw),
6782 raster_rect,
6783 };
6784 }
6785
6786 let Some(visible_rect) = logical_rect.intersect(clip) else {
6787 return TextRasterSource {
6788 draw: Cow::Borrowed(text_draw),
6789 raster_rect,
6790 };
6791 };
6792
6793 let line_count = line_starts.len().max(1);
6794 let line_height = logical_rect.height / line_count as f32;
6795 if !line_height.is_finite() || line_height <= 0.0 {
6796 return TextRasterSource {
6797 draw: Cow::Borrowed(text_draw),
6798 raster_rect,
6799 };
6800 }
6801
6802 let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
6803 let visible_bottom =
6804 ((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
6805 let start_line = visible_top.saturating_sub(1).max(0) as usize;
6806 let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
6807 let end_line = end_line.min(line_count);
6808 if start_line == 0 && end_line >= line_count {
6809 return TextRasterSource {
6810 draw: Cow::Borrowed(text_draw),
6811 raster_rect,
6812 };
6813 }
6814
6815 let byte_start = line_starts[start_line];
6816 let byte_end = line_end_offset(text_draw.text.string().text(), line_starts, end_line - 1);
6817 if byte_start >= byte_end {
6818 return TextRasterSource {
6819 draw: Cow::Borrowed(text_draw),
6820 raster_rect,
6821 };
6822 }
6823
6824 let slice_y = logical_rect.y + start_line as f32 * line_height;
6825 let slice_height = (end_line - start_line) as f32 * line_height;
6826 let mut slice_raster_rect = Rect {
6827 x: logical_rect.x * root_scale,
6828 y: slice_y * root_scale,
6829 width: logical_rect.width * root_scale,
6830 height: slice_height * root_scale,
6831 };
6832 slice_raster_rect.x = slice_raster_rect.x.round();
6833 slice_raster_rect.y = slice_raster_rect.y.round();
6834 slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
6835 slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
6836
6837 let mut sliced_draw = text_draw.clone();
6838 sliced_draw.rect = Rect {
6839 x: logical_rect.x,
6840 y: slice_y,
6841 width: logical_rect.width,
6842 height: slice_height,
6843 };
6844 sliced_draw.text = DrawnText::of(
6845 Arc::new(text_draw.text.string().subsequence(byte_start..byte_end)),
6846 &text_draw.text_style,
6847 );
6848
6849 TextRasterSource {
6850 draw: Cow::Owned(sliced_draw),
6851 raster_rect: slice_raster_rect,
6852 }
6853}
6854
6855fn line_start_offsets(text: &str) -> Vec<usize> {
6856 let mut starts =
6857 Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
6858 starts.push(0);
6859 starts.extend(
6860 text.char_indices()
6861 .filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
6862 );
6863 starts
6864}
6865
6866fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
6867 line_starts.get(line + 1).copied().unwrap_or(text.len())
6868}
6869
6870fn align_to(value: u32, alignment: u32) -> u32 {
6871 debug_assert!(alignment > 0);
6872 value.div_ceil(alignment) * alignment
6873}
6874
6875impl GpuRenderer {
6876 fn convert_surface_pixels_to_rgba(&self, pixels: &[u8]) -> Result<Vec<u8>, String> {
6877 if !pixels.len().is_multiple_of(4) {
6878 return Err("Screenshot readback has an incomplete pixel".to_string());
6879 }
6880 Ok(pixels.to_vec())
6881 }
6882}
6883
6884pub(crate) fn scissor_rect_for_rect(
6888 rect: Rect,
6889 root_scale: f32,
6890 viewport: ViewportUniformParams,
6891) -> Option<(u32, u32, u32, u32)> {
6892 let width = viewport.width as f32;
6893 let height = viewport.height as f32;
6894 let mut left = canonicalize_device_coordinate(rect.x * root_scale);
6895 let mut top = canonicalize_device_coordinate(rect.y * root_scale);
6896 let mut right = canonicalize_device_coordinate((rect.x + rect.width) * root_scale);
6897 let mut bottom = canonicalize_device_coordinate((rect.y + rect.height) * root_scale);
6898 if !viewport.transform.is_identity() {
6899 let bounds = viewport.transform.target_bounds(Rect {
6900 x: left,
6901 y: top,
6902 width: right - left,
6903 height: bottom - top,
6904 });
6905 (left, top) = (bounds.x, bounds.y);
6906 (right, bottom) = (bounds.x + bounds.width, bounds.y + bounds.height);
6907 }
6908 let left = (left - viewport.offset[0]).clamp(0.0, width).floor();
6909 let top = (top - viewport.offset[1]).clamp(0.0, height).floor();
6910 let right = (right - viewport.offset[0]).clamp(0.0, width).ceil();
6911 let bottom = (bottom - viewport.offset[1]).clamp(0.0, height).ceil();
6912 if right <= left || bottom <= top {
6913 return None;
6914 }
6915 Some((
6916 left as u32,
6917 top as u32,
6918 (right - left) as u32,
6919 (bottom - top) as u32,
6920 ))
6921}
6922
6923fn scissor_rect_for_layer(
6924 rect: Rect,
6925 clip: Option<Rect>,
6926 root_scale: f32,
6927 viewport: ViewportUniformParams,
6928) -> Option<(u32, u32, u32, u32)> {
6929 let clipped_rect = match clip {
6930 Some(clip_rect) => rect.intersect(clip_rect)?,
6931 None => rect,
6932 };
6933 scissor_rect_for_rect(clipped_rect, root_scale, viewport)
6934}
6935fn tint_for_image(
6936 color_filter: Option<ColorFilter>,
6937 alpha: f32,
6938) -> ([f32; 4], Option<ColorFilter>) {
6939 let alpha = alpha.clamp(0.0, 1.0);
6940 match color_filter {
6941 Some(filter) if filter.supports_gpu_vertex_modulation() => {
6942 let Some(tint) = filter.gpu_vertex_tint() else {
6943 return ([1.0, 1.0, 1.0, alpha], Some(filter));
6944 };
6945 (
6946 [
6947 tint[0].clamp(0.0, 1.0),
6948 tint[1].clamp(0.0, 1.0),
6949 tint[2].clamp(0.0, 1.0),
6950 (tint[3] * alpha).clamp(0.0, 1.0),
6951 ],
6952 None,
6953 )
6954 }
6955 Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
6956 None => ([1.0, 1.0, 1.0, alpha], None),
6957 }
6958}
6959
6960#[repr(C)]
6963#[derive(Clone, Copy, Debug, Pod, Zeroable)]
6964pub(crate) struct DeviceRoundedClip {
6965 rect: [f32; 4],
6966 radius: f32,
6967}
6968
6969impl DeviceRoundedClip {
6970 const ATTRIBS: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![
6971 4 => Float32x4,
6972 5 => Float32,
6973 ];
6974
6975 fn desc() -> wgpu::VertexBufferLayout<'static> {
6976 wgpu::VertexBufferLayout {
6977 array_stride: std::mem::size_of::<Self>() as wgpu::BufferAddress,
6978 step_mode: wgpu::VertexStepMode::Instance,
6979 attributes: &Self::ATTRIBS,
6980 }
6981 }
6982
6983 fn of(image: &ImageDraw, root_scale: f32) -> Option<Self> {
6986 let rounding = image.rounding.filter(|_| image.clip.is_some())?;
6987 let device =
6988 crate::geometry::device_rect(rounding.rect, root_scale, image.snap_anchor.is_some());
6989 Some(Self {
6990 rect: [device.x, device.y, device.width, device.height],
6991 radius: rounding.radius * root_scale,
6992 })
6993 }
6994}
6995
6996fn push_image_quad(
6999 (vertices, indices): (&mut Vec<Vertex>, &mut Vec<u32>),
7000 device_quad: [[f32; 2]; 4],
7001 color: [f32; 4],
7002 uv_rect: &ImageUvRect,
7003) -> u32 {
7004 let base_vertex = vertices.len() as u32;
7005 let index_start = indices.len() as u32;
7006 indices.extend_from_slice(&[
7007 base_vertex,
7008 base_vertex + 1,
7009 base_vertex + 2,
7010 base_vertex + 2,
7011 base_vertex + 1,
7012 base_vertex + 3,
7013 ]);
7014 let corners = [
7015 [uv_rect.min[0], uv_rect.min[1]],
7016 [uv_rect.max[0], uv_rect.min[1]],
7017 [uv_rect.min[0], uv_rect.max[1]],
7018 [uv_rect.max[0], uv_rect.max[1]],
7019 ];
7020 vertices.extend(
7021 device_quad
7022 .into_iter()
7023 .zip(corners)
7024 .map(|(position, uv)| Vertex {
7025 position,
7026 color,
7027 uv,
7028 uv_bounds: uv_rect.sample_bounds,
7029 }),
7030 );
7031 index_start
7032}
7033
7034fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
7035 let Some(src) = src_rect else {
7036 return Some(ImageUvRect {
7037 min: [0.0, 0.0],
7038 max: [1.0, 1.0],
7039 sample_bounds: [0.0, 0.0, 1.0, 1.0],
7040 });
7041 };
7042
7043 let (u_min, u_max, u_bound_min, u_bound_max) =
7044 source_axis_uv(src.x, src.width, image.width() as f32)?;
7045 let (v_min, v_max, v_bound_min, v_bound_max) =
7046 source_axis_uv(src.y, src.height, image.height() as f32)?;
7047
7048 Some(ImageUvRect {
7049 min: [u_min, v_min],
7050 max: [u_max, v_max],
7051 sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
7052 })
7053}
7054
7055fn glyph_atlas_uv_rect(entry: GlyphAtlasEntry, texel: f32) -> ImageUvRect {
7058 let min = [entry.x as f32 * texel, entry.y as f32 * texel];
7059 let max = [
7060 (entry.x + entry.width) as f32 * texel,
7061 (entry.y + entry.height) as f32 * texel,
7062 ];
7063 let center_min = [
7064 (entry.x as f32 + 0.5) * texel,
7065 (entry.y as f32 + 0.5) * texel,
7066 ];
7067 let center_max = [
7068 (entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) * texel,
7069 (entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) * texel,
7070 ];
7071 ImageUvRect {
7072 min,
7073 max,
7074 sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
7075 }
7076}
7077
7078#[derive(Clone, Copy)]
7079pub(crate) struct ResolvedImageGeometry {
7080 pub(crate) rect: Rect,
7081 pub(crate) clip: Option<Rect>,
7082 pub(crate) sampling: ImageSampling,
7083 quad: [[f32; 2]; 4],
7084 snap_anchored: bool,
7085}
7086
7087pub(crate) fn resolve_image_geometry(image: &ImageDraw, root_scale: f32) -> ResolvedImageGeometry {
7088 let snap_delta = image
7089 .snap_anchor
7090 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
7091 .unwrap_or_default();
7092 resolve_image_geometry_with_delta(image, root_scale, snap_delta)
7093}
7094
7095fn resolve_image_geometry_with_delta(
7096 image: &ImageDraw,
7097 root_scale: f32,
7098 snap_delta: Point,
7099) -> ResolvedImageGeometry {
7100 let mut rect = image.rect.translate(snap_delta.x, snap_delta.y);
7101 let mut quad = translate_quad(image.quad, snap_delta);
7102 snap_nearest_image_to_device_pixels(image.sampling, &mut rect, &mut quad, root_scale);
7103 ResolvedImageGeometry {
7104 rect,
7105 clip: image.clip,
7106 sampling: image.sampling,
7107 quad,
7108 snap_anchored: image.snap_anchor.is_some(),
7109 }
7110}
7111
7112impl ResolvedImageGeometry {
7113 pub(crate) fn device_quad(self, root_scale: f32) -> [[f32; 2]; 4] {
7114 nearest_image_device_quad(self.sampling, self.quad, root_scale).unwrap_or_else(|| {
7115 if self.snap_anchored {
7116 canonicalized_scaled_quad(self.quad, root_scale)
7117 } else {
7118 scaled_quad(self.quad, root_scale)
7119 }
7120 })
7121 }
7122}
7123
7124fn snap_nearest_image_to_device_pixels(
7125 sampling: ImageSampling,
7126 rect: &mut Rect,
7127 quad: &mut [[f32; 2]; 4],
7128 root_scale: f32,
7129) {
7130 if sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
7131 return;
7132 }
7133
7134 let Some(quad_rect) = axis_aligned_quad_rect(*quad) else {
7135 return;
7136 };
7137
7138 let left_px = (quad_rect.x * root_scale).round();
7139 let top_px = (quad_rect.y * root_scale).round();
7140 let width_px = (quad_rect.width * root_scale).round().max(1.0);
7141 let height_px = (quad_rect.height * root_scale).round().max(1.0);
7142 let snapped = Rect {
7143 x: left_px / root_scale,
7144 y: top_px / root_scale,
7145 width: width_px / root_scale,
7146 height: height_px / root_scale,
7147 };
7148
7149 *rect = snapped;
7150 *quad = crate::rect_to_quad(snapped);
7151}
7152
7153fn nearest_image_device_quad(
7154 sampling: ImageSampling,
7155 quad: [[f32; 2]; 4],
7156 root_scale: f32,
7157) -> Option<[[f32; 2]; 4]> {
7158 if sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
7159 return None;
7160 }
7161
7162 let rect = axis_aligned_quad_rect(quad)?;
7163 let left_px = (rect.x * root_scale).round();
7164 let top_px = (rect.y * root_scale).round();
7165 let width_px = (rect.width * root_scale).round().max(1.0);
7166 let height_px = (rect.height * root_scale).round().max(1.0);
7167 let right_px = left_px + width_px;
7168 let bottom_px = top_px + height_px;
7169 Some([
7170 [left_px, top_px],
7171 [right_px, top_px],
7172 [left_px, bottom_px],
7173 [right_px, bottom_px],
7174 ])
7175}
7176
7177fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
7178 if !start.is_finite()
7179 || !extent.is_finite()
7180 || !image_extent.is_finite()
7181 || extent == 0.0
7182 || image_extent <= 0.0
7183 {
7184 return None;
7185 }
7186
7187 let end = start + extent;
7188 let edge_min = start.min(end).clamp(0.0, image_extent);
7189 let edge_max = start.max(end).clamp(0.0, image_extent);
7190 if edge_max <= edge_min {
7191 return None;
7192 }
7193
7194 let center_min = edge_min + 0.5;
7195 let center_max = edge_max - 0.5;
7196 let (bound_min, bound_max) = if center_min <= center_max {
7197 (center_min, center_max)
7198 } else {
7199 let center = (edge_min + edge_max) * 0.5;
7200 (center, center)
7201 };
7202
7203 Some((
7204 edge_min / image_extent,
7205 edge_max / image_extent,
7206 bound_min / image_extent,
7207 bound_max / image_extent,
7208 ))
7209}
7210
7211fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
7212 let pixel_count = image.width() as usize * image.height() as usize;
7213 let mut filtered = Vec::with_capacity(pixel_count * 4);
7214 for index in 0..pixel_count {
7215 let pixel = image.rgba8_pixel(index);
7216 let rgba = [
7217 pixel[0] as f32 / 255.0,
7218 pixel[1] as f32 / 255.0,
7219 pixel[2] as f32 / 255.0,
7220 pixel[3] as f32 / 255.0,
7221 ];
7222 let out = filter.apply_rgba(rgba);
7223 filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
7224 filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
7225 filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
7226 filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
7227 }
7228 ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
7229 .map_err(|error| format!("failed to build filtered bitmap: {error}"))
7230}
7231
7232fn sampling_under(sampling: ImageSampling, transform: SegmentTransform) -> ImageSampling {
7236 if transform.is_identity() {
7237 sampling
7238 } else {
7239 ImageSampling::Linear
7240 }
7241}
7242
7243pub(crate) fn shadow_composite_mask(
7247 shadow: &ShadowDraw,
7248 snap_anchor: Option<SnapAnchor>,
7249 root_scale: f32,
7250) -> Option<RoundedCompositeMask> {
7251 inner_shadow_composite_mask(shadow, root_scale).or_else(|| {
7252 shadow.rounded_clip.map(|clip| RoundedCompositeMask {
7253 rect: mask_rect(anchored_device_rect(clip.rect, snap_anchor, root_scale)),
7254 radii: clip.radii.map(|radius| radius * root_scale),
7255 })
7256 })
7257}
7258
7259fn shadow_scene(
7263 shapes: Option<&RunDraw>,
7264 texts: &[TextDraw],
7265 input_margin: f32,
7266) -> CompositorScene {
7267 let mut scene = CompositorScene::new();
7268 if let Some(run) = shapes {
7269 scene.push_run(run.clone());
7270 }
7271 for text in texts {
7272 let z_index = scene.next_z();
7273 scene.draw_ops.push(DrawOp {
7274 z_index,
7275 kind: DrawOpKind::Text(scene.texts.len()),
7276 });
7277 let mut source_text = text.clone();
7278 source_text.clip = source_text
7279 .clip
7280 .map(|clip| expand_rect(clip, input_margin, input_margin));
7281 scene.texts.push(source_text);
7282 scene.next_z += 1;
7283 }
7284 scene
7285}
7286
7287fn device_pixel_bounds(
7290 rect: Rect,
7291 root_scale: f32,
7292 max_texture_dim: u32,
7293) -> Option<DevicePixelBounds> {
7294 let x = (rect.x * root_scale).floor();
7295 let y = (rect.y * root_scale).floor();
7296 let right = ((rect.x + rect.width) * root_scale).ceil();
7297 let bottom = ((rect.y + rect.height) * root_scale).ceil();
7298 let width = (right - x).max(0.0) as u32;
7299 let height = (bottom - y).max(0.0) as u32;
7300 if width == 0 || height == 0 || width > max_texture_dim || height > max_texture_dim {
7301 return None;
7302 }
7303 Some(DevicePixelBounds {
7304 x,
7305 y,
7306 width,
7307 height,
7308 })
7309}
7310fn inner_shadow_composite_mask(
7311 shadow: &ShadowDraw,
7312 root_scale: f32,
7313) -> Option<RoundedCompositeMask> {
7314 let run = shadow.shapes.as_ref()?;
7315 if !run
7316 .tables()
7317 .shapes
7318 .iter()
7319 .any(|record| record.blend_mode() == BlendMode::DstOut)
7320 {
7321 return None;
7322 }
7323 let fill = run.tables().shapes.get(0)?;
7324 let rect = run.placement.translated_bounds(fill.stored_rect());
7325 if rect.width <= 0.0 || rect.height <= 0.0 {
7326 return None;
7327 }
7328 let resolved =
7329 cranpose_ui_graphics::RoundedCornerShape::with_radii(cranpose_ui_graphics::CornerRadii {
7330 top_left: fill.radii[0],
7331 top_right: fill.radii[1],
7332 bottom_right: fill.radii[2],
7333 bottom_left: fill.radii[3],
7334 })
7335 .resolve(rect.width, rect.height);
7336 let radii = [
7337 resolved.top_left * root_scale,
7338 resolved.top_right * root_scale,
7339 resolved.bottom_left * root_scale,
7340 resolved.bottom_right * root_scale,
7341 ];
7342
7343 Some(RoundedCompositeMask {
7344 rect: mask_rect(anchored_device_rect(
7345 rect,
7346 run.placement.snap_anchor,
7347 root_scale,
7348 )),
7349 radii,
7350 })
7351}
7352
7353#[derive(Clone, Debug, PartialEq, Eq)]
7356pub(crate) struct PlannedRunDraw {
7357 pub(crate) key: ShapePipelineKey,
7358 pub(crate) band_class: u8,
7359 pub(crate) indices: std::ops::Range<u32>,
7360 pub(crate) records: std::ops::Range<u32>,
7361}
7362
7363impl PlannedRunDraw {
7364 fn paint(draw: &RunDrawCall) -> Self {
7366 Self {
7367 key: draw.key,
7368 band_class: draw.band_class,
7369 indices: draw.indices(),
7370 records: draw.records.clone(),
7371 }
7372 }
7373}
7374
7375pub(crate) fn interior_run_draws(draws: &[RunDrawCall]) -> SmallVec<[PlannedRunDraw; 4]> {
7383 struct Stretch {
7384 key: ShapePipelineKey,
7385 start: u32,
7386 span: Option<PlannedRunDraw>,
7387 }
7388 let mut planned: SmallVec<[PlannedRunDraw; 4]> = SmallVec::new();
7389 let mut stretch: Option<Stretch> = None;
7390 for draw in draws.iter().rev() {
7391 let Some(key) = draw.key.interior() else {
7392 planned.extend(stretch.take().and_then(|open| open.span));
7393 continue;
7394 };
7395 if stretch
7396 .as_ref()
7397 .is_some_and(|open| open.key != key || open.start != draw.records.end)
7398 {
7399 planned.extend(stretch.take().and_then(|open| open.span));
7400 }
7401 let open = stretch.get_or_insert(Stretch {
7402 key,
7403 start: draw.records.end,
7404 span: None,
7405 });
7406 open.start = draw.records.start;
7407 if !draw.occluders {
7408 continue;
7409 }
7410 match &mut open.span {
7411 Some(span) => span.records.start = draw.records.start,
7412 None => {
7413 open.span = Some(PlannedRunDraw {
7414 key,
7415 band_class: draw.band_class,
7416 indices: 0..cranpose_ui_graphics::strip_indices(1),
7417 records: draw.records.clone(),
7418 });
7419 }
7420 }
7421 }
7422 planned.extend(stretch.and_then(|open| open.span));
7423 planned
7424}
7425
7426fn evict_idle<K: Clone + Eq + std::hash::Hash, V>(
7430 cache: &mut BoundedLruCache<K, V>,
7431 frame: u64,
7432 idle_frames: u64,
7433 last_frame: impl Fn(&V) -> u64,
7434) {
7435 while cache
7436 .peek_lru()
7437 .is_some_and(|(_, value)| frame.saturating_sub(last_frame(value)) > idle_frames)
7438 {
7439 cache.pop_lru();
7440 }
7441}
7442
7443fn window_draws(draws: &mut Vec<RunDrawCall>, start: usize, window: &std::ops::Range<u32>) {
7446 let mut relative = 0u32;
7447 let mut kept = start;
7448 for index in start..draws.len() {
7449 let draw = &mut draws[index];
7450 let count = draw.records.end - draw.records.start;
7451 let first = relative;
7452 relative += count;
7453 let keep_start = window.start.max(first).min(first + count);
7454 let keep_end = window.end.min(first + count).max(keep_start);
7455 draw.records =
7456 draw.records.start + (keep_start - first)..draw.records.start + (keep_end - first);
7457 if draw.records.start < draw.records.end {
7458 draws.swap(kept, index);
7459 kept += 1;
7460 }
7461 }
7462 draws.truncate(kept);
7463}
7464
7465#[cfg(test)]
7466#[path = "tests/render_text_bounds_tests.rs"]
7467mod text_bounds_tests;
7468
7469#[cfg(test)]
7470#[path = "tests/render_retained_glyph_tests.rs"]
7471mod retained_glyph_tests;
7472
7473#[cfg(test)]
7474#[path = "tests/frame_clear_tests.rs"]
7475mod frame_clear_tests;
7476
7477#[cfg(test)]
7478#[path = "tests/render_clip_scissor_tests.rs"]
7479mod clip_scissor_tests;
7480
7481#[cfg(test)]
7482#[path = "tests/glyph_kind_tests.rs"]
7483mod glyph_kind_tests;
7484
7485#[cfg(test)]
7486#[path = "tests/glyph_run_bounds_tests.rs"]
7487mod glyph_run_bounds_tests;
7488
7489#[cfg(test)]
7490#[path = "tests/shape_variant_tests.rs"]
7491mod shape_variant_tests;