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