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