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