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