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