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