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