1use crate::display_clip::{self, DisplayVisibleRegion};
4use crate::effect_renderer::{
5 projective_dest_bounds_rect, CompositeBatchItem, CompositeSampleMode, EffectRenderer,
6 EffectScratchTargetProvider, ProjectiveSurfaceComposite, RoundedCompositeMask,
7 ShaderCompositeBatchItem,
8};
9#[cfg(not(target_arch = "wasm32"))]
10use crate::effect_renderer::{PreparedProjectiveComposite, ProjectiveCompositeItem};
11use crate::frame_graph::{
12 FrameCommandRecorder, FrameTextureDescriptor, WgpuFrameGraph, WgpuFrameGraphExecutor,
13};
14use crate::frame_packet::{
15 CancelReason, FramePacket, PacketRoot, PresentOutcome, RenderReturns, RootSurfacePacket,
16};
17use crate::layer_events::{
18 collect_effect_ranges, collect_layer_events, LayerEvent, LayerEventKind,
19};
20use crate::layer_surface_cache::LayerSurfaceCache;
21#[cfg(not(target_arch = "wasm32"))]
22use crate::lazy_resource::LazyGpuResource;
23use crate::lazy_resource::PassPipeline;
24#[cfg(test)]
25use crate::normalized_scene::{
26 build_scene_window, collect_layer_contents, collect_layer_contents_with_translation_context,
27 filtered_effect_layer_index, scene_bounds, SceneWindowSource,
28};
29#[cfg(test)]
30use crate::normalized_scene::{estimate_layer_surface_rect, motion_stable_capture_bounds};
31use crate::normalized_scene::{translate_quad, ChildLayerComposite, CollectedLayer};
32use crate::offscreen::OffscreenTarget;
33use crate::rect_to_quad;
34use crate::scene::{
35 BackdropLayer, CompositorScene, DrawOp, DrawOpKind, DrawShape, EffectLayer, ImageDraw,
36 RetainedDraw, SceneBrush, ShadowDraw, SimilarityTransform, SnapAnchor, TextDraw,
37};
38#[cfg(not(target_arch = "wasm32"))]
39use crate::segment_surface::{
40 Affine2, CaptureRect, SegmentSurfaceCache, SegmentSurfaceDecision, SegmentSurfaceKey,
41 SEGMENT_CAPTURE_SLOTS, SEGMENT_CAPTURE_UNIFORM_STRIDE,
42};
43use crate::shaders;
44#[cfg(test)]
45use crate::surface_executor::surface_target_size;
46use crate::surface_executor::{
47 apply_backdrop_layer_to_target as execute_apply_backdrop_layer_to_target,
48 axis_aligned_quad_rect, backdrop_underlay_is_covered_by_local_content,
49 canonicalize_device_coordinate, canonicalized_scaled_quad, canonicalized_scaled_rect,
50 composite_surface_to_view as execute_composite_surface_to_view, device_pixel_bounds_for_rect,
51 offscreen_byte_size, render_effect_layer_to_target as execute_render_effect_layer_to_target,
52 render_layer_surface as execute_render_layer_surface,
53 render_root_direct as execute_render_root_direct, root_direct_scene_events_are_supported,
54 scaled_quad, snap_delta_for_anchor, snap_motion_stable_dest_quad,
55 translation_stable_anchored_device_pixel_bounds, DevicePixelBounds, LayerSurfaceTexture,
56 SurfaceExecutionBackend,
57};
58#[cfg(test)]
59use crate::surface_executor::{clamp_effect_surface_scale, visible_layer_rect};
60#[cfg(test)]
61use crate::surface_plan::root_can_render_directly_cached;
62#[cfg(test)]
63use crate::surface_plan::{
64 composite_sample_mode_for_effect_layer, composite_sample_mode_for_requirements,
65 direct_translation, effect_layer_target_scale, layer_contains_descendant_backdrop,
66 layer_surface_requirements, layer_surface_requirements_cached, layer_surface_scale,
67 layer_surface_target_scale, layer_uses_external_backdrop_input, TranslatedContentAxes,
68};
69use crate::surface_plan::{LayerSurfaceRequest, TranslationRenderContext};
70#[cfg(test)]
71use crate::surface_requirements::SurfaceRequirement;
72use crate::surface_requirements::SurfaceRequirementSet;
73use crate::DebugCpuAllocationStats;
74use bytemuck::{Pod, Zeroable};
75#[cfg(any(not(target_arch = "wasm32"), test))]
76use cranpose_core::collections::map::HashMap;
77use cranpose_core::{hash::default as default_hash, NodeId};
78use cranpose_render_common::bounded_lru_cache::BoundedLruCache;
79use cranpose_render_common::geometry::blur_extent_margin;
80use cranpose_render_common::graph::quad_bounds;
81#[cfg(test)]
82use cranpose_render_common::graph::{
83 CachePolicy, LayerNode, PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform,
84 RenderNode,
85};
86use cranpose_render_common::raster_cache::LayerRasterCacheKey;
87#[cfg(test)]
88use cranpose_render_common::raster_cache::ScaleBucket;
89use cranpose_render_common::software_text_raster::{
90 collect_solid_text_atlas_run, measure_text_with_font,
91 rasterize_annotated_text_to_image_with_glyph_cache, rasterize_text_to_image_with_glyph_cache,
92 SoftwareGlyphAtlasGlyph, SoftwareGlyphAtlasKey, SoftwareGlyphAtlasPlacement,
93 SoftwareGlyphAtlasRunGlyph, SoftwareGlyphRasterCache, SoftwareTextFontSet,
94};
95#[cfg(test)]
96use cranpose_ui_graphics::GraphicsLayer;
97use cranpose_ui_graphics::{
98 BlendMode, Brush, Color, ColorFilter, FxHasher, ImageBitmap, ImageSampling, Point, Rect,
99 RenderEffect, RenderHash, RuntimeShader, StrokeCap, StrokeJoin, TileMode,
100};
101use std::borrow::Cow;
102use std::cell::Cell;
103use std::hash::{Hash, Hasher};
104use std::ops::Range;
105use std::rc::Rc;
106#[cfg(not(target_arch = "wasm32"))]
107use std::sync::atomic::{AtomicUsize, Ordering};
108use std::sync::{mpsc, Arc};
109use std::time::Duration;
110use web_time::Instant;
111
112use crate::gpu_stats;
113use crate::gpu_stats::gpu_stats_enabled;
114use crate::pipeline::push_layer_shadow;
115
116#[cfg(target_arch = "wasm32")]
121const MAX_SHAPES_PER_BATCH: usize = 102;
122#[cfg(not(target_arch = "wasm32"))]
123const MAX_SHAPES_PER_BATCH: usize = 768;
124#[cfg(target_arch = "wasm32")]
125const MAX_GRADIENT_STOPS: usize = 256;
126#[cfg(not(target_arch = "wasm32"))]
127const MAX_GRADIENT_STOPS: usize = 1024;
128
129#[cfg(not(target_arch = "wasm32"))]
136const MAX_SHAPES_PER_STORAGE_BATCH: usize = 1 << 16;
137#[cfg(not(target_arch = "wasm32"))]
138const MAX_GRADIENT_STOPS_PER_STORAGE_BATCH: usize = 1 << 16;
139
140#[cfg(not(target_arch = "wasm32"))]
146const INITIAL_STORAGE_BATCH_CAPACITY: usize = 1024;
147
148#[derive(Clone, Copy, Debug, Eq, PartialEq)]
160struct ShapeBatchLimits {
161 max_shapes_per_batch: usize,
162 max_gradient_stops: usize,
163 storage: bool,
164}
165
166impl ShapeBatchLimits {
167 fn for_device(device: &wgpu::Device, downlevel: wgpu::DownlevelFlags) -> Self {
168 Self::select(&device.limits(), downlevel)
169 }
170
171 fn select(limits: &wgpu::Limits, downlevel: wgpu::DownlevelFlags) -> Self {
178 #[cfg(not(target_arch = "wasm32"))]
179 if limits.max_storage_buffers_per_shader_stage >= 2
180 && downlevel.contains(wgpu::DownlevelFlags::VERTEX_STORAGE)
200 {
201 return Self::for_storage_binding_size(limits.max_storage_buffer_binding_size);
202 }
203 Self::for_uniform_binding_size(limits.max_uniform_buffer_binding_size)
204 }
205
206 fn for_uniform_binding_size(max_uniform_buffer_binding_size: u64) -> Self {
207 let binding = max_uniform_buffer_binding_size as usize;
208 Self {
209 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
210 .clamp(1, MAX_SHAPES_PER_BATCH),
211 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
212 .clamp(1, MAX_GRADIENT_STOPS),
213 storage: false,
214 }
215 }
216
217 #[cfg(not(target_arch = "wasm32"))]
218 fn for_storage_binding_size(max_storage_buffer_binding_size: u64) -> Self {
219 let binding = max_storage_buffer_binding_size as usize;
220 Self {
221 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
222 .clamp(1, MAX_SHAPES_PER_STORAGE_BATCH),
223 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
224 .clamp(1, MAX_GRADIENT_STOPS_PER_STORAGE_BATCH),
225 storage: true,
226 }
227 }
228
229 fn initial_shape_capacity(&self) -> usize {
230 #[cfg(not(target_arch = "wasm32"))]
231 if self.storage {
232 return self
233 .max_shapes_per_batch
234 .min(INITIAL_STORAGE_BATCH_CAPACITY);
235 }
236 self.max_shapes_per_batch
237 }
238
239 fn initial_gradient_capacity(&self) -> usize {
240 #[cfg(not(target_arch = "wasm32"))]
241 if self.storage {
242 return self.max_gradient_stops.min(INITIAL_STORAGE_BATCH_CAPACITY);
243 }
244 self.max_gradient_stops
245 }
246
247 fn data_buffer_usage(&self) -> wgpu::BufferUsages {
248 if self.storage {
249 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST
250 } else {
251 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST
252 }
253 }
254
255 fn data_binding_type(&self) -> wgpu::BufferBindingType {
256 if self.storage {
257 wgpu::BufferBindingType::Storage { read_only: true }
258 } else {
259 wgpu::BufferBindingType::Uniform
260 }
261 }
262
263 #[cfg(test)]
264 fn desktop() -> Self {
265 Self::for_uniform_binding_size(wgpu::Limits::default().max_uniform_buffer_binding_size)
266 }
267}
268#[cfg(target_arch = "wasm32")]
269const HARD_MAX_BUFFER_MB: usize = 64; const MAX_SHADOW_SURFACE_CACHE_ITEMS: usize = 512;
271const MAX_SHADOW_SURFACE_CACHE_BYTES: u64 = 192 * 1024 * 1024;
275const MAX_TEXT_IMAGE_CACHE_ITEMS: usize = 1024;
276const MAX_TEXT_GLYPH_MASK_CACHE_ITEMS: usize = 8192;
277const MAX_TEXT_GLYPH_ATLAS_ITEMS: usize = 8192;
278const MAX_TEXT_GLYPH_RUN_CACHE_ITEMS: usize = 1024;
279#[cfg(not(target_arch = "wasm32"))]
280const MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS: usize = 1024;
281#[cfg(not(target_arch = "wasm32"))]
282const MIN_RETAINED_TEXT_GLYPH_QUADS: usize = 192;
283#[cfg(not(target_arch = "wasm32"))]
284const OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS: f64 = 0.75;
285#[cfg(not(target_arch = "wasm32"))]
286const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES: usize = 2;
287#[cfg(not(target_arch = "wasm32"))]
288const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS: usize = 160;
289#[cfg(not(target_arch = "wasm32"))]
290const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS: usize = 160;
291const TEXT_GLYPH_ATLAS_MIN_SIZE: u32 = 512;
303const TEXT_GLYPH_ATLAS_MAX_SIZE: u32 = 4096;
304const TEXT_GLYPH_ATLAS_PADDING: u32 = 1;
305const MAX_TEXT_LINE_INDEX_CACHE_ITEMS: usize = 512;
306const MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER: usize = 2;
307const MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES: usize = 128;
308const CACHE_MISS_WARMUP_FRAMES: u8 = 1;
309pub(crate) const CLEAR_COLOR: wgpu::Color = wgpu::Color {
310 r: cranpose_render_common::FRAME_CLEAR_COLOR[0] as f64,
311 g: cranpose_render_common::FRAME_CLEAR_COLOR[1] as f64,
312 b: cranpose_render_common::FRAME_CLEAR_COLOR[2] as f64,
313 a: cranpose_render_common::FRAME_CLEAR_COLOR[3] as f64,
314};
315#[cfg(not(target_arch = "wasm32"))]
316const INITIAL_UPLOAD_BUFFER_BYTES: u64 = 4 * 1024;
317#[cfg(not(target_arch = "wasm32"))]
318const INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS: usize = 128;
319const MAX_TEXTURE_CACHE_ITEMS: usize = 256;
320const MAX_IMAGE_TEXTURE_CACHE_BYTES: usize = 256 * 1024 * 1024;
324const RETAINED_STAGED_UPLOAD_BYTES: usize = 256 * 1024;
325const RETAINED_STAGED_UPLOAD_COPIES: usize = 128;
326pub(crate) const RETAINED_LAYER_REQUIREMENTS_CAPACITY: usize = 512;
327const DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
328#[cfg(not(target_arch = "wasm32"))]
329static SEGMENT_DIAG_LINES: AtomicUsize = AtomicUsize::new(0);
330fn wgpu_render_stage_telemetry_threshold_ms() -> Option<f64> {
334 static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
335 *THRESHOLD_MS.get_or_init(|| {
336 let explicit = std::env::var("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY_MS")
337 .ok()
338 .and_then(|value| value.parse::<f64>().ok())
339 .filter(|value| value.is_finite() && *value >= 0.0);
340 explicit.or_else(|| {
341 std::env::var_os("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY")
342 .is_some()
343 .then_some(DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS)
344 })
345 })
346}
347
348pub(crate) fn instant_ms(start: Instant, end: Instant) -> f64 {
349 end.duration_since(start).as_secs_f64() * 1000.0
350}
351
352pub(crate) fn should_log_wgpu_render_stage(start: Instant, end: Instant) -> Option<f64> {
353 let threshold_ms = wgpu_render_stage_telemetry_threshold_ms()?;
354 let total_ms = instant_ms(start, end);
355 (total_ms >= threshold_ms).then_some(total_ms)
356}
357
358fn admit_layer_surface_cache_miss_impl(
359 key: &LayerRasterCacheKey,
360 observed_scene_range_misses: &mut BoundedLruCache<LayerRasterCacheKey, ()>,
361) -> bool {
362 if !key.is_scene_range() {
363 return true;
364 }
365 if observed_scene_range_misses.contains(key) {
366 return true;
367 }
368 observed_scene_range_misses.put(*key, ());
369 false
370}
371
372#[cfg(test)]
373fn first_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
374 let mut observed_scene_range_misses =
375 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
376 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
377}
378
379#[cfg(test)]
380fn repeated_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
381 let mut observed_scene_range_misses =
382 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
383 let _ = admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses);
384 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
385}
386
387pub static PRESENTED_FRAMES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
388
389pub fn frames_presented() -> u64 {
390 PRESENTED_FRAMES.load(std::sync::atomic::Ordering::Relaxed)
391}
392
393fn frame_stats_need_warmup_frame(snapshot: &gpu_stats::FrameStatsSnapshot) -> bool {
394 snapshot.layer_cache_misses > 0
395 || snapshot.shadow_shape_cache_misses > 0
396 || snapshot.text_image_cache_misses > 0
397 || snapshot.text_glyph_atlas_misses > 0
398}
399
400fn update_frame_warmup_budget(pending_frames: &mut u8, snapshot: &gpu_stats::FrameStatsSnapshot) {
401 if *pending_frames > 0 {
402 *pending_frames = pending_frames.saturating_sub(1);
403 } else if frame_stats_need_warmup_frame(snapshot) {
404 *pending_frames = CACHE_MISS_WARMUP_FRAMES;
405 }
406}
407
408fn text_atlas_fallback_diag_enabled() -> bool {
409 cranpose_core::env_flag!("CRANPOSE_TEXT_ATLAS_FALLBACK_DIAG")
410}
411
412fn text_glyph_run_diag_enabled() -> bool {
413 cranpose_core::env_flag!("CRANPOSE_TEXT_GLYPH_RUN_DIAG")
414}
415
416fn root_direct_diag_enabled() -> bool {
417 cranpose_core::env_flag!("CRANPOSE_ROOT_DIRECT_DIAG")
418}
419
420fn scene_layer_events_precede_z(scene: &CompositorScene, z_index: usize) -> bool {
421 scene
422 .effect_layers
423 .iter()
424 .any(|layer| layer.z_start < z_index && 0 < layer.z_end)
425 || scene
426 .backdrop_layers
427 .iter()
428 .any(|layer| layer.z_index < z_index)
429}
430
431fn direct_root_child_can_be_replayed_into_later_underlay(child: &ChildLayerComposite) -> bool {
432 child.backdrop.is_none()
433 && !child.has_effect
434 && child.shadow_draws.is_empty()
435 && axis_aligned_quad_rect(child.dest_quad).is_some()
436}
437
438fn rects_overlap(a: Rect, b: Rect) -> bool {
439 let a_right = a.x + a.width;
440 let a_bottom = a.y + a.height;
441 let b_right = b.x + b.width;
442 let b_bottom = b.y + b.height;
443 a.x < b_right && b.x < a_right && a.y < b_bottom && b.y < a_bottom
444}
445
446pub(crate) fn direct_root_child_underlays_are_supported(collected: &CollectedLayer) -> bool {
447 for (child_index, child) in collected.child_layers.iter().enumerate() {
448 if child.backdrop.is_some() {
449 if root_direct_diag_enabled() {
450 log::warn!(
451 "[root-direct-diag] reject self-backdrop child node={:?}",
452 child.node_id
453 );
454 }
455 return false;
456 }
457 if child.needs_nested_underlay {
458 let Some(dest_rect) = axis_aligned_quad_rect(child.dest_quad) else {
459 if root_direct_diag_enabled() {
460 log::warn!(
461 "[root-direct-diag] reject projective underlay child node={:?}",
462 child.node_id
463 );
464 }
465 return false;
466 };
467 let translation_only = (dest_rect.width - child.logical_rect.width).abs() <= 0.001
468 && (dest_rect.height - child.logical_rect.height).abs() <= 0.001;
469 let unsupported_preceding_child_layer = collected.child_layers[..child_index]
470 .iter()
471 .any(|preceding| {
472 if direct_root_child_can_be_replayed_into_later_underlay(preceding) {
473 return false;
474 }
475 axis_aligned_quad_rect(preceding.dest_quad)
476 .is_none_or(|preceding_rect| rects_overlap(preceding_rect, dest_rect))
477 });
478 let preceding_scene_events =
479 scene_layer_events_precede_z(&collected.scene, child.z_index);
480 if unsupported_preceding_child_layer || preceding_scene_events || !translation_only {
481 if root_direct_diag_enabled() {
482 log::warn!(
483 "[root-direct-diag] reject underlay child node={:?} unsupported_preceding_child_layer={} preceding_scene_events={} translation_only={} dest=({:.1},{:.1},{:.1},{:.1}) logical=({:.1},{:.1},{:.1},{:.1})",
484 child.node_id,
485 unsupported_preceding_child_layer,
486 preceding_scene_events,
487 translation_only,
488 dest_rect.x,
489 dest_rect.y,
490 dest_rect.width,
491 dest_rect.height,
492 child.logical_rect.x,
493 child.logical_rect.y,
494 child.logical_rect.width,
495 child.logical_rect.height
496 );
497 }
498 return false;
499 }
500 }
501 }
502 true
503}
504
505#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
506struct ShadowSurfaceCacheKey {
507 content_hash: u64,
508 pixel_size: [u32; 2],
509 root_scale_bits: u32,
510 blur_radius_bits: u32,
511}
512
513struct CachedShadowSurface {
514 target: Rc<OffscreenTarget>,
515 byte_size: u64,
516}
517
518struct CachedShadowComposite {
519 source: Rc<OffscreenTarget>,
520 scissor: Option<(u32, u32, u32, u32)>,
521 rounded_mask: Option<RoundedCompositeMask>,
522 dest_viewport: Option<(f32, f32, f32, f32)>,
523}
524
525impl CachedShadowComposite {
526 fn batch_item(&self) -> CompositeBatchItem<'_> {
527 CompositeBatchItem {
528 source: &self.source,
529 alpha: 1.0,
530 scissor: self.scissor,
531 rounded_mask: self.rounded_mask,
532 blend_mode: BlendMode::SrcOver,
533 dest_viewport: self.dest_viewport,
534 source_viewport: None,
535 sample_mode: CompositeSampleMode::Nearest,
536 }
537 }
538}
539
540#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
541struct TextImageCacheKey(u64);
542
543struct CachedTextImage {
544 image: ImageBitmap,
545}
546
547#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
548struct TextGlyphRunCacheKey(u64);
549
550#[derive(Clone, Copy)]
551struct CachedTextGlyphQuad {
552 x: i32,
553 y: i32,
554 width: usize,
555 height: usize,
556 color: (f32, f32, f32, f32),
557 uv: ImageUvRect,
558}
559
560struct CachedTextGlyphRun {
561 glyphs: Rc<[SoftwareGlyphAtlasPlacement]>,
562 quads: Option<Rc<[CachedTextGlyphQuad]>>,
563 atlas_generation: u64,
564}
565
566const TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER: f32 = 2.0;
567
568#[cfg(not(target_arch = "wasm32"))]
569struct CachedGpuTextGlyphRun {
570 vertex_buffer: wgpu::Buffer,
571 index_buffer: wgpu::Buffer,
572 index_count: u32,
573 atlas_generation: u64,
574}
575
576#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
577struct TextLineIndexCacheKey(usize);
578
579struct CachedTextLineIndex {
580 text: std::sync::Weak<cranpose_ui::text::RenderString>,
581 len: usize,
582 starts: Rc<[usize]>,
583}
584
585struct TextLineIndexCache {
586 entries: BoundedLruCache<TextLineIndexCacheKey, CachedTextLineIndex>,
587}
588
589impl TextLineIndexCache {
590 fn new(capacity: usize) -> Self {
591 Self {
592 entries: BoundedLruCache::with_capacity_at_least_one(capacity),
593 }
594 }
595
596 fn line_starts(&mut self, text: &Arc<cranpose_ui::text::RenderString>) -> Rc<[usize]> {
597 let key = TextLineIndexCacheKey(Arc::as_ptr(text) as usize);
598 if let Some(cached) = self.entries.get(&key) {
599 if cached.len == text.text.len()
600 && cached
601 .text
602 .upgrade()
603 .is_some_and(|cached_text| Arc::ptr_eq(&cached_text, text))
604 {
605 return cached.starts.clone();
606 }
607 }
608
609 let starts = Rc::<[usize]>::from(line_start_offsets(text.text.as_str()));
610 self.entries.put(
611 key,
612 CachedTextLineIndex {
613 text: Arc::downgrade(text),
614 len: text.text.len(),
615 starts: starts.clone(),
616 },
617 );
618 starts
619 }
620}
621
622#[derive(Clone, Copy, Debug, PartialEq)]
623struct ShapeShadowSurfacePlan {
624 source_device_bounds: DevicePixelBounds,
625 processing_scissor: Option<(u32, u32, u32, u32)>,
626 pixel_radius: f32,
627}
628
629#[derive(Default)]
645struct DeviceErrorSentry {
646 errors: std::sync::atomic::AtomicU64,
648 poisoned: std::sync::atomic::AtomicBool,
650}
651
652impl DeviceErrorSentry {
653 fn record(&self, error: &wgpu::Error) {
656 use std::sync::atomic::Ordering;
657 self.poisoned.store(true, Ordering::Release);
658 let count = self.errors.fetch_add(1, Ordering::Relaxed) + 1;
659 if count.is_power_of_two() {
665 log::error!("[gpu-device] uncaptured wgpu error #{count}: {error}");
666 }
667 }
668
669 fn take_poison(&self) -> bool {
670 self.poisoned
671 .swap(false, std::sync::atomic::Ordering::AcqRel)
672 }
673
674 fn error_count(&self) -> u64 {
675 self.errors.load(std::sync::atomic::Ordering::Relaxed)
676 }
677}
678
679#[derive(Default)]
680struct RendererWarningState {
681 unsupported_effect_reported: Cell<bool>,
682}
683
684impl RendererWarningState {
685 fn warn_unsupported_effect_once(&self) {
686 if !self.unsupported_effect_reported.replace(true) {
687 log::warn!(
688 "WGPU renderer received an unsupported RenderEffect variant; falling back to passthrough compositing"
689 );
690 }
691 }
692}
693
694fn is_blend_mode_supported(mode: BlendMode) -> bool {
695 matches!(mode, BlendMode::SrcOver | BlendMode::DstOut)
696}
697
698fn blend_state_for_mode(mode: BlendMode) -> wgpu::BlendState {
699 match mode {
700 BlendMode::DstOut => wgpu::BlendState {
701 color: wgpu::BlendComponent {
702 src_factor: wgpu::BlendFactor::Zero,
703 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
704 operation: wgpu::BlendOperation::Add,
705 },
706 alpha: wgpu::BlendComponent {
707 src_factor: wgpu::BlendFactor::Zero,
708 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
709 operation: wgpu::BlendOperation::Add,
710 },
711 },
712 _ => wgpu::BlendState::ALPHA_BLENDING,
713 }
714}
715
716fn supported_blend_mode(mode: BlendMode) -> BlendMode {
717 if is_blend_mode_supported(mode) {
718 return mode;
719 }
720
721 BlendMode::SrcOver
722}
723
724fn direct_shader_composite_viewport(
725 alpha: f32,
726 blend_mode: BlendMode,
727 dest_viewport: Option<(f32, f32, f32, f32)>,
728 sample_mode: CompositeSampleMode,
729 source_size: (u32, u32),
730) -> Option<(f32, f32, f32, f32)> {
731 if alpha != 1.0 || supported_blend_mode(blend_mode) != BlendMode::SrcOver {
732 return None;
733 }
734 let viewport = dest_viewport?;
735 if viewport.2 <= 0.0 || viewport.3 <= 0.0 {
736 return None;
737 }
738 match sample_mode {
739 CompositeSampleMode::Linear | CompositeSampleMode::Nearest => Some(viewport),
740 CompositeSampleMode::Box4
741 if shader_composite_preserves_source_pixel_grid(viewport, source_size) =>
742 {
743 Some(viewport)
744 }
745 CompositeSampleMode::Box4 => None,
746 }
747}
748
749fn shader_composite_preserves_source_pixel_grid(
750 viewport: (f32, f32, f32, f32),
751 source_size: (u32, u32),
752) -> bool {
753 const EPSILON: f32 = 0.01;
754 let (x, y, width, height) = viewport;
755 let (source_width, source_height) = source_size;
756 (x - x.round()).abs() <= EPSILON
757 && (y - y.round()).abs() <= EPSILON
758 && (width - source_width as f32).abs() <= EPSILON
759 && (height - source_height as f32).abs() <= EPSILON
760}
761
762type DirectShaderTailComposite<'a> = (&'a RenderEffect, &'a RuntimeShader, (f32, f32, f32, f32));
763
764fn direct_shader_tail_composite(
765 effect: &RenderEffect,
766 alpha: f32,
767 blend_mode: BlendMode,
768 dest_viewport: Option<(f32, f32, f32, f32)>,
769 sample_mode: CompositeSampleMode,
770 source_size: (u32, u32),
771) -> Option<DirectShaderTailComposite<'_>> {
772 let viewport = direct_shader_composite_viewport(
773 alpha,
774 blend_mode,
775 dest_viewport,
776 sample_mode,
777 source_size,
778 )?;
779 let RenderEffect::Chain { first, second } = effect else {
780 return None;
781 };
782 let RenderEffect::Shader { shader } = second.as_ref() else {
783 return None;
784 };
785 Some((first.as_ref(), shader, viewport))
786}
787
788fn hash_f32_for_cache<H: Hasher>(value: f32, state: &mut H) {
789 value.to_bits().hash(state);
790}
791
792fn hash_text_raster_geometry_for_cache<H: Hasher>(
793 rect: Rect,
794 static_text_motion: bool,
795 state: &mut H,
796) {
797 hash_f32_for_cache(rect.width, state);
798 hash_f32_for_cache(rect.height, state);
799 static_text_motion.hash(state);
800 if !static_text_motion {
801 hash_f32_for_cache(rect.x.fract(), state);
802 hash_f32_for_cache(rect.y.fract(), state);
803 }
804}
805
806fn text_raster_geometry_for_draw(
807 text_draw: &TextDraw,
808 root_scale: f32,
809) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
810 if text_draw.text.is_empty()
811 || text_draw.rect.width <= 0.0
812 || text_draw.rect.height <= 0.0
813 || !root_scale.is_finite()
814 || root_scale <= 0.0
815 {
816 return None;
817 }
818
819 let text_scale = text_draw.scale * root_scale;
820 if !text_scale.is_finite() || text_scale <= 0.0 {
821 return None;
822 }
823
824 let static_text_motion = text_draw
825 .text_style
826 .paragraph_style
827 .text_motion
828 .unwrap_or(cranpose_ui::text::TextMotion::Static)
829 == cranpose_ui::text::TextMotion::Static;
830 let snap_delta = text_draw
831 .snap_anchor
832 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
833 .unwrap_or_default();
834 let logical_rect = text_draw.rect.translate(snap_delta.x, snap_delta.y);
835 let clip = text_draw.clip;
838 let mut raster_rect = Rect {
839 x: logical_rect.x * root_scale,
840 y: logical_rect.y * root_scale,
841 width: logical_rect.width * root_scale,
842 height: logical_rect.height * root_scale,
843 };
844 if text_draw.snap_anchor.is_some() {
845 raster_rect.x = canonicalize_device_coordinate(raster_rect.x);
846 raster_rect.y = canonicalize_device_coordinate(raster_rect.y);
847 }
848 if static_text_motion {
849 raster_rect.x = raster_rect.x.round();
850 raster_rect.y = raster_rect.y.round();
851 }
852 raster_rect.width = raster_rect.width.ceil().max(1.0);
853 raster_rect.height = raster_rect.height.ceil().max(1.0);
854 Some((
855 logical_rect,
856 raster_rect,
857 clip,
858 text_scale,
859 static_text_motion,
860 ))
861}
862
863fn text_draw_is_visible_in_viewport(
864 logical_rect: Rect,
865 clip: Option<Rect>,
866 viewport: ViewportUniformParams,
867 root_scale: f32,
868) -> bool {
869 draw_rect_is_visible_in_viewport(logical_rect, clip, viewport, root_scale)
870}
871
872fn text_draw_should_prewarm_in_viewport(
873 logical_rect: Rect,
874 clip: Option<Rect>,
875 viewport: ViewportUniformParams,
876 root_scale: f32,
877) -> bool {
878 if !root_scale.is_finite() || root_scale <= 0.0 {
879 return false;
880 }
881 let viewport_rect = Rect {
882 x: viewport.offset[0] / root_scale,
883 y: viewport.offset[1] / root_scale,
884 width: viewport.width as f32 / root_scale,
885 height: viewport.height as f32 / root_scale,
886 };
887 let margin_x = viewport_rect.width * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
888 let margin_y = viewport_rect.height * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
889 let prewarm_viewport = expand_rect(viewport_rect, margin_x, margin_y);
890 let prewarm_rect = match clip {
891 Some(clip) => expand_rect(clip, margin_x, margin_y).intersect(prewarm_viewport),
892 None => Some(prewarm_viewport),
893 };
894 prewarm_rect.is_some_and(|rect| logical_rect.intersect(rect).is_some())
895}
896
897fn expand_rect(rect: Rect, margin_x: f32, margin_y: f32) -> Rect {
898 Rect {
899 x: rect.x - margin_x,
900 y: rect.y - margin_y,
901 width: rect.width + margin_x * 2.0,
902 height: rect.height + margin_y * 2.0,
903 }
904}
905
906fn draw_rect_is_visible_in_viewport(
907 rect: Rect,
908 clip: Option<Rect>,
909 viewport: ViewportUniformParams,
910 root_scale: f32,
911) -> bool {
912 if !root_scale.is_finite() || root_scale <= 0.0 {
913 return false;
914 }
915 let viewport_rect = Rect {
916 x: viewport.offset[0] / root_scale,
917 y: viewport.offset[1] / root_scale,
918 width: viewport.width as f32 / root_scale,
919 height: viewport.height as f32 / root_scale,
920 };
921 let visible_rect = match clip {
922 Some(clip) => clip.intersect(viewport_rect),
923 None => Some(viewport_rect),
924 };
925 visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
926}
927
928fn shape_draw_is_visible_in_viewport(
929 shape: &DrawShape,
930 viewport: ViewportUniformParams,
931 root_scale: f32,
932) -> bool {
933 let Some(viewport_rect) = viewport_rect_in_logical(viewport, root_scale) else {
934 return false;
935 };
936 shape_draw_is_visible_in_rect(shape, viewport_rect, root_scale)
937}
938
939fn viewport_rect_in_logical(viewport: ViewportUniformParams, root_scale: f32) -> Option<Rect> {
943 if !root_scale.is_finite() || root_scale <= 0.0 {
944 return None;
945 }
946 Some(Rect {
947 x: viewport.offset[0] / root_scale,
948 y: viewport.offset[1] / root_scale,
949 width: viewport.width as f32 / root_scale,
950 height: viewport.height as f32 / root_scale,
951 })
952}
953
954fn shape_draw_is_visible_in_rect(shape: &DrawShape, viewport_rect: Rect, root_scale: f32) -> bool {
957 let snap_delta = shape
958 .snap_anchor
959 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
960 .unwrap_or_default();
961 let rect = quad_bounds(translate_quad(shape.quad, snap_delta));
962 let visible_rect = match shape.clip {
963 Some(clip) => clip.intersect(viewport_rect),
964 None => Some(viewport_rect),
965 };
966 visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
967}
968
969fn cached_text_glyph_quad(
970 glyph: &SoftwareGlyphAtlasPlacement,
971 entry: GlyphAtlasEntry,
972 atlas_size: u32,
973) -> CachedTextGlyphQuad {
974 CachedTextGlyphQuad {
975 x: glyph.x,
976 y: glyph.y,
977 width: glyph.width,
978 height: glyph.height,
979 color: (
980 glyph.color.0.clamp(0.0, 1.0),
981 glyph.color.1.clamp(0.0, 1.0),
982 glyph.color.2.clamp(0.0, 1.0),
983 glyph.color.3.clamp(0.0, 1.0),
984 ),
985 uv: glyph_atlas_uv_rect(entry, atlas_size),
986 }
987}
988
989fn append_cached_text_glyph_quad(
990 source_raster_rect: Rect,
991 quad: &CachedTextGlyphQuad,
992 image_vertices: &mut Vec<Vertex>,
993 image_indices: &mut Vec<u32>,
994) -> bool {
995 if quad.width == 0 || quad.height == 0 || quad.color.3 <= 0.0 {
996 return false;
997 }
998
999 let base_vertex = image_vertices.len() as u32;
1000 image_indices.extend_from_slice(&[
1001 base_vertex,
1002 base_vertex + 1,
1003 base_vertex + 2,
1004 base_vertex + 2,
1005 base_vertex + 1,
1006 base_vertex + 3,
1007 ]);
1008
1009 let x0 = source_raster_rect.x + quad.x as f32;
1010 let y0 = source_raster_rect.y + quad.y as f32;
1011 let x1 = x0 + quad.width as f32;
1012 let y1 = y0 + quad.height as f32;
1013 let color = [quad.color.0, quad.color.1, quad.color.2, quad.color.3];
1014
1015 image_vertices.extend_from_slice(&[
1016 Vertex {
1017 position: [x0, y0],
1018 color,
1019 uv: [quad.uv.min[0], quad.uv.min[1]],
1020 uv_bounds: quad.uv.sample_bounds,
1021 },
1022 Vertex {
1023 position: [x1, y0],
1024 color,
1025 uv: [quad.uv.max[0], quad.uv.min[1]],
1026 uv_bounds: quad.uv.sample_bounds,
1027 },
1028 Vertex {
1029 position: [x0, y1],
1030 color,
1031 uv: [quad.uv.min[0], quad.uv.max[1]],
1032 uv_bounds: quad.uv.sample_bounds,
1033 },
1034 Vertex {
1035 position: [x1, y1],
1036 color,
1037 uv: [quad.uv.max[0], quad.uv.max[1]],
1038 uv_bounds: quad.uv.sample_bounds,
1039 },
1040 ]);
1041 true
1042}
1043
1044fn cached_text_glyph_quad_logical_rect(
1045 source_raster_rect: Rect,
1046 quad: &CachedTextGlyphQuad,
1047 root_scale: f32,
1048) -> Option<Rect> {
1049 if !root_scale.is_finite() || root_scale <= 0.0 {
1050 return None;
1051 }
1052 Some(Rect {
1053 x: (source_raster_rect.x + quad.x as f32) / root_scale,
1054 y: (source_raster_rect.y + quad.y as f32) / root_scale,
1055 width: quad.width as f32 / root_scale,
1056 height: quad.height as f32 / root_scale,
1057 })
1058}
1059
1060fn cached_text_glyph_quad_is_visible_in_viewport(
1061 source_raster_rect: Rect,
1062 quad: &CachedTextGlyphQuad,
1063 clip: Option<Rect>,
1064 viewport: ViewportUniformParams,
1065 root_scale: f32,
1066) -> bool {
1067 cached_text_glyph_quad_logical_rect(source_raster_rect, quad, root_scale)
1068 .is_some_and(|rect| draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale))
1069}
1070
1071#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1072enum TextGlyphDrawAction {
1073 DrawVisible,
1074 PrewarmOffscreen,
1075 Skip,
1076}
1077
1078fn text_glyph_draw_action(
1079 is_visible: bool,
1080 is_prewarm_candidate: bool,
1081 allow_offscreen_prewarm: bool,
1082) -> TextGlyphDrawAction {
1083 if is_visible {
1084 TextGlyphDrawAction::DrawVisible
1085 } else if allow_offscreen_prewarm && is_prewarm_candidate {
1086 TextGlyphDrawAction::PrewarmOffscreen
1087 } else {
1088 TextGlyphDrawAction::Skip
1089 }
1090}
1091
1092#[cfg(not(target_arch = "wasm32"))]
1093fn should_use_retained_text_glyph_run(quads_len: usize, clip: Option<Rect>) -> bool {
1094 clip.is_none() && quads_len >= MIN_RETAINED_TEXT_GLYPH_QUADS
1095}
1096
1097#[cfg(not(target_arch = "wasm32"))]
1098fn offscreen_text_glyph_prewarm_work_is_bounded(
1099 cached_glyphs: Option<usize>,
1100 text_len: usize,
1101) -> bool {
1102 match cached_glyphs {
1103 Some(glyphs) => glyphs <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS,
1104 None => text_len <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
1105 }
1106}
1107
1108#[cfg(not(target_arch = "wasm32"))]
1109fn offscreen_text_glyph_prewarm_budget_exhausted(
1110 start: Instant,
1111 admitted_candidates: usize,
1112) -> bool {
1113 admitted_candidates >= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES
1114 || instant_ms(start, Instant::now()) >= OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS
1115}
1116
1117fn text_draws_for_ordered_range<'a>(
1118 ordered_items: &'a [(usize, SegmentDrawItem)],
1119 texts: &'a [TextDraw],
1120 start: usize,
1121 end: usize,
1122) -> Result<impl Iterator<Item = &'a TextDraw>, String> {
1123 let range_items = ordered_items
1124 .get(start..end)
1125 .ok_or_else(|| format!("text batch range {start}..{end} is outside ordered draw items"))?;
1126 for (_, item) in range_items {
1127 match item {
1128 SegmentDrawItem::Text(text_index) if *text_index < texts.len() => {}
1129 SegmentDrawItem::Text(text_index) => {
1130 return Err(format!(
1131 "text batch references missing text draw index: {text_index}"
1132 ));
1133 }
1134 _ => return Err(format!("text batch contains non-text draw item: {item:?}")),
1135 }
1136 }
1137
1138 Ok(range_items.iter().filter_map(move |(_, item)| match item {
1139 SegmentDrawItem::Text(text_index) => texts.get(*text_index),
1140 _ => None,
1141 }))
1142}
1143
1144const SHADOW_CACHE_DEVICE_QUANT: f32 = 16.0;
1149
1150fn hash_shadow_device_offset<H: Hasher>(value: f32, origin: f32, root_scale: f32, state: &mut H) {
1151 let quantized = ((value - origin) * root_scale * SHADOW_CACHE_DEVICE_QUANT).round();
1152 (quantized as i64).hash(state);
1153}
1154
1155fn hash_shadow_device_rect<H: Hasher>(
1156 rect: Rect,
1157 origin_x: f32,
1158 origin_y: f32,
1159 root_scale: f32,
1160 state: &mut H,
1161) {
1162 hash_shadow_device_offset(rect.x, origin_x, root_scale, state);
1163 hash_shadow_device_offset(rect.y, origin_y, root_scale, state);
1164 hash_shadow_device_offset(rect.width, 0.0, root_scale, state);
1165 hash_shadow_device_offset(rect.height, 0.0, root_scale, state);
1166}
1167
1168fn hash_shape_shadow_item<H: Hasher>(
1169 shape: &DrawShape,
1170 brushes: &[Brush],
1171 blend_mode: BlendMode,
1172 origin_x: f32,
1173 origin_y: f32,
1174 root_scale: f32,
1175 state: &mut H,
1176) {
1177 hash_shadow_device_rect(shape.rect, origin_x, origin_y, root_scale, state);
1178 hash_shadow_device_rect(shape.local_rect, origin_x, origin_y, root_scale, state);
1179 for point in shape.quad {
1180 hash_shadow_device_offset(point[0], origin_x, root_scale, state);
1181 hash_shadow_device_offset(point[1], origin_y, root_scale, state);
1182 }
1183 match shape.snap_anchor {
1184 Some(anchor) => {
1185 1u8.hash(state);
1186 hash_shadow_device_offset(anchor.origin.x, origin_x, root_scale, state);
1187 hash_shadow_device_offset(anchor.origin.y, origin_y, root_scale, state);
1188 hash_f32_for_cache(anchor.device_pixel_step, state);
1189 }
1190 None => 0u8.hash(state),
1191 }
1192 shape.brush.render_hash(brushes).hash(state);
1193 match shape.shape {
1194 Some(corner_shape) => {
1195 1u8.hash(state);
1196 corner_shape.radii().render_hash().hash(state);
1197 }
1198 None => 0u8.hash(state),
1199 }
1200 match shape.clip {
1201 Some(clip) => {
1202 1u8.hash(state);
1203 hash_shadow_device_rect(clip, origin_x, origin_y, root_scale, state);
1204 }
1205 None => 0u8.hash(state),
1206 }
1207 blend_mode.hash(state);
1208 shape.blend_mode.hash(state);
1209}
1210
1211fn shape_shadow_content_hash(
1212 shapes: &[(DrawShape, BlendMode)],
1213 brushes: &[Brush],
1214 root_scale: f32,
1215) -> u64 {
1216 let mut hasher = FxHasher::default();
1217 let origin = shape_shadow_bounds(shapes).unwrap_or(Rect {
1222 x: 0.0,
1223 y: 0.0,
1224 width: 0.0,
1225 height: 0.0,
1226 });
1227
1228 shapes.len().hash(&mut hasher);
1229 for (shape, blend_mode) in shapes {
1230 hash_shape_shadow_item(
1231 shape,
1232 brushes,
1233 *blend_mode,
1234 origin.x,
1235 origin.y,
1236 root_scale,
1237 &mut hasher,
1238 );
1239 }
1240 hasher.finish()
1241}
1242
1243fn shape_shadow_surface_cache_key(
1244 shapes: &[(DrawShape, BlendMode)],
1245 brushes: &[Brush],
1246 device_bounds: DevicePixelBounds,
1247 pixel_radius: f32,
1248 root_scale: f32,
1249) -> Option<ShadowSurfaceCacheKey> {
1250 (root_scale.is_finite() && root_scale > 0.0).then(|| ShadowSurfaceCacheKey {
1251 content_hash: shape_shadow_content_hash(shapes, brushes, root_scale),
1252 pixel_size: [device_bounds.width, device_bounds.height],
1253 root_scale_bits: root_scale.to_bits(),
1254 blur_radius_bits: pixel_radius.to_bits(),
1255 })
1256}
1257
1258fn shape_shadow_bounds(shapes: &[(DrawShape, BlendMode)]) -> Option<Rect> {
1259 shapes
1260 .iter()
1261 .map(|(shape, _)| shape.rect)
1262 .reduce(|a, b| Rect {
1263 x: a.x.min(b.x),
1264 y: a.y.min(b.y),
1265 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1266 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1267 })
1268}
1269
1270fn shared_shape_shadow_snap_anchor(shapes: &[(DrawShape, BlendMode)]) -> Option<SnapAnchor> {
1271 let anchor = shapes.first()?.0.snap_anchor?;
1272 shapes
1273 .iter()
1274 .all(|(shape, _)| shape.snap_anchor == Some(anchor))
1275 .then_some(anchor)
1276}
1277
1278fn shadow_draw_bounds(shadow: &ShadowDraw) -> Option<Rect> {
1279 shadow
1280 .shapes
1281 .iter()
1282 .map(|(shape, _)| shape.rect)
1283 .chain(shadow.texts.iter().map(|text| text.rect))
1284 .reduce(|a, b| Rect {
1285 x: a.x.min(b.x),
1286 y: a.y.min(b.y),
1287 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1288 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1289 })
1290}
1291
1292fn shadow_draw_may_render(
1293 shadow: &ShadowDraw,
1294 width: u32,
1295 height: u32,
1296 root_scale: f32,
1297 max_texture_dim: u32,
1298) -> bool {
1299 if shadow.texts.is_empty() && !shadow.shapes.is_empty() && shadow.blur_radius > 0.0 {
1300 return shape_shadow_surface_plan(
1301 &shadow.shapes,
1302 shadow.clip,
1303 shadow.blur_radius,
1304 width,
1305 height,
1306 root_scale,
1307 max_texture_dim,
1308 )
1309 .is_some();
1310 }
1311
1312 let Some(bounds) = shadow_draw_bounds(shadow) else {
1313 return false;
1314 };
1315 let blur_margin = blur_extent_margin(shadow.blur_radius);
1316 let mut visible_bounds = Rect {
1317 x: bounds.x - blur_margin,
1318 y: bounds.y - blur_margin,
1319 width: bounds.width + blur_margin * 2.0,
1320 height: bounds.height + blur_margin * 2.0,
1321 };
1322 if let Some(clip) = shadow.clip {
1323 let clip_expanded = Rect {
1324 x: clip.x - blur_margin,
1325 y: clip.y - blur_margin,
1326 width: clip.width + blur_margin * 2.0,
1327 height: clip.height + blur_margin * 2.0,
1328 };
1329 let Some(intersection) = visible_bounds.intersect(clip_expanded) else {
1330 return false;
1331 };
1332 visible_bounds = intersection;
1333 }
1334
1335 scissor_rect_for_rect(visible_bounds, root_scale, width, height).is_some()
1336}
1337
1338fn shape_shadow_surface_plan(
1339 shapes: &[(DrawShape, BlendMode)],
1340 clip: Option<Rect>,
1341 blur_radius: f32,
1342 width: u32,
1343 height: u32,
1344 root_scale: f32,
1345 max_texture_dim: u32,
1346) -> Option<ShapeShadowSurfacePlan> {
1347 let shape_bounds = shape_shadow_bounds(shapes)?;
1348 let blur_margin = blur_extent_margin(blur_radius);
1349 let source_blur_bounds = Rect {
1350 x: shape_bounds.x - blur_margin,
1351 y: shape_bounds.y - blur_margin,
1352 width: shape_bounds.width + blur_margin * 2.0,
1353 height: shape_bounds.height + blur_margin * 2.0,
1354 };
1355
1356 let mut visible_blur_bounds = source_blur_bounds;
1357 if let Some(clip) = clip {
1358 let clip_expanded = Rect {
1359 x: clip.x - blur_margin,
1360 y: clip.y - blur_margin,
1361 width: clip.width + blur_margin * 2.0,
1362 height: clip.height + blur_margin * 2.0,
1363 };
1364 visible_blur_bounds = visible_blur_bounds.intersect(clip_expanded)?;
1365 }
1366
1367 let processing_scissor = scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
1368 processing_scissor?;
1369 let visible_device_bounds =
1370 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)?;
1371 let source_device_bounds = translation_stable_anchored_device_pixel_bounds(
1372 source_blur_bounds,
1373 shared_shape_shadow_snap_anchor(shapes),
1374 root_scale,
1375 max_texture_dim,
1376 )
1377 .unwrap_or(visible_device_bounds);
1378
1379 Some(ShapeShadowSurfacePlan {
1380 source_device_bounds,
1381 processing_scissor,
1382 pixel_radius: blur_radius * root_scale,
1383 })
1384}
1385
1386fn is_render_effect_supported(effect: &RenderEffect) -> bool {
1387 match effect {
1388 RenderEffect::Blur { .. } => true,
1389 RenderEffect::Offset { .. } => true,
1390 RenderEffect::Shader { .. } => true,
1391 RenderEffect::Chain { first, second } => {
1392 is_render_effect_supported(first) && is_render_effect_supported(second)
1393 }
1394 }
1395}
1396
1397fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
1398 if value.is_finite() {
1399 origin + value
1400 } else if value.is_sign_positive() {
1401 origin + extent
1402 } else {
1403 origin
1404 }
1405}
1406
1407fn gradient_tile_mode_value(tile_mode: TileMode) -> u32 {
1408 match tile_mode {
1409 TileMode::Clamp => 0,
1410 TileMode::Repeated => 1,
1411 TileMode::Mirror => 2,
1412 TileMode::Decal => 3,
1413 }
1414}
1415
1416fn shape_shader_base(solid_trim: bool) -> Cow<'static, str> {
1424 if solid_trim {
1425 return Cow::Owned(format!(
1426 "{}\n{}",
1427 shaders::SHADER,
1428 shaders::SOLID_TRIM_APPENDIX
1429 ));
1430 }
1431 Cow::Borrowed(shaders::SHADER)
1432}
1433
1434#[cfg(not(target_arch = "wasm32"))]
1435fn shape_shader_source(batch_limits: ShapeBatchLimits, solid_trim: bool) -> Cow<'static, str> {
1436 let base = shape_shader_base(solid_trim);
1437 if batch_limits.storage {
1441 return Cow::Owned(
1442 base.replace(
1443 "var<uniform> shape_data: array<ShapeData, 102>;",
1444 "var<storage, read> shape_data: array<ShapeData>;",
1445 )
1446 .replace(
1447 "var<uniform> gradient_stops: array<GradientStop, 256>;",
1448 "var<storage, read> gradient_stops: array<GradientStop>;\n\n\
1455 @group(1) @binding(3)\n\
1456 var<storage, read> paint: array<vec4<f32>>;",
1457 )
1458 .replace(
1459 "output.color = shape.color;",
1460 "output.color = \
1461 select(shape.color, paint[shape_idx], similarity.paint_select > 0.5);",
1462 ),
1463 );
1464 }
1465 Cow::Owned(
1466 base.replace(
1467 "array<ShapeData, 102>",
1468 &format!("array<ShapeData, {}>", batch_limits.max_shapes_per_batch),
1469 )
1470 .replace(
1471 "array<GradientStop, 256>",
1472 &format!("array<GradientStop, {}>", batch_limits.max_gradient_stops),
1473 ),
1474 )
1475}
1476
1477#[cfg(target_arch = "wasm32")]
1478fn shape_shader_source(_batch_limits: ShapeBatchLimits, solid_trim: bool) -> Cow<'static, str> {
1479 shape_shader_base(solid_trim)
1483}
1484
1485pub(crate) fn create_render_pipeline_logged<'a>(
1490 device: &wgpu::Device,
1491 cache: Option<&'a wgpu::PipelineCache>,
1492 tag: &str,
1493 mut descriptor: wgpu::RenderPipelineDescriptor<'a>,
1494) -> wgpu::RenderPipeline {
1495 descriptor.cache = cache;
1496 let started = Instant::now();
1497 let pipeline = device.create_render_pipeline(&descriptor);
1498 log::info!(
1499 "[pipeline-create] {tag} {:.1}ms",
1500 instant_ms(started, Instant::now())
1501 );
1502 pipeline
1503}
1504
1505#[cfg(not(target_arch = "wasm32"))]
1508fn pipeline_prewarm_enabled() -> bool {
1509 std::env::var("CRANPOSE_PIPELINE_PREWARM").as_deref() != Ok("0")
1510}
1511
1512#[cfg(not(target_arch = "wasm32"))]
1513struct PipelinePrewarmInputs {
1514 device: Arc<wgpu::Device>,
1515 cache: Option<wgpu::PipelineCache>,
1516 surface_format: wgpu::TextureFormat,
1517 uniform_layout: wgpu::BindGroupLayout,
1518 shape_layout: wgpu::BindGroupLayout,
1519 image_layout: wgpu::BindGroupLayout,
1520 batch_limits: ShapeBatchLimits,
1521 instanced: bool,
1522}
1523
1524#[cfg(not(target_arch = "wasm32"))]
1535fn spawn_pipeline_prewarm(inputs: PipelinePrewarmInputs) {
1536 if !pipeline_prewarm_enabled() {
1537 return;
1538 }
1539 let spawned = std::thread::Builder::new()
1540 .name("cranpose-pl-warm".into())
1541 .spawn(move || {
1542 let started = Instant::now();
1543 let cache = inputs.cache.as_ref();
1544 let device = &inputs.device;
1545 let solid_trim = solid_trim_varyings_enabled();
1546 let mut built = 0_u32;
1547 if inputs.instanced {
1548 let (vertex_entry, fragment_entry) = if solid_trim {
1549 ("vs_solid_instanced", "fs_solid_trim")
1550 } else {
1551 ("vs_shape_instanced", "fs_solid")
1552 };
1553 drop(create_instanced_shape_pipeline(
1554 device,
1555 cache,
1556 inputs.surface_format,
1557 &inputs.uniform_layout,
1558 &inputs.shape_layout,
1559 BlendMode::SrcOver,
1560 inputs.batch_limits,
1561 solid_trim,
1562 vertex_entry,
1563 fragment_entry,
1564 false,
1565 ));
1566 drop(create_instanced_shape_pipeline(
1567 device,
1568 cache,
1569 inputs.surface_format,
1570 &inputs.uniform_layout,
1571 &inputs.shape_layout,
1572 BlendMode::SrcOver,
1573 inputs.batch_limits,
1574 false,
1575 "vs_shape_instanced",
1576 "fs_main",
1577 false,
1578 ));
1579 } else {
1580 let (vertex_entry, fragment_entry) = if solid_trim {
1581 ("vs_solid", "fs_solid_trim")
1582 } else {
1583 ("vs_main", "fs_solid")
1584 };
1585 drop(create_shape_pipeline(
1586 device,
1587 cache,
1588 inputs.surface_format,
1589 &inputs.uniform_layout,
1590 &inputs.shape_layout,
1591 BlendMode::SrcOver,
1592 inputs.batch_limits,
1593 solid_trim,
1594 vertex_entry,
1595 fragment_entry,
1596 false,
1597 ));
1598 drop(create_shape_pipeline(
1599 device,
1600 cache,
1601 inputs.surface_format,
1602 &inputs.uniform_layout,
1603 &inputs.shape_layout,
1604 BlendMode::SrcOver,
1605 inputs.batch_limits,
1606 false,
1607 "vs_main",
1608 "fs_main",
1609 false,
1610 ));
1611 }
1612 built += 2;
1613 if inputs.batch_limits.storage {
1614 drop(create_mesh_shape_pipeline(
1615 device,
1616 cache,
1617 inputs.surface_format,
1618 &inputs.uniform_layout,
1619 &inputs.shape_layout,
1620 inputs.batch_limits,
1621 false,
1622 ));
1623 built += 1;
1624 }
1625 drop(create_glyph_atlas_pipeline(
1626 device,
1627 cache,
1628 inputs.surface_format,
1629 &inputs.uniform_layout,
1630 &inputs.image_layout,
1631 false,
1632 ));
1633 built += 1;
1634 log::info!(
1635 "[pipeline-prewarm] {built} pipelines in {:.1} ms",
1636 instant_ms(started, Instant::now())
1637 );
1638 });
1639 if let Err(error) = spawned {
1640 log::warn!("[pipeline-prewarm] thread failed to spawn: {error}");
1641 }
1642}
1643
1644#[allow(clippy::too_many_arguments)]
1645fn create_shape_pipeline(
1646 device: &wgpu::Device,
1647 cache: Option<&wgpu::PipelineCache>,
1648 surface_format: wgpu::TextureFormat,
1649 uniform_layout: &wgpu::BindGroupLayout,
1650 shape_layout: &wgpu::BindGroupLayout,
1651 blend_mode: BlendMode,
1652 batch_limits: ShapeBatchLimits,
1653 solid_trim: bool,
1654 vertex_entry: &'static str,
1655 fragment_entry: &'static str,
1656 depth: bool,
1657) -> wgpu::RenderPipeline {
1658 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1659 label: Some("Shape Shader"),
1660 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1661 shape_shader_source(batch_limits, solid_trim),
1662 depth,
1663 )),
1664 });
1665
1666 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1667 label: Some("Render Pipeline Layout"),
1668 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1669 immediate_size: 0,
1670 });
1671
1672 create_render_pipeline_logged(
1673 device,
1674 cache,
1675 &format!("shape entry={fragment_entry} blend={blend_mode:?} depth={depth}"),
1676 wgpu::RenderPipelineDescriptor {
1677 label: Some("Render Pipeline"),
1678 layout: Some(&pipeline_layout),
1679 vertex: wgpu::VertexState {
1680 module: &shader,
1681 entry_point: Some(vertex_entry),
1682 compilation_options: wgpu::PipelineCompilationOptions::default(),
1683 buffers: &[],
1686 },
1687 fragment: Some(wgpu::FragmentState {
1688 module: &shader,
1689 entry_point: Some(fragment_entry),
1690 compilation_options: wgpu::PipelineCompilationOptions::default(),
1691 targets: &[Some(wgpu::ColorTargetState {
1692 format: surface_format,
1693 blend: Some(blend_state_for_mode(blend_mode)),
1694 write_mask: wgpu::ColorWrites::ALL,
1695 })],
1696 }),
1697 primitive: wgpu::PrimitiveState {
1698 topology: wgpu::PrimitiveTopology::TriangleList,
1699 strip_index_format: None,
1700 front_face: wgpu::FrontFace::Ccw,
1701 cull_mode: None,
1702 unclipped_depth: false,
1703 polygon_mode: wgpu::PolygonMode::Fill,
1704 conservative: false,
1705 },
1706 depth_stencil: display_clip::content_depth_state(depth),
1707 multisample: wgpu::MultisampleState::default(),
1708 multiview_mask: None,
1709 cache: None,
1710 },
1711 )
1712}
1713
1714#[cfg(not(target_arch = "wasm32"))]
1721fn create_mesh_shape_pipeline(
1722 device: &wgpu::Device,
1723 cache: Option<&wgpu::PipelineCache>,
1724 surface_format: wgpu::TextureFormat,
1725 uniform_layout: &wgpu::BindGroupLayout,
1726 shape_layout: &wgpu::BindGroupLayout,
1727 batch_limits: ShapeBatchLimits,
1728 depth: bool,
1729) -> wgpu::RenderPipeline {
1730 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1731 label: Some("Shape Mesh Shader"),
1732 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1735 shape_shader_source(batch_limits, false),
1736 depth,
1737 )),
1738 });
1739
1740 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1741 label: Some("Mesh Render Pipeline Layout"),
1742 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1743 immediate_size: 0,
1744 });
1745
1746 create_render_pipeline_logged(
1747 device,
1748 cache,
1749 &format!("mesh depth={depth}"),
1750 wgpu::RenderPipelineDescriptor {
1751 label: Some("Retained Mesh Pipeline"),
1752 layout: Some(&pipeline_layout),
1753 vertex: wgpu::VertexState {
1754 module: &shader,
1755 entry_point: Some("vs_mesh"),
1756 compilation_options: wgpu::PipelineCompilationOptions::default(),
1757 buffers: &[MeshVertex::desc()],
1758 },
1759 fragment: Some(wgpu::FragmentState {
1760 module: &shader,
1761 entry_point: Some("fs_main"),
1762 compilation_options: wgpu::PipelineCompilationOptions::default(),
1763 targets: &[Some(wgpu::ColorTargetState {
1764 format: surface_format,
1765 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1766 write_mask: wgpu::ColorWrites::ALL,
1767 })],
1768 }),
1769 primitive: wgpu::PrimitiveState {
1770 topology: wgpu::PrimitiveTopology::TriangleList,
1771 strip_index_format: None,
1772 front_face: wgpu::FrontFace::Ccw,
1773 cull_mode: None,
1774 unclipped_depth: false,
1775 polygon_mode: wgpu::PolygonMode::Fill,
1776 conservative: false,
1777 },
1778 depth_stencil: display_clip::content_depth_state(depth),
1779 multisample: wgpu::MultisampleState::default(),
1780 multiview_mask: None,
1781 cache: None,
1782 },
1783 )
1784}
1785
1786#[cfg(not(target_arch = "wasm32"))]
1794#[allow(clippy::too_many_arguments)]
1795fn create_instanced_shape_pipeline(
1796 device: &wgpu::Device,
1797 cache: Option<&wgpu::PipelineCache>,
1798 surface_format: wgpu::TextureFormat,
1799 uniform_layout: &wgpu::BindGroupLayout,
1800 shape_layout: &wgpu::BindGroupLayout,
1801 blend_mode: BlendMode,
1802 batch_limits: ShapeBatchLimits,
1803 solid_trim: bool,
1804 vertex_entry: &'static str,
1805 fragment_entry: &'static str,
1806 depth: bool,
1807) -> wgpu::RenderPipeline {
1808 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1809 label: Some("Shape Instanced Shader"),
1810 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1811 shape_shader_source(batch_limits, solid_trim),
1812 depth,
1813 )),
1814 });
1815
1816 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1817 label: Some("Instanced Render Pipeline Layout"),
1818 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1819 immediate_size: 0,
1820 });
1821
1822 create_render_pipeline_logged(
1823 device,
1824 cache,
1825 &format!("instanced entry={fragment_entry} blend={blend_mode:?} depth={depth}"),
1826 wgpu::RenderPipelineDescriptor {
1827 label: Some("Instanced Render Pipeline"),
1828 layout: Some(&pipeline_layout),
1829 vertex: wgpu::VertexState {
1830 module: &shader,
1831 entry_point: Some(vertex_entry),
1832 compilation_options: wgpu::PipelineCompilationOptions::default(),
1833 buffers: &[],
1837 },
1838 fragment: Some(wgpu::FragmentState {
1839 module: &shader,
1840 entry_point: Some(fragment_entry),
1841 compilation_options: wgpu::PipelineCompilationOptions::default(),
1842 targets: &[Some(wgpu::ColorTargetState {
1843 format: surface_format,
1844 blend: Some(blend_state_for_mode(blend_mode)),
1845 write_mask: wgpu::ColorWrites::ALL,
1846 })],
1847 }),
1848 primitive: wgpu::PrimitiveState {
1849 topology: wgpu::PrimitiveTopology::TriangleList,
1850 strip_index_format: None,
1851 front_face: wgpu::FrontFace::Ccw,
1852 cull_mode: None,
1853 unclipped_depth: false,
1854 polygon_mode: wgpu::PolygonMode::Fill,
1855 conservative: false,
1856 },
1857 depth_stencil: display_clip::content_depth_state(depth),
1858 multisample: wgpu::MultisampleState::default(),
1859 multiview_mask: None,
1860 cache: None,
1861 },
1862 )
1863}
1864
1865fn create_image_pipeline(
1866 device: &wgpu::Device,
1867 cache: Option<&wgpu::PipelineCache>,
1868 surface_format: wgpu::TextureFormat,
1869 uniform_layout: &wgpu::BindGroupLayout,
1870 image_layout: &wgpu::BindGroupLayout,
1871 blend_mode: BlendMode,
1872 depth: bool,
1873) -> wgpu::RenderPipeline {
1874 let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1875 label: Some("Image Shader"),
1876 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1877 shaders::IMAGE_SHADER.into(),
1878 depth,
1879 )),
1880 });
1881
1882 let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1883 label: Some("Image Pipeline Layout"),
1884 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1885 immediate_size: 0,
1886 });
1887
1888 create_render_pipeline_logged(
1889 device,
1890 cache,
1891 &format!("image blend={blend_mode:?} depth={depth}"),
1892 wgpu::RenderPipelineDescriptor {
1893 label: Some("Image Pipeline"),
1894 layout: Some(&image_pipeline_layout),
1895 vertex: wgpu::VertexState {
1896 module: &image_shader,
1897 entry_point: Some("image_vs_main"),
1898 compilation_options: wgpu::PipelineCompilationOptions::default(),
1899 buffers: &[Vertex::desc()],
1900 },
1901 fragment: Some(wgpu::FragmentState {
1902 module: &image_shader,
1903 entry_point: Some("image_fs_main"),
1904 compilation_options: wgpu::PipelineCompilationOptions::default(),
1905 targets: &[Some(wgpu::ColorTargetState {
1906 format: surface_format,
1907 blend: Some(blend_state_for_mode(blend_mode)),
1908 write_mask: wgpu::ColorWrites::ALL,
1909 })],
1910 }),
1911 primitive: wgpu::PrimitiveState {
1912 topology: wgpu::PrimitiveTopology::TriangleList,
1913 strip_index_format: None,
1914 front_face: wgpu::FrontFace::Ccw,
1915 cull_mode: None,
1916 unclipped_depth: false,
1917 polygon_mode: wgpu::PolygonMode::Fill,
1918 conservative: false,
1919 },
1920 depth_stencil: display_clip::content_depth_state(depth),
1921 multisample: wgpu::MultisampleState::default(),
1922 multiview_mask: None,
1923 cache: None,
1924 },
1925 )
1926}
1927
1928fn create_glyph_atlas_pipeline(
1929 device: &wgpu::Device,
1930 cache: Option<&wgpu::PipelineCache>,
1931 surface_format: wgpu::TextureFormat,
1932 uniform_layout: &wgpu::BindGroupLayout,
1933 image_layout: &wgpu::BindGroupLayout,
1934 depth: bool,
1935) -> wgpu::RenderPipeline {
1936 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1937 label: Some("Glyph Atlas Shader"),
1938 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1939 shaders::GLYPH_ATLAS_SHADER.into(),
1940 depth,
1941 )),
1942 });
1943
1944 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1945 label: Some("Glyph Atlas Pipeline Layout"),
1946 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1947 immediate_size: 0,
1948 });
1949
1950 create_render_pipeline_logged(
1951 device,
1952 cache,
1953 &format!("glyph-atlas depth={depth}"),
1954 wgpu::RenderPipelineDescriptor {
1955 label: Some("Glyph Atlas Pipeline"),
1956 layout: Some(&pipeline_layout),
1957 vertex: wgpu::VertexState {
1958 module: &shader,
1959 entry_point: Some("glyph_atlas_vs_main"),
1960 compilation_options: wgpu::PipelineCompilationOptions::default(),
1961 buffers: &[Vertex::desc()],
1962 },
1963 fragment: Some(wgpu::FragmentState {
1964 module: &shader,
1965 entry_point: Some("glyph_atlas_fs_main"),
1966 compilation_options: wgpu::PipelineCompilationOptions::default(),
1967 targets: &[Some(wgpu::ColorTargetState {
1968 format: surface_format,
1969 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1970 write_mask: wgpu::ColorWrites::ALL,
1971 })],
1972 }),
1973 primitive: wgpu::PrimitiveState {
1974 topology: wgpu::PrimitiveTopology::TriangleList,
1975 strip_index_format: None,
1976 front_face: wgpu::FrontFace::Ccw,
1977 cull_mode: None,
1978 unclipped_depth: false,
1979 polygon_mode: wgpu::PolygonMode::Fill,
1980 conservative: false,
1981 },
1982 depth_stencil: display_clip::content_depth_state(depth),
1983 multisample: wgpu::MultisampleState::default(),
1984 multiview_mask: None,
1985 cache: None,
1986 },
1987 )
1988}
1989
1990#[cfg(not(target_arch = "wasm32"))]
1998fn create_display_clip_occluder_pipeline(
1999 device: &wgpu::Device,
2000 cache: Option<&wgpu::PipelineCache>,
2001 surface_format: wgpu::TextureFormat,
2002) -> wgpu::RenderPipeline {
2003 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2004 label: Some("Display Clip Occluder Shader"),
2005 source: wgpu::ShaderSource::Wgsl(display_clip::OCCLUDER_SHADER.into()),
2006 });
2007 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2008 label: Some("Display Clip Occluder Pipeline Layout"),
2009 bind_group_layouts: &[],
2010 immediate_size: 0,
2011 });
2012 create_render_pipeline_logged(
2013 device,
2014 cache,
2015 "occluder",
2016 wgpu::RenderPipelineDescriptor {
2017 label: Some("Display Clip Occluder Pipeline"),
2018 layout: Some(&pipeline_layout),
2019 vertex: wgpu::VertexState {
2020 module: &shader,
2021 entry_point: Some("mask_vs"),
2022 compilation_options: wgpu::PipelineCompilationOptions::default(),
2023 buffers: &[wgpu::VertexBufferLayout {
2024 array_stride: (std::mem::size_of::<[f32; 2]>()) as wgpu::BufferAddress,
2025 step_mode: wgpu::VertexStepMode::Vertex,
2026 attributes: &[wgpu::VertexAttribute {
2027 offset: 0,
2028 shader_location: 0,
2029 format: wgpu::VertexFormat::Float32x2,
2030 }],
2031 }],
2032 },
2033 fragment: Some(wgpu::FragmentState {
2034 module: &shader,
2035 entry_point: Some("mask_fs"),
2036 compilation_options: wgpu::PipelineCompilationOptions::default(),
2037 targets: &[Some(wgpu::ColorTargetState {
2038 format: surface_format,
2039 blend: None,
2040 write_mask: wgpu::ColorWrites::empty(),
2041 })],
2042 }),
2043 primitive: wgpu::PrimitiveState {
2044 topology: wgpu::PrimitiveTopology::TriangleList,
2045 strip_index_format: None,
2046 front_face: wgpu::FrontFace::Ccw,
2047 cull_mode: None,
2048 unclipped_depth: false,
2049 polygon_mode: wgpu::PolygonMode::Fill,
2050 conservative: false,
2051 },
2052 depth_stencil: Some(wgpu::DepthStencilState {
2053 format: display_clip::DISPLAY_CLIP_DEPTH_FORMAT,
2054 depth_write_enabled: Some(true),
2055 depth_compare: Some(wgpu::CompareFunction::Always),
2056 stencil: wgpu::StencilState::default(),
2057 bias: wgpu::DepthBiasState::default(),
2058 }),
2059 multisample: wgpu::MultisampleState::default(),
2060 multiview_mask: None,
2061 cache: None,
2062 },
2063 )
2064}
2065
2066#[repr(C)]
2067#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2068struct Vertex {
2069 position: [f32; 2],
2070 color: [f32; 4],
2071 uv: [f32; 2],
2072 uv_bounds: [f32; 4],
2073}
2074
2075impl Vertex {
2076 const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
2077 0 => Float32x2,
2078 1 => Float32x4,
2079 2 => Float32x2,
2080 3 => Float32x4
2081 ];
2082
2083 fn desc() -> wgpu::VertexBufferLayout<'static> {
2084 wgpu::VertexBufferLayout {
2085 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
2086 step_mode: wgpu::VertexStepMode::Vertex,
2087 attributes: &Self::ATTRIBS,
2088 }
2089 }
2090}
2091
2092#[repr(C)]
2093#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2094struct Uniforms {
2095 viewport: [f32; 2],
2096 viewport_offset: [f32; 2],
2097}
2098
2099#[repr(C)]
2105#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2106struct ShapeData {
2107 rect: [f32; 4], radii: [f32; 4],
2112 gradient_params: [f32; 4], clip_rect: [f32; 4], stroke_params: [f32; 4],
2117 arc_params: [f32; 4],
2119 quad01: [f32; 4],
2121 quad23: [f32; 4],
2123 color: [f32; 4],
2125 brush_type: u32, gradient_start: u32, gradient_count: u32, gradient_tile_mode: u32, }
2130
2131const SHAPE_KIND_FILL: u32 = 0;
2133const SHAPE_KIND_STROKE: u32 = 1;
2134const SHAPE_KIND_ARC: u32 = 2;
2135
2136fn stroke_cap_code(cap: StrokeCap) -> u32 {
2137 match cap {
2138 StrokeCap::Butt => 0,
2139 StrokeCap::Round => 1,
2140 StrokeCap::Square => 2,
2141 }
2142}
2143
2144fn stroke_join_code(join: StrokeJoin) -> u32 {
2145 match join {
2146 StrokeJoin::Miter => 0,
2147 StrokeJoin::Round => 1,
2148 StrokeJoin::Bevel => 2,
2149 }
2150}
2151
2152fn pack_shape_flags(kind: u32, cap: StrokeCap, join: StrokeJoin) -> f32 {
2158 ((kind & 3) | (stroke_cap_code(cap) << 2) | (stroke_join_code(join) << 4)) as f32
2159}
2160
2161#[cfg(not(target_arch = "wasm32"))]
2169static SHAPE_CONVERT_TUNER: crate::cost_tuner::CostTuner =
2170 crate::cost_tuner::CostTuner::new("shape-convert", 256, 400_000);
2171
2172#[cfg(not(target_arch = "wasm32"))]
2173pub(crate) fn shape_convert_worker_count() -> usize {
2174 static WORKERS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2175 *WORKERS.get_or_init(|| {
2176 let cpus = std::thread::available_parallelism()
2177 .map(|count| count.get())
2178 .unwrap_or(1);
2179 let workers = cpus.clamp(1, 4);
2180 log::info!("[shape-convert] fan-out width {workers} (available parallelism {cpus})");
2184 workers
2185 })
2186}
2187
2188#[cfg(target_arch = "wasm32")]
2189pub(crate) fn shape_convert_worker_count() -> usize {
2190 1
2191}
2192
2193fn shape_gradient_stop_count(shape: &DrawShape, brushes: &[Brush]) -> usize {
2194 match shape.brush {
2195 SceneBrush::Solid(_) => 0,
2196 SceneBrush::Gradient(index) => match &brushes[index as usize] {
2197 Brush::Solid(_) => 0,
2198 Brush::LinearGradient { colors, .. }
2199 | Brush::RadialGradient { colors, .. }
2200 | Brush::SweepGradient { colors, .. } => colors.len(),
2201 },
2202 }
2203}
2204
2205fn convert_shape_into_slots(
2210 shape: &DrawShape,
2211 brushes: &[Brush],
2212 root_scale: f32,
2213 gradient_start: u32,
2214 shape_out: &mut ShapeData,
2215 gradient_out: &mut [GradientStop],
2216) {
2217 let snap_delta = shape
2218 .snap_anchor
2219 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
2220 .unwrap_or_default();
2221 let local_rect = shape.local_rect.translate(snap_delta.x, snap_delta.y);
2222 let quad = translate_quad(shape.quad, snap_delta);
2223 let clip = shape.clip;
2226 let canonicalize = shape.snap_anchor.is_some();
2227 let device_local_rect = if canonicalize {
2228 canonicalized_scaled_rect(local_rect, root_scale)
2229 } else {
2230 Rect {
2231 x: local_rect.x * root_scale,
2232 y: local_rect.y * root_scale,
2233 width: local_rect.width * root_scale,
2234 height: local_rect.height * root_scale,
2235 }
2236 };
2237 let device_quad = if canonicalize {
2238 canonicalized_scaled_quad(quad, root_scale)
2239 } else {
2240 scaled_quad(quad, root_scale)
2241 };
2242 let canonicalize_brush_coordinate = |value| {
2243 if canonicalize {
2244 canonicalize_device_coordinate(value)
2245 } else {
2246 value
2247 }
2248 };
2249
2250 let clip_rect = if let Some(clip) = clip {
2252 let device_clip = if canonicalize {
2253 canonicalized_scaled_rect(clip, root_scale)
2254 } else {
2255 Rect {
2256 x: clip.x * root_scale,
2257 y: clip.y * root_scale,
2258 width: clip.width * root_scale,
2259 height: clip.height * root_scale,
2260 }
2261 };
2262 [
2263 device_clip.x,
2264 device_clip.y,
2265 device_clip.width,
2266 device_clip.height,
2267 ]
2268 } else {
2269 [0.0, 0.0, 0.0, 0.0]
2270 };
2271
2272 let mut fill_gradient_entries = |colors: &[Color], stops: Option<&[f32]>| {
2274 let count = colors.len();
2275 let explicit_stops = stops.filter(|values| values.len() == count);
2276 for (index, color) in colors.iter().enumerate() {
2277 let position = explicit_stops
2278 .map(|values| values[index])
2279 .unwrap_or_else(|| {
2280 if count <= 1 {
2281 0.0
2282 } else {
2283 index as f32 / (count - 1) as f32
2284 }
2285 });
2286 gradient_out[index] = GradientStop {
2287 color: [color.r(), color.g(), color.b(), color.a()],
2288 position: [position, 0.0, 0.0, 0.0],
2289 };
2290 }
2291 count as u32
2292 };
2293 let mut gradient_params = [0.0f32; 4];
2294 let (brush_type, gradient_count, gradient_tile_mode) = match &shape.brush {
2295 SceneBrush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
2296 SceneBrush::Gradient(index) => match &brushes[*index as usize] {
2297 Brush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
2298 Brush::LinearGradient {
2299 colors,
2300 stops,
2301 start,
2302 end,
2303 tile_mode,
2304 } => {
2305 let count = fill_gradient_entries(colors, stops.as_deref());
2306 gradient_params = [
2307 canonicalize_brush_coordinate(resolve_gradient_point(
2308 device_local_rect.x,
2309 device_local_rect.width,
2310 start.x * root_scale,
2311 )),
2312 canonicalize_brush_coordinate(resolve_gradient_point(
2313 device_local_rect.y,
2314 device_local_rect.height,
2315 start.y * root_scale,
2316 )),
2317 canonicalize_brush_coordinate(resolve_gradient_point(
2318 device_local_rect.x,
2319 device_local_rect.width,
2320 end.x * root_scale,
2321 )),
2322 canonicalize_brush_coordinate(resolve_gradient_point(
2323 device_local_rect.y,
2324 device_local_rect.height,
2325 end.y * root_scale,
2326 )),
2327 ];
2328 (1u32, count, gradient_tile_mode_value(*tile_mode))
2329 }
2330 Brush::RadialGradient {
2331 colors,
2332 stops,
2333 center,
2334 radius,
2335 tile_mode,
2336 } => {
2337 let count = fill_gradient_entries(colors, stops.as_deref());
2338 gradient_params = [
2339 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
2340 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
2341 (radius * root_scale).max(f32::EPSILON),
2342 0.0,
2343 ];
2344 (2u32, count, gradient_tile_mode_value(*tile_mode))
2345 }
2346 Brush::SweepGradient {
2347 colors,
2348 stops,
2349 center,
2350 } => {
2351 let count = fill_gradient_entries(colors, stops.as_deref());
2352 gradient_params = [
2353 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
2354 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
2355 0.0,
2356 0.0,
2357 ];
2358 (3u32, count, gradient_tile_mode_value(TileMode::Clamp))
2359 }
2360 },
2361 };
2362
2363 let stroke_outset = shape
2368 .stroke
2369 .map(|stroke| stroke.half_width())
2370 .unwrap_or(0.0);
2371 let geometry_width = (local_rect.width - stroke_outset * 2.0).max(0.0);
2372 let geometry_height = (local_rect.height - stroke_outset * 2.0).max(0.0);
2373
2374 let radii = if let Some(arc) = shape.arc {
2375 if arc.sweep_angle >= cranpose_ui_graphics::TAU && arc.start_angle == 0.0 {
2385 [0.0, -1.0, 0.0, -1.0]
2386 } else {
2387 let half_sweep = arc.sweep_angle.clamp(0.0, cranpose_ui_graphics::TAU) * 0.5;
2388 let (mid_sin, mid_cos) = (arc.start_angle + half_sweep).sin_cos();
2389 let (half_sin, half_cos) = half_sweep.sin_cos();
2390 [mid_sin, mid_cos, half_sin.max(0.0), half_cos]
2391 }
2392 } else if let Some(rounded) = shape.shape {
2393 let resolved = rounded.resolve(geometry_width, geometry_height);
2394 [
2395 resolved.top_left * root_scale,
2396 resolved.top_right * root_scale,
2397 resolved.bottom_left * root_scale,
2398 resolved.bottom_right * root_scale,
2399 ]
2400 } else {
2401 [0.0, 0.0, 0.0, 0.0]
2402 };
2403
2404 let device_rect = [
2405 device_local_rect.x,
2406 device_local_rect.y,
2407 device_local_rect.width,
2408 device_local_rect.height,
2409 ];
2410
2411 let (stroke_params, arc_params) = match (shape.arc, shape.stroke) {
2415 (Some(arc), _) => (
2416 [
2417 0.0,
2418 pack_shape_flags(SHAPE_KIND_ARC, arc.cap, StrokeJoin::Miter),
2419 arc.outer_radius * root_scale,
2420 arc.inner_radius * root_scale,
2421 ],
2422 [
2423 (arc.center.x + snap_delta.x) * root_scale,
2424 (arc.center.y + snap_delta.y) * root_scale,
2425 arc.start_angle,
2426 arc.sweep_angle,
2427 ],
2428 ),
2429 (None, Some(stroke)) => (
2430 [
2431 stroke.width.max(0.0) * root_scale,
2432 pack_shape_flags(SHAPE_KIND_STROKE, stroke.cap, stroke.join),
2433 0.0,
2434 0.0,
2435 ],
2436 [0.0; 4],
2437 ),
2438 (None, None) => (
2439 [
2440 0.0,
2441 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
2442 0.0,
2443 0.0,
2444 ],
2445 [0.0; 4],
2446 ),
2447 };
2448
2449 let color = match &shape.brush {
2450 SceneBrush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2451 SceneBrush::Gradient(index) => match &brushes[*index as usize] {
2452 Brush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2453 Brush::LinearGradient { colors, .. } => {
2454 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2455 [first.r(), first.g(), first.b(), first.a()]
2456 }
2457 Brush::RadialGradient { colors, .. } | Brush::SweepGradient { colors, .. } => {
2458 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2459 [first.r(), first.g(), first.b(), first.a()]
2460 }
2461 },
2462 };
2463
2464 *shape_out = ShapeData {
2465 rect: device_rect,
2466 radii,
2467 gradient_params,
2468 clip_rect,
2469 stroke_params,
2470 arc_params,
2471 quad01: [
2472 device_quad[0][0],
2473 device_quad[0][1],
2474 device_quad[1][0],
2475 device_quad[1][1],
2476 ],
2477 quad23: [
2478 device_quad[2][0],
2479 device_quad[2][1],
2480 device_quad[3][0],
2481 device_quad[3][1],
2482 ],
2483 color,
2484 brush_type,
2485 gradient_start,
2486 gradient_count,
2487 gradient_tile_mode,
2488 };
2489}
2490
2491fn quad_area_diag_enabled() -> bool {
2498 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2499 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_QUAD_AREA_DIAG").is_some())
2500}
2501
2502fn convert_shapes_into_outputs(
2508 shape_refs: &[&DrawShape],
2509 brushes: &[Brush],
2510 gradient_offsets: &[u32],
2511 root_scale: f32,
2512 shape_data_out: &mut [ShapeData],
2513 gradients_out: &mut [GradientStop],
2514) {
2515 let shape_count = shape_refs.len();
2516 #[cfg(not(target_arch = "wasm32"))]
2517 let convert_started = Instant::now();
2518 #[cfg(not(target_arch = "wasm32"))]
2519 let parallel =
2520 SHAPE_CONVERT_TUNER.choose_parallel(shape_count) && shape_convert_worker_count() > 1;
2521 if quad_area_diag_enabled() {
2522 let quad_area = |q: [[f32; 2]; 4]| {
2523 let poly = [q[0], q[1], q[3], q[2]];
2525 let mut twice = 0.0f64;
2526 for i in 0..4 {
2527 let a = poly[i];
2528 let b = poly[(i + 1) % 4];
2529 twice += a[0] as f64 * b[1] as f64 - b[0] as f64 * a[1] as f64;
2530 }
2531 twice.abs() * 0.5
2532 };
2533 let mut arc_quad = 0.0f64; let mut arc_band = 0.0f64; let mut arc_count = 0usize;
2536 let mut ring_count = 0usize;
2537 let mut other_quad = 0.0f64;
2538 let mut other_count = 0usize;
2539 let mut top_other: Vec<(f64, usize)> = Vec::new();
2542 for (index, shape) in shape_refs.iter().enumerate() {
2543 let area = quad_area(shape.quad);
2544 if let Some(arc) = shape.arc {
2545 arc_quad += area;
2546 arc_count += 1;
2547 if arc.sweep_angle >= cranpose_ui_graphics::TAU {
2548 ring_count += 1;
2549 }
2550 let ra = arc.mid_radius() as f64;
2551 let rb = arc.half_thickness() as f64;
2552 arc_band +=
2553 arc.sweep_angle as f64 * ra * (2.0 * rb) + std::f64::consts::PI * rb * rb;
2554 } else {
2555 other_quad += area;
2556 other_count += 1;
2557 top_other.push((area, index));
2558 }
2559 }
2560 let scale2 = (root_scale as f64) * (root_scale as f64);
2561 eprintln!(
2562 "[quad-area] arcs={arc_count} (rings={ring_count}) arc_quad_px={:.0} arc_band_px={:.0} | other={other_count} other_px={:.0}",
2563 arc_quad * scale2,
2564 arc_band * scale2,
2565 other_quad * scale2,
2566 );
2567 top_other.sort_by(|a, b| b.0.total_cmp(&a.0));
2568 for &(area, index) in top_other.iter().take(4) {
2569 let shape = shape_refs[index];
2570 let brush = match shape.brush.resolve(brushes).as_ref() {
2571 cranpose_ui_graphics::Brush::Solid(color) => format!("solid a={:.2}", color.3),
2572 cranpose_ui_graphics::Brush::LinearGradient { colors, .. } => {
2573 format!("linear n={}", colors.len())
2574 }
2575 cranpose_ui_graphics::Brush::RadialGradient { colors, .. } => {
2576 format!("radial n={}", colors.len())
2577 }
2578 cranpose_ui_graphics::Brush::SweepGradient { colors, .. } => {
2579 format!("sweep n={}", colors.len())
2580 }
2581 };
2582 eprintln!(
2583 "[quad-area] top other: {:.0}px {}x{} at ({:.0},{:.0}) {} shape={} stroke={} clip={} blend={:?} z={}",
2584 area * scale2,
2585 shape.rect.width.round(),
2586 shape.rect.height.round(),
2587 shape.rect.x,
2588 shape.rect.y,
2589 brush,
2590 shape.shape.is_some(),
2591 shape.stroke.is_some(),
2592 shape.clip.is_some(),
2593 shape.blend_mode,
2594 shape.z_index,
2595 );
2596 }
2597 }
2598 #[cfg(target_arch = "wasm32")]
2599 let parallel = false;
2600 let workers = if parallel {
2601 shape_convert_worker_count()
2602 } else {
2603 1
2604 };
2605 if workers <= 1 {
2606 for (idx, shape) in shape_refs.iter().enumerate() {
2607 let gradient_start = gradient_offsets[idx];
2608 let gradient_end = gradient_offsets[idx + 1];
2609 convert_shape_into_slots(
2610 shape,
2611 brushes,
2612 root_scale,
2613 gradient_start,
2614 &mut shape_data_out[idx],
2615 &mut gradients_out[gradient_start as usize..gradient_end as usize],
2616 );
2617 }
2618 #[cfg(not(target_arch = "wasm32"))]
2619 SHAPE_CONVERT_TUNER.record(
2620 false,
2621 shape_count,
2622 convert_started.elapsed().as_nanos() as u64,
2623 );
2624 return;
2625 }
2626
2627 let chunk_len = shape_count.div_ceil(workers);
2628 let mut shape_data_rest = shape_data_out;
2629 let mut gradients_rest = gradients_out;
2630 std::thread::scope(|scope| {
2631 let mut chunk_start = 0usize;
2632 while chunk_start < shape_count {
2633 let chunk_end = (chunk_start + chunk_len).min(shape_count);
2634 let count = chunk_end - chunk_start;
2635 let gradient_base = gradient_offsets[chunk_start];
2636 let gradient_span = (gradient_offsets[chunk_end] - gradient_base) as usize;
2637 let (shape_data_chunk, rest) = std::mem::take(&mut shape_data_rest).split_at_mut(count);
2638 shape_data_rest = rest;
2639 let (gradient_chunk, rest) =
2640 std::mem::take(&mut gradients_rest).split_at_mut(gradient_span);
2641 gradients_rest = rest;
2642 let chunk_refs = &shape_refs[chunk_start..chunk_end];
2643 let chunk_offsets = &gradient_offsets[chunk_start..=chunk_end];
2644 let mut convert_chunk = move || {
2645 for (j, shape) in chunk_refs.iter().enumerate() {
2646 let gradient_start = chunk_offsets[j];
2647 let local_start = (gradient_start - gradient_base) as usize;
2648 let local_end = (chunk_offsets[j + 1] - gradient_base) as usize;
2649 convert_shape_into_slots(
2650 shape,
2651 brushes,
2652 root_scale,
2653 gradient_start,
2654 &mut shape_data_chunk[j],
2655 &mut gradient_chunk[local_start..local_end],
2656 );
2657 }
2658 };
2659 if chunk_end == shape_count {
2660 convert_chunk();
2664 } else {
2665 scope.spawn(convert_chunk);
2666 }
2667 chunk_start = chunk_end;
2668 }
2669 });
2670 #[cfg(not(target_arch = "wasm32"))]
2671 SHAPE_CONVERT_TUNER.record(
2672 true,
2673 shape_count,
2674 convert_started.elapsed().as_nanos() as u64,
2675 );
2676}
2677
2678#[repr(C)]
2679#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2680struct GradientStop {
2681 color: [f32; 4],
2682 position: [f32; 4],
2683}
2684
2685#[cfg(not(target_arch = "wasm32"))]
2689const MAX_REPLAY_SLOTS: u32 = 128;
2690#[cfg(not(target_arch = "wasm32"))]
2691const REPLAY_TRANSFORM_STRIDE: u64 = 256;
2692
2693#[cfg(not(target_arch = "wasm32"))]
2700struct ReplaySlot {
2701 paint_buffer: wgpu::Buffer,
2706 bind_group: wgpu::BindGroup,
2707 shape_count: u32,
2708 paint_mirror: Vec<[f32; 4]>,
2714 mesh: Option<ReplaySlotMesh>,
2719 capture_epoch: u64,
2724 has_gradient: bool,
2729 fill_diag_shapes: Vec<FillDiagShapeRecord>,
2735 shape_aabbs: Vec<[f32; 4]>,
2741 area_prefix: Vec<f32>,
2745 submitted_area_scale: f32,
2749}
2750
2751#[cfg(not(target_arch = "wasm32"))]
2761struct ReplaySlotMesh {
2762 vertex_buffer: wgpu::Buffer,
2763 index_buffer: wgpu::Buffer,
2769 index_prefix: Vec<u32>,
2775 meshed_arcs: usize,
2780 meshed_rims: usize,
2781 passthrough: usize,
2782}
2783
2784#[cfg(not(target_arch = "wasm32"))]
2788#[repr(C)]
2789#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2790struct MeshVertex {
2791 position: [f32; 2],
2792 uv: [f32; 2],
2793 shape_idx: u32,
2794}
2795
2796#[cfg(not(target_arch = "wasm32"))]
2797impl MeshVertex {
2798 const ATTRIBS: [wgpu::VertexAttribute; 3] =
2799 wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32];
2800
2801 fn desc() -> wgpu::VertexBufferLayout<'static> {
2802 wgpu::VertexBufferLayout {
2803 array_stride: std::mem::size_of::<MeshVertex>() as wgpu::BufferAddress,
2804 step_mode: wgpu::VertexStepMode::Vertex,
2805 attributes: &Self::ATTRIBS,
2806 }
2807 }
2808}
2809
2810#[cfg(not(target_arch = "wasm32"))]
2815fn arc_mesh_enabled() -> bool {
2816 matches!(std::env::var("CRANPOSE_ARC_MESH").as_deref(), Ok("1"))
2832}
2833
2834#[cfg(not(target_arch = "wasm32"))]
2840const ARC_MESH_MARGIN: f32 = 1.0;
2841
2842#[cfg(not(target_arch = "wasm32"))]
2846const ARC_MESH_OVERSHOOT: f32 = 2.0;
2847
2848#[cfg(not(target_arch = "wasm32"))]
2849const ARC_MESH_MIN_SEGMENTS: usize = 4;
2850#[cfg(not(target_arch = "wasm32"))]
2851const ARC_MESH_MAX_SEGMENTS: usize = 64;
2852
2853#[cfg(not(target_arch = "wasm32"))]
2862const ARC_MESH_BUDGET_BYTES_PER_SHAPE: usize = 48 * std::mem::size_of::<MeshVertex>();
2863#[cfg(not(target_arch = "wasm32"))]
2864const ARC_MESH_BUDGET_FLOOR_BYTES: usize = 4096 * std::mem::size_of::<MeshVertex>();
2865
2866#[cfg(not(target_arch = "wasm32"))]
2869fn arc_mesh_bytes(vertices: usize, indices: usize) -> usize {
2870 vertices * std::mem::size_of::<MeshVertex>() + indices * std::mem::size_of::<u32>()
2871}
2872
2873#[cfg(not(target_arch = "wasm32"))]
2884const MESH_SLOT_MAX_STRETCHES: usize = 8;
2885
2886#[cfg(not(target_arch = "wasm32"))]
2909const RETAINED_MESH_MIN_PX2_DEFAULT: usize = 16384;
2910#[cfg(not(target_arch = "wasm32"))]
2915const RETAINED_MESH_MIN_PX2_RANGE: std::ops::RangeInclusive<usize> = 1024..=262144;
2916
2917#[cfg(not(target_arch = "wasm32"))]
2923fn retained_mesh_min_px2() -> f64 {
2924 parse_retained_mesh_min_px2(std::env::var("CRANPOSE_RETAINED_MESH_PX2").ok().as_deref())
2925}
2926
2927#[cfg(not(target_arch = "wasm32"))]
2928fn parse_retained_mesh_min_px2(value: Option<&str>) -> f64 {
2929 value
2930 .and_then(|value| value.trim().parse::<usize>().ok())
2931 .map(|px2| {
2932 px2.clamp(
2933 *RETAINED_MESH_MIN_PX2_RANGE.start(),
2934 *RETAINED_MESH_MIN_PX2_RANGE.end(),
2935 )
2936 })
2937 .unwrap_or(RETAINED_MESH_MIN_PX2_DEFAULT) as f64
2938}
2939
2940#[cfg(not(target_arch = "wasm32"))]
2945struct ArcMeshBand {
2946 center: [f32; 2],
2947 inner: f32,
2948 outer: f32,
2949 start: f32,
2950 sweep: f32,
2951}
2952
2953#[cfg(not(target_arch = "wasm32"))]
2954fn arc_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
2955 let flags = shape.stroke_params[1].max(0.0) as u32;
2957 if flags & 3 != SHAPE_KIND_ARC {
2958 return None;
2959 }
2960 if shape.brush_type != 0 {
2964 return None;
2965 }
2966 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
2971 return None;
2972 }
2973 let [_, _, w, h] = shape.rect;
2974 if !(w > 0.0 && h > 0.0) {
2975 return None;
2976 }
2977 let [left, top, right, _] = shape.quad01;
2985 let [bl_x, bottom, br_x, br_y] = shape.quad23;
2986 let axis_aligned = shape.quad01[3] == top
2987 && bl_x == left
2988 && br_x == right
2989 && br_y == bottom
2990 && left < right
2991 && top < bottom;
2992 if !axis_aligned {
2993 return None;
2994 }
2995 let center = [shape.arc_params[0], shape.arc_params[1]];
2996 let start = shape.arc_params[2];
2997 let sweep = shape.arc_params[3];
2998 let outer = shape.stroke_params[2];
2999 let inner = shape.stroke_params[3];
3000 let finite = center[0].is_finite()
3001 && center[1].is_finite()
3002 && start.is_finite()
3003 && sweep.is_finite()
3004 && outer.is_finite()
3005 && inner.is_finite();
3006 if !finite || outer <= 0.0 || sweep <= 0.0 {
3007 return None;
3008 }
3009 Some(ArcMeshBand {
3010 center,
3011 inner,
3012 outer,
3013 start,
3014 sweep,
3015 })
3016}
3017
3018#[cfg(not(target_arch = "wasm32"))]
3027fn rim_mesh_enabled() -> bool {
3028 !matches!(std::env::var("CRANPOSE_RIM_MESH").as_deref(), Ok("0"))
3029}
3030
3031#[cfg(not(target_arch = "wasm32"))]
3039const RIM_MESH_VERTEX_CAPACITY: usize = 8192;
3040#[cfg(not(target_arch = "wasm32"))]
3043const RIM_MESH_INDEX_CAPACITY: usize = 32768;
3044
3045#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
3051#[derive(Clone, Copy, Debug)]
3052struct RimDraw {
3053 shape_index: u32,
3054 first_index: u32,
3055 index_count: u32,
3056}
3057
3058#[cfg(not(target_arch = "wasm32"))]
3062fn rim_mesh_capacity_warn() {
3063 use std::sync::atomic::{AtomicU64, Ordering};
3064 static OVERFLOWS: AtomicU64 = AtomicU64::new(0);
3065 let count = OVERFLOWS.fetch_add(1, Ordering::Relaxed);
3066 if count.is_multiple_of(512) {
3067 log::warn!(
3068 "[rim-mesh] transient buffers full; rim falls back to quad expansion \
3069 (lifetime overflows {})",
3070 count + 1,
3071 );
3072 }
3073}
3074
3075#[cfg(not(target_arch = "wasm32"))]
3104fn rim_band_geometry(shape: &ShapeData) -> Option<ArcMeshBand> {
3105 let flags = shape.stroke_params[1].max(0.0) as u32;
3107 if flags & 3 != SHAPE_KIND_STROKE {
3108 return None;
3109 }
3110 if shape.brush_type != 0 {
3113 return None;
3114 }
3115 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
3119 return None;
3120 }
3121 let [x, y, w, h] = shape.rect;
3122 if !(w > 0.0 && h > 0.0) {
3123 return None;
3124 }
3125 let [left, top, right, _] = shape.quad01;
3130 let [bl_x, bottom, br_x, br_y] = shape.quad23;
3131 let axis_aligned = shape.quad01[3] == top
3132 && bl_x == left
3133 && br_x == right
3134 && br_y == bottom
3135 && left < right
3136 && top < bottom;
3137 if !axis_aligned {
3138 return None;
3139 }
3140 if w.to_bits() != h.to_bits() {
3142 return None;
3143 }
3144 let [r0, r1, r2, r3] = shape.radii;
3146 if r0.to_bits() != r1.to_bits() || r0.to_bits() != r2.to_bits() || r0.to_bits() != r3.to_bits()
3147 {
3148 return None;
3149 }
3150 if !r0.is_finite() || r0 <= 0.0 {
3151 return None;
3152 }
3153 let sw = shape.stroke_params[0];
3154 if !sw.is_finite() || sw <= 0.0 {
3155 return None;
3156 }
3157 let geom_half = (w - sw) * 0.5;
3160 let center = [x + w * 0.5, y + h * 0.5];
3161 let inner = geom_half - sw * 0.5;
3162 let outer = geom_half + sw * 0.5;
3163 let finite =
3164 center[0].is_finite() && center[1].is_finite() && inner.is_finite() && outer.is_finite();
3165 if !finite || outer <= 0.0 {
3166 return None;
3167 }
3168 if (r0 - geom_half).abs() > 0.01 {
3170 return None;
3171 }
3172 Some(ArcMeshBand {
3173 center,
3174 inner,
3175 outer,
3176 start: 0.0,
3177 sweep: cranpose_ui_graphics::TAU,
3178 })
3179}
3180
3181#[cfg(not(target_arch = "wasm32"))]
3185fn rim_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
3186 let [_, _, w, h] = shape.rect;
3187 if w * h < 65536.0 {
3188 return None;
3189 }
3190 rim_band_geometry(shape)
3191}
3192
3193#[cfg(not(target_arch = "wasm32"))]
3200fn static_span_enabled() -> bool {
3201 !matches!(std::env::var("CRANPOSE_STATIC_SPAN").as_deref(), Ok("0"))
3202}
3203
3204#[cfg(not(target_arch = "wasm32"))]
3208const STATIC_SPAN_MAX_SHAPES: usize = 16;
3209
3210#[cfg(not(target_arch = "wasm32"))]
3214const STATIC_SPAN_UPGRADE_FRAMES: u32 = 30;
3215
3216#[cfg(not(target_arch = "wasm32"))]
3219#[derive(Clone, Copy, Debug, PartialEq)]
3220enum StaticSpanDecision {
3221 Pass,
3223 Hit { skip: usize },
3226 Capture { len: usize, clear: wgpu::Color },
3229}
3230
3231#[cfg(not(target_arch = "wasm32"))]
3279#[derive(Default)]
3280struct StaticSpanCache {
3281 texture: Option<OffscreenTarget>,
3285 key_shapes: Vec<ShapeData>,
3287 key_gradients: Vec<GradientStop>,
3289 key_width: u32,
3290 key_height: u32,
3291 key_clear: [u64; 4],
3295 key_has_gradient: bool,
3300 prev_shapes: Vec<ShapeData>,
3303 prev_gradients: Vec<GradientStop>,
3304 extension_stable_frames: u32,
3307 armed: bool,
3312 hits: u64,
3313 recaptures: u64,
3314}
3315
3316#[cfg(not(target_arch = "wasm32"))]
3317impl StaticSpanCache {
3318 fn engage(
3324 &mut self,
3325 load_op: wgpu::LoadOp<wgpu::Color>,
3326 first_batch: Option<(usize, BlendMode, bool)>,
3327 width: u32,
3328 height: u32,
3329 shapes: &[ShapeData],
3330 gradients: &[GradientStop],
3331 ) -> StaticSpanDecision {
3332 if !self.armed || !static_span_enabled() {
3333 return StaticSpanDecision::Pass;
3334 }
3335 let wgpu::LoadOp::Clear(clear) = load_op else {
3336 return StaticSpanDecision::Pass;
3337 };
3338 if clear.a != 1.0 {
3342 return StaticSpanDecision::Pass;
3343 }
3344 self.armed = false;
3345 let Some((batch_len, blend_mode, has_gradient)) = first_batch else {
3346 self.forget_observation();
3347 return StaticSpanDecision::Pass;
3348 };
3349 if blend_mode != BlendMode::SrcOver || batch_len == 0 {
3352 self.forget_observation();
3353 return StaticSpanDecision::Pass;
3354 }
3355 let leading = &shapes[..batch_len.min(STATIC_SPAN_MAX_SHAPES).min(shapes.len())];
3356 if leading.is_empty() {
3357 self.forget_observation();
3358 return StaticSpanDecision::Pass;
3359 }
3360 if !static_span_fullscreen_opaque(&leading[0], width, height) {
3361 self.forget_observation();
3362 return StaticSpanDecision::Pass;
3363 }
3364 let mut eligible = 1;
3367 while eligible < leading.len() && rim_mesh_band(&leading[eligible]).is_none() {
3368 eligible += 1;
3369 }
3370 let leading = &leading[..eligible];
3371 let clear_key = [
3372 clear.r.to_bits(),
3373 clear.g.to_bits(),
3374 clear.b.to_bits(),
3375 clear.a.to_bits(),
3376 ];
3377
3378 let key_len = self.key_shapes.len();
3379 let valid = self.texture.is_some()
3380 && key_len > 0
3381 && key_len <= leading.len()
3382 && self.key_width == width
3383 && self.key_height == height
3384 && self.key_clear == clear_key
3385 && self.key_has_gradient == has_gradient
3386 && span_records_equal(
3387 &self.key_shapes,
3388 &leading[..key_len],
3389 &self.key_gradients,
3390 gradients,
3391 );
3392
3393 let mut stable = 0;
3397 while stable < leading.len()
3398 && stable < self.prev_shapes.len()
3399 && span_records_equal(
3400 &self.prev_shapes[stable..stable + 1],
3401 &leading[stable..stable + 1],
3402 &self.prev_gradients,
3403 gradients,
3404 )
3405 {
3406 stable += 1;
3407 }
3408 self.remember_observation(leading, gradients);
3409
3410 if valid {
3411 if stable > key_len {
3423 self.extension_stable_frames += 1;
3424 if self.extension_stable_frames >= STATIC_SPAN_UPGRADE_FRAMES {
3425 self.extension_stable_frames = 0;
3426 return StaticSpanDecision::Capture { len: stable, clear };
3427 }
3428 } else {
3429 self.extension_stable_frames = 0;
3430 }
3431 self.hits += 1;
3432 if self.hits.is_multiple_of(600) {
3433 log::debug!(
3434 "[static-span] {} hits / {} recaptures lifetime (span {} shapes, {}x{})",
3435 self.hits,
3436 self.recaptures,
3437 key_len,
3438 width,
3439 height,
3440 );
3441 }
3442 return StaticSpanDecision::Hit { skip: key_len };
3443 }
3444
3445 self.extension_stable_frames = 0;
3446 if stable == 0 || span_gradient_len(&leading[..stable]) == 0 {
3455 return StaticSpanDecision::Pass;
3456 }
3457 StaticSpanDecision::Capture { len: stable, clear }
3458 }
3459
3460 fn remember_observation(&mut self, leading: &[ShapeData], gradients: &[GradientStop]) {
3462 self.prev_shapes.clear();
3463 self.prev_shapes.extend_from_slice(leading);
3464 let stop_len = span_gradient_len(leading);
3465 self.prev_gradients.clear();
3466 self.prev_gradients
3467 .extend_from_slice(&gradients[..stop_len]);
3468 }
3469
3470 fn forget_observation(&mut self) {
3471 self.prev_shapes.clear();
3472 self.prev_gradients.clear();
3473 self.extension_stable_frames = 0;
3474 }
3475
3476 #[allow(clippy::too_many_arguments)]
3479 fn store_key(
3480 &mut self,
3481 span: &[ShapeData],
3482 gradients: &[GradientStop],
3483 width: u32,
3484 height: u32,
3485 clear: wgpu::Color,
3486 has_gradient: bool,
3487 ) {
3488 self.key_shapes.clear();
3489 self.key_shapes.extend_from_slice(span);
3490 let stop_len = span_gradient_len(span);
3491 self.key_gradients.clear();
3492 self.key_gradients.extend_from_slice(&gradients[..stop_len]);
3493 self.key_width = width;
3494 self.key_height = height;
3495 self.key_clear = [
3496 clear.r.to_bits(),
3497 clear.g.to_bits(),
3498 clear.b.to_bits(),
3499 clear.a.to_bits(),
3500 ];
3501 self.key_has_gradient = has_gradient;
3502 self.recaptures += 1;
3503 if self.recaptures.is_multiple_of(64) || self.recaptures == 1 {
3504 log::debug!(
3505 "[static-span] recapture #{} (span {} shapes, {} stops, {}x{}; {} hits lifetime)",
3506 self.recaptures,
3507 self.key_shapes.len(),
3508 self.key_gradients.len(),
3509 width,
3510 height,
3511 self.hits,
3512 );
3513 }
3514 }
3515}
3516
3517#[cfg(not(target_arch = "wasm32"))]
3521fn span_gradient_len(span: &[ShapeData]) -> usize {
3522 span.iter().map(|shape| shape.gradient_count as usize).sum()
3523}
3524
3525#[cfg(not(target_arch = "wasm32"))]
3532fn span_records_equal(
3533 expected: &[ShapeData],
3534 actual: &[ShapeData],
3535 expected_gradients: &[GradientStop],
3536 actual_gradients: &[GradientStop],
3537) -> bool {
3538 if bytemuck::cast_slice::<ShapeData, u8>(expected)
3539 != bytemuck::cast_slice::<ShapeData, u8>(actual)
3540 {
3541 return false;
3542 }
3543 for shape in expected {
3544 let start = shape.gradient_start as usize;
3545 let end = start + shape.gradient_count as usize;
3546 if end > expected_gradients.len() || end > actual_gradients.len() {
3547 return false;
3548 }
3549 if bytemuck::cast_slice::<GradientStop, u8>(&expected_gradients[start..end])
3550 != bytemuck::cast_slice::<GradientStop, u8>(&actual_gradients[start..end])
3551 {
3552 return false;
3553 }
3554 }
3555 true
3556}
3557
3558#[cfg(not(target_arch = "wasm32"))]
3567fn static_span_fullscreen_opaque(shape: &ShapeData, width: u32, height: u32) -> bool {
3568 if shape.brush_type != 0 || shape.gradient_count != 0 {
3569 return false;
3570 }
3571 if shape.color[3] != 1.0 {
3572 return false;
3573 }
3574 if shape.clip_rect != [0.0; 4] || shape.stroke_params != [0.0; 4] || shape.radii != [0.0; 4] {
3575 return false;
3576 }
3577 let [left, top, right, top_right_y] = shape.quad01;
3580 let [bl_x, bottom, br_x, br_y] = shape.quad23;
3581 let axis_aligned = top_right_y == top
3582 && bl_x == left
3583 && br_x == right
3584 && br_y == bottom
3585 && left < right
3586 && top < bottom;
3587 axis_aligned && left <= 0.0 && top <= 0.0 && right >= width as f32 && bottom >= height as f32
3588}
3589
3590#[cfg(not(target_arch = "wasm32"))]
3601fn clip_polygon_axis(
3602 input: &[[f32; 2]],
3603 axis: usize,
3604 bound: f32,
3605 keep_at_most: bool,
3606 output: &mut Vec<[f32; 2]>,
3607) {
3608 output.clear();
3609 let inside = |p: [f32; 2]| {
3610 if keep_at_most {
3611 p[axis] <= bound
3612 } else {
3613 p[axis] >= bound
3614 }
3615 };
3616 let intersect = |a: [f32; 2], b: [f32; 2]| {
3617 let (p, q) = if (b[0], b[1]) < (a[0], a[1]) {
3618 (b, a)
3619 } else {
3620 (a, b)
3621 };
3622 let t = (bound - p[axis]) / (q[axis] - p[axis]);
3623 let mut point = [0.0f32; 2];
3624 point[axis] = bound;
3625 point[1 - axis] = p[1 - axis] + t * (q[1 - axis] - p[1 - axis]);
3626 point
3627 };
3628 for (index, ¤t) in input.iter().enumerate() {
3629 let previous = input[(index + input.len() - 1) % input.len()];
3630 match (inside(previous), inside(current)) {
3631 (true, true) => output.push(current),
3632 (true, false) => output.push(intersect(previous, current)),
3633 (false, true) => {
3634 output.push(intersect(previous, current));
3635 output.push(current);
3636 }
3637 (false, false) => {}
3638 }
3639 }
3640}
3641
3642#[cfg(not(target_arch = "wasm32"))]
3677fn emit_arc_band_mesh(
3678 shape: &ShapeData,
3679 shape_idx: u32,
3680 band: &ArcMeshBand,
3681 vertices: &mut Vec<MeshVertex>,
3682 indices: &mut Vec<u32>,
3683) -> Option<usize> {
3684 let [cx, cy] = band.center;
3685 let ra = (band.outer + band.inner) * 0.5;
3686 let rb = ((band.outer - band.inner) * 0.5).max(0.0);
3687 let rb_m = rb + ARC_MESH_MARGIN;
3688 let ro = ra + rb_m;
3689 let ri = (ra - rb_m).max(0.0);
3690 let tau = cranpose_ui_graphics::TAU;
3691
3692 let (range_start, range) = if band.sweep >= tau {
3693 (0.0, tau)
3694 } else {
3695 let pad = if rb_m < ra {
3696 (rb_m / ra).asin() + 0.05
3697 } else {
3698 std::f32::consts::PI
3701 };
3702 let padded = band.sweep + pad + pad;
3703 if padded >= tau {
3704 (0.0, tau)
3705 } else {
3706 (band.start - pad, padded)
3707 }
3708 };
3709 let closed = range >= tau;
3710
3711 let dtheta = (2.0 * (ro / (ro + ARC_MESH_OVERSHOOT)).acos()).clamp(tau / 64.0, tau / 6.0);
3712 let segments =
3713 ((range / dtheta).ceil() as usize).clamp(ARC_MESH_MIN_SEGMENTS, ARC_MESH_MAX_SEGMENTS);
3714 let step = range / segments as f32;
3715 let rc = ro / (step * 0.5).cos();
3716
3717 let boundary_count = if closed { segments } else { segments + 1 };
3721 let mut boundaries = Vec::with_capacity(boundary_count);
3722 for j in 0..boundary_count {
3723 let (sin, cos) = (range_start + step * j as f32).sin_cos();
3724 boundaries.push((
3725 [cx + cos * ri, cy + sin * ri],
3726 [cx + cos * rc, cy + sin * rc],
3727 ));
3728 }
3729
3730 let quad_min = [shape.quad01[0], shape.quad01[1]];
3731 let quad_max = [shape.quad23[2], shape.quad23[3]];
3732
3733 enum SegmentGeometry {
3738 Shared,
3739 Fan(Vec<[f32; 2]>),
3740 Empty,
3741 }
3742
3743 let mut polygon: Vec<[f32; 2]> = Vec::with_capacity(8);
3745 let mut scratch: Vec<[f32; 2]> = Vec::with_capacity(8);
3746 let mut segment_geometry = Vec::with_capacity(segments);
3747 let mut boundary_used = vec![false; boundary_count];
3748 for j in 0..segments {
3749 let jb = (j + 1) % boundary_count;
3750 let (inner_a, outer_a) = boundaries[j];
3751 let (inner_b, outer_b) = boundaries[jb];
3752 polygon.clear();
3753 polygon.extend_from_slice(&[inner_a, outer_a, outer_b, inner_b]);
3754 clip_polygon_axis(&polygon, 0, quad_min[0], false, &mut scratch);
3755 clip_polygon_axis(&scratch, 0, quad_max[0], true, &mut polygon);
3756 clip_polygon_axis(&polygon, 1, quad_min[1], false, &mut scratch);
3757 clip_polygon_axis(&scratch, 1, quad_max[1], true, &mut polygon);
3758 scratch.clear();
3761 for &point in polygon.iter() {
3762 if scratch.last() != Some(&point) {
3763 scratch.push(point);
3764 }
3765 }
3766 while scratch.len() > 1 && scratch.first() == scratch.last() {
3767 scratch.pop();
3768 }
3769 if scratch.len() < 3 {
3770 segment_geometry.push(SegmentGeometry::Empty);
3771 } else if scratch[..] == [inner_a, outer_a, outer_b, inner_b] {
3772 boundary_used[j] = true;
3773 boundary_used[jb] = true;
3774 segment_geometry.push(SegmentGeometry::Shared);
3775 } else {
3776 segment_geometry.push(SegmentGeometry::Fan(scratch.clone()));
3777 }
3778 }
3779
3780 let push_vertex = |vertices: &mut Vec<MeshVertex>, position: [f32; 2]| -> u32 {
3781 let index = vertices.len() as u32;
3782 vertices.push(MeshVertex {
3783 position,
3784 uv: [
3785 (position[0] - shape.rect[0]) / shape.rect[2],
3786 (position[1] - shape.rect[1]) / shape.rect[3],
3787 ],
3788 shape_idx,
3789 });
3790 index
3791 };
3792
3793 let mut boundary_vertex = vec![[0u32; 2]; boundary_count];
3796 for (j, used) in boundary_used.iter().enumerate() {
3797 if *used {
3798 let (inner, outer) = boundaries[j];
3799 boundary_vertex[j] = [push_vertex(vertices, inner), push_vertex(vertices, outer)];
3800 }
3801 }
3802
3803 let start_len = indices.len();
3806 for (j, geometry) in segment_geometry.iter().enumerate() {
3807 match geometry {
3808 SegmentGeometry::Empty => {}
3809 SegmentGeometry::Shared => {
3810 let jb = (j + 1) % boundary_count;
3811 let [in_a, out_a] = boundary_vertex[j];
3812 let [in_b, out_b] = boundary_vertex[jb];
3813 indices.extend_from_slice(&[in_a, out_a, out_b, in_a, out_b, in_b]);
3817 }
3818 SegmentGeometry::Fan(points) => {
3819 let base = vertices.len() as u32;
3820 for &point in points {
3821 push_vertex(vertices, point);
3822 }
3823 for i in 1..points.len() as u32 - 1 {
3824 indices.extend_from_slice(&[base, base + i, base + i + 1]);
3825 }
3826 }
3827 }
3828 }
3829 if indices.len() == start_len {
3830 return None;
3831 }
3832 Some(segments)
3833}
3834
3835#[cfg(not(target_arch = "wasm32"))]
3838fn triangles_shoelace_area(vertices: &[MeshVertex], indices: &[u32]) -> f64 {
3839 indices
3840 .as_chunks::<3>()
3841 .0
3842 .iter()
3843 .map(|tri| {
3844 let [a, b, c] = [
3845 vertices[tri[0] as usize].position,
3846 vertices[tri[1] as usize].position,
3847 vertices[tri[2] as usize].position,
3848 ];
3849 let cross = (b[0] as f64 - a[0] as f64) * (c[1] as f64 - a[1] as f64)
3850 - (b[1] as f64 - a[1] as f64) * (c[0] as f64 - a[0] as f64);
3851 cross.abs() * 0.5
3852 })
3853 .sum()
3854}
3855
3856#[cfg(not(target_arch = "wasm32"))]
3859fn quad_shoelace_area(shape: &ShapeData) -> f64 {
3860 let corners = [
3861 [shape.quad01[0] as f64, shape.quad01[1] as f64],
3862 [shape.quad01[2] as f64, shape.quad01[3] as f64],
3863 [shape.quad23[0] as f64, shape.quad23[1] as f64],
3864 [shape.quad23[2] as f64, shape.quad23[3] as f64],
3865 ];
3866 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
3867 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
3868 };
3869 tri(corners[0], corners[1], corners[2]) + tri(corners[2], corners[1], corners[3])
3870}
3871
3872#[cfg(not(target_arch = "wasm32"))]
3877pub(crate) fn fill_area_diag_enabled() -> bool {
3878 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3879 *ENABLED.get_or_init(
3880 || matches!(std::env::var("CRANPOSE_FILL_DIAG").as_deref(), Ok(value) if value != "0"),
3881 )
3882}
3883
3884#[cfg(not(target_arch = "wasm32"))]
3886const FILL_DIAG_WINDOW_FRAMES: u32 = 120;
3887
3888#[cfg(not(target_arch = "wasm32"))]
3889const FILL_DIAG_BUCKETS: usize = 9;
3890
3891#[cfg(not(target_arch = "wasm32"))]
3898#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3899enum FillOpacityClass {
3900 Opaque = 0,
3901 Translucent = 1,
3902 NonSolid = 2,
3903}
3904
3905#[cfg(not(target_arch = "wasm32"))]
3906fn fill_opacity_class(shape: &ShapeData) -> FillOpacityClass {
3907 if shape.brush_type != 0 {
3908 FillOpacityClass::NonSolid
3909 } else if shape.color[3] == 1.0 {
3910 FillOpacityClass::Opaque
3911 } else {
3912 FillOpacityClass::Translucent
3913 }
3914}
3915
3916#[cfg(not(target_arch = "wasm32"))]
3921fn fill_diag_bucket(shape: &ShapeData) -> usize {
3922 match shape.stroke_params[1].max(0.0) as u32 & 3 {
3923 SHAPE_KIND_ARC => FillAreaDiag::ARC,
3924 SHAPE_KIND_STROKE => FillAreaDiag::RRECT_STROKE,
3925 _ if shape.radii.iter().any(|radius| *radius > 0.0) => FillAreaDiag::RRECT_FILL,
3926 _ => FillAreaDiag::RECT,
3927 }
3928}
3929
3930#[cfg(not(target_arch = "wasm32"))]
3931fn fill_diag_bucket_name(bucket: usize) -> &'static str {
3932 match bucket {
3933 FillAreaDiag::ARC => "arc",
3934 FillAreaDiag::RRECT_STROKE => "rrect-stroke",
3935 FillAreaDiag::RRECT_FILL => "rrect-fill",
3936 FillAreaDiag::RECT => "rect",
3937 FillAreaDiag::MESH => "mesh",
3938 FillAreaDiag::RETAINED => "retained",
3939 FillAreaDiag::IMAGE_GLYPH => "img+glyph",
3940 FillAreaDiag::EFFECT_COMPOSITE => "effect-comp",
3941 FillAreaDiag::OFFSCREEN_SOURCE => "offscr-src",
3942 _ => "?",
3943 }
3944}
3945
3946#[cfg(not(target_arch = "wasm32"))]
3967fn analytic_covered_area(shape: &ShapeData) -> f64 {
3968 let flags = shape.stroke_params[1].max(0.0) as u32;
3969 match flags & 3 {
3970 SHAPE_KIND_ARC => {
3971 let outer = f64::from(shape.stroke_params[2]).max(0.0);
3972 let inner = f64::from(shape.stroke_params[3]).clamp(0.0, outer);
3973 let tau = f64::from(cranpose_ui_graphics::TAU);
3974 let sweep = f64::from(shape.arc_params[3]).clamp(0.0, tau);
3975 let thickness = outer - inner;
3976 let band = sweep * 0.5 * (outer + inner) * thickness;
3977 let caps = if sweep >= tau {
3978 0.0
3979 } else {
3980 match (flags >> 2) & 3 {
3981 1 | 2 => std::f64::consts::PI * (thickness * 0.5) * (thickness * 0.5),
3985 _ => 0.0,
3986 }
3987 };
3988 band + caps
3989 }
3990 SHAPE_KIND_STROKE => {
3991 let stroke_width = f64::from(shape.stroke_params[0]).max(0.0);
3992 let geom_w = (f64::from(shape.rect[2]) - stroke_width).max(0.0);
3994 let geom_h = (f64::from(shape.rect[3]) - stroke_width).max(0.0);
3995 let max_radius = geom_w.min(geom_h) * 0.5;
3996 let radii_sum: f64 = shape
3997 .radii
3998 .iter()
3999 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
4000 .sum();
4001 let perimeter =
4002 2.0 * (geom_w + geom_h) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum;
4003 perimeter.max(0.0) * stroke_width
4004 }
4005 _ => {
4006 let width = f64::from(shape.rect[2]).max(0.0);
4007 let height = f64::from(shape.rect[3]).max(0.0);
4008 let max_radius = width.min(height) * 0.5;
4009 let radii_sq: f64 = shape
4010 .radii
4011 .iter()
4012 .map(|radius| {
4013 let radius = f64::from(*radius).clamp(0.0, max_radius);
4014 radius * radius
4015 })
4016 .sum();
4017 width * height - (1.0 - std::f64::consts::FRAC_PI_4) * radii_sq
4018 }
4019 }
4020}
4021
4022#[cfg(not(target_arch = "wasm32"))]
4028fn aa_perimeter_allowance(shape: &ShapeData) -> f64 {
4029 let flags = shape.stroke_params[1].max(0.0) as u32;
4030 match flags & 3 {
4031 SHAPE_KIND_ARC => {
4032 let outer = f64::from(shape.stroke_params[2]).max(0.0);
4033 let inner = f64::from(shape.stroke_params[3]).clamp(0.0, outer);
4034 let tau = f64::from(cranpose_ui_graphics::TAU);
4035 let sweep = f64::from(shape.arc_params[3]).clamp(0.0, tau);
4036 let ends = if sweep >= tau {
4037 0.0
4038 } else {
4039 2.0 * (outer - inner)
4040 };
4041 sweep * (outer + inner) + ends
4042 }
4043 SHAPE_KIND_STROKE => {
4044 let stroke_width = f64::from(shape.stroke_params[0]).max(0.0);
4045 let geom_w = (f64::from(shape.rect[2]) - stroke_width).max(0.0);
4046 let geom_h = (f64::from(shape.rect[3]) - stroke_width).max(0.0);
4047 let max_radius = geom_w.min(geom_h) * 0.5;
4048 let radii_sum: f64 = shape
4049 .radii
4050 .iter()
4051 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
4052 .sum();
4053 let perimeter =
4054 2.0 * (geom_w + geom_h) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum;
4055 2.0 * perimeter.max(0.0)
4056 }
4057 _ if shape.radii.iter().any(|radius| *radius > 0.0) => {
4058 let width = f64::from(shape.rect[2]).max(0.0);
4059 let height = f64::from(shape.rect[3]).max(0.0);
4060 let max_radius = width.min(height) * 0.5;
4061 let radii_sum: f64 = shape
4062 .radii
4063 .iter()
4064 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
4065 .sum();
4066 (2.0 * (width + height) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum).max(0.0)
4067 }
4068 _ => 0.0,
4069 }
4070}
4071
4072#[cfg(not(target_arch = "wasm32"))]
4076fn analytic_lit_area(shape: &ShapeData) -> f64 {
4077 analytic_covered_area(shape) + aa_perimeter_allowance(shape)
4078}
4079
4080#[cfg(not(target_arch = "wasm32"))]
4082fn quad_aabb(shape: &ShapeData) -> [f64; 4] {
4083 let xs = [
4084 f64::from(shape.quad01[0]),
4085 f64::from(shape.quad01[2]),
4086 f64::from(shape.quad23[0]),
4087 f64::from(shape.quad23[2]),
4088 ];
4089 let ys = [
4090 f64::from(shape.quad01[1]),
4091 f64::from(shape.quad01[3]),
4092 f64::from(shape.quad23[1]),
4093 f64::from(shape.quad23[3]),
4094 ];
4095 let fold = |values: [f64; 4], pick: fn(f64, f64) -> f64| {
4096 values.into_iter().reduce(pick).unwrap_or(0.0)
4097 };
4098 [
4099 fold(xs, f64::min),
4100 fold(ys, f64::min),
4101 fold(xs, f64::max),
4102 fold(ys, f64::max),
4103 ]
4104}
4105
4106#[cfg(not(target_arch = "wasm32"))]
4110const CORNER_FILL_STRIPS: usize = 32;
4111
4112#[cfg(not(target_arch = "wasm32"))]
4121fn area_outside_inscribed_circle(aabb: [f64; 4], viewport: (u32, u32)) -> f64 {
4122 let viewport_w = f64::from(viewport.0);
4123 let viewport_h = f64::from(viewport.1);
4124 if viewport_w <= 0.0 || viewport_h <= 0.0 {
4125 return 0.0;
4126 }
4127 let x0 = aabb[0].max(0.0);
4128 let y0 = aabb[1].max(0.0);
4129 let x1 = aabb[2].min(viewport_w);
4130 let y1 = aabb[3].min(viewport_h);
4131 if x1 <= x0 || y1 <= y0 {
4132 return 0.0;
4133 }
4134 let center_x = viewport_w * 0.5;
4135 let center_y = viewport_h * 0.5;
4136 let radius = viewport_w.min(viewport_h) * 0.5;
4137 let strip = (x1 - x0) / CORNER_FILL_STRIPS as f64;
4138 let mut outside = 0.0;
4139 for index in 0..CORNER_FILL_STRIPS {
4140 let x = x0 + (index as f64 + 0.5) * strip;
4141 let dx = x - center_x;
4142 let chord_sq = radius * radius - dx * dx;
4143 let inside = if chord_sq > 0.0 {
4144 let half_chord = chord_sq.sqrt();
4145 (y1.min(center_y + half_chord) - y0.max(center_y - half_chord)).max(0.0)
4146 } else {
4147 0.0
4148 };
4149 outside += ((y1 - y0) - inside) * strip;
4150 }
4151 outside
4152}
4153
4154#[cfg(not(target_arch = "wasm32"))]
4158#[derive(Clone, Copy, Debug)]
4159struct FillDiagShapeRecord {
4160 drawn_px2: f64,
4164 lit_px2: f64,
4166 bucket: usize,
4168 opacity: FillOpacityClass,
4169 aabb: [f64; 4],
4171}
4172
4173#[cfg(not(target_arch = "wasm32"))]
4177fn fill_diag_capture_records(
4178 shape_data: &[ShapeData],
4179 mesh: Option<(&[MeshVertex], &[u32], &[u32])>,
4180) -> Vec<FillDiagShapeRecord> {
4181 shape_data
4182 .iter()
4183 .enumerate()
4184 .map(|(index, shape)| {
4185 let drawn_px2 = match mesh {
4186 Some((vertices, indices, index_prefix))
4190 if index_prefix[index + 1] > index_prefix[index] =>
4191 {
4192 let start = index_prefix[index] as usize;
4193 let end = index_prefix[index + 1] as usize;
4194 triangles_shoelace_area(vertices, &indices[start..end])
4195 }
4196 _ => quad_shoelace_area(shape),
4197 };
4198 FillDiagShapeRecord {
4199 drawn_px2,
4200 lit_px2: analytic_lit_area(shape).clamp(0.0, drawn_px2),
4201 bucket: fill_diag_bucket(shape),
4202 opacity: fill_opacity_class(shape),
4203 aabb: quad_aabb(shape),
4204 }
4205 })
4206 .collect()
4207}
4208
4209#[cfg(not(target_arch = "wasm32"))]
4212#[derive(Clone, Copy, Debug)]
4213struct FillDiagSlackEntry {
4214 slot: u32,
4215 shape: u32,
4216 bucket: usize,
4217 drawn_px2: f64,
4218 lit_px2: f64,
4219}
4220
4221#[cfg(not(target_arch = "wasm32"))]
4222const FILL_DIAG_SLACK_TOP: usize = 10;
4223
4224#[cfg(not(target_arch = "wasm32"))]
4269#[derive(Default)]
4270struct FillAreaDiag {
4271 frame: [std::cell::Cell<f64>; FILL_DIAG_BUCKETS],
4275 frame_lit: [std::cell::Cell<f64>; FILL_DIAG_BUCKETS],
4277 frame_opacity: [std::cell::Cell<f64>; 3],
4279 frame_corner: std::cell::Cell<f64>,
4281 viewport: std::cell::Cell<(u32, u32)>,
4284 window: [f64; FILL_DIAG_BUCKETS],
4286 window_lit: [f64; FILL_DIAG_BUCKETS],
4287 window_opacity: [f64; 3],
4288 window_corner: f64,
4289 window_frames: u32,
4290 slack_top: Vec<FillDiagSlackEntry>,
4293 slack_dumped: bool,
4294}
4295
4296#[cfg(not(target_arch = "wasm32"))]
4297impl FillAreaDiag {
4298 const ARC: usize = 0;
4299 const RRECT_STROKE: usize = 1;
4300 const RRECT_FILL: usize = 2;
4301 const RECT: usize = 3;
4302 const MESH: usize = 4;
4303 const RETAINED: usize = 5;
4304 const IMAGE_GLYPH: usize = 6;
4305 const EFFECT_COMPOSITE: usize = 7;
4306 const OFFSCREEN_SOURCE: usize = 8;
4307
4308 fn add(&self, bucket: usize, area_px2: f64) {
4309 let cell = &self.frame[bucket];
4310 cell.set(cell.get() + area_px2);
4311 }
4312
4313 fn add_lit(&self, bucket: usize, lit_px2: f64) {
4314 let cell = &self.frame_lit[bucket];
4315 cell.set(cell.get() + lit_px2);
4316 }
4317
4318 fn add_corner(&self, px2: f64) {
4319 self.frame_corner.set(self.frame_corner.get() + px2);
4320 }
4321
4322 fn is_full_frame(&self, viewport: ViewportUniformParams) -> bool {
4326 let (width, height) = self.viewport.get();
4327 width > 0
4328 && height > 0
4329 && viewport.width == width
4330 && viewport.height == height
4331 && viewport.offset == [0.0, 0.0]
4332 }
4333
4334 fn add_shape_quads(&self, shapes: &[ShapeData], viewport: ViewportUniformParams) {
4339 let full_frame = self.is_full_frame(viewport);
4340 let frame_viewport = self.viewport.get();
4341 let mut buckets = [0.0_f64; FILL_DIAG_BUCKETS];
4342 let mut lit_buckets = [0.0_f64; FILL_DIAG_BUCKETS];
4343 let mut opacity = [0.0_f64; 3];
4344 let mut corner = 0.0_f64;
4345 for shape in shapes {
4346 let bucket = fill_diag_bucket(shape);
4347 let quad = quad_shoelace_area(shape);
4348 let lit = analytic_lit_area(shape).clamp(0.0, quad);
4349 buckets[bucket] += quad;
4350 lit_buckets[bucket] += lit;
4351 opacity[fill_opacity_class(shape) as usize] += lit;
4352 if full_frame {
4353 corner += area_outside_inscribed_circle(quad_aabb(shape), frame_viewport);
4354 }
4355 }
4356 for (bucket, area) in buckets.into_iter().enumerate() {
4357 if area > 0.0 {
4358 self.add(bucket, area);
4359 }
4360 }
4361 for (bucket, lit) in lit_buckets.into_iter().enumerate() {
4362 if lit > 0.0 {
4363 self.add_lit(bucket, lit);
4364 }
4365 }
4366 for (class, lit) in self.frame_opacity.iter().zip(opacity) {
4367 class.set(class.get() + lit);
4368 }
4369 if corner > 0.0 {
4370 self.add_corner(corner);
4371 }
4372 }
4373
4374 fn note_static_span_skip(&self, shapes: &[ShapeData]) {
4383 for shape in shapes {
4384 let bucket = fill_diag_bucket(shape);
4385 let quad = quad_shoelace_area(shape);
4386 let lit = analytic_lit_area(shape).clamp(0.0, quad);
4387 self.add(bucket, -quad);
4388 self.add_lit(bucket, -lit);
4389 let class = &self.frame_opacity[fill_opacity_class(shape) as usize];
4390 class.set(class.get() - lit);
4391 }
4392 }
4393
4394 fn note_rim_mesh(&self, shape: &ShapeData, mesh_px2: f64) {
4401 let quad = quad_shoelace_area(shape);
4402 let lit = analytic_lit_area(shape).clamp(0.0, quad);
4403 self.add(Self::RRECT_STROKE, -quad);
4404 self.add_lit(Self::RRECT_STROKE, -lit);
4405 self.add(Self::MESH, mesh_px2);
4406 self.add_lit(Self::MESH, lit.min(mesh_px2));
4407 }
4408
4409 fn add_retained_range(
4415 &self,
4416 records: &[FillDiagShapeRecord],
4417 first: u32,
4418 last: u32,
4419 transform: &SimilarityTransform,
4420 ) {
4421 let Some(range) = records.get(first as usize..last as usize) else {
4422 return;
4423 };
4424 let scale = f64::from(transform.scale);
4425 let factor = scale * scale;
4426 let identity = transform.rot == [1.0, 0.0] && transform.scale == 1.0;
4427 let frame_viewport = self.viewport.get();
4428 let mut drawn = 0.0_f64;
4429 let mut lit = 0.0_f64;
4430 let mut opacity = [0.0_f64; 3];
4431 let mut corner = 0.0_f64;
4432 for record in range {
4433 drawn += record.drawn_px2;
4434 lit += record.lit_px2;
4435 opacity[record.opacity as usize] += record.lit_px2;
4436 if identity {
4437 corner += area_outside_inscribed_circle(record.aabb, frame_viewport);
4438 }
4439 }
4440 self.add(Self::RETAINED, drawn * factor);
4441 self.add_lit(Self::RETAINED, lit * factor);
4442 for (class, value) in self.frame_opacity.iter().zip(opacity) {
4443 class.set(class.get() + value * factor);
4444 }
4445 if corner > 0.0 {
4446 self.add_corner(corner);
4447 }
4448 }
4449
4450 fn note_retained_capture(&mut self, slot: u32, records: &[FillDiagShapeRecord]) {
4454 if self.slack_dumped {
4455 return;
4456 }
4457 for (index, record) in records.iter().enumerate() {
4458 if record.drawn_px2 - record.lit_px2 <= 0.0 {
4459 continue;
4460 }
4461 self.slack_top.push(FillDiagSlackEntry {
4462 slot,
4463 shape: index as u32,
4464 bucket: record.bucket,
4465 drawn_px2: record.drawn_px2,
4466 lit_px2: record.lit_px2,
4467 });
4468 }
4469 self.slack_top
4470 .sort_by(|a, b| (b.drawn_px2 - b.lit_px2).total_cmp(&(a.drawn_px2 - a.lit_px2)));
4471 self.slack_top.truncate(FILL_DIAG_SLACK_TOP);
4472 }
4473
4474 fn add_image_quad(&self, quad: &[[f32; 2]; 4]) {
4478 let corner = |index: usize| [f64::from(quad[index][0]), f64::from(quad[index][1])];
4479 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
4480 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
4481 };
4482 let [a, b, c, d] = [corner(0), corner(1), corner(2), corner(3)];
4483 let area = tri(a, b, c) + tri(c, b, d);
4484 self.add(Self::IMAGE_GLYPH, area);
4485 self.add_lit(Self::IMAGE_GLYPH, area);
4486 }
4487
4488 fn add_glyph_quad(&self, quad: &CachedTextGlyphQuad) {
4490 let area = quad.width as f64 * quad.height as f64;
4491 self.add(Self::IMAGE_GLYPH, area);
4492 self.add_lit(Self::IMAGE_GLYPH, area);
4493 }
4494
4495 fn add_effect_fill(&self, composite_px2: f64, offscreen_px2: f64) {
4499 if composite_px2 > 0.0 {
4500 self.add(Self::EFFECT_COMPOSITE, composite_px2);
4501 self.add_lit(Self::EFFECT_COMPOSITE, composite_px2);
4502 }
4503 if offscreen_px2 > 0.0 {
4504 self.add(Self::OFFSCREEN_SOURCE, offscreen_px2);
4505 self.add_lit(Self::OFFSCREEN_SOURCE, offscreen_px2);
4506 }
4507 }
4508
4509 fn add_offscreen_target_fill(&self, px2: f64) {
4515 if px2 > 0.0 {
4516 self.add(Self::OFFSCREEN_SOURCE, px2);
4517 self.add_lit(Self::OFFSCREEN_SOURCE, px2);
4518 }
4519 }
4520
4521 fn reset_frame(&self, width: u32, height: u32) {
4525 for cell in &self.frame {
4526 cell.set(0.0);
4527 }
4528 for cell in &self.frame_lit {
4529 cell.set(0.0);
4530 }
4531 for cell in &self.frame_opacity {
4532 cell.set(0.0);
4533 }
4534 self.frame_corner.set(0.0);
4535 self.viewport.set((width, height));
4536 }
4537
4538 fn finish_frame(&mut self, width: u32, height: u32) {
4543 for (total, cell) in self.window.iter_mut().zip(&self.frame) {
4544 *total += cell.get();
4545 }
4546 for (total, cell) in self.window_lit.iter_mut().zip(&self.frame_lit) {
4547 *total += cell.get();
4548 }
4549 for (total, cell) in self.window_opacity.iter_mut().zip(&self.frame_opacity) {
4550 *total += cell.get();
4551 }
4552 self.window_corner += self.frame_corner.get();
4553 self.window_frames += 1;
4554 if self.window_frames < FILL_DIAG_WINDOW_FRAMES {
4555 return;
4556 }
4557 let frames = f64::from(self.window_frames);
4558 let mega = |bucket: usize| self.window[bucket] / frames / 1e6;
4559 let total_mega = self.window.iter().sum::<f64>() / frames / 1e6;
4560 let screen_mega = f64::from(width) * f64::from(height) / 1e6;
4561 let overdraw = if screen_mega > 0.0 {
4562 total_mega / screen_mega
4563 } else {
4564 0.0
4565 };
4566 log::warn!(
4567 "[fill-diag] Mpx/frame: arc {:.1}, rrect-stroke {:.1}, rrect-fill {:.1}, \
4568 rect {:.1}, mesh {:.1}, retained {:.1}, img+glyph {:.1}, \
4569 effect-comp {:.1}, offscr-src {:.1}, total {:.1} \
4570 ({:.1}x overdraw of {:.3} Mpx)",
4571 mega(Self::ARC),
4572 mega(Self::RRECT_STROKE),
4573 mega(Self::RRECT_FILL),
4574 mega(Self::RECT),
4575 mega(Self::MESH),
4576 mega(Self::RETAINED),
4577 mega(Self::IMAGE_GLYPH),
4578 mega(Self::EFFECT_COMPOSITE),
4579 mega(Self::OFFSCREEN_SOURCE),
4580 total_mega,
4581 overdraw,
4582 screen_mega,
4583 );
4584 let lit = |bucket: usize| self.window_lit[bucket] / frames / 1e6;
4588 let slack = |bucket: usize| (mega(bucket) - lit(bucket)).max(0.0);
4589 let truth = |bucket: usize| format!("{:.2}|{:.2}", lit(bucket), slack(bucket));
4590 log::warn!(
4591 "[fill-truth] Mpx/frame lit|slack: arc {}, rrect-stroke {}, rrect-fill {}, \
4592 rect {}, mesh {}, retained {}, img+glyph {}, effect-comp {}, offscr-src {}; \
4593 lit alpha Mpx: opaque {:.2}, translucent {:.2}, nonsolid {:.2}; \
4594 corner-outside {:.2}",
4595 truth(Self::ARC),
4596 truth(Self::RRECT_STROKE),
4597 truth(Self::RRECT_FILL),
4598 truth(Self::RECT),
4599 truth(Self::MESH),
4600 truth(Self::RETAINED),
4601 truth(Self::IMAGE_GLYPH),
4602 truth(Self::EFFECT_COMPOSITE),
4603 truth(Self::OFFSCREEN_SOURCE),
4604 self.window_opacity[FillOpacityClass::Opaque as usize] / frames / 1e6,
4605 self.window_opacity[FillOpacityClass::Translucent as usize] / frames / 1e6,
4606 self.window_opacity[FillOpacityClass::NonSolid as usize] / frames / 1e6,
4607 self.window_corner / frames / 1e6,
4608 );
4609 if !self.slack_dumped && !self.slack_top.is_empty() {
4610 log::warn!("[fill-truth] top retained slack (once per process, capture-space px):");
4611 for (rank, entry) in self.slack_top.iter().enumerate() {
4612 log::warn!(
4613 "[fill-truth] #{} slot {} shape {} {}: quad {:.0}, lit {:.0}, \
4614 slack {:.0}",
4615 rank + 1,
4616 entry.slot,
4617 entry.shape,
4618 fill_diag_bucket_name(entry.bucket),
4619 entry.drawn_px2,
4620 entry.lit_px2,
4621 entry.drawn_px2 - entry.lit_px2,
4622 );
4623 }
4624 self.slack_dumped = true;
4625 self.slack_top = Vec::new();
4626 }
4627 self.window = [0.0; FILL_DIAG_BUCKETS];
4628 self.window_lit = [0.0; FILL_DIAG_BUCKETS];
4629 self.window_opacity = [0.0; 3];
4630 self.window_corner = 0.0;
4631 self.window_frames = 0;
4632 }
4633}
4634
4635#[cfg(not(target_arch = "wasm32"))]
4636struct ArcMeshBuild {
4637 vertices: Vec<MeshVertex>,
4638 indices: Vec<u32>,
4640 index_prefix: Vec<u32>,
4646 meshed_arcs: usize,
4647 meshed_rims: usize,
4648 meshed_segments: usize,
4649 passthrough: usize,
4650 meshed_stretches: usize,
4654 quad_area: f64,
4655 mesh_area: f64,
4658}
4659
4660#[cfg(not(target_arch = "wasm32"))]
4674fn build_arc_mesh_vertices(shape_data: &[ShapeData], min_mesh_px2: f64) -> Option<ArcMeshBuild> {
4675 let budget_bytes =
4676 (shape_data.len() * ARC_MESH_BUDGET_BYTES_PER_SHAPE).max(ARC_MESH_BUDGET_FLOOR_BYTES);
4677 let mut build = ArcMeshBuild {
4678 vertices: Vec::new(),
4679 indices: Vec::new(),
4680 index_prefix: Vec::with_capacity(shape_data.len() + 1),
4681 meshed_arcs: 0,
4682 meshed_rims: 0,
4683 meshed_segments: 0,
4684 passthrough: 0,
4685 meshed_stretches: 0,
4686 quad_area: 0.0,
4687 mesh_area: 0.0,
4688 };
4689 build.index_prefix.push(0);
4690 let mut previous_meshed = false;
4691 for (index, shape) in shape_data.iter().enumerate() {
4692 let start = build.indices.len();
4693 let quad_px2 = quad_shoelace_area(shape);
4694 let band = if quad_px2 >= min_mesh_px2 {
4703 arc_mesh_band(shape)
4704 .map(|band| (band, false))
4705 .or_else(|| rim_band_geometry(shape).map(|band| (band, true)))
4706 } else {
4707 None
4708 };
4709 let meshed = band.and_then(|(band, is_rim)| {
4710 emit_arc_band_mesh(
4711 shape,
4712 index as u32,
4713 &band,
4714 &mut build.vertices,
4715 &mut build.indices,
4716 )
4717 .map(|segments| (segments, is_rim))
4718 });
4719 match meshed {
4720 Some((segments, is_rim)) => {
4721 if is_rim {
4722 build.meshed_rims += 1;
4723 } else {
4724 build.meshed_arcs += 1;
4725 }
4726 build.meshed_segments += segments;
4727 if !previous_meshed {
4728 build.meshed_stretches += 1;
4729 }
4730 previous_meshed = true;
4731 build.mesh_area +=
4732 triangles_shoelace_area(&build.vertices, &build.indices[start..]);
4733 }
4734 None => {
4735 build.passthrough += 1;
4736 previous_meshed = false;
4737 build.mesh_area += quad_px2;
4738 }
4739 }
4740 if arc_mesh_bytes(build.vertices.len(), build.indices.len()) > budget_bytes {
4741 return None;
4742 }
4743 build.index_prefix.push(build.indices.len() as u32);
4744 build.quad_area += quad_px2;
4745 }
4746 Some(build)
4747}
4748
4749#[cfg(not(target_arch = "wasm32"))]
4752struct ReplaySlotStore {
4753 slots: std::collections::HashMap<u32, ReplaySlot, cranpose_ui_graphics::FxBuildHasher>,
4754 transform_buffer: wgpu::Buffer,
4755 free_ids: Vec<u32>,
4756 next_capture_epoch: u64,
4760}
4761
4762#[cfg(not(target_arch = "wasm32"))]
4763impl ReplaySlotStore {
4764 fn new(device: &wgpu::Device) -> Self {
4765 let transform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4766 label: Some("Replay Transform Buffer"),
4767 size: (MAX_REPLAY_SLOTS + SEGMENT_CAPTURE_SLOTS) as u64 * REPLAY_TRANSFORM_STRIDE,
4774 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4775 mapped_at_creation: false,
4776 });
4777 Self {
4778 slots: std::collections::HashMap::default(),
4779 transform_buffer,
4780 free_ids: (0..MAX_REPLAY_SLOTS).rev().collect(),
4781 next_capture_epoch: 1,
4782 }
4783 }
4784}
4785
4786#[cfg(not(target_arch = "wasm32"))]
4792fn retained_bundles_enabled() -> bool {
4793 std::env::var("CRANPOSE_RETAINED_BUNDLES").as_deref() != Ok("0")
4794}
4795
4796#[cfg(not(target_arch = "wasm32"))]
4804fn instanced_quads_enabled() -> bool {
4805 std::env::var("CRANPOSE_INSTANCED_QUADS").as_deref() != Ok("0")
4806}
4807
4808fn solid_trim_varyings_enabled() -> bool {
4821 std::env::var("CRANPOSE_SOLID_TRIM_VARYINGS").as_deref() == Ok("1")
4822}
4823
4824fn survive_gpu_errors_enabled() -> bool {
4833 std::env::var("CRANPOSE_SURVIVE_GPU_ERRORS").as_deref() != Ok("0")
4834}
4835
4836#[cfg(not(target_arch = "wasm32"))]
4856fn display_clip_cull_enabled() -> bool {
4857 std::env::var("CRANPOSE_ROUND_CULL").as_deref() == Ok("1")
4858}
4859
4860#[cfg(not(target_arch = "wasm32"))]
4864const INSTANCED_QUAD_INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
4865
4866#[cfg(not(target_arch = "wasm32"))]
4872struct InstancedQuadPipelines {
4873 pipeline: PassPipeline,
4874 pipeline_dst_out: PassPipeline,
4875 pipeline_solid: PassPipeline,
4879 index_buffer: wgpu::Buffer,
4882}
4883
4884#[cfg(not(target_arch = "wasm32"))]
4896enum RetainedCmd<'r> {
4897 Pipeline(&'r wgpu::RenderPipeline),
4898 Uniforms(&'r wgpu::BindGroup),
4900 SlotBindings(&'r wgpu::BindGroup, u32),
4902 MeshVertices(&'r wgpu::Buffer),
4904 Index(&'r wgpu::Buffer, wgpu::IndexFormat),
4905 Draw(Range<u32>),
4907 DrawIndexed(Range<u32>, Range<u32>),
4909}
4910
4911#[cfg(not(target_arch = "wasm32"))]
4922#[derive(Clone, Debug, PartialEq, Eq, Hash)]
4923struct RetainedBundleOpKey {
4924 slot: u32,
4925 capture_epoch: Option<u64>,
4930 first: u32,
4931 last: u32,
4932 retained_index: u32,
4933 has_mesh: bool,
4934}
4935
4936#[cfg(not(target_arch = "wasm32"))]
4940#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
4941struct RetainedBundleKey {
4942 depth: bool,
4947 ops: Vec<RetainedBundleOpKey>,
4948}
4949
4950#[cfg(not(target_arch = "wasm32"))]
4951struct RetainedBundleCacheEntry<B> {
4952 bundle: B,
4953 last_used_frame: u64,
4954}
4955
4956#[cfg(not(target_arch = "wasm32"))]
4965struct RetainedBundleCacheImpl<B> {
4966 entries: HashMap<RetainedBundleKey, RetainedBundleCacheEntry<B>>,
4967 frame: u64,
4968 rebuilds: u64,
4969 cached_executes: u64,
4970 window_rebuilds: u64,
4971 window_executes: u64,
4972}
4973
4974#[cfg(not(target_arch = "wasm32"))]
4975type RetainedBundleCache = RetainedBundleCacheImpl<wgpu::RenderBundle>;
4976
4977#[cfg(not(target_arch = "wasm32"))]
4978impl<B> RetainedBundleCacheImpl<B> {
4979 fn new() -> Self {
4980 Self {
4981 entries: HashMap::default(),
4982 frame: 0,
4983 rebuilds: 0,
4984 cached_executes: 0,
4985 window_rebuilds: 0,
4986 window_executes: 0,
4987 }
4988 }
4989
4990 fn hit(&mut self, key: &RetainedBundleKey) -> bool {
4993 let frame = self.frame;
4994 match self.entries.get_mut(key) {
4995 Some(entry) => {
4996 entry.last_used_frame = frame;
4997 self.cached_executes += 1;
4998 self.window_executes += 1;
4999 true
5000 }
5001 None => false,
5002 }
5003 }
5004
5005 fn insert(&mut self, key: RetainedBundleKey, bundle: B) {
5007 self.rebuilds += 1;
5008 self.window_rebuilds += 1;
5009 self.entries.insert(
5010 key,
5011 RetainedBundleCacheEntry {
5012 bundle,
5013 last_used_frame: self.frame,
5014 },
5015 );
5016 }
5017
5018 fn get(&self, key: &RetainedBundleKey) -> Option<&B> {
5019 self.entries.get(key).map(|entry| &entry.bundle)
5020 }
5021
5022 fn clear(&mut self) {
5027 self.entries.clear();
5028 }
5029
5030 fn end_frame(&mut self) {
5034 let frame = self.frame;
5035 self.entries
5036 .retain(|_, entry| entry.last_used_frame >= frame);
5037 self.frame = self.frame.wrapping_add(1);
5038 let due = self.frame.is_multiple_of(1024)
5043 || (cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG")
5044 && self.frame.is_multiple_of(120));
5045 if due && self.window_rebuilds + self.window_executes > 0 {
5046 log::warn!(
5047 "[retained-bundles] {} stretches, {} rebuilds, {} cached executes ({} live bundles)",
5048 self.window_rebuilds + self.window_executes,
5049 self.window_rebuilds,
5050 self.window_executes,
5051 self.entries.len(),
5052 );
5053 self.window_rebuilds = 0;
5054 self.window_executes = 0;
5055 }
5056 }
5057
5058 fn stats(&self) -> (u64, u64) {
5060 (self.rebuilds, self.cached_executes)
5061 }
5062}
5063
5064struct CachedImageTexture {
5065 _texture: wgpu::Texture,
5066 _view: wgpu::TextureView,
5067 nearest_bind_group: wgpu::BindGroup,
5068 linear_bind_group: wgpu::BindGroup,
5069 bytes: usize,
5076}
5077
5078impl CachedImageTexture {
5079 fn bind_group(&self, sampling: ImageSampling) -> &wgpu::BindGroup {
5080 match sampling {
5081 ImageSampling::Nearest => &self.nearest_bind_group,
5082 ImageSampling::Linear => &self.linear_bind_group,
5083 }
5084 }
5085}
5086
5087#[derive(Clone, Copy)]
5088struct GlyphAtlasEntry {
5089 x: u32,
5090 y: u32,
5091 width: u32,
5092 height: u32,
5093}
5094
5095fn next_glyph_atlas_size(current: u32, max: u32) -> u32 {
5102 current.saturating_mul(2).clamp(1, max.max(1))
5103}
5104
5105struct TextGlyphAtlas {
5106 texture: wgpu::Texture,
5107 _view: wgpu::TextureView,
5108 bind_group: wgpu::BindGroup,
5109 entries: BoundedLruCache<SoftwareGlyphAtlasKey, GlyphAtlasEntry>,
5110 generation: u64,
5111 size: u32,
5115 max_size: u32,
5121 cursor_x: u32,
5122 cursor_y: u32,
5123 row_height: u32,
5124 upload_scratch: Vec<u8>,
5125}
5126
5127impl TextGlyphAtlas {
5128 fn new(
5129 device: &wgpu::Device,
5130 image_layout: &wgpu::BindGroupLayout,
5131 sampler: &wgpu::Sampler,
5132 size: u32,
5133 ) -> Self {
5134 let max_size = TEXT_GLYPH_ATLAS_MAX_SIZE.min(device.limits().max_texture_dimension_2d);
5135 let size = size.clamp(TEXT_GLYPH_ATLAS_MIN_SIZE.min(max_size), max_size);
5136 let texture = Self::create_texture(device, size);
5137 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
5138 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5139 label: Some("Text Glyph Atlas Bind Group"),
5140 layout: image_layout,
5141 entries: &[
5142 wgpu::BindGroupEntry {
5143 binding: 0,
5144 resource: wgpu::BindingResource::TextureView(&view),
5145 },
5146 wgpu::BindGroupEntry {
5147 binding: 1,
5148 resource: wgpu::BindingResource::Sampler(sampler),
5149 },
5150 ],
5151 });
5152 Self {
5153 texture,
5154 _view: view,
5155 bind_group,
5156 entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
5157 generation: 0,
5158 size,
5159 max_size,
5160 cursor_x: TEXT_GLYPH_ATLAS_PADDING,
5161 cursor_y: TEXT_GLYPH_ATLAS_PADDING,
5162 row_height: 0,
5163 upload_scratch: Vec::new(),
5164 }
5165 }
5166
5167 fn create_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
5168 device.create_texture(&wgpu::TextureDescriptor {
5169 label: Some("Text Glyph Atlas Texture"),
5170 size: wgpu::Extent3d {
5171 width: size,
5172 height: size,
5173 depth_or_array_layers: 1,
5174 },
5175 mip_level_count: 1,
5176 sample_count: 1,
5177 dimension: wgpu::TextureDimension::D2,
5178 format: wgpu::TextureFormat::R8Unorm,
5179 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
5180 view_formats: &[],
5181 })
5182 }
5183
5184 fn reset(
5200 &mut self,
5201 device: &wgpu::Device,
5202 image_layout: &wgpu::BindGroupLayout,
5203 sampler: &wgpu::Sampler,
5204 ) {
5205 let generation = self.generation.wrapping_add(1);
5206 let grown = next_glyph_atlas_size(self.size, self.max_size);
5207 let mut next = Self::new(device, image_layout, sampler, grown);
5208 next.generation = generation;
5209 *self = next;
5210 }
5211
5212 fn generation(&self) -> u64 {
5213 self.generation
5214 }
5215
5216 fn size(&self) -> u32 {
5217 self.size
5218 }
5219
5220 fn entry(&mut self, key: &SoftwareGlyphAtlasKey) -> Option<GlyphAtlasEntry> {
5221 self.entries.get(key).copied()
5222 }
5223
5224 fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
5225 if width == 0
5226 || height == 0
5227 || width + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
5228 || height + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
5229 {
5230 return None;
5231 }
5232
5233 if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > self.size {
5234 self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
5235 self.cursor_y = self
5236 .cursor_y
5237 .saturating_add(self.row_height)
5238 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
5239 self.row_height = 0;
5240 }
5241 if self.cursor_y + height + TEXT_GLYPH_ATLAS_PADDING > self.size {
5242 return None;
5243 }
5244
5245 let entry = GlyphAtlasEntry {
5246 x: self.cursor_x,
5247 y: self.cursor_y,
5248 width,
5249 height,
5250 };
5251 self.cursor_x = self
5252 .cursor_x
5253 .saturating_add(width)
5254 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
5255 self.row_height = self.row_height.max(height);
5256 Some(entry)
5257 }
5258
5259 fn upload_glyph(
5260 &mut self,
5261 key: SoftwareGlyphAtlasKey,
5262 glyph: &SoftwareGlyphAtlasGlyph,
5263 queue: &wgpu::Queue,
5264 executor: &mut WgpuFrameGraphExecutor,
5265 frame_stats: &mut gpu_stats::FrameStats,
5266 ) -> Option<GlyphAtlasEntry> {
5267 if let Some(entry) = self.entry(&key) {
5268 frame_stats.record_text_glyph_atlas_hit();
5269 return Some(entry);
5270 }
5271
5272 let width = u32::try_from(glyph.mask.width).ok()?;
5273 let height = u32::try_from(glyph.mask.height).ok()?;
5274 let entry = self.allocate(width, height)?;
5275 self.upload_scratch.clear();
5276 self.upload_scratch.reserve(
5277 glyph
5278 .mask
5279 .alpha
5280 .len()
5281 .saturating_sub(self.upload_scratch.capacity()),
5282 );
5283 self.upload_scratch.extend(
5284 glyph
5285 .mask
5286 .alpha
5287 .iter()
5288 .map(|alpha| (alpha.clamp(0.0, 1.0) * 255.0).round() as u8),
5289 );
5290
5291 let upload_stats = executor.upload_texture(
5292 queue,
5293 wgpu::TexelCopyTextureInfo {
5294 texture: &self.texture,
5295 mip_level: 0,
5296 origin: wgpu::Origin3d {
5297 x: entry.x,
5298 y: entry.y,
5299 z: 0,
5300 },
5301 aspect: wgpu::TextureAspect::All,
5302 },
5303 &self.upload_scratch,
5304 wgpu::TexelCopyBufferLayout {
5305 offset: 0,
5306 bytes_per_row: Some(entry.width),
5307 rows_per_image: Some(entry.height),
5308 },
5309 wgpu::Extent3d {
5310 width: entry.width,
5311 height: entry.height,
5312 depth_or_array_layers: 1,
5313 },
5314 );
5315 frame_stats.record_command_stats(upload_stats);
5316 frame_stats.record_text_glyph_atlas_miss(entry.width, entry.height);
5317 self.entries.put(key, entry);
5318 Some(entry)
5319 }
5320}
5321
5322struct ImageDrawCmd {
5323 index_start: u32,
5324 scissor: (u32, u32, u32, u32),
5325 image_id: u64,
5326 sampling: ImageSampling,
5327}
5328
5329#[derive(Clone, Copy)]
5330enum GlyphDrawSource {
5331 Shared {
5332 index_start: u32,
5333 index_count: u32,
5334 },
5335 #[cfg(not(target_arch = "wasm32"))]
5336 Retained {
5337 cache_key: TextGlyphRunCacheKey,
5338 uniform_slot: usize,
5339 },
5340}
5341
5342#[derive(Clone, Copy)]
5343struct GlyphDrawCmd {
5344 source: GlyphDrawSource,
5345 scissor: (u32, u32, u32, u32),
5346}
5347
5348impl GlyphDrawCmd {
5349 fn shared(index_start: u32, index_count: u32, scissor: (u32, u32, u32, u32)) -> Self {
5350 Self {
5351 source: GlyphDrawSource::Shared {
5352 index_start,
5353 index_count,
5354 },
5355 scissor,
5356 }
5357 }
5358
5359 #[cfg(not(target_arch = "wasm32"))]
5360 fn retained(
5361 cache_key: TextGlyphRunCacheKey,
5362 uniform_slot: usize,
5363 scissor: (u32, u32, u32, u32),
5364 ) -> Self {
5365 Self {
5366 source: GlyphDrawSource::Retained {
5367 cache_key,
5368 uniform_slot,
5369 },
5370 scissor,
5371 }
5372 }
5373}
5374
5375#[derive(Clone, Copy, Debug, PartialEq)]
5376struct ImageUvRect {
5377 min: [f32; 2],
5378 max: [f32; 2],
5379 sample_bounds: [f32; 4],
5380}
5381
5382struct ShapeBatchBuffers {
5389 shape_buffer: wgpu::Buffer,
5390 gradient_buffer: wgpu::Buffer,
5391 bind_group: wgpu::BindGroup,
5392 shape_capacity: usize,
5393 gradient_capacity: usize,
5394 batch_limits: ShapeBatchLimits,
5395}
5396
5397#[cfg(target_arch = "wasm32")]
5398struct UniformBatchBuffer {
5399 buffer: wgpu::Buffer,
5400 bind_group: wgpu::BindGroup,
5401}
5402
5403#[cfg(target_arch = "wasm32")]
5404struct ImageBatchBuffers {
5405 vertex_buffer: wgpu::Buffer,
5406 index_buffer: wgpu::Buffer,
5407 vertex_capacity: usize,
5408 index_capacity: usize,
5409}
5410
5411#[derive(Clone, Copy, Debug, PartialEq)]
5412struct ViewportUniformParams {
5413 width: u32,
5414 height: u32,
5415 offset: [f32; 2],
5416}
5417
5418#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5419#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
5420enum UploadTarget {
5421 Uniform,
5422 ShapeData,
5423 ShapeGradient,
5424 ImageVertex,
5425 ImageIndex,
5426 #[cfg(not(target_arch = "wasm32"))]
5427 RetainedGlyphUniform,
5428 #[cfg(not(target_arch = "wasm32"))]
5431 ReplayTransform,
5432 #[cfg(not(target_arch = "wasm32"))]
5434 ReplayPaintData(u32),
5435}
5436
5437#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5438#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
5439struct PendingBufferCopy {
5440 source_offset: u64,
5441 target_offset: u64,
5442 size: u64,
5443 target: UploadTarget,
5444}
5445
5446#[derive(Default)]
5447struct StagedBufferUploads {
5448 bytes: Vec<u8>,
5449 copies: Vec<PendingBufferCopy>,
5450}
5451
5452impl StagedBufferUploads {
5453 fn clear(&mut self) {
5454 self.bytes.clear();
5455 self.copies.clear();
5456 }
5457
5458 fn shrink_retained_capacity(&mut self, max_bytes: usize, max_copies: usize) -> bool {
5459 let mut shrunk = false;
5460 if self.bytes.len() <= max_bytes && self.bytes.capacity() > max_bytes {
5461 self.bytes.shrink_to(max_bytes);
5462 shrunk = true;
5463 }
5464 if self.copies.len() <= max_copies && self.copies.capacity() > max_copies {
5465 self.copies.shrink_to(max_copies);
5466 shrunk = true;
5467 }
5468 shrunk
5469 }
5470
5471 fn is_empty(&self) -> bool {
5472 self.copies.is_empty()
5473 }
5474
5475 #[cfg(test)]
5476 fn payload_for_copy(&self, copy: PendingBufferCopy) -> &[u8] {
5477 let start = copy.source_offset as usize;
5478 let end = start + copy.size as usize;
5479 &self.bytes[start..end]
5480 }
5481
5482 #[cfg(not(target_arch = "wasm32"))]
5483 fn stage(&mut self, target: UploadTarget, bytes: &[u8]) {
5484 self.stage_at(target, 0, bytes);
5485 }
5486
5487 #[cfg(not(target_arch = "wasm32"))]
5492 fn record_upload_copy(
5493 &mut self,
5494 target: UploadTarget,
5495 source_offset: u64,
5496 target_offset: u64,
5497 size: u64,
5498 ) {
5499 if size == 0 {
5500 return;
5501 }
5502 self.copies.push(PendingBufferCopy {
5503 source_offset,
5504 target_offset,
5505 size,
5506 target,
5507 });
5508 }
5509
5510 #[cfg(not(target_arch = "wasm32"))]
5511 fn stage_at(&mut self, target: UploadTarget, target_offset: u64, bytes: &[u8]) {
5512 if bytes.is_empty() {
5513 return;
5514 }
5515
5516 debug_assert_eq!(
5517 bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize,
5518 0,
5519 "buffer uploads must be aligned to copy requirements"
5520 );
5521
5522 let aligned_offset = align_usize_to(self.bytes.len(), wgpu::COPY_BUFFER_ALIGNMENT as usize);
5523 if aligned_offset > self.bytes.len() {
5524 self.bytes.resize(aligned_offset, 0);
5525 }
5526
5527 let source_offset = self.bytes.len() as u64;
5528 self.bytes.extend_from_slice(bytes);
5529 self.copies.push(PendingBufferCopy {
5530 source_offset,
5531 target_offset,
5532 size: bytes.len() as u64,
5533 target,
5534 });
5535 }
5536
5537 fn truncate(&mut self, bytes_len: usize, copies_len: usize) {
5538 self.bytes.truncate(bytes_len);
5539 self.copies.truncate(copies_len);
5540 }
5541}
5542
5543fn shape_batch_bind_group_entries<'a>(
5548 shape_buffer: &'a wgpu::Buffer,
5549 gradient_buffer: &'a wgpu::Buffer,
5550 similarity_buffer: &'a wgpu::Buffer,
5551 paint_buffer: Option<&'a wgpu::Buffer>,
5552) -> Vec<wgpu::BindGroupEntry<'a>> {
5553 let mut entries = vec![
5554 wgpu::BindGroupEntry {
5555 binding: 0,
5556 resource: shape_buffer.as_entire_binding(),
5557 },
5558 wgpu::BindGroupEntry {
5559 binding: 1,
5560 resource: gradient_buffer.as_entire_binding(),
5561 },
5562 wgpu::BindGroupEntry {
5563 binding: 2,
5564 resource: similarity_buffer.as_entire_binding(),
5565 },
5566 ];
5567 if let Some(paint_buffer) = paint_buffer {
5568 entries.push(wgpu::BindGroupEntry {
5569 binding: 3,
5570 resource: paint_buffer.as_entire_binding(),
5571 });
5572 }
5573 entries
5574}
5575
5576impl ShapeBatchBuffers {
5577 fn new(
5578 device: &wgpu::Device,
5579 bind_group_layout: &wgpu::BindGroupLayout,
5580 similarity_buffer: &wgpu::Buffer,
5581 paint_buffer: Option<&wgpu::Buffer>,
5582 batch_limits: ShapeBatchLimits,
5583 ) -> Self {
5584 debug_assert_eq!(
5585 paint_buffer.is_some(),
5586 batch_limits.storage,
5587 "the paint binding exists exactly when the layout is in storage mode"
5588 );
5589 let initial_shape_cap = batch_limits.initial_shape_capacity();
5590 let initial_gradient_cap = batch_limits.initial_gradient_capacity();
5591
5592 let shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5593 label: Some("Shape Data Buffer"),
5594 size: (std::mem::size_of::<ShapeData>() * initial_shape_cap) as u64,
5595 usage: batch_limits.data_buffer_usage(),
5596 mapped_at_creation: false,
5597 });
5598
5599 let gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5600 label: Some("Gradient Buffer"),
5601 size: (std::mem::size_of::<GradientStop>() * initial_gradient_cap) as u64,
5602 usage: batch_limits.data_buffer_usage(),
5603 mapped_at_creation: false,
5604 });
5605
5606 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5607 label: Some("Shape Bind Group"),
5608 layout: bind_group_layout,
5609 entries: &shape_batch_bind_group_entries(
5610 &shape_buffer,
5611 &gradient_buffer,
5612 similarity_buffer,
5613 paint_buffer,
5614 ),
5615 });
5616
5617 Self {
5618 shape_buffer,
5619 gradient_buffer,
5620 bind_group,
5621 shape_capacity: initial_shape_cap,
5622 gradient_capacity: initial_gradient_cap,
5623 batch_limits,
5624 }
5625 }
5626
5627 fn ensure_capacity(
5630 &mut self,
5631 device: &wgpu::Device,
5632 bind_group_layout: &wgpu::BindGroupLayout,
5633 similarity_buffer: &wgpu::Buffer,
5634 paint_buffer: Option<&wgpu::Buffer>,
5635 shapes_needed: usize,
5636 gradients_needed: usize,
5637 ) {
5638 let mut need_bind_group_update = false;
5639
5640 if shapes_needed > self.shape_capacity
5644 && self.shape_capacity < self.batch_limits.max_shapes_per_batch
5645 {
5646 let new_cap = shapes_needed
5647 .next_power_of_two()
5648 .min(self.batch_limits.max_shapes_per_batch);
5649 self.shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5650 label: Some("Shape Data Buffer"),
5651 size: (std::mem::size_of::<ShapeData>() * new_cap) as u64,
5652 usage: self.batch_limits.data_buffer_usage(),
5653 mapped_at_creation: false,
5654 });
5655 self.shape_capacity = new_cap;
5656 need_bind_group_update = true;
5657 }
5658
5659 if gradients_needed > self.gradient_capacity
5660 && self.gradient_capacity < self.batch_limits.max_gradient_stops
5661 {
5662 let new_cap = gradients_needed
5663 .max(1)
5664 .next_power_of_two()
5665 .min(self.batch_limits.max_gradient_stops);
5666 self.gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5667 label: Some("Gradient Buffer"),
5668 size: (std::mem::size_of::<GradientStop>() * new_cap) as u64,
5669 usage: self.batch_limits.data_buffer_usage(),
5670 mapped_at_creation: false,
5671 });
5672 self.gradient_capacity = new_cap;
5673 need_bind_group_update = true;
5674 }
5675
5676 if need_bind_group_update {
5677 self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5678 label: Some("Shape Bind Group"),
5679 layout: bind_group_layout,
5680 entries: &shape_batch_bind_group_entries(
5681 &self.shape_buffer,
5682 &self.gradient_buffer,
5683 similarity_buffer,
5684 paint_buffer,
5685 ),
5686 });
5687 }
5688 }
5689}
5690
5691#[cfg(target_arch = "wasm32")]
5692impl UniformBatchBuffer {
5693 fn new(device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout) -> Self {
5694 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
5695 label: Some("Viewport Uniform Batch Buffer"),
5696 size: std::mem::size_of::<Uniforms>() as u64,
5697 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
5698 mapped_at_creation: false,
5699 });
5700 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5701 label: Some("Viewport Uniform Batch Bind Group"),
5702 layout: bind_group_layout,
5703 entries: &[wgpu::BindGroupEntry {
5704 binding: 0,
5705 resource: buffer.as_entire_binding(),
5706 }],
5707 });
5708 Self { buffer, bind_group }
5709 }
5710}
5711
5712#[cfg(target_arch = "wasm32")]
5713impl ImageBatchBuffers {
5714 fn new(device: &wgpu::Device) -> Self {
5715 let vertex_capacity = 4;
5716 let index_capacity = 6;
5717 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5718 label: Some("Image Vertex Batch Buffer"),
5719 size: (std::mem::size_of::<Vertex>() * vertex_capacity) as u64,
5720 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
5721 mapped_at_creation: false,
5722 });
5723 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5724 label: Some("Image Index Batch Buffer"),
5725 size: (std::mem::size_of::<u32>() * index_capacity) as u64,
5726 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
5727 mapped_at_creation: false,
5728 });
5729 Self {
5730 vertex_buffer,
5731 index_buffer,
5732 vertex_capacity,
5733 index_capacity,
5734 }
5735 }
5736
5737 fn ensure_capacity(
5738 &mut self,
5739 device: &wgpu::Device,
5740 vertices_needed: usize,
5741 indices_needed: usize,
5742 ) {
5743 let hard_max_bytes = HARD_MAX_BUFFER_MB * 1024 * 1024;
5744 if vertices_needed > self.vertex_capacity {
5745 let desired = vertices_needed.next_power_of_two();
5746 let max_count = hard_max_bytes / std::mem::size_of::<Vertex>();
5747 let new_cap = desired.min(max_count);
5748 self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5749 label: Some("Image Vertex Batch Buffer"),
5750 size: (std::mem::size_of::<Vertex>() * new_cap) as u64,
5751 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
5752 mapped_at_creation: false,
5753 });
5754 self.vertex_capacity = new_cap;
5755 }
5756 if indices_needed > self.index_capacity {
5757 let desired = indices_needed.next_power_of_two();
5758 let max_count = hard_max_bytes / std::mem::size_of::<u32>();
5759 let new_cap = desired.min(max_count);
5760 self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5761 label: Some("Image Index Batch Buffer"),
5762 size: (std::mem::size_of::<u32>() * new_cap) as u64,
5763 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
5764 mapped_at_creation: false,
5765 });
5766 self.index_capacity = new_cap;
5767 }
5768 }
5769}
5770
5771pub struct GpuRenderer {
5774 pub(crate) device: Arc<wgpu::Device>,
5775 pub(crate) queue: Arc<wgpu::Queue>,
5776 device_errors: Arc<DeviceErrorSentry>,
5782 renderer_epoch: u64,
5787 #[cfg(not(target_arch = "wasm32"))]
5793 store_feed_generation: u64,
5794 surface_format: wgpu::TextureFormat,
5795 adapter_backend: wgpu::Backend,
5796 shape_batch_limits: ShapeBatchLimits,
5797 pipeline_cache: Option<wgpu::PipelineCache>,
5803 pipeline: PassPipeline,
5804 pipeline_dst_out: PassPipeline,
5805 pipeline_solid: PassPipeline,
5807 #[cfg(not(target_arch = "wasm32"))]
5811 mesh_pipeline: PassPipeline,
5812 #[cfg(not(target_arch = "wasm32"))]
5818 instanced_quads: Option<InstancedQuadPipelines>,
5819 uniform_bind_group_layout: wgpu::BindGroupLayout,
5820 shape_bind_group_layout: wgpu::BindGroupLayout,
5821 dummy_paint_buffer: Option<wgpu::Buffer>,
5824 identity_similarity_buffer: wgpu::Buffer,
5827 #[cfg(not(target_arch = "wasm32"))]
5828 replay_slots: ReplaySlotStore,
5829 image_pipeline: PassPipeline,
5830 image_pipeline_dst_out: PassPipeline,
5831 glyph_atlas_pipeline: PassPipeline,
5832 #[cfg(not(target_arch = "wasm32"))]
5833 retained_glyph_atlas_pipeline: PassPipeline,
5834 image_bind_group_layout: wgpu::BindGroupLayout,
5835 #[cfg(not(target_arch = "wasm32"))]
5836 retained_glyph_uniform_bind_group_layout: wgpu::BindGroupLayout,
5837 image_nearest_sampler: wgpu::Sampler,
5838 image_linear_sampler: wgpu::Sampler,
5839 text_fonts: SoftwareTextFontSet,
5840 #[cfg(not(target_arch = "wasm32"))]
5842 upload_buffer: wgpu::Buffer,
5843 #[cfg(not(target_arch = "wasm32"))]
5844 uniform_buffer: wgpu::Buffer,
5845 #[cfg(not(target_arch = "wasm32"))]
5846 uniform_bind_group: wgpu::BindGroup,
5847 #[cfg(not(target_arch = "wasm32"))]
5848 shape_buffers: ShapeBatchBuffers,
5849 #[cfg(not(target_arch = "wasm32"))]
5850 image_vertex_buffer: wgpu::Buffer,
5851 #[cfg(not(target_arch = "wasm32"))]
5852 image_index_buffer: wgpu::Buffer,
5853 #[cfg(not(target_arch = "wasm32"))]
5854 retained_glyph_uniform_buffer: wgpu::Buffer,
5855 #[cfg(not(target_arch = "wasm32"))]
5856 retained_glyph_uniform_bind_group: wgpu::BindGroup,
5857 #[cfg(not(target_arch = "wasm32"))]
5858 retained_glyph_uniform_stride: u64,
5859 #[cfg(not(target_arch = "wasm32"))]
5860 retained_glyph_uniform_capacity: usize,
5861 #[cfg(not(target_arch = "wasm32"))]
5862 retained_glyph_uniform_cursor: usize,
5863 #[cfg(target_arch = "wasm32")]
5864 wasm_uniform_batches: Vec<UniformBatchBuffer>,
5865 #[cfg(target_arch = "wasm32")]
5866 wasm_uniform_batch_cursor: usize,
5867 #[cfg(target_arch = "wasm32")]
5868 wasm_shape_batches: Vec<ShapeBatchBuffers>,
5869 #[cfg(target_arch = "wasm32")]
5870 wasm_shape_batch_cursor: usize,
5871 #[cfg(target_arch = "wasm32")]
5872 wasm_image_batches: Vec<ImageBatchBuffers>,
5873 #[cfg(target_arch = "wasm32")]
5874 wasm_image_batch_cursor: usize,
5875 image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
5876 image_texture_cache_bytes: usize,
5878 text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
5879 text_glyph_atlas: TextGlyphAtlas,
5880 text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
5881 #[cfg(not(target_arch = "wasm32"))]
5882 text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedGpuTextGlyphRun>,
5883 text_glyph_mask_cache: SoftwareGlyphRasterCache,
5884 text_line_index_cache: TextLineIndexCache,
5885 scratch_shape_data: Vec<ShapeData>,
5886 scratch_gradients: Vec<GradientStop>,
5887 scratch_image_vertices: Vec<Vertex>,
5888 scratch_image_indices: Vec<u32>,
5889 scratch_image_cmds: Vec<ImageDrawCmd>,
5890 scratch_glyph_cmds: Vec<GlyphDrawCmd>,
5891 scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
5892 scratch_text_glyph_placements: Vec<SoftwareGlyphAtlasPlacement>,
5893 scratch_text_glyph_quads: Vec<CachedTextGlyphQuad>,
5894 scratch_segment_items: Vec<(usize, SegmentDrawItem)>,
5895 scratch_effect_ranges: Vec<Range<usize>>,
5896 scratch_layer_events: Vec<LayerEvent>,
5897 staged_uploads: StagedBufferUploads,
5898 frame_graph_executor: WgpuFrameGraphExecutor,
5899 deferred_offscreen_releases: Vec<OffscreenTarget>,
5900 effect_renderer: EffectRenderer,
5901 layer_surface_cache: LayerSurfaceCache,
5902 observed_scene_range_cache_misses: BoundedLruCache<LayerRasterCacheKey, ()>,
5903 shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
5904 shadow_surface_cache_bytes: u64,
5905 frame_stats: gpu_stats::FrameStats,
5906 last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
5907 pending_frame_warmup_frames: u8,
5908 frame_count: u64,
5909 gpu_stats_enabled: bool,
5910 warning_state: RendererWarningState,
5911 #[cfg(not(target_arch = "wasm32"))]
5912 replay_upload_stats: ReplayUploadStats,
5913 #[cfg(not(target_arch = "wasm32"))]
5914 segment_encode_stats: SegmentEncodeStats,
5915 #[cfg(not(target_arch = "wasm32"))]
5922 replay_color_patches: Vec<crate::scene::ColorPatch>,
5923 #[cfg(not(target_arch = "wasm32"))]
5927 color_patch_scratch: Vec<crate::scene::ColorPatch>,
5928 #[cfg(not(target_arch = "wasm32"))]
5934 replay_capture_shape_scratch: Vec<ShapeData>,
5935 #[cfg(not(target_arch = "wasm32"))]
5937 replay_capture_gradient_scratch: Vec<GradientStop>,
5938 #[cfg(not(target_arch = "wasm32"))]
5943 replay_ack_confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
5944 #[cfg(not(target_arch = "wasm32"))]
5949 replay_generation_drops: u64,
5950 #[cfg(not(target_arch = "wasm32"))]
5953 retained_bundle_cache: RetainedBundleCache,
5954 #[cfg(not(target_arch = "wasm32"))]
5959 rim_mesh_vertices: Vec<MeshVertex>,
5960 #[cfg(not(target_arch = "wasm32"))]
5961 rim_mesh_indices: Vec<u32>,
5962 #[cfg(not(target_arch = "wasm32"))]
5968 rim_mesh_vertex_buffer: Option<wgpu::Buffer>,
5969 #[cfg(not(target_arch = "wasm32"))]
5970 rim_mesh_index_buffer: Option<wgpu::Buffer>,
5971 #[cfg(not(target_arch = "wasm32"))]
5974 rim_mesh_uploaded_vertices: usize,
5975 #[cfg(not(target_arch = "wasm32"))]
5976 rim_mesh_uploaded_indices: usize,
5977 #[cfg(not(target_arch = "wasm32"))]
5980 rim_meshes_emitted: u64,
5981 #[cfg(not(target_arch = "wasm32"))]
5984 fill_area_diag: FillAreaDiag,
5985 #[cfg(not(target_arch = "wasm32"))]
5989 static_span: StaticSpanCache,
5990 #[cfg(not(target_arch = "wasm32"))]
5995 segment_surfaces: SegmentSurfaceCache,
5996 #[cfg(not(target_arch = "wasm32"))]
6001 display_clip: DisplayClipState,
6002}
6003
6004#[cfg(not(target_arch = "wasm32"))]
6007type DisplayClipResourceKey = ((u32, u32), DisplayVisibleRegion);
6008
6009#[cfg(not(target_arch = "wasm32"))]
6011struct DisplayClipState {
6012 visible_region: DisplayVisibleRegion,
6018 frame_root_view: Option<wgpu::TextureView>,
6024 pass_depth: Cell<bool>,
6029 resources: Option<(DisplayClipResourceKey, Option<DisplayClipResources>)>,
6034 occluder_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
6035}
6036
6037#[cfg(not(target_arch = "wasm32"))]
6038impl DisplayClipState {
6039 fn new() -> Self {
6040 Self {
6041 visible_region: DisplayVisibleRegion::Full,
6042 frame_root_view: None,
6043 pass_depth: Cell::new(false),
6044 resources: None,
6045 occluder_pipeline: LazyGpuResource::new("display-clip/occluder"),
6046 }
6047 }
6048}
6049
6050#[cfg(not(target_arch = "wasm32"))]
6052struct DisplayClipResources {
6053 depth_view: wgpu::TextureView,
6056 occluder_vertex_buffer: wgpu::Buffer,
6059 occluder_vertex_count: u32,
6060}
6061
6062#[cfg(not(target_arch = "wasm32"))]
6068#[derive(Default)]
6069struct ReplayUploadStats {
6070 calls: u64,
6071 patched_calls: u64,
6072 patches: u64,
6073 slots: u64,
6074 records: u64,
6075 bytes: u64,
6076 ideal_bytes: u64,
6077 max_frame_bytes: u64,
6078}
6079
6080#[cfg(not(target_arch = "wasm32"))]
6081impl ReplayUploadStats {
6082 const REPORT_CALLS: u64 = 1024;
6092
6093 fn note_frame(&mut self, patches: u64, slots: u64, records: u64, bytes: u64, ideal: u64) {
6094 self.calls += 1;
6095 if patches > 0 {
6096 self.patched_calls += 1;
6097 self.patches += patches;
6098 self.slots += slots;
6099 self.records += records;
6100 self.bytes += bytes;
6101 self.ideal_bytes += ideal;
6102 self.max_frame_bytes = self.max_frame_bytes.max(bytes);
6103 }
6104 if self.calls >= Self::REPORT_CALLS {
6105 let patched = self.patched_calls.max(1);
6106 log::warn!(
6107 "[replay-upload] {} patched of {} drains: avg {:.1} KB/frame (max {:.1} KB), \
6108 color-only would be {:.1} KB/frame; avg {} patches over {} records in {} slots",
6109 self.patched_calls,
6110 self.calls,
6111 self.bytes as f64 / patched as f64 / 1024.0,
6112 self.max_frame_bytes as f64 / 1024.0,
6113 self.ideal_bytes as f64 / patched as f64 / 1024.0,
6114 self.patches / patched,
6115 self.records / patched,
6116 self.slots / patched,
6117 );
6118 *self = Self::default();
6119 }
6120 }
6121}
6122
6123#[cfg(not(target_arch = "wasm32"))]
6130#[derive(Default)]
6131struct SegmentEncodeStats {
6132 calls: u64,
6133 partitions: u64,
6134 max_partitions: u64,
6135 encode_micros: u64,
6136 max_call_micros: u64,
6137}
6138
6139#[cfg(not(target_arch = "wasm32"))]
6140impl SegmentEncodeStats {
6141 const REPORT_CALLS: u64 = 1024;
6142
6143 fn note_call(&mut self, partitions: u64, micros: u64) {
6144 self.calls += 1;
6145 self.partitions += partitions;
6146 self.max_partitions = self.max_partitions.max(partitions);
6147 self.encode_micros += micros;
6148 self.max_call_micros = self.max_call_micros.max(micros);
6149 if self.calls >= Self::REPORT_CALLS {
6150 log::warn!(
6151 "[segment-encode] {} chunks: avg {:.1} partitions (max {}), \
6152 avg {:.2} ms encode (max {:.2})",
6153 self.calls,
6154 self.partitions as f64 / self.calls as f64,
6155 self.max_partitions,
6156 self.encode_micros as f64 / self.calls as f64 / 1000.0,
6157 self.max_call_micros as f64 / 1000.0,
6158 );
6159 *self = Self::default();
6160 }
6161 }
6162}
6163
6164fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
6165 let filter = match sampling {
6166 ImageSampling::Nearest => wgpu::FilterMode::Nearest,
6167 ImageSampling::Linear => wgpu::FilterMode::Linear,
6168 };
6169 wgpu::SamplerDescriptor {
6170 label: Some(match sampling {
6171 ImageSampling::Nearest => "Nearest Image Sampler",
6172 ImageSampling::Linear => "Linear Image Sampler",
6173 }),
6174 address_mode_u: wgpu::AddressMode::ClampToEdge,
6175 address_mode_v: wgpu::AddressMode::ClampToEdge,
6176 address_mode_w: wgpu::AddressMode::ClampToEdge,
6177 mag_filter: filter,
6178 min_filter: filter,
6179 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
6180 ..Default::default()
6181 }
6182}
6183
6184#[cfg(test)]
6185fn layer_raster_cache_candidate(
6186 layer: &LayerNode,
6187 root_scale: f32,
6188 has_backdrop_underlay: bool,
6189 allow_runtime_cache: bool,
6190) -> Option<(LayerRasterCacheKey, Rect)> {
6191 let mut layer_surface_requirements_cache = HashMap::new();
6192 let surface_requirements =
6193 layer_surface_requirements_cached(layer, &mut layer_surface_requirements_cache);
6194 let runtime_cache_is_safe = allow_runtime_cache
6195 && surface_requirements
6196 .surface_requirements
6197 .has_isolating_requirement()
6198 && !surface_requirements.contains_runtime_shader;
6199 let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
6200 || (allow_runtime_cache && surface_requirements.has_renderer_forced_surface())
6201 || runtime_cache_is_safe;
6202 if !cache_is_allowed {
6203 return None;
6204 }
6205 if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
6206 return None;
6207 }
6208 if surface_requirements.contains_runtime_shader {
6211 return None;
6212 }
6213
6214 let logical_rect = estimate_layer_surface_rect(layer);
6215 let pixel_size = surface_target_size(logical_rect, root_scale, u32::MAX);
6216 Some((
6217 LayerRasterCacheKey::new(
6218 layer.node_id,
6219 layer.target_content_hash(),
6220 layer.effect_hash(),
6221 logical_rect,
6222 pixel_size,
6223 ScaleBucket::from_scale(root_scale),
6224 ),
6225 logical_rect,
6226 ))
6227}
6228
6229impl GpuRenderer {
6230 #[allow(clippy::too_many_arguments)]
6231 pub fn new(
6232 device: Arc<wgpu::Device>,
6233 queue: Arc<wgpu::Queue>,
6234 surface_format: wgpu::TextureFormat,
6235 adapter_backend: wgpu::Backend,
6236 adapter_downlevel: wgpu::DownlevelFlags,
6242 text_fonts: SoftwareTextFontSet,
6243 renderer_epoch: u64,
6244 store_feed_generation: u64,
6245 ) -> Self {
6246 #[cfg(target_arch = "wasm32")]
6247 let _ = store_feed_generation;
6248 let construction_started = Instant::now();
6256 let device_errors = Arc::new(DeviceErrorSentry::default());
6262 if survive_gpu_errors_enabled() {
6263 let sentry = Arc::clone(&device_errors);
6264 device.on_uncaptured_error(Arc::new(move |error| sentry.record(&error)));
6265 }
6266 let shape_batch_limits = ShapeBatchLimits::for_device(&device, adapter_downlevel);
6267 let uniform_bind_group_layout =
6268 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6269 label: Some("Uniform Bind Group Layout"),
6270 entries: &[wgpu::BindGroupLayoutEntry {
6271 binding: 0,
6272 visibility: wgpu::ShaderStages::VERTEX,
6273 ty: wgpu::BindingType::Buffer {
6274 ty: wgpu::BufferBindingType::Uniform,
6275 has_dynamic_offset: false,
6276 min_binding_size: None,
6277 },
6278 count: None,
6279 }],
6280 });
6281 #[cfg(not(target_arch = "wasm32"))]
6282 let retained_glyph_uniform_bind_group_layout =
6283 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6284 label: Some("Retained Glyph Dynamic Uniform Bind Group Layout"),
6285 entries: &[wgpu::BindGroupLayoutEntry {
6286 binding: 0,
6287 visibility: wgpu::ShaderStages::VERTEX,
6288 ty: wgpu::BindingType::Buffer {
6289 ty: wgpu::BufferBindingType::Uniform,
6290 has_dynamic_offset: true,
6291 min_binding_size: wgpu::BufferSize::new(
6292 std::mem::size_of::<Uniforms>() as u64
6293 ),
6294 },
6295 count: None,
6296 }],
6297 });
6298
6299 let mut shape_bind_group_layout_entries = vec![
6309 wgpu::BindGroupLayoutEntry {
6310 binding: 0,
6311 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
6312 ty: wgpu::BindingType::Buffer {
6313 ty: shape_batch_limits.data_binding_type(),
6314 has_dynamic_offset: false,
6315 min_binding_size: None,
6316 },
6317 count: None,
6318 },
6319 wgpu::BindGroupLayoutEntry {
6320 binding: 1,
6321 visibility: wgpu::ShaderStages::FRAGMENT,
6322 ty: wgpu::BindingType::Buffer {
6323 ty: shape_batch_limits.data_binding_type(),
6324 has_dynamic_offset: false,
6325 min_binding_size: None,
6326 },
6327 count: None,
6328 },
6329 wgpu::BindGroupLayoutEntry {
6334 binding: 2,
6335 visibility: wgpu::ShaderStages::VERTEX,
6336 ty: wgpu::BindingType::Buffer {
6337 ty: wgpu::BufferBindingType::Uniform,
6338 has_dynamic_offset: true,
6339 min_binding_size: wgpu::BufferSize::new(
6340 std::mem::size_of::<SimilarityTransform>() as u64,
6341 ),
6342 },
6343 count: None,
6344 },
6345 ];
6346 if shape_batch_limits.storage {
6352 shape_bind_group_layout_entries.push(wgpu::BindGroupLayoutEntry {
6353 binding: 3,
6354 visibility: wgpu::ShaderStages::VERTEX,
6355 ty: wgpu::BindingType::Buffer {
6356 ty: wgpu::BufferBindingType::Storage { read_only: true },
6357 has_dynamic_offset: false,
6358 min_binding_size: None,
6359 },
6360 count: None,
6361 });
6362 }
6363 let shape_bind_group_layout =
6364 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6365 label: Some("Shape Bind Group Layout"),
6366 entries: &shape_bind_group_layout_entries,
6367 });
6368
6369 let identity_similarity_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6370 label: Some("Identity Similarity Buffer"),
6371 size: std::mem::size_of::<SimilarityTransform>() as u64,
6372 usage: wgpu::BufferUsages::UNIFORM,
6373 mapped_at_creation: true,
6374 });
6375 identity_similarity_buffer
6376 .slice(..)
6377 .get_mapped_range_mut()
6378 .copy_from_slice(bytemuck::bytes_of(&SimilarityTransform::IDENTITY));
6379 identity_similarity_buffer.unmap();
6380
6381 let dummy_paint_buffer = shape_batch_limits.storage.then(|| {
6386 device.create_buffer(&wgpu::BufferDescriptor {
6387 label: Some("Dummy Paint Buffer"),
6388 size: std::mem::size_of::<[f32; 4]>() as u64,
6389 usage: wgpu::BufferUsages::STORAGE,
6390 mapped_at_creation: false,
6391 })
6392 });
6393 #[cfg(not(target_arch = "wasm32"))]
6394 let replay_slot_store = ReplaySlotStore::new(&device);
6395
6396 let pipeline = PassPipeline::new("shape/src-over", "shape/src-over-depth");
6397 let pipeline_dst_out = PassPipeline::new("shape/dst-out", "shape/dst-out-depth");
6398 let pipeline_solid =
6399 PassPipeline::new("shape/solid-src-over", "shape/solid-src-over-depth");
6400 #[cfg(not(target_arch = "wasm32"))]
6401 let mesh_pipeline = PassPipeline::new("shape/mesh", "shape/mesh-depth");
6402 #[cfg(not(target_arch = "wasm32"))]
6408 let instanced_quads =
6409 (shape_batch_limits.storage && instanced_quads_enabled()).then(|| {
6410 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6411 label: Some("Instanced Quad Index Buffer"),
6412 size: std::mem::size_of_val(&INSTANCED_QUAD_INDICES) as u64,
6413 usage: wgpu::BufferUsages::INDEX,
6414 mapped_at_creation: true,
6415 });
6416 index_buffer
6417 .slice(..)
6418 .get_mapped_range_mut()
6419 .copy_from_slice(bytemuck::cast_slice(&INSTANCED_QUAD_INDICES));
6420 index_buffer.unmap();
6421 InstancedQuadPipelines {
6422 pipeline: PassPipeline::new(
6423 "shape/instanced-src-over",
6424 "shape/instanced-src-over-depth",
6425 ),
6426 pipeline_dst_out: PassPipeline::new(
6427 "shape/instanced-dst-out",
6428 "shape/instanced-dst-out-depth",
6429 ),
6430 pipeline_solid: PassPipeline::new(
6431 "shape/instanced-solid",
6432 "shape/instanced-solid-depth",
6433 ),
6434 index_buffer,
6435 }
6436 });
6437
6438 let image_bind_group_layout =
6439 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6440 label: Some("Image Texture Bind Group Layout"),
6441 entries: &[
6442 wgpu::BindGroupLayoutEntry {
6443 binding: 0,
6444 visibility: wgpu::ShaderStages::FRAGMENT,
6445 ty: wgpu::BindingType::Texture {
6446 multisampled: false,
6447 view_dimension: wgpu::TextureViewDimension::D2,
6448 sample_type: wgpu::TextureSampleType::Float { filterable: true },
6449 },
6450 count: None,
6451 },
6452 wgpu::BindGroupLayoutEntry {
6453 binding: 1,
6454 visibility: wgpu::ShaderStages::FRAGMENT,
6455 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
6456 count: None,
6457 },
6458 ],
6459 });
6460
6461 let image_pipeline = PassPipeline::new("image/src-over", "image/src-over-depth");
6462 let image_pipeline_dst_out = PassPipeline::new("image/dst-out", "image/dst-out-depth");
6463 let glyph_atlas_pipeline = PassPipeline::new("glyph/shared", "glyph/shared-depth");
6464 #[cfg(not(target_arch = "wasm32"))]
6465 let retained_glyph_atlas_pipeline =
6466 PassPipeline::new("glyph/retained", "glyph/retained-depth");
6467
6468 #[cfg(not(target_arch = "wasm32"))]
6469 let upload_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6470 label: Some("Frame Upload Buffer"),
6471 size: INITIAL_UPLOAD_BUFFER_BYTES,
6472 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
6473 mapped_at_creation: false,
6474 });
6475
6476 #[cfg(not(target_arch = "wasm32"))]
6477 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6478 label: Some("Uniform Buffer"),
6479 size: std::mem::size_of::<Uniforms>() as u64,
6480 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
6481 mapped_at_creation: false,
6482 });
6483
6484 #[cfg(not(target_arch = "wasm32"))]
6485 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
6486 label: Some("Uniform Bind Group"),
6487 layout: &uniform_bind_group_layout,
6488 entries: &[wgpu::BindGroupEntry {
6489 binding: 0,
6490 resource: uniform_buffer.as_entire_binding(),
6491 }],
6492 });
6493
6494 #[cfg(not(target_arch = "wasm32"))]
6495 let shape_buffers = ShapeBatchBuffers::new(
6496 &device,
6497 &shape_bind_group_layout,
6498 &identity_similarity_buffer,
6499 dummy_paint_buffer.as_ref(),
6500 shape_batch_limits,
6501 );
6502
6503 let image_nearest_sampler =
6504 device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
6505 let image_linear_sampler =
6506 device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
6507 let text_glyph_atlas = TextGlyphAtlas::new(
6508 &device,
6509 &image_bind_group_layout,
6510 &image_nearest_sampler,
6511 TEXT_GLYPH_ATLAS_MIN_SIZE,
6512 );
6513
6514 #[cfg(not(target_arch = "wasm32"))]
6515 let image_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6516 label: Some("Image Vertex Buffer"),
6517 size: (std::mem::size_of::<Vertex>() * 4) as u64,
6518 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
6519 mapped_at_creation: false,
6520 });
6521
6522 #[cfg(not(target_arch = "wasm32"))]
6523 let image_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6524 label: Some("Image Index Buffer"),
6525 size: (std::mem::size_of::<u32>() * 6) as u64,
6526 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
6527 mapped_at_creation: false,
6528 });
6529 #[cfg(not(target_arch = "wasm32"))]
6530 let retained_glyph_uniform_stride = align_usize_to(
6531 std::mem::size_of::<Uniforms>(),
6532 (device.limits().min_uniform_buffer_offset_alignment as usize)
6533 .max(wgpu::COPY_BUFFER_ALIGNMENT as usize),
6534 ) as u64;
6535 #[cfg(not(target_arch = "wasm32"))]
6536 let retained_glyph_uniform_capacity = INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS;
6537 #[cfg(not(target_arch = "wasm32"))]
6538 let retained_glyph_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6539 label: Some("Retained Glyph Uniform Buffer"),
6540 size: retained_glyph_uniform_stride * retained_glyph_uniform_capacity as u64,
6541 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
6542 mapped_at_creation: false,
6543 });
6544 #[cfg(not(target_arch = "wasm32"))]
6545 let retained_glyph_uniform_bind_group =
6546 device.create_bind_group(&wgpu::BindGroupDescriptor {
6547 label: Some("Retained Glyph Uniform Bind Group"),
6548 layout: &retained_glyph_uniform_bind_group_layout,
6549 entries: &[wgpu::BindGroupEntry {
6550 binding: 0,
6551 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
6552 buffer: &retained_glyph_uniform_buffer,
6553 offset: 0,
6554 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
6555 }),
6556 }],
6557 });
6558
6559 #[cfg(not(target_arch = "wasm32"))]
6566 let pipeline_cache = crate::pipeline_disk_cache::load(&device);
6567 #[cfg(target_arch = "wasm32")]
6568 let pipeline_cache: Option<wgpu::PipelineCache> = None;
6569 #[cfg(not(target_arch = "wasm32"))]
6570 if let Some(cache) = pipeline_cache.clone() {
6571 crate::pipeline_disk_cache::spawn_persist_schedule(cache);
6572 spawn_pipeline_prewarm(PipelinePrewarmInputs {
6573 device: Arc::clone(&device),
6574 cache: pipeline_cache.clone(),
6575 surface_format,
6576 uniform_layout: uniform_bind_group_layout.clone(),
6577 shape_layout: shape_bind_group_layout.clone(),
6578 image_layout: image_bind_group_layout.clone(),
6579 batch_limits: shape_batch_limits,
6580 instanced: instanced_quads.is_some(),
6581 });
6582 }
6583
6584 let effects_started = Instant::now();
6585 let effect_renderer = EffectRenderer::new(
6586 &device,
6587 pipeline_cache.clone(),
6588 surface_format,
6589 adapter_backend,
6590 );
6591 let effects_ms = instant_ms(effects_started, Instant::now());
6592
6593 let renderer = Self {
6594 device,
6595 queue,
6596 device_errors,
6597 renderer_epoch,
6598 #[cfg(not(target_arch = "wasm32"))]
6599 store_feed_generation,
6600 surface_format,
6601 adapter_backend,
6602 shape_batch_limits,
6603 pipeline_cache,
6604 pipeline,
6605 pipeline_dst_out,
6606 pipeline_solid,
6607 #[cfg(not(target_arch = "wasm32"))]
6608 mesh_pipeline,
6609 #[cfg(not(target_arch = "wasm32"))]
6610 instanced_quads,
6611 uniform_bind_group_layout,
6612 shape_bind_group_layout,
6613 dummy_paint_buffer,
6614 identity_similarity_buffer,
6615 #[cfg(not(target_arch = "wasm32"))]
6616 replay_slots: replay_slot_store,
6617 image_pipeline,
6618 image_pipeline_dst_out,
6619 glyph_atlas_pipeline,
6620 #[cfg(not(target_arch = "wasm32"))]
6621 retained_glyph_atlas_pipeline,
6622 image_bind_group_layout,
6623 #[cfg(not(target_arch = "wasm32"))]
6624 retained_glyph_uniform_bind_group_layout,
6625 image_nearest_sampler,
6626 image_linear_sampler,
6627 text_fonts,
6628 #[cfg(not(target_arch = "wasm32"))]
6629 upload_buffer,
6630 #[cfg(not(target_arch = "wasm32"))]
6631 uniform_buffer,
6632 #[cfg(not(target_arch = "wasm32"))]
6633 uniform_bind_group,
6634 #[cfg(not(target_arch = "wasm32"))]
6635 shape_buffers,
6636 #[cfg(not(target_arch = "wasm32"))]
6637 image_vertex_buffer,
6638 #[cfg(not(target_arch = "wasm32"))]
6639 image_index_buffer,
6640 #[cfg(not(target_arch = "wasm32"))]
6641 retained_glyph_uniform_buffer,
6642 #[cfg(not(target_arch = "wasm32"))]
6643 retained_glyph_uniform_bind_group,
6644 #[cfg(not(target_arch = "wasm32"))]
6645 retained_glyph_uniform_stride,
6646 #[cfg(not(target_arch = "wasm32"))]
6647 retained_glyph_uniform_capacity,
6648 #[cfg(not(target_arch = "wasm32"))]
6649 retained_glyph_uniform_cursor: 0,
6650 #[cfg(target_arch = "wasm32")]
6651 wasm_uniform_batches: Vec::new(),
6652 #[cfg(target_arch = "wasm32")]
6653 wasm_uniform_batch_cursor: 0,
6654 #[cfg(target_arch = "wasm32")]
6655 wasm_shape_batches: Vec::new(),
6656 #[cfg(target_arch = "wasm32")]
6657 wasm_shape_batch_cursor: 0,
6658 #[cfg(target_arch = "wasm32")]
6659 wasm_image_batches: Vec::new(),
6660 #[cfg(target_arch = "wasm32")]
6661 wasm_image_batch_cursor: 0,
6662 image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
6663 MAX_TEXTURE_CACHE_ITEMS,
6664 ),
6665 image_texture_cache_bytes: 0,
6666 text_image_cache: BoundedLruCache::with_capacity_at_least_one(
6667 MAX_TEXT_IMAGE_CACHE_ITEMS,
6668 ),
6669 text_glyph_atlas,
6670 text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
6671 MAX_TEXT_GLYPH_RUN_CACHE_ITEMS,
6672 ),
6673 #[cfg(not(target_arch = "wasm32"))]
6674 text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
6675 MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
6676 ),
6677 text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
6678 MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
6679 ),
6680 text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_CACHE_ITEMS),
6681 scratch_shape_data: Vec::new(),
6682 scratch_gradients: Vec::new(),
6683 scratch_image_vertices: Vec::new(),
6684 scratch_image_indices: Vec::new(),
6685 scratch_image_cmds: Vec::new(),
6686 scratch_glyph_cmds: Vec::new(),
6687 scratch_text_glyph_run: Vec::new(),
6688 scratch_text_glyph_placements: Vec::new(),
6689 scratch_text_glyph_quads: Vec::new(),
6690 scratch_segment_items: Vec::new(),
6691 scratch_effect_ranges: Vec::new(),
6692 scratch_layer_events: Vec::new(),
6693 staged_uploads: StagedBufferUploads::default(),
6694 frame_graph_executor: WgpuFrameGraphExecutor::new(),
6695 deferred_offscreen_releases: Vec::new(),
6696 effect_renderer,
6697 layer_surface_cache: LayerSurfaceCache::new(),
6698 observed_scene_range_cache_misses: BoundedLruCache::with_capacity_at_least_one(
6699 MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES,
6700 ),
6701 shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
6702 MAX_SHADOW_SURFACE_CACHE_ITEMS,
6703 ),
6704 shadow_surface_cache_bytes: 0,
6705 frame_stats: gpu_stats::FrameStats::default(),
6706 last_frame_stats: None,
6707 pending_frame_warmup_frames: 0,
6708 frame_count: 0,
6709 gpu_stats_enabled: gpu_stats_enabled(),
6710 warning_state: RendererWarningState::default(),
6711 #[cfg(not(target_arch = "wasm32"))]
6712 replay_upload_stats: ReplayUploadStats::default(),
6713 #[cfg(not(target_arch = "wasm32"))]
6714 segment_encode_stats: SegmentEncodeStats::default(),
6715 #[cfg(not(target_arch = "wasm32"))]
6716 replay_color_patches: Vec::new(),
6717 #[cfg(not(target_arch = "wasm32"))]
6718 color_patch_scratch: Vec::new(),
6719 #[cfg(not(target_arch = "wasm32"))]
6720 replay_capture_shape_scratch: Vec::new(),
6721 #[cfg(not(target_arch = "wasm32"))]
6722 replay_capture_gradient_scratch: Vec::new(),
6723 #[cfg(not(target_arch = "wasm32"))]
6724 replay_ack_confirmations: Vec::new(),
6725 #[cfg(not(target_arch = "wasm32"))]
6726 replay_generation_drops: 0,
6727 #[cfg(not(target_arch = "wasm32"))]
6728 retained_bundle_cache: RetainedBundleCache::new(),
6729 #[cfg(not(target_arch = "wasm32"))]
6730 rim_mesh_vertices: Vec::new(),
6731 #[cfg(not(target_arch = "wasm32"))]
6732 rim_mesh_indices: Vec::new(),
6733 #[cfg(not(target_arch = "wasm32"))]
6734 rim_mesh_vertex_buffer: None,
6735 #[cfg(not(target_arch = "wasm32"))]
6736 rim_mesh_index_buffer: None,
6737 #[cfg(not(target_arch = "wasm32"))]
6738 rim_mesh_uploaded_vertices: 0,
6739 #[cfg(not(target_arch = "wasm32"))]
6740 rim_mesh_uploaded_indices: 0,
6741 #[cfg(not(target_arch = "wasm32"))]
6742 rim_meshes_emitted: 0,
6743 #[cfg(not(target_arch = "wasm32"))]
6744 fill_area_diag: FillAreaDiag::default(),
6745 #[cfg(not(target_arch = "wasm32"))]
6746 static_span: StaticSpanCache::default(),
6747 #[cfg(not(target_arch = "wasm32"))]
6748 segment_surfaces: SegmentSurfaceCache::default(),
6749 #[cfg(not(target_arch = "wasm32"))]
6750 display_clip: DisplayClipState::new(),
6751 };
6752 log::info!(
6753 "[gpu-init] {:?} renderer ready in {:.1} ms (effects {:.1} ms); \
6754 pipelines build on first use",
6755 adapter_backend,
6756 instant_ms(construction_started, Instant::now()),
6757 effects_ms,
6758 );
6759 renderer
6760 }
6761
6762 #[cfg(not(target_arch = "wasm32"))]
6768 pub fn set_display_visible_region(&mut self, region: DisplayVisibleRegion) {
6769 self.display_clip.visible_region = region;
6770 }
6771
6772 #[cfg(not(target_arch = "wasm32"))]
6776 fn pass_depth(&self) -> bool {
6777 self.display_clip.pass_depth.get()
6778 }
6779
6780 #[cfg(target_arch = "wasm32")]
6781 fn pass_depth(&self) -> bool {
6782 false
6783 }
6784
6785 #[cfg(not(target_arch = "wasm32"))]
6793 fn display_clip_pass_depth_view(
6794 &mut self,
6795 target_view: &wgpu::TextureView,
6796 width: u32,
6797 height: u32,
6798 ) -> Option<wgpu::TextureView> {
6799 if !self.display_clip.visible_region.cullable() {
6800 return None;
6801 }
6802 if self.display_clip.frame_root_view.as_ref() != Some(target_view) {
6803 return None;
6804 }
6805 if !display_clip_cull_enabled() {
6806 return None;
6807 }
6808 self.ensure_display_clip_resources(width, height)
6809 }
6810
6811 #[cfg(not(target_arch = "wasm32"))]
6817 fn ensure_display_clip_resources(
6818 &mut self,
6819 width: u32,
6820 height: u32,
6821 ) -> Option<wgpu::TextureView> {
6822 let region = self.display_clip.visible_region;
6823 let key = ((width, height), region);
6824 if let Some((cached_key, resources)) = &self.display_clip.resources {
6825 if *cached_key == key {
6826 return resources
6827 .as_ref()
6828 .map(|resources| resources.depth_view.clone());
6829 }
6830 }
6831 let built = display_clip::tessellate_complement(region, width, height).map(|mesh| {
6832 let occluder_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
6833 label: Some("Display Clip Occluder Vertices"),
6834 size: std::mem::size_of_val(mesh.vertices.as_slice()) as u64,
6835 usage: wgpu::BufferUsages::VERTEX,
6836 mapped_at_creation: true,
6837 });
6838 occluder_vertex_buffer
6839 .slice(..)
6840 .get_mapped_range_mut()
6841 .copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
6842 occluder_vertex_buffer.unmap();
6843 let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
6844 label: Some("Display Clip Depth"),
6845 size: wgpu::Extent3d {
6846 width,
6847 height,
6848 depth_or_array_layers: 1,
6849 },
6850 mip_level_count: 1,
6851 sample_count: 1,
6852 dimension: wgpu::TextureDimension::D2,
6853 format: display_clip::DISPLAY_CLIP_DEPTH_FORMAT,
6854 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
6855 view_formats: &[],
6856 });
6857 match region {
6861 DisplayVisibleRegion::InscribedCircle => log::info!(
6862 "[display-clip] round display: corner cull active ({} px masked) at {width}x{height}",
6863 mesh.masked_px,
6864 ),
6865 _ => log::info!(
6866 "[display-clip] visible-region cull active for {region:?} ({} px masked) at {width}x{height}",
6867 mesh.masked_px,
6868 ),
6869 }
6870 DisplayClipResources {
6871 depth_view: depth_texture.create_view(&wgpu::TextureViewDescriptor::default()),
6872 occluder_vertex_buffer,
6873 occluder_vertex_count: mesh.vertices.len() as u32,
6874 }
6875 });
6876 let view = built.as_ref().map(|resources| resources.depth_view.clone());
6877 self.display_clip.resources = Some((key, built));
6878 view
6879 }
6880
6881 #[cfg(not(target_arch = "wasm32"))]
6885 fn draw_display_clip_occluder(
6886 &self,
6887 render_pass: &mut wgpu::RenderPass<'_>,
6888 width: u32,
6889 height: u32,
6890 ) {
6891 let Some((((size_w, size_h), _), Some(resources))) = &self.display_clip.resources else {
6892 return;
6893 };
6894 debug_assert_eq!((*size_w, *size_h), (width, height));
6895 let pipeline =
6896 self.display_clip
6897 .occluder_pipeline
6898 .get_or_init(self.adapter_backend, || {
6899 create_display_clip_occluder_pipeline(
6900 &self.device,
6901 self.pipeline_cache.as_ref(),
6902 self.surface_format,
6903 )
6904 });
6905 render_pass.set_scissor_rect(0, 0, width, height);
6906 render_pass.set_pipeline(pipeline);
6907 render_pass.set_vertex_buffer(0, resources.occluder_vertex_buffer.slice(..));
6908 render_pass.draw(0..resources.occluder_vertex_count, 0..1);
6909 self.frame_stats.add_draw_calls(1);
6910 }
6911
6912 fn shape_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
6913 let resource = match blend_mode {
6914 BlendMode::DstOut => &self.pipeline_dst_out,
6915 _ => &self.pipeline,
6916 };
6917 resource.get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
6918 create_shape_pipeline(
6919 &self.device,
6920 self.pipeline_cache.as_ref(),
6921 self.surface_format,
6922 &self.uniform_bind_group_layout,
6923 &self.shape_bind_group_layout,
6924 blend_mode,
6925 self.shape_batch_limits,
6926 false,
6927 "vs_main",
6928 "fs_main",
6929 depth,
6930 )
6931 })
6932 }
6933
6934 fn shape_pipeline_solid(&self) -> &wgpu::RenderPipeline {
6943 self.pipeline_solid
6944 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
6945 let solid_trim = solid_trim_varyings_enabled();
6946 let (vertex_entry, fragment_entry) = if solid_trim {
6947 ("vs_solid", "fs_solid_trim")
6948 } else {
6949 ("vs_main", "fs_solid")
6950 };
6951 create_shape_pipeline(
6952 &self.device,
6953 self.pipeline_cache.as_ref(),
6954 self.surface_format,
6955 &self.uniform_bind_group_layout,
6956 &self.shape_bind_group_layout,
6957 BlendMode::SrcOver,
6958 self.shape_batch_limits,
6959 solid_trim,
6960 vertex_entry,
6961 fragment_entry,
6962 depth,
6963 )
6964 })
6965 }
6966
6967 #[cfg(not(target_arch = "wasm32"))]
6968 fn mesh_pipeline(&self) -> &wgpu::RenderPipeline {
6969 self.mesh_pipeline
6970 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
6971 create_mesh_shape_pipeline(
6972 &self.device,
6973 self.pipeline_cache.as_ref(),
6974 self.surface_format,
6975 &self.uniform_bind_group_layout,
6976 &self.shape_bind_group_layout,
6977 self.shape_batch_limits,
6978 depth,
6979 )
6980 })
6981 }
6982
6983 #[cfg(not(target_arch = "wasm32"))]
6984 fn instanced_pipeline<'a>(
6985 &'a self,
6986 instanced: &'a InstancedQuadPipelines,
6987 blend_mode: BlendMode,
6988 ) -> &'a wgpu::RenderPipeline {
6989 let resource = match blend_mode {
6990 BlendMode::DstOut => &instanced.pipeline_dst_out,
6991 _ => &instanced.pipeline,
6992 };
6993 resource.get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
6994 create_instanced_shape_pipeline(
6995 &self.device,
6996 self.pipeline_cache.as_ref(),
6997 self.surface_format,
6998 &self.uniform_bind_group_layout,
6999 &self.shape_bind_group_layout,
7000 blend_mode,
7001 self.shape_batch_limits,
7002 false,
7003 "vs_shape_instanced",
7004 "fs_main",
7005 depth,
7006 )
7007 })
7008 }
7009
7010 #[cfg(not(target_arch = "wasm32"))]
7014 fn instanced_pipeline_solid<'a>(
7015 &'a self,
7016 instanced: &'a InstancedQuadPipelines,
7017 ) -> &'a wgpu::RenderPipeline {
7018 instanced
7019 .pipeline_solid
7020 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7021 let solid_trim = solid_trim_varyings_enabled();
7022 let (vertex_entry, fragment_entry) = if solid_trim {
7023 ("vs_solid_instanced", "fs_solid_trim")
7024 } else {
7025 ("vs_shape_instanced", "fs_solid")
7026 };
7027 create_instanced_shape_pipeline(
7028 &self.device,
7029 self.pipeline_cache.as_ref(),
7030 self.surface_format,
7031 &self.uniform_bind_group_layout,
7032 &self.shape_bind_group_layout,
7033 BlendMode::SrcOver,
7034 self.shape_batch_limits,
7035 solid_trim,
7036 vertex_entry,
7037 fragment_entry,
7038 depth,
7039 )
7040 })
7041 }
7042
7043 fn image_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
7044 let resource = match blend_mode {
7045 BlendMode::DstOut => &self.image_pipeline_dst_out,
7046 _ => &self.image_pipeline,
7047 };
7048 resource.get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7049 create_image_pipeline(
7050 &self.device,
7051 self.pipeline_cache.as_ref(),
7052 self.surface_format,
7053 &self.uniform_bind_group_layout,
7054 &self.image_bind_group_layout,
7055 blend_mode,
7056 depth,
7057 )
7058 })
7059 }
7060
7061 fn glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
7062 self.glyph_atlas_pipeline
7063 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7064 create_glyph_atlas_pipeline(
7065 &self.device,
7066 self.pipeline_cache.as_ref(),
7067 self.surface_format,
7068 &self.uniform_bind_group_layout,
7069 &self.image_bind_group_layout,
7070 depth,
7071 )
7072 })
7073 }
7074
7075 #[cfg(not(target_arch = "wasm32"))]
7076 fn retained_glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
7077 self.retained_glyph_atlas_pipeline.get_or_init(
7078 self.adapter_backend,
7079 self.pass_depth(),
7080 |depth| {
7081 create_glyph_atlas_pipeline(
7082 &self.device,
7083 self.pipeline_cache.as_ref(),
7084 self.surface_format,
7085 &self.retained_glyph_uniform_bind_group_layout,
7086 &self.image_bind_group_layout,
7087 depth,
7088 )
7089 },
7090 )
7091 }
7092
7093 fn ensure_image_cached(&mut self, image: &ImageBitmap) -> Result<(), String> {
7094 if self.image_texture_cache.get(&image.id()).is_some() {
7095 return Ok(());
7096 }
7097
7098 let size = wgpu::Extent3d {
7099 width: image.width(),
7100 height: image.height(),
7101 depth_or_array_layers: 1,
7102 };
7103
7104 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
7105 label: Some("Image Texture"),
7106 size,
7107 mip_level_count: 1,
7108 sample_count: 1,
7109 dimension: wgpu::TextureDimension::D2,
7110 format: wgpu::TextureFormat::Rgba8Unorm,
7111 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
7112 view_formats: &[],
7113 });
7114
7115 let upload_stats = self.frame_graph_executor.upload_texture(
7116 &self.queue,
7117 wgpu::TexelCopyTextureInfo {
7118 texture: &texture,
7119 mip_level: 0,
7120 origin: wgpu::Origin3d::ZERO,
7121 aspect: wgpu::TextureAspect::All,
7122 },
7123 image.pixels(),
7124 wgpu::TexelCopyBufferLayout {
7125 offset: 0,
7126 bytes_per_row: Some(4 * image.width()),
7127 rows_per_image: Some(image.height()),
7128 },
7129 size,
7130 );
7131 self.frame_stats.record_command_stats(upload_stats);
7132
7133 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
7134 let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
7135 let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
7136
7137 let bytes = image.width() as usize * image.height() as usize * 4;
7138 if let Some(replaced) = self.image_texture_cache.put(
7139 image.id(),
7140 CachedImageTexture {
7141 _texture: texture,
7142 _view: view,
7143 nearest_bind_group,
7144 linear_bind_group,
7145 bytes,
7146 },
7147 ) {
7148 self.image_texture_cache_bytes = self
7149 .image_texture_cache_bytes
7150 .saturating_sub(replaced.bytes);
7151 }
7152 self.image_texture_cache_bytes += bytes;
7153 while self.image_texture_cache_bytes > MAX_IMAGE_TEXTURE_CACHE_BYTES
7156 && self.image_texture_cache.len() > 1
7157 {
7158 let Some((_, evicted)) = self.image_texture_cache.pop_lru() else {
7159 break;
7160 };
7161 self.image_texture_cache_bytes =
7162 self.image_texture_cache_bytes.saturating_sub(evicted.bytes);
7163 }
7164 Ok(())
7165 }
7166
7167 fn image_bind_group(
7168 &self,
7169 view: &wgpu::TextureView,
7170 sampler: &wgpu::Sampler,
7171 ) -> wgpu::BindGroup {
7172 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
7173 label: Some("Image Texture Bind Group"),
7174 layout: &self.image_bind_group_layout,
7175 entries: &[
7176 wgpu::BindGroupEntry {
7177 binding: 0,
7178 resource: wgpu::BindingResource::TextureView(view),
7179 },
7180 wgpu::BindGroupEntry {
7181 binding: 1,
7182 resource: wgpu::BindingResource::Sampler(sampler),
7183 },
7184 ],
7185 })
7186 }
7187
7188 fn max_texture_dim(&self) -> u32 {
7191 self.effect_renderer.max_texture_dim()
7192 }
7193
7194 fn acquire_offscreen(&mut self, width: u32, height: u32) -> OffscreenTarget {
7195 self.effect_renderer
7196 .acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
7197 }
7198
7199 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
7200 self.acquire_offscreen(width, height)
7201 }
7202
7203 fn transient_offscreen_descriptor(
7204 &self,
7205 label: &'static str,
7206 width: u32,
7207 height: u32,
7208 ) -> FrameTextureDescriptor {
7209 let max_texture_dim = self.max_texture_dim();
7210 FrameTextureDescriptor::render_attachment(
7211 label,
7212 width.min(max_texture_dim),
7213 height.min(max_texture_dim),
7214 self.surface_format,
7215 )
7216 }
7217
7218 fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
7219 self.deferred_offscreen_releases.push(target);
7220 }
7221
7222 fn flush_deferred_offscreen_releases(&mut self) {
7223 for target in self.deferred_offscreen_releases.drain(..) {
7224 self.effect_renderer.release_offscreen(target);
7225 }
7226 }
7227
7228 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
7229 if let LayerSurfaceTexture::Owned(target) = target {
7230 self.defer_offscreen_release(target);
7231 }
7232 }
7233
7234 fn cached_layer_surface(
7235 &mut self,
7236 key: &LayerRasterCacheKey,
7237 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
7238 self.layer_surface_cache.get(key, &self.frame_stats)
7239 }
7240
7241 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
7242 admit_layer_surface_cache_miss_impl(key, &mut self.observed_scene_range_cache_misses)
7243 }
7244
7245 fn insert_cached_layer_surface(
7246 &mut self,
7247 key: LayerRasterCacheKey,
7248 target: OffscreenTarget,
7249 logical_rect: Rect,
7250 ) -> Rc<OffscreenTarget> {
7251 self.layer_surface_cache
7252 .insert(key, target, logical_rect, &self.frame_stats)
7253 }
7254
7255 fn cached_shadow_surface(
7256 &mut self,
7257 key: &ShadowSurfaceCacheKey,
7258 ) -> Option<Rc<OffscreenTarget>> {
7259 self.shadow_surface_cache
7260 .get(key)
7261 .map(|cached| cached.target.clone())
7262 }
7263
7264 fn cached_shape_shadow_composite(
7265 &mut self,
7266 shadow: &ShadowDraw,
7267 width: u32,
7268 height: u32,
7269 root_scale: f32,
7270 ) -> Option<CachedShadowComposite> {
7271 if shadow.blur_radius <= 0.0 || shadow.shapes.is_empty() || !shadow.texts.is_empty() {
7272 return None;
7273 }
7274
7275 let plan = shape_shadow_surface_plan(
7276 &shadow.shapes,
7277 shadow.clip,
7278 shadow.blur_radius,
7279 width,
7280 height,
7281 root_scale,
7282 self.max_texture_dim(),
7283 )?;
7284 let key = shape_shadow_surface_cache_key(
7285 &shadow.shapes,
7286 &shadow.brushes,
7287 plan.source_device_bounds,
7288 plan.pixel_radius,
7289 root_scale,
7290 )?;
7291 let cached = self.cached_shadow_surface(&key)?;
7292 let viewport_offset = [plan.source_device_bounds.x, plan.source_device_bounds.y];
7293 self.frame_stats.record_shadow_shape_cache_hit(
7294 plan.source_device_bounds.width,
7295 plan.source_device_bounds.height,
7296 );
7297
7298 let clip_scissor = shadow
7299 .clip
7300 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
7301 let scissor = clip_scissor.or(plan.processing_scissor);
7302 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
7303 mask.rect[0] -= viewport_offset[0];
7304 mask.rect[1] -= viewport_offset[1];
7305 mask
7306 });
7307 let dest_viewport = Some((
7308 viewport_offset[0],
7309 viewport_offset[1],
7310 plan.source_device_bounds.width as f32,
7311 plan.source_device_bounds.height as f32,
7312 ));
7313
7314 Some(CachedShadowComposite {
7315 source: cached,
7316 scissor,
7317 rounded_mask,
7318 dest_viewport,
7319 })
7320 }
7321
7322 fn insert_cached_shadow_surface(
7323 &mut self,
7324 key: ShadowSurfaceCacheKey,
7325 target: OffscreenTarget,
7326 ) {
7327 let byte_size = offscreen_byte_size(target.width, target.height);
7328 while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
7329 let Some((_evicted_key, evicted_entry)) = self.shadow_surface_cache.pop_lru() else {
7330 break;
7331 };
7332 self.shadow_surface_cache_bytes = self
7333 .shadow_surface_cache_bytes
7334 .saturating_sub(evicted_entry.byte_size);
7335 }
7336
7337 let cached = CachedShadowSurface {
7338 target: Rc::new(target),
7339 byte_size,
7340 };
7341 if let Some((_replaced_key, replaced_entry)) = self.shadow_surface_cache.push(key, cached) {
7342 self.shadow_surface_cache_bytes = self
7343 .shadow_surface_cache_bytes
7344 .saturating_sub(replaced_entry.byte_size);
7345 }
7346 self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
7347 }
7348
7349 fn supports_render_effect(&self, effect: &RenderEffect) -> bool {
7350 is_render_effect_supported(effect)
7351 }
7352}
7353
7354struct RecordingSurfaceBackend<'renderer, 'recorder, C: FrameCommandRecorder> {
7355 renderer: &'renderer mut GpuRenderer,
7356 recorder: &'recorder mut C,
7357}
7358
7359impl<C: FrameCommandRecorder> RecordingSurfaceBackend<'_, '_, C> {
7360 #[allow(clippy::too_many_arguments)]
7361 fn render_range_with_layer_events_to_target_recorded(
7362 &mut self,
7363 target: &OffscreenTarget,
7364 shapes: &[DrawShape],
7365 brushes: &[Brush],
7366 images: &[ImageDraw],
7367 texts: &[TextDraw],
7368 shadow_draws: &[ShadowDraw],
7369 draw_ops: &[DrawOp],
7370 effect_layers: &[EffectLayer],
7371 backdrop_layers: &[BackdropLayer],
7372 z_start: usize,
7373 z_end: usize,
7374 excluded_effect_layer: Option<usize>,
7375 width: u32,
7376 height: u32,
7377 root_scale: f32,
7378 backdrop_underlay: Option<&OffscreenTarget>,
7379 initial_load_op: wgpu::LoadOp<wgpu::Color>,
7380 ) -> Result<(), String> {
7381 if z_start >= z_end {
7382 if matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
7383 self.clear_target_view_with_load_op(&target.view, initial_load_op);
7384 }
7385 return Ok(());
7386 }
7387
7388 let mut effect_z_ranges = std::mem::take(&mut self.renderer.scratch_effect_ranges);
7389 collect_effect_ranges(
7390 effect_layers,
7391 z_start,
7392 z_end,
7393 excluded_effect_layer,
7394 &mut effect_z_ranges,
7395 );
7396 let mut events = std::mem::take(&mut self.renderer.scratch_layer_events);
7397 collect_layer_events(
7398 effect_layers,
7399 backdrop_layers,
7400 z_start,
7401 z_end,
7402 excluded_effect_layer,
7403 &mut events,
7404 );
7405
7406 let result = (|| -> Result<(), String> {
7407 let mut next_load_op = initial_load_op;
7408 let mut cursor_z = z_start;
7409 for event in &events {
7410 if event.z_index > cursor_z {
7411 self.render_non_effect_segment(
7412 &target.view,
7413 shapes,
7414 brushes,
7415 images,
7416 texts,
7417 shadow_draws,
7418 &[],
7421 draw_ops,
7422 cursor_z,
7423 event.z_index,
7424 &effect_z_ranges,
7425 width,
7426 height,
7427 root_scale,
7428 next_load_op,
7429 )?;
7430 next_load_op = wgpu::LoadOp::Load;
7431 cursor_z = event.z_index;
7432 } else if event.z_index < cursor_z {
7433 continue;
7434 }
7435
7436 if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
7437 self.clear_target_view_with_load_op(&target.view, next_load_op);
7438 next_load_op = wgpu::LoadOp::Load;
7439 }
7440
7441 match event.kind {
7442 LayerEventKind::Backdrop(index) => {
7443 let layer = &backdrop_layers[index];
7444 let effective_backdrop_underlay = if backdrop_underlay.is_some()
7445 && backdrop_underlay_is_covered_by_local_content(
7446 shapes,
7447 brushes,
7448 images,
7449 shadow_draws,
7450 draw_ops,
7451 effect_layers,
7452 backdrop_layers,
7453 layer,
7454 ) {
7455 None
7456 } else {
7457 backdrop_underlay
7458 };
7459 execute_apply_backdrop_layer_to_target(
7460 self,
7461 target,
7462 layer,
7463 effective_backdrop_underlay,
7464 width,
7465 height,
7466 root_scale,
7467 None,
7468 )?;
7469 }
7470 LayerEventKind::Effect(index) => {
7471 let layer = &effect_layers[index];
7472 if layer.z_start < cursor_z {
7473 continue;
7474 }
7475 execute_render_effect_layer_to_target(
7476 self,
7477 target,
7478 shapes,
7479 brushes,
7480 images,
7481 texts,
7482 shadow_draws,
7483 draw_ops,
7484 effect_layers,
7485 backdrop_layers,
7486 index,
7487 backdrop_underlay,
7488 width,
7489 height,
7490 root_scale,
7491 )?;
7492 cursor_z = cursor_z.max(layer.z_end);
7493 }
7494 }
7495 }
7496
7497 if cursor_z < z_end {
7498 self.render_non_effect_segment(
7499 &target.view,
7500 shapes,
7501 brushes,
7502 images,
7503 texts,
7504 shadow_draws,
7505 &[],
7506 draw_ops,
7507 cursor_z,
7508 z_end,
7509 &effect_z_ranges,
7510 width,
7511 height,
7512 root_scale,
7513 next_load_op,
7514 )?;
7515 } else if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
7516 self.clear_target_view_with_load_op(&target.view, next_load_op);
7517 }
7518
7519 Ok(())
7520 })();
7521
7522 self.renderer.scratch_effect_ranges = effect_z_ranges;
7523 self.renderer.scratch_layer_events = events;
7524 result
7525 }
7526
7527 #[allow(clippy::too_many_arguments)]
7528 fn record_shader_composite(
7529 &mut self,
7530 source: &OffscreenTarget,
7531 shader: &RuntimeShader,
7532 effect_rect: [f32; 4],
7533 dest_view: &wgpu::TextureView,
7534 alpha: f32,
7535 load_op: wgpu::LoadOp<wgpu::Color>,
7536 scissor: Option<(u32, u32, u32, u32)>,
7537 blend_mode: BlendMode,
7538 dest_viewport: Option<(f32, f32, f32, f32)>,
7539 sample_mode: CompositeSampleMode,
7540 ) {
7541 let device = self.renderer.device.clone();
7542 if let Some(viewport) = direct_shader_composite_viewport(
7543 alpha,
7544 blend_mode,
7545 dest_viewport,
7546 sample_mode,
7547 (source.width, source.height),
7548 ) {
7549 let shader_applied = self
7550 .renderer
7551 .effect_renderer
7552 .encode_shader_src_over_to_view(
7553 self.recorder,
7554 &device,
7555 source,
7556 dest_view,
7557 shader,
7558 effect_rect,
7559 load_op,
7560 scissor,
7561 viewport,
7562 );
7563 if shader_applied {
7564 self.renderer
7565 .effect_renderer
7566 .debug_effects
7567 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7568 self.recorder.record_pass();
7569 self.renderer.effect_renderer.record_composite_pass();
7570 return;
7571 }
7572 }
7573 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7574 "Shader Effect Composite Scratch",
7575 source.width,
7576 source.height,
7577 );
7578 let scratch = self
7579 .recorder
7580 .acquire_transient_offscreen(&device, scratch_descriptor);
7581 let shader_applied = {
7582 self.renderer.effect_renderer.encode_shader(
7583 self.recorder,
7584 &device,
7585 source,
7586 &scratch.view,
7587 shader,
7588 effect_rect,
7589 )
7590 };
7591 let composite_source = if shader_applied {
7592 self.renderer
7593 .effect_renderer
7594 .debug_effects
7595 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7596 self.recorder.record_pass();
7597 &scratch
7598 } else {
7599 source
7600 };
7601 {
7602 self.renderer
7603 .effect_renderer
7604 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
7605 self.recorder,
7606 &device,
7607 composite_source,
7608 dest_view,
7609 alpha,
7610 load_op,
7611 scissor,
7612 None,
7613 supported_blend_mode(blend_mode),
7614 dest_viewport,
7615 sample_mode,
7616 );
7617 }
7618 self.recorder.record_pass();
7619 self.renderer.effect_renderer.record_composite_pass();
7620 self.recorder
7621 .release_transient_offscreen(scratch_descriptor, scratch);
7622 }
7623
7624 #[allow(clippy::too_many_arguments)]
7625 fn record_shader_projective_composite(
7626 &mut self,
7627 source: &OffscreenTarget,
7628 shader: &RuntimeShader,
7629 effect_rect: [f32; 4],
7630 dest_view: &wgpu::TextureView,
7631 viewport: (u32, u32),
7632 source_size: (f32, f32),
7633 inverse_matrix: [[f32; 3]; 3],
7634 dest_bounds: [[f32; 2]; 4],
7635 alpha: f32,
7636 load_op: wgpu::LoadOp<wgpu::Color>,
7637 scissor: Option<(u32, u32, u32, u32)>,
7638 blend_mode: BlendMode,
7639 sample_mode: CompositeSampleMode,
7640 ) {
7641 if projective_dest_bounds_rect(dest_bounds).is_none() {
7642 return;
7643 }
7644 let device = self.renderer.device.clone();
7645 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7646 "Shader Projective Composite Scratch",
7647 source.width,
7648 source.height,
7649 );
7650 let scratch = self
7651 .recorder
7652 .acquire_transient_offscreen(&device, scratch_descriptor);
7653 let shader_applied = {
7654 self.renderer.effect_renderer.encode_shader(
7655 self.recorder,
7656 &device,
7657 source,
7658 &scratch.view,
7659 shader,
7660 effect_rect,
7661 )
7662 };
7663 let composite_source = if shader_applied {
7664 self.renderer
7665 .effect_renderer
7666 .debug_effects
7667 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7668 self.recorder.record_pass();
7669 &scratch
7670 } else {
7671 source
7672 };
7673 let composited = {
7674 self.renderer
7675 .effect_renderer
7676 .encode_composite_to_view_projective(
7677 self.recorder,
7678 &device,
7679 composite_source,
7680 dest_view,
7681 viewport,
7682 source_size,
7683 inverse_matrix,
7684 dest_bounds,
7685 alpha,
7686 load_op,
7687 scissor,
7688 supported_blend_mode(blend_mode),
7689 sample_mode,
7690 )
7691 };
7692 if composited {
7693 self.recorder.record_pass();
7694 self.renderer.effect_renderer.record_composite_pass();
7695 }
7696 self.recorder
7697 .release_transient_offscreen(scratch_descriptor, scratch);
7698 }
7699
7700 #[allow(clippy::too_many_arguments)]
7701 fn record_effect_with_direct_shader_tail_composite(
7702 &mut self,
7703 source: &OffscreenTarget,
7704 first_effect: &RenderEffect,
7705 shader: &RuntimeShader,
7706 effect_rect: [f32; 4],
7707 dest_view: &wgpu::TextureView,
7708 load_op: wgpu::LoadOp<wgpu::Color>,
7709 scissor: Option<(u32, u32, u32, u32)>,
7710 dest_viewport: (f32, f32, f32, f32),
7711 ) -> Result<bool, String> {
7712 let device = self.renderer.device.clone();
7713 let intermediate_descriptor = self.renderer.transient_offscreen_descriptor(
7714 "Render Effect Direct Shader Tail Intermediate",
7715 source.width,
7716 source.height,
7717 );
7718 let intermediate = self
7719 .recorder
7720 .acquire_transient_offscreen(&device, intermediate_descriptor);
7721 let effect_scratch_targets = self
7722 .renderer
7723 .effect_renderer
7724 .acquire_recorded_effect_scratch_targets(
7725 self.recorder,
7726 &device,
7727 first_effect,
7728 source.width,
7729 source.height,
7730 self.renderer.surface_format,
7731 );
7732 let first_passes = {
7733 let mut effect_scratch_refs = effect_scratch_targets.refs();
7734 let pass_count = self.renderer.effect_renderer.encode_effect(
7735 self.recorder,
7736 &device,
7737 source,
7738 &intermediate.view,
7739 first_effect,
7740 effect_rect,
7741 &mut effect_scratch_refs,
7742 );
7743 match pass_count {
7744 Ok(pass_count) => effect_scratch_refs.assert_consumed().map(|()| pass_count),
7745 Err(error) => Err(error),
7746 }
7747 };
7748 let first_passes = match first_passes {
7749 Ok(pass_count) => pass_count,
7750 Err(error) => {
7751 effect_scratch_targets.release_into(self.recorder);
7752 self.recorder
7753 .release_transient_offscreen(intermediate_descriptor, intermediate);
7754 return Err(error);
7755 }
7756 };
7757 let shader_applied = self
7758 .renderer
7759 .effect_renderer
7760 .encode_shader_src_over_to_view(
7761 self.recorder,
7762 &device,
7763 &intermediate,
7764 dest_view,
7765 shader,
7766 effect_rect,
7767 load_op,
7768 scissor,
7769 dest_viewport,
7770 );
7771 self.recorder
7772 .record_passes(first_passes.saturating_add(u32::from(shader_applied)));
7773 effect_scratch_targets.release_into(self.recorder);
7774 self.recorder
7775 .release_transient_offscreen(intermediate_descriptor, intermediate);
7776 if !shader_applied {
7777 return Ok(false);
7778 }
7779 self.renderer
7780 .effect_renderer
7781 .debug_effects
7782 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7783 self.renderer.effect_renderer.record_composite_pass();
7784 Ok(true)
7785 }
7786
7787 #[allow(clippy::too_many_arguments)]
7788 fn record_effect_composite(
7789 &mut self,
7790 source: &OffscreenTarget,
7791 effect: &RenderEffect,
7792 effect_rect: [f32; 4],
7793 dest_view: &wgpu::TextureView,
7794 alpha: f32,
7795 load_op: wgpu::LoadOp<wgpu::Color>,
7796 scissor: Option<(u32, u32, u32, u32)>,
7797 blend_mode: BlendMode,
7798 dest_viewport: Option<(f32, f32, f32, f32)>,
7799 sample_mode: CompositeSampleMode,
7800 ) -> Result<(), String> {
7801 if let (
7802 RenderEffect::Chain { first, second },
7803 Some(viewport),
7804 BlendMode::SrcOver,
7805 CompositeSampleMode::Linear,
7806 ) = (
7807 effect,
7808 dest_viewport,
7809 supported_blend_mode(blend_mode),
7810 sample_mode,
7811 ) {
7812 if let (
7813 RenderEffect::Blur {
7814 radius_x,
7815 radius_y,
7816 edge_treatment,
7817 },
7818 RenderEffect::Shader { shader },
7819 ) = (first.as_ref(), second.as_ref())
7820 {
7821 if *radius_x > 0.0 || *radius_y > 0.0 {
7822 let device = self.renderer.device.clone();
7823 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7824 "Blur Rounded Mask Scratch",
7825 source.width,
7826 source.height,
7827 );
7828 let scratch = self
7829 .recorder
7830 .acquire_transient_offscreen(&device, scratch_descriptor);
7831 let fused = self
7832 .renderer
7833 .effect_renderer
7834 .encode_blur_then_rounded_mask_src_over_to_view(
7835 self.recorder,
7836 &device,
7837 source,
7838 &scratch,
7839 dest_view,
7840 *radius_x,
7841 *radius_y,
7842 *edge_treatment,
7843 shader,
7844 effect_rect,
7845 load_op,
7846 scissor,
7847 viewport,
7848 );
7849 if fused {
7850 self.recorder.record_passes(2);
7851 self.renderer.effect_renderer.record_blur_pass();
7852 self.renderer
7853 .effect_renderer
7854 .debug_effects
7855 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7856 self.renderer.effect_renderer.record_composite_pass();
7857 self.recorder
7858 .release_transient_offscreen(scratch_descriptor, scratch);
7859 return Ok(());
7860 }
7861 self.recorder
7862 .release_transient_offscreen(scratch_descriptor, scratch);
7863 }
7864 }
7865 }
7866 if let Some((first_effect, shader, viewport)) = direct_shader_tail_composite(
7867 effect,
7868 alpha,
7869 blend_mode,
7870 dest_viewport,
7871 sample_mode,
7872 (source.width, source.height),
7873 ) {
7874 if self.record_effect_with_direct_shader_tail_composite(
7875 source,
7876 first_effect,
7877 shader,
7878 effect_rect,
7879 dest_view,
7880 load_op,
7881 scissor,
7882 viewport,
7883 )? {
7884 return Ok(());
7885 }
7886 }
7887 let device = self.renderer.device.clone();
7888 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7889 "Render Effect Composite Scratch",
7890 source.width,
7891 source.height,
7892 );
7893 let scratch = self
7894 .recorder
7895 .acquire_transient_offscreen(&device, scratch_descriptor);
7896 let effect_scratch_targets = self
7897 .renderer
7898 .effect_renderer
7899 .acquire_recorded_effect_scratch_targets(
7900 self.recorder,
7901 &device,
7902 effect,
7903 source.width,
7904 source.height,
7905 self.renderer.surface_format,
7906 );
7907 let effect_passes = {
7908 let mut effect_scratch_refs = effect_scratch_targets.refs();
7909 let pass_count = self.renderer.effect_renderer.encode_effect(
7910 self.recorder,
7911 &device,
7912 source,
7913 &scratch.view,
7914 effect,
7915 effect_rect,
7916 &mut effect_scratch_refs,
7917 )?;
7918 effect_scratch_refs.assert_consumed()?;
7919 Ok(pass_count)
7920 };
7921 let effect_passes = match effect_passes {
7922 Ok(pass_count) => pass_count,
7923 Err(error) => {
7924 effect_scratch_targets.release_into(self.recorder);
7925 self.recorder
7926 .release_transient_offscreen(scratch_descriptor, scratch);
7927 return Err(error);
7928 }
7929 };
7930 {
7931 self.renderer
7932 .effect_renderer
7933 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
7934 self.recorder,
7935 &device,
7936 &scratch,
7937 dest_view,
7938 alpha,
7939 load_op,
7940 scissor,
7941 None,
7942 supported_blend_mode(blend_mode),
7943 dest_viewport,
7944 sample_mode,
7945 );
7946 }
7947 self.recorder.record_passes(effect_passes.saturating_add(1));
7948 self.renderer.effect_renderer.record_composite_pass();
7949 effect_scratch_targets.release_into(self.recorder);
7950 self.recorder
7951 .release_transient_offscreen(scratch_descriptor, scratch);
7952 Ok(())
7953 }
7954
7955 #[allow(clippy::too_many_arguments)]
7956 fn record_effect_projective_composite(
7957 &mut self,
7958 source: &OffscreenTarget,
7959 effect: &RenderEffect,
7960 effect_rect: [f32; 4],
7961 dest_view: &wgpu::TextureView,
7962 viewport: (u32, u32),
7963 source_size: (f32, f32),
7964 inverse_matrix: [[f32; 3]; 3],
7965 dest_bounds: [[f32; 2]; 4],
7966 alpha: f32,
7967 load_op: wgpu::LoadOp<wgpu::Color>,
7968 scissor: Option<(u32, u32, u32, u32)>,
7969 blend_mode: BlendMode,
7970 sample_mode: CompositeSampleMode,
7971 ) -> Result<(), String> {
7972 if projective_dest_bounds_rect(dest_bounds).is_none() {
7973 return Ok(());
7974 }
7975 let device = self.renderer.device.clone();
7976 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7977 "Render Effect Projective Composite Scratch",
7978 source.width,
7979 source.height,
7980 );
7981 let scratch = self
7982 .recorder
7983 .acquire_transient_offscreen(&device, scratch_descriptor);
7984 let effect_scratch_targets = self
7985 .renderer
7986 .effect_renderer
7987 .acquire_recorded_effect_scratch_targets(
7988 self.recorder,
7989 &device,
7990 effect,
7991 source.width,
7992 source.height,
7993 self.renderer.surface_format,
7994 );
7995 let effect_passes = {
7996 let mut effect_scratch_refs = effect_scratch_targets.refs();
7997 let pass_count = self.renderer.effect_renderer.encode_effect(
7998 self.recorder,
7999 &device,
8000 source,
8001 &scratch.view,
8002 effect,
8003 effect_rect,
8004 &mut effect_scratch_refs,
8005 )?;
8006 effect_scratch_refs.assert_consumed()?;
8007 Ok(pass_count)
8008 };
8009 let effect_passes = match effect_passes {
8010 Ok(pass_count) => pass_count,
8011 Err(error) => {
8012 effect_scratch_targets.release_into(self.recorder);
8013 self.recorder
8014 .release_transient_offscreen(scratch_descriptor, scratch);
8015 return Err(error);
8016 }
8017 };
8018 let composited = {
8019 self.renderer
8020 .effect_renderer
8021 .encode_composite_to_view_projective(
8022 self.recorder,
8023 &device,
8024 &scratch,
8025 dest_view,
8026 viewport,
8027 source_size,
8028 inverse_matrix,
8029 dest_bounds,
8030 alpha,
8031 load_op,
8032 scissor,
8033 supported_blend_mode(blend_mode),
8034 sample_mode,
8035 )
8036 };
8037 if composited {
8038 self.recorder.record_passes(effect_passes.saturating_add(1));
8039 self.renderer.effect_renderer.record_composite_pass();
8040 } else {
8041 self.recorder.record_passes(effect_passes);
8042 }
8043 effect_scratch_targets.release_into(self.recorder);
8044 self.recorder
8045 .release_transient_offscreen(scratch_descriptor, scratch);
8046 Ok(())
8047 }
8048}
8049
8050impl<C: FrameCommandRecorder> SurfaceExecutionBackend for RecordingSurfaceBackend<'_, '_, C> {
8051 fn max_texture_dim(&self) -> u32 {
8052 self.renderer.max_texture_dim()
8053 }
8054
8055 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
8056 self.renderer.acquire_retained_surface(width, height)
8057 }
8058
8059 fn acquire_frame_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
8060 let descriptor =
8061 self.renderer
8062 .transient_offscreen_descriptor("Frame Surface", width, height);
8063 self.recorder
8064 .acquire_transient_offscreen(&self.renderer.device, descriptor)
8065 }
8066
8067 fn release_frame_surface(&mut self, target: OffscreenTarget) {
8068 let descriptor = self.renderer.transient_offscreen_descriptor(
8069 "Frame Surface",
8070 target.width,
8071 target.height,
8072 );
8073 self.recorder
8074 .release_transient_offscreen(descriptor, target);
8075 }
8076
8077 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
8078 self.renderer.release_layer_surface_target(target);
8079 }
8080
8081 fn cached_layer_surface(
8082 &mut self,
8083 key: &LayerRasterCacheKey,
8084 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
8085 self.renderer.cached_layer_surface(key)
8086 }
8087
8088 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
8089 self.renderer.admit_layer_surface_cache_miss(key)
8090 }
8091
8092 fn insert_cached_layer_surface(
8093 &mut self,
8094 key: LayerRasterCacheKey,
8095 target: OffscreenTarget,
8096 logical_rect: Rect,
8097 ) -> Rc<OffscreenTarget> {
8098 self.renderer
8099 .insert_cached_layer_surface(key, target, logical_rect)
8100 }
8101
8102 fn clear_target_view_with_load_op(
8103 &mut self,
8104 target_view: &wgpu::TextureView,
8105 load_op: wgpu::LoadOp<wgpu::Color>,
8106 ) {
8107 {
8108 let _clear = self
8109 .recorder
8110 .encoder()
8111 .begin_render_pass(&wgpu::RenderPassDescriptor {
8112 label: Some("Layer Event Clear Pass"),
8113 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8114 view: target_view,
8115 resolve_target: None,
8116 depth_slice: None,
8117 ops: wgpu::Operations {
8118 load: load_op,
8119 store: wgpu::StoreOp::Store,
8120 },
8121 })],
8122 depth_stencil_attachment: None,
8123 timestamp_writes: None,
8124 occlusion_query_set: None,
8125 multiview_mask: None,
8126 });
8127 }
8128 self.recorder.record_pass();
8129 }
8130
8131 #[allow(clippy::too_many_arguments)]
8132 fn render_non_effect_segment(
8133 &mut self,
8134 target_view: &wgpu::TextureView,
8135 shapes: &[DrawShape],
8136 brushes: &[Brush],
8137 images: &[ImageDraw],
8138 texts: &[TextDraw],
8139 shadow_draws: &[ShadowDraw],
8140 retained_draws: &[RetainedDraw],
8141 draw_ops: &[DrawOp],
8142 z_start: usize,
8143 z_end: usize,
8144 effect_z_ranges: &[Range<usize>],
8145 width: u32,
8146 height: u32,
8147 root_scale: f32,
8148 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8149 ) -> Result<(), String> {
8150 self.render_non_effect_segment_with_composites(
8151 target_view,
8152 shapes,
8153 brushes,
8154 images,
8155 texts,
8156 shadow_draws,
8157 retained_draws,
8158 draw_ops,
8159 z_start,
8160 z_end,
8161 effect_z_ranges,
8162 &[],
8163 &[],
8164 width,
8165 height,
8166 root_scale,
8167 initial_load_op,
8168 )
8169 }
8170
8171 #[allow(clippy::too_many_arguments)]
8172 fn render_non_effect_segment_with_composites(
8173 &mut self,
8174 target_view: &wgpu::TextureView,
8175 shapes: &[DrawShape],
8176 brushes: &[Brush],
8177 images: &[ImageDraw],
8178 texts: &[TextDraw],
8179 shadow_draws: &[ShadowDraw],
8180 retained_draws: &[RetainedDraw],
8181 draw_ops: &[DrawOp],
8182 z_start: usize,
8183 z_end: usize,
8184 effect_z_ranges: &[Range<usize>],
8185 composites: &[(usize, CompositeBatchItem<'_>)],
8186 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
8187 width: u32,
8188 height: u32,
8189 root_scale: f32,
8190 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8191 ) -> Result<(), String> {
8192 let mut ordered_items = std::mem::take(&mut self.renderer.scratch_segment_items);
8193 collect_non_effect_segment_items(
8194 shapes,
8195 images,
8196 texts,
8197 shadow_draws,
8198 draw_ops,
8199 z_start,
8200 z_end,
8201 effect_z_ranges,
8202 width,
8203 height,
8204 root_scale,
8205 &mut ordered_items,
8206 );
8207 #[cfg(not(target_arch = "wasm32"))]
8208 let raw_shadow_items = ordered_items
8209 .iter()
8210 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
8211 .count();
8212 let culled_shadow_items = retain_renderable_shadow_items(
8213 &mut ordered_items,
8214 shadow_draws,
8215 width,
8216 height,
8217 root_scale,
8218 self.renderer.max_texture_dim(),
8219 );
8220 #[cfg(target_arch = "wasm32")]
8221 let _ = culled_shadow_items;
8222 let mut cached_shadow_composites: Vec<(usize, CachedShadowComposite)> = Vec::new();
8223 ordered_items.extend(
8224 composites
8225 .iter()
8226 .enumerate()
8227 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::Composite(index))),
8228 );
8229 ordered_items.extend(
8230 shader_composites
8231 .iter()
8232 .enumerate()
8233 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::ShaderComposite(index))),
8234 );
8235 for (z_index, item) in &mut ordered_items {
8236 let SegmentDrawItem::Shadow(shadow_index) = *item else {
8237 continue;
8238 };
8239 let Some(composite) = self.renderer.cached_shape_shadow_composite(
8240 &shadow_draws[shadow_index],
8241 width,
8242 height,
8243 root_scale,
8244 ) else {
8245 continue;
8246 };
8247 let composite_index = composites.len() + cached_shadow_composites.len();
8248 cached_shadow_composites.push((*z_index, composite));
8249 *item = SegmentDrawItem::Composite(composite_index);
8250 }
8251 let mut merged_composites = Vec::with_capacity(
8252 composites
8253 .len()
8254 .saturating_add(cached_shadow_composites.len()),
8255 );
8256 merged_composites.extend(composites.iter().copied());
8257 merged_composites.extend(
8258 cached_shadow_composites
8259 .iter()
8260 .map(|(z_index, composite)| (*z_index, composite.batch_item())),
8261 );
8262 ordered_items.sort_unstable_by_key(|(z_index, _)| *z_index);
8266 #[cfg(not(target_arch = "wasm32"))]
8267 maybe_print_segment_diag(
8268 z_start..z_end,
8269 &ordered_items,
8270 shapes,
8271 brushes,
8272 images,
8273 SegmentDiagCounts {
8274 raw_shadow_items,
8275 culled_shadow_items,
8276 cached_shadow_composites: cached_shadow_composites.len(),
8277 composite_items: merged_composites.len(),
8278 shader_composite_items: shader_composites.len(),
8279 },
8280 self.renderer.shape_batch_limits,
8281 );
8282 let result = if ordered_items.is_empty() {
8283 Ok(SegmentCommandEncodeOutcome { first_batch: true })
8284 } else {
8285 self.renderer.encode_non_effect_segment_commands(
8286 self.recorder,
8287 target_view,
8288 &ordered_items,
8289 &merged_composites,
8290 shader_composites,
8291 shapes,
8292 brushes,
8293 images,
8294 texts,
8295 shadow_draws,
8296 retained_draws,
8297 initial_load_op,
8298 width,
8299 height,
8300 root_scale,
8301 )
8302 };
8303 self.renderer.scratch_segment_items = ordered_items;
8304 let outcome = result?;
8305 if outcome.first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
8306 self.clear_target_view_with_load_op(target_view, initial_load_op);
8307 }
8308 Ok(())
8309 }
8310
8311 fn render_range_with_layer_events_to_target(
8312 &mut self,
8313 target: &OffscreenTarget,
8314 shapes: &[DrawShape],
8315 brushes: &[Brush],
8316 images: &[ImageDraw],
8317 texts: &[TextDraw],
8318 shadow_draws: &[ShadowDraw],
8319 draw_ops: &[DrawOp],
8320 effect_layers: &[EffectLayer],
8321 backdrop_layers: &[BackdropLayer],
8322 z_start: usize,
8323 z_end: usize,
8324 excluded_effect_layer: Option<usize>,
8325 width: u32,
8326 height: u32,
8327 root_scale: f32,
8328 backdrop_underlay: Option<&OffscreenTarget>,
8329 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8330 ) -> Result<(), String> {
8331 self.render_range_with_layer_events_to_target_recorded(
8332 target,
8333 shapes,
8334 brushes,
8335 images,
8336 texts,
8337 shadow_draws,
8338 draw_ops,
8339 effect_layers,
8340 backdrop_layers,
8341 z_start,
8342 z_end,
8343 excluded_effect_layer,
8344 width,
8345 height,
8346 root_scale,
8347 backdrop_underlay,
8348 initial_load_op,
8349 )
8350 }
8351
8352 fn render_shadow_draw(
8353 &mut self,
8354 target_view: &wgpu::TextureView,
8355 shadow: &ShadowDraw,
8356 width: u32,
8357 height: u32,
8358 root_scale: f32,
8359 ) {
8360 self.renderer.encode_shadow_draw(
8361 self.recorder,
8362 target_view,
8363 shadow,
8364 width,
8365 height,
8366 root_scale,
8367 );
8368 }
8369
8370 fn composite_to_view_projective(
8371 &mut self,
8372 source: &OffscreenTarget,
8373 dest_view: &wgpu::TextureView,
8374 viewport: (u32, u32),
8375 source_size: (f32, f32),
8376 inverse_matrix: [[f32; 3]; 3],
8377 dest_bounds: [[f32; 2]; 4],
8378 alpha: f32,
8379 load_op: wgpu::LoadOp<wgpu::Color>,
8380 scissor: Option<(u32, u32, u32, u32)>,
8381 blend_mode: BlendMode,
8382 sample_mode: CompositeSampleMode,
8383 ) {
8384 let device = self.renderer.device.clone();
8385 let composited = {
8386 self.renderer
8387 .effect_renderer
8388 .encode_composite_to_view_projective(
8389 self.recorder,
8390 &device,
8391 source,
8392 dest_view,
8393 viewport,
8394 source_size,
8395 inverse_matrix,
8396 dest_bounds,
8397 alpha,
8398 load_op,
8399 scissor,
8400 supported_blend_mode(blend_mode),
8401 sample_mode,
8402 )
8403 };
8404 if composited {
8405 self.recorder.record_pass();
8406 self.renderer.effect_renderer.record_composite_pass();
8407 }
8408 }
8409
8410 fn composite_projective_surfaces_to_view(
8411 &mut self,
8412 dest_view: &wgpu::TextureView,
8413 viewport: (u32, u32),
8414 composites: &[ProjectiveSurfaceComposite<'_>],
8415 ) {
8416 let device = self.renderer.device.clone();
8417 let mut composite_count = 0_u32;
8418 for composite in composites
8419 .iter()
8420 .copied()
8421 .filter(|composite| projective_dest_bounds_rect(composite.dest_bounds).is_some())
8422 {
8423 let composited = {
8424 self.renderer
8425 .effect_renderer
8426 .encode_composite_to_view_projective(
8427 self.recorder,
8428 &device,
8429 composite.source,
8430 dest_view,
8431 viewport,
8432 composite.source_size,
8433 composite.inverse_matrix,
8434 composite.dest_bounds,
8435 composite.alpha,
8436 composite.load_op,
8437 composite.scissor,
8438 supported_blend_mode(composite.blend_mode),
8439 composite.sample_mode,
8440 )
8441 };
8442 if composited {
8443 composite_count = composite_count.saturating_add(1);
8444 }
8445 }
8446 if composite_count > 0 {
8447 self.recorder.record_passes(composite_count);
8448 self.renderer
8449 .effect_renderer
8450 .debug_composites
8451 .set(self.renderer.effect_renderer.debug_composites.get() + composite_count);
8452 }
8453 }
8454
8455 fn composite_surface_batch_to_view(
8456 &mut self,
8457 dest_view: &wgpu::TextureView,
8458 viewport: (u32, u32),
8459 load_op: wgpu::LoadOp<wgpu::Color>,
8460 composites: &[CompositeBatchItem<'_>],
8461 ) {
8462 if composites.is_empty() {
8463 return;
8464 }
8465 let device = self.renderer.device.clone();
8466 self.renderer
8467 .effect_renderer
8468 .encode_composite_batch_to_view_pass(
8469 self.recorder,
8470 &device,
8471 dest_view,
8472 viewport,
8473 load_op,
8474 composites,
8475 );
8476 self.recorder.record_pass();
8477 self.renderer.effect_renderer.record_composite_pass();
8478 }
8479
8480 fn copy_texture_region_to_target(
8481 &mut self,
8482 source: &OffscreenTarget,
8483 source_origin: (u32, u32),
8484 target: &OffscreenTarget,
8485 size: (u32, u32),
8486 ) -> bool {
8487 let (width, height) = size;
8488 if width == 0 || height == 0 || width > target.width || height > target.height {
8489 return false;
8490 }
8491 let Some(source_right) = source_origin.0.checked_add(width) else {
8492 return false;
8493 };
8494 let Some(source_bottom) = source_origin.1.checked_add(height) else {
8495 return false;
8496 };
8497 if source_right > source.width || source_bottom > source.height {
8498 return false;
8499 }
8500
8501 self.recorder.encoder().copy_texture_to_texture(
8502 wgpu::TexelCopyTextureInfo {
8503 texture: source.texture(),
8504 mip_level: 0,
8505 origin: wgpu::Origin3d {
8506 x: source_origin.0,
8507 y: source_origin.1,
8508 z: 0,
8509 },
8510 aspect: wgpu::TextureAspect::All,
8511 },
8512 wgpu::TexelCopyTextureInfo {
8513 texture: target.texture(),
8514 mip_level: 0,
8515 origin: wgpu::Origin3d::ZERO,
8516 aspect: wgpu::TextureAspect::All,
8517 },
8518 wgpu::Extent3d {
8519 width,
8520 height,
8521 depth_or_array_layers: 1,
8522 },
8523 );
8524 true
8525 }
8526
8527 fn shader_composite_batch_to_view(
8528 &mut self,
8529 dest_view: &wgpu::TextureView,
8530 viewport: (u32, u32),
8531 load_op: wgpu::LoadOp<wgpu::Color>,
8532 composites: &[ShaderCompositeBatchItem<'_>],
8533 ) -> bool {
8534 if composites.is_empty() {
8535 return true;
8536 }
8537 let device = self.renderer.device.clone();
8538 let encoded = self
8539 .renderer
8540 .effect_renderer
8541 .encode_shader_batch_src_over_to_view(
8542 self.recorder,
8543 &device,
8544 dest_view,
8545 viewport,
8546 load_op,
8547 composites,
8548 );
8549 if encoded {
8550 self.recorder.record_pass();
8551 self.renderer.effect_renderer.record_composite_pass();
8552 self.renderer
8553 .effect_renderer
8554 .debug_effects
8555 .set(self.renderer.effect_renderer.debug_effects.get() + composites.len() as u32);
8556 }
8557 encoded
8558 }
8559
8560 fn composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8561 &mut self,
8562 source: &OffscreenTarget,
8563 dest_view: &wgpu::TextureView,
8564 alpha: f32,
8565 load_op: wgpu::LoadOp<wgpu::Color>,
8566 scissor: Option<(u32, u32, u32, u32)>,
8567 rounded_mask: Option<RoundedCompositeMask>,
8568 blend_mode: BlendMode,
8569 dest_viewport: Option<(f32, f32, f32, f32)>,
8570 sample_mode: CompositeSampleMode,
8571 ) {
8572 let device = self.renderer.device.clone();
8573 {
8574 self.renderer
8575 .effect_renderer
8576 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8577 self.recorder,
8578 &device,
8579 source,
8580 dest_view,
8581 alpha,
8582 load_op,
8583 scissor,
8584 rounded_mask,
8585 supported_blend_mode(blend_mode),
8586 dest_viewport,
8587 sample_mode,
8588 );
8589 }
8590 self.recorder.record_pass();
8591 self.renderer.effect_renderer.record_composite_pass();
8592 }
8593
8594 fn apply_effect_and_composite_to_view(
8595 &mut self,
8596 source: &OffscreenTarget,
8597 effect: &RenderEffect,
8598 effect_rect: [f32; 4],
8599 dest_view: &wgpu::TextureView,
8600 alpha: f32,
8601 load_op: wgpu::LoadOp<wgpu::Color>,
8602 scissor: Option<(u32, u32, u32, u32)>,
8603 blend_mode: BlendMode,
8604 dest_viewport: Option<(f32, f32, f32, f32)>,
8605 sample_mode: CompositeSampleMode,
8606 ) -> Result<(), String> {
8607 self.record_effect_composite(
8608 source,
8609 effect,
8610 effect_rect,
8611 dest_view,
8612 alpha,
8613 load_op,
8614 scissor,
8615 blend_mode,
8616 dest_viewport,
8617 sample_mode,
8618 )
8619 }
8620
8621 fn apply_shader_and_composite_to_view(
8622 &mut self,
8623 source: &OffscreenTarget,
8624 shader: &RuntimeShader,
8625 effect_rect: [f32; 4],
8626 dest_view: &wgpu::TextureView,
8627 alpha: f32,
8628 load_op: wgpu::LoadOp<wgpu::Color>,
8629 scissor: Option<(u32, u32, u32, u32)>,
8630 blend_mode: BlendMode,
8631 dest_viewport: Option<(f32, f32, f32, f32)>,
8632 sample_mode: CompositeSampleMode,
8633 ) {
8634 self.record_shader_composite(
8635 source,
8636 shader,
8637 effect_rect,
8638 dest_view,
8639 alpha,
8640 load_op,
8641 scissor,
8642 blend_mode,
8643 dest_viewport,
8644 sample_mode,
8645 );
8646 }
8647
8648 fn apply_shader_and_composite_to_view_projective(
8649 &mut self,
8650 source: &OffscreenTarget,
8651 shader: &RuntimeShader,
8652 effect_rect: [f32; 4],
8653 dest_view: &wgpu::TextureView,
8654 viewport: (u32, u32),
8655 source_size: (f32, f32),
8656 inverse_matrix: [[f32; 3]; 3],
8657 dest_bounds: [[f32; 2]; 4],
8658 alpha: f32,
8659 load_op: wgpu::LoadOp<wgpu::Color>,
8660 scissor: Option<(u32, u32, u32, u32)>,
8661 blend_mode: BlendMode,
8662 sample_mode: CompositeSampleMode,
8663 ) {
8664 self.record_shader_projective_composite(
8665 source,
8666 shader,
8667 effect_rect,
8668 dest_view,
8669 viewport,
8670 source_size,
8671 inverse_matrix,
8672 dest_bounds,
8673 alpha,
8674 load_op,
8675 scissor,
8676 blend_mode,
8677 sample_mode,
8678 );
8679 }
8680
8681 fn apply_effect_and_composite_to_view_projective(
8682 &mut self,
8683 source: &OffscreenTarget,
8684 effect: &RenderEffect,
8685 effect_rect: [f32; 4],
8686 dest_view: &wgpu::TextureView,
8687 viewport: (u32, u32),
8688 source_size: (f32, f32),
8689 inverse_matrix: [[f32; 3]; 3],
8690 dest_bounds: [[f32; 2]; 4],
8691 alpha: f32,
8692 load_op: wgpu::LoadOp<wgpu::Color>,
8693 scissor: Option<(u32, u32, u32, u32)>,
8694 blend_mode: BlendMode,
8695 sample_mode: CompositeSampleMode,
8696 ) -> Result<(), String> {
8697 self.record_effect_projective_composite(
8698 source,
8699 effect,
8700 effect_rect,
8701 dest_view,
8702 viewport,
8703 source_size,
8704 inverse_matrix,
8705 dest_bounds,
8706 alpha,
8707 load_op,
8708 scissor,
8709 blend_mode,
8710 sample_mode,
8711 )
8712 }
8713
8714 fn is_render_effect_supported(&self, effect: &RenderEffect) -> bool {
8715 self.renderer.supports_render_effect(effect)
8716 }
8717
8718 fn warn_unsupported_effect_once(&self) {
8719 self.renderer.warning_state.warn_unsupported_effect_once();
8720 }
8721
8722 fn record_layer_cache_miss(&self, width: u32, height: u32) {
8723 self.renderer
8724 .frame_stats
8725 .record_layer_cache_miss(width, height);
8726 }
8727
8728 fn record_isolated_layer_render(
8729 &self,
8730 width: u32,
8731 height: u32,
8732 node_id: Option<NodeId>,
8733 logical_rect: Rect,
8734 requirements: SurfaceRequirementSet,
8735 ) {
8736 self.renderer.frame_stats.record_isolated_layer_render(
8737 width,
8738 height,
8739 node_id,
8740 logical_rect,
8741 requirements.into(),
8742 );
8743 }
8744}
8745
8746impl GpuRenderer {
8747 pub fn render(
8748 &mut self,
8749 view: &wgpu::TextureView,
8750 width: u32,
8751 height: u32,
8752 mut packet: FramePacket,
8753 surface_epoch: u64,
8754 returns: &mut RenderReturns,
8755 ) -> Result<(), String> {
8756 if let Some(confirmations) = packet.recycled_confirmations.take() {
8761 self.restore_replay_ack_confirmations(confirmations);
8762 }
8763 let cancel_reason = if packet.renderer_epoch != self.renderer_epoch {
8769 Some(CancelReason::RendererEpoch)
8770 } else if packet.surface_epoch != surface_epoch {
8771 Some(CancelReason::SurfaceEpoch)
8772 } else if packet.viewport != (width, height) {
8773 Some(CancelReason::Viewport)
8774 } else {
8775 None
8776 };
8777 if let Some(reason) = cancel_reason {
8778 return Self::cancel_packet(packet, reason, returns);
8779 }
8780 if self.device_errors.take_poison() {
8787 return Self::cancel_packet(packet, CancelReason::DeviceError, returns);
8788 }
8789 returns.frame_id = packet.frame_id;
8790 log::trace!("🎨 Rendering graph to {}x{}", width, height);
8791 let render_start = Instant::now();
8792
8793 #[cfg(target_arch = "wasm32")]
8794 {
8795 self.wasm_uniform_batch_cursor = 0;
8796 self.wasm_shape_batch_cursor = 0;
8797 self.wasm_image_batch_cursor = 0;
8798 }
8799 #[cfg(not(target_arch = "wasm32"))]
8800 {
8801 self.retained_glyph_uniform_cursor = 0;
8802 self.rim_mesh_vertices.clear();
8807 self.rim_mesh_indices.clear();
8808 self.rim_mesh_uploaded_vertices = 0;
8809 self.rim_mesh_uploaded_indices = 0;
8810 if fill_area_diag_enabled() {
8811 self.fill_area_diag.reset_frame(width, height);
8812 }
8813 self.static_span.armed = true;
8816 self.segment_surfaces.begin_frame();
8819 self.display_clip.frame_root_view = Some(view.clone());
8823 }
8824
8825 let text_cache_len = packet.text_cache_len;
8829 let result = self.render_graph(view, packet, returns);
8830 #[cfg(not(target_arch = "wasm32"))]
8831 {
8832 self.display_clip.frame_root_view = None;
8833 }
8834 let after_graph = Instant::now();
8835 self.flush_deferred_offscreen_releases();
8836 #[cfg(not(target_arch = "wasm32"))]
8837 {
8838 if fill_area_diag_enabled() {
8839 let (composite_px2, offscreen_px2) = self.effect_renderer.take_fill_diag_fill_px2();
8843 self.fill_area_diag
8844 .add_effect_fill(composite_px2, offscreen_px2);
8845 self.fill_area_diag.finish_frame(width, height);
8846 }
8847 }
8848
8849 #[cfg(target_arch = "wasm32")]
8850 {
8851 const WASM_BATCH_POOL_MARGIN: usize = 4;
8852 self.wasm_uniform_batches.truncate(
8853 self.wasm_uniform_batch_cursor
8854 .saturating_add(WASM_BATCH_POOL_MARGIN),
8855 );
8856 self.wasm_shape_batches.truncate(
8857 self.wasm_shape_batch_cursor
8858 .saturating_add(WASM_BATCH_POOL_MARGIN),
8859 );
8860 self.wasm_image_batches.truncate(
8861 self.wasm_image_batch_cursor
8862 .saturating_add(WASM_BATCH_POOL_MARGIN),
8863 );
8864 }
8865 self.staged_uploads
8866 .shrink_retained_capacity(RETAINED_STAGED_UPLOAD_BYTES, RETAINED_STAGED_UPLOAD_COPIES);
8867
8868 self.layer_surface_cache.finish_frame(&self.frame_stats);
8869 #[cfg(not(target_arch = "wasm32"))]
8870 self.retained_bundle_cache.end_frame();
8871
8872 self.frame_stats.offscreen_pool_size.set(
8873 self.effect_renderer
8874 .retained_offscreen_count()
8875 .saturating_add(self.frame_graph_executor.retained_texture_count())
8876 as u32,
8877 );
8878 self.frame_stats.offscreen_pool_bytes.set(
8879 (self.effect_renderer.retained_offscreen_bytes() as u64)
8880 .saturating_add(self.frame_graph_executor.retained_texture_bytes()),
8881 );
8882 self.frame_stats
8883 .text_pool_size
8884 .set(self.text_image_cache.len() as u32);
8885 self.frame_stats
8886 .image_cache_size
8887 .set(self.image_texture_cache.len() as u32);
8888 self.frame_stats.text_cache_size.set(text_cache_len as u32);
8889 self.effect_renderer
8890 .merge_and_reset_debug_counters(&self.frame_stats);
8891 self.frame_graph_executor.reset_upload_allocators();
8892 let snapshot = self.frame_stats.snapshot();
8893 self.last_frame_stats = Some(snapshot);
8894 PRESENTED_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8895 update_frame_warmup_budget(&mut self.pending_frame_warmup_frames, &snapshot);
8896 self.frame_stats.maybe_print_snapshot(
8897 snapshot,
8898 &mut self.frame_count,
8899 self.gpu_stats_enabled,
8900 );
8901 if self.gpu_stats_enabled && self.frame_count.is_multiple_of(60) {
8902 gpu_stats::print_gpu_memory_report(&self.device, self.frame_count);
8903 }
8904 self.frame_stats.reset();
8905 let after_stats = Instant::now();
8906 if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
8907 log::warn!(
8908 "[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
8909 instant_ms(render_start, after_graph),
8910 instant_ms(after_graph, after_stats),
8911 );
8912 }
8913 if result.is_ok() {
8914 returns.outcome = PresentOutcome::Presented;
8917 }
8918 result
8919 }
8920
8921 pub(crate) fn cancel_packet(
8934 packet: FramePacket,
8935 reason: CancelReason,
8936 returns: &mut RenderReturns,
8937 ) -> Result<(), String> {
8938 let FramePacket {
8939 frame_id,
8940 viewport: _,
8941 renderer_epoch: _,
8942 surface_epoch: _,
8943 root_scale: _,
8944 root,
8945 overlay: _,
8946 replay,
8947 text_cache_len: _,
8948 recycled_confirmations: _,
8949 replay_preconsumed,
8950 } = packet;
8951 match root {
8952 PacketRoot::Direct(root) => {
8953 returns.scene = Some(root.scene);
8963 #[cfg(not(target_arch = "wasm32"))]
8964 if !replay_preconsumed {
8965 returns.cancelled_replay = Some(replay);
8966 }
8967 }
8968 PacketRoot::Surface(_) => {}
8969 }
8970 #[cfg(target_arch = "wasm32")]
8971 let _ = (replay, replay_preconsumed);
8972 returns.ack = None;
8973 returns.frame_id = frame_id;
8974 returns.outcome = PresentOutcome::Cancelled(reason);
8975 Ok(())
8976 }
8977
8978 pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
8979 self.last_frame_stats
8980 }
8981
8982 pub fn needs_frame_warmup(&self) -> bool {
8983 self.pending_frame_warmup_frames > 0
8984 }
8985
8986 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
8987 let layer_surface_cache_stats = self.layer_surface_cache.debug_stats();
8988 DebugCpuAllocationStats {
8989 scene_graph_node_count: 0,
8990 scene_graph_heap_bytes: 0,
8991 scene_hits_len: 0,
8992 scene_hits_cap: 0,
8993 scene_node_index_len: 0,
8994 scene_node_index_cap: 0,
8995 text_renderer_pool_len: self.text_image_cache.len(),
8996 text_renderer_pool_cap: self.text_image_cache.cap().get(),
8997 swash_image_cache_len: 0,
8998 swash_image_cache_cap: 0,
8999 swash_outline_cache_len: 0,
9000 swash_outline_cache_cap: 0,
9001 image_texture_cache_len: self.image_texture_cache.len(),
9002 image_texture_cache_cap: self.image_texture_cache.cap().get(),
9003 scratch_shape_data_cap: self.scratch_shape_data.capacity(),
9004 scratch_gradients_cap: self.scratch_gradients.capacity(),
9005 scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
9006 scratch_image_indices_cap: self.scratch_image_indices.capacity(),
9007 scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
9008 scratch_segment_items_cap: self.scratch_segment_items.capacity(),
9009 scratch_effect_ranges_cap: self.scratch_effect_ranges.capacity(),
9010 scratch_layer_events_cap: self.scratch_layer_events.capacity(),
9011 staged_upload_bytes_cap: self.staged_uploads.bytes.capacity(),
9012 staged_upload_copies_cap: self.staged_uploads.copies.capacity(),
9013 layer_surface_cache_len: layer_surface_cache_stats.entries_len,
9014 layer_surface_cache_cap: layer_surface_cache_stats.entries_cap,
9015 layer_surface_cache_identity_len: layer_surface_cache_stats.identity_len,
9016 layer_surface_cache_identity_cap: layer_surface_cache_stats.identity_cap,
9017 layer_surface_rect_cache_len: 0,
9020 layer_surface_rect_cache_cap: 0,
9021 layer_surface_requirements_cache_len: 0,
9022 layer_surface_requirements_cache_cap: 0,
9023 layer_cache_seen_this_frame_len: layer_surface_cache_stats.seen_this_frame_len,
9024 layer_cache_seen_this_frame_cap: layer_surface_cache_stats.seen_this_frame_cap,
9025 }
9026 }
9027
9028 pub fn render_to_rgba_pixels(
9029 &mut self,
9030 width: u32,
9031 height: u32,
9032 packet: FramePacket,
9033 surface_epoch: u64,
9034 returns: &mut RenderReturns,
9035 ) -> Result<Vec<u8>, String> {
9036 if width == 0 || height == 0 {
9037 return Err("Screenshot size must be non-zero".to_string());
9038 }
9039
9040 let output_texture = self.device.create_texture(&wgpu::TextureDescriptor {
9041 label: Some("Screenshot Output Texture"),
9042 size: wgpu::Extent3d {
9043 width,
9044 height,
9045 depth_or_array_layers: 1,
9046 },
9047 mip_level_count: 1,
9048 sample_count: 1,
9049 dimension: wgpu::TextureDimension::D2,
9050 format: self.surface_format,
9051 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
9052 view_formats: &[],
9053 });
9054 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
9055
9056 self.render(&output_view, width, height, packet, surface_epoch, returns)?;
9057
9058 let bytes_per_pixel = 4u32;
9059 let unpadded_bytes_per_row = width
9060 .checked_mul(bytes_per_pixel)
9061 .ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
9062 let padded_bytes_per_row =
9063 align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
9064 let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
9065
9066 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9067 label: Some("Screenshot Readback Buffer"),
9068 size: output_buffer_size,
9069 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
9070 mapped_at_creation: false,
9071 });
9072
9073 let device = self.device.clone();
9074 let queue = self.queue.clone();
9075 let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
9076 let source = graph.import_surface("screenshot-copy-source");
9077 graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
9078 context.encoder.copy_texture_to_buffer(
9079 wgpu::TexelCopyTextureInfo {
9080 texture: &output_texture,
9081 mip_level: 0,
9082 origin: wgpu::Origin3d::ZERO,
9083 aspect: wgpu::TextureAspect::All,
9084 },
9085 wgpu::TexelCopyBufferInfo {
9086 buffer: &output_buffer,
9087 layout: wgpu::TexelCopyBufferLayout {
9088 offset: 0,
9089 bytes_per_row: Some(padded_bytes_per_row),
9090 rows_per_image: Some(height),
9091 },
9092 },
9093 wgpu::Extent3d {
9094 width,
9095 height,
9096 depth_or_array_layers: 1,
9097 },
9098 );
9099 Ok(())
9100 });
9101 let mut executor = std::mem::take(&mut self.frame_graph_executor);
9102 let execution = executor.execute_recorded_graph(&device, &queue, graph);
9103 self.frame_graph_executor = executor;
9104 let execution = execution.map_err(|error| error.to_string())?;
9105 let submission_index = execution.submission;
9106 let copy_stats = execution.stats;
9107 self.last_frame_stats = self
9108 .last_frame_stats
9109 .map(|snapshot| snapshot.with_command_stats_added(copy_stats));
9110
9111 let buffer_slice = output_buffer.slice(..);
9112 let (tx, rx) = mpsc::channel();
9113 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
9114 let _ = tx.send(result);
9115 });
9116 let _ = self.device.poll(wgpu::PollType::Wait {
9117 submission_index: Some(submission_index),
9118 timeout: None,
9119 });
9120
9121 match rx.recv_timeout(Duration::from_secs(3)) {
9122 Ok(Ok(())) => {}
9123 Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
9124 Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
9125 }
9126
9127 let mapped = buffer_slice.get_mapped_range();
9128 let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
9129
9130 let src_row_len = padded_bytes_per_row as usize;
9131 let dst_row_len = unpadded_bytes_per_row as usize;
9132 for row in 0..height as usize {
9133 let src_offset = row * src_row_len;
9134 let dst_offset = row * dst_row_len;
9135 pixels[dst_offset..dst_offset + dst_row_len]
9136 .copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
9137 }
9138 drop(mapped);
9139 output_buffer.unmap();
9140
9141 self.convert_surface_pixels_to_rgba(&mut pixels)?;
9142 Ok(pixels)
9143 }
9144
9145 fn render_graph(
9146 &mut self,
9147 surface_view: &wgpu::TextureView,
9148 packet: FramePacket,
9149 returns: &mut RenderReturns,
9150 ) -> Result<(), String> {
9151 let device = self.device.clone();
9152 let queue = self.queue.clone();
9153 let graph_start = Instant::now();
9154
9155 #[cfg(not(target_arch = "wasm32"))]
9156 {
9157 let mut executor = std::mem::take(&mut self.frame_graph_executor);
9158 let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
9159 let surface = frame_graph.import_surface("renderer-surface");
9160 frame_graph.add_fallible_recorded_command_pass(
9161 Some("Renderer Frame Pass"),
9162 &[],
9163 &[surface],
9164 |frame_encoder| {
9165 self.render_graph_recorded(surface_view, packet, returns, frame_encoder)
9166 },
9167 );
9168 let after_build = Instant::now();
9169 let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
9170 let after_execute = Instant::now();
9171 self.frame_graph_executor = executor;
9172 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
9173 log::warn!(
9174 "[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
9175 instant_ms(graph_start, after_build),
9176 instant_ms(after_build, after_execute),
9177 );
9178 }
9179
9180 match execution {
9181 Ok(execution) => {
9182 if execution.stats.pass_count > 0 {
9183 self.frame_stats.record_command_stats(execution.stats);
9184 }
9185 Ok(())
9186 }
9187 Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => Ok(()),
9188 Err(error) => Err(error.to_string()),
9189 }
9190 }
9191
9192 #[cfg(target_arch = "wasm32")]
9193 {
9194 let mut executor = std::mem::take(&mut self.frame_graph_executor);
9195 let (result, execution) = {
9196 let mut frame_encoder =
9197 executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
9198 let initial_pass_count = frame_encoder.recorded_pass_count();
9199 let result =
9200 self.render_graph_recorded(surface_view, packet, returns, &mut frame_encoder);
9201 let execution =
9202 if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
9203 Some(frame_encoder.finish())
9204 } else {
9205 None
9206 };
9207 (result, execution)
9208 };
9209 let after_execute = Instant::now();
9210 self.frame_graph_executor = executor;
9211 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
9212 log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
9213 }
9214 if let Some(execution) = execution {
9215 self.frame_stats.record_command_stats(execution.stats);
9216 }
9217 result
9218 }
9219 }
9220
9221 fn render_graph_recorded<C: FrameCommandRecorder>(
9222 &mut self,
9223 surface_view: &wgpu::TextureView,
9224 packet: FramePacket,
9225 returns: &mut RenderReturns,
9226 frame_encoder: &mut C,
9227 ) -> Result<(), String> {
9228 let recorded_start = Instant::now();
9229
9230 #[cfg(not(target_arch = "wasm32"))]
9245 let mut packet = packet;
9246 #[cfg(not(target_arch = "wasm32"))]
9247 if !packet.replay_preconsumed {
9248 if let PacketRoot::Direct(root) = &packet.root {
9249 let ops = std::mem::take(&mut packet.replay);
9250 let (ack, recycled) = self.consume_replay_ops(
9251 ops,
9252 &root.scene.shapes,
9253 &root.scene.brushes,
9254 packet.root_scale,
9255 );
9256 returns.ack = Some((ack, recycled));
9257 }
9258 }
9259
9260 let FramePacket {
9261 frame_id,
9262 viewport: (width, height),
9263 renderer_epoch: _,
9264 surface_epoch: _,
9265 root_scale,
9266 root,
9267 overlay,
9268 replay: _,
9269 text_cache_len: _,
9270 recycled_confirmations: _,
9271 replay_preconsumed: _,
9272 } = packet;
9273
9274 let mut backend = RecordingSurfaceBackend {
9275 renderer: self,
9276 recorder: frame_encoder,
9277 };
9278
9279 let surface_packet = match root {
9280 PacketRoot::Direct(root) => {
9281 let direct_render_start = Instant::now();
9282 let result = match execute_render_root_direct(
9283 &mut backend,
9284 surface_view,
9285 *root,
9286 width,
9287 height,
9288 root_scale,
9289 wgpu::LoadOp::Clear(CLEAR_COLOR),
9290 ) {
9291 Ok(scene) => {
9296 returns.scene = Some(scene);
9297 Ok(())
9298 }
9299 Err((error, scene)) => {
9300 returns.scene = Some(scene);
9301 Err(error)
9302 }
9303 };
9304 if result.is_ok() {
9305 if let Some(overlay) = overlay {
9306 Self::render_overlay_packet(
9307 &mut backend,
9308 surface_view,
9309 overlay,
9310 width,
9311 height,
9312 root_scale,
9313 )?;
9314 }
9315 }
9316 let after_direct_render = Instant::now();
9317 if let Some(total_ms) =
9318 should_log_wgpu_render_stage(recorded_start, after_direct_render)
9319 {
9320 log::warn!(
9321 "[wgpu-render-stage:recorded-direct-root] frame={frame_id} total_ms={total_ms:.2} render_ms={:.2}",
9322 instant_ms(direct_render_start, after_direct_render),
9323 );
9324 }
9325 return result;
9326 }
9327 PacketRoot::Surface(surface_packet) => surface_packet,
9328 };
9329 let after_root_collect = Instant::now();
9330
9331 let RootSurfacePacket {
9332 lowered,
9333 source,
9334 transform_to_parent,
9335 node_id,
9336 backdrop,
9337 graphics_layer,
9338 local_bounds,
9339 clip_rect,
9340 shadow_clip,
9341 } = *surface_packet;
9342 let mut lowered = lowered;
9343 lowered.source = source;
9344
9345 let viewport_rect = Rect {
9350 x: 0.0,
9351 y: 0.0,
9352 width: width as f32 / root_scale,
9353 height: height as f32 / root_scale,
9354 };
9355 let root_surface = execute_render_layer_surface(
9356 &mut backend,
9357 &mut lowered,
9358 LayerSurfaceRequest {
9359 root_scale,
9360 backdrop_underlay: None,
9361 allow_runtime_cache: false,
9362 logical_rect_override: Some(viewport_rect),
9363 capture_clip_override: None,
9364 activates_nested_capture: false,
9365 translation_context: TranslationRenderContext::default(),
9366 },
9367 )?;
9368 let root_quad = transform_to_parent.map_rect(root_surface.logical_rect);
9369 let root_dest_quad = scaled_quad(root_quad, root_scale);
9370
9371 let needs_root_composite_target =
9372 backdrop.is_some() || graphics_layer.shadow_elevation > 0.0;
9373
9374 if needs_root_composite_target {
9375 let composite_target = backend.acquire_frame_surface(width, height);
9376 backend.clear_target_view_with_load_op(
9377 &composite_target.view,
9378 wgpu::LoadOp::Clear(CLEAR_COLOR),
9379 );
9380
9381 if let Some(backdrop) = &backdrop {
9382 execute_apply_backdrop_layer_to_target(
9383 &mut backend,
9384 &composite_target,
9385 &BackdropLayer {
9386 node_id,
9387 rect: quad_bounds(transform_to_parent.map_rect(local_bounds)),
9388 clip: clip_rect.map(|clip| quad_bounds(transform_to_parent.map_rect(clip))),
9389 snap_anchor: None,
9390 effect: backdrop.clone(),
9391 z_index: 0,
9392 },
9393 None,
9394 width,
9395 height,
9396 root_scale,
9397 None,
9398 )?;
9399 }
9400
9401 let mut root_shadow_scene = CompositorScene::new();
9402 let root_shadow_clip =
9403 shadow_clip.map(|clip| quad_bounds(transform_to_parent.map_rect(clip)));
9404 push_layer_shadow(
9405 &mut root_shadow_scene,
9406 &graphics_layer,
9407 local_bounds,
9408 quad_bounds(transform_to_parent.map_rect(local_bounds)),
9409 root_shadow_clip,
9410 );
9411 for shadow in &root_shadow_scene.shadow_draws {
9412 backend.render_shadow_draw(
9413 &composite_target.view,
9414 shadow,
9415 width,
9416 height,
9417 root_scale,
9418 );
9419 }
9420
9421 let composite_dest_quad =
9422 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
9423 execute_composite_surface_to_view(
9424 &mut backend,
9425 root_surface.target.target(),
9426 &composite_target.view,
9427 (width, height),
9428 composite_dest_quad,
9429 root_surface.composite_alpha,
9430 wgpu::LoadOp::Load,
9431 None,
9432 root_surface.blend_mode,
9433 root_surface.sample_mode,
9434 )?;
9435 backend.composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
9436 &composite_target,
9437 surface_view,
9438 1.0,
9439 wgpu::LoadOp::Clear(CLEAR_COLOR),
9440 None,
9441 None,
9442 BlendMode::SrcOver,
9443 None,
9444 CompositeSampleMode::Linear,
9445 );
9446 backend.release_frame_surface(composite_target);
9447 } else {
9448 let composite_dest_quad =
9449 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
9450 execute_composite_surface_to_view(
9451 &mut backend,
9452 root_surface.target.target(),
9453 surface_view,
9454 (width, height),
9455 composite_dest_quad,
9456 root_surface.composite_alpha,
9457 wgpu::LoadOp::Clear(CLEAR_COLOR),
9458 None,
9459 root_surface.blend_mode,
9460 root_surface.sample_mode,
9461 )?;
9462 }
9463 backend.release_layer_surface_target(root_surface.target);
9464 if let Some(overlay) = overlay {
9465 Self::render_overlay_packet(
9466 &mut backend,
9467 surface_view,
9468 overlay,
9469 width,
9470 height,
9471 root_scale,
9472 )?;
9473 }
9474 let after_layer_render = Instant::now();
9475 if let Some(total_ms) = should_log_wgpu_render_stage(recorded_start, after_layer_render) {
9476 log::warn!(
9477 "[wgpu-render-stage:recorded-layer-root] total_ms={total_ms:.2} collect_ms={:.2} render_ms={:.2}",
9478 instant_ms(recorded_start, after_root_collect),
9479 instant_ms(after_root_collect, after_layer_render),
9480 );
9481 }
9482 Ok(())
9483 }
9484
9485 fn render_overlay_packet<C: FrameCommandRecorder>(
9489 backend: &mut RecordingSurfaceBackend<'_, '_, C>,
9490 surface_view: &wgpu::TextureView,
9491 overlay: CollectedLayer,
9492 width: u32,
9493 height: u32,
9494 root_scale: f32,
9495 ) -> Result<(), String> {
9496 if !overlay.child_layers.is_empty()
9497 || !root_direct_scene_events_are_supported(&overlay.scene)
9498 || !direct_root_child_underlays_are_supported(&overlay)
9499 {
9500 return Err("dev overlay graph must stay directly renderable".to_string());
9501 }
9502 execute_render_root_direct(
9503 backend,
9504 surface_view,
9505 overlay,
9506 width,
9507 height,
9508 root_scale,
9509 wgpu::LoadOp::Load,
9510 )
9511 .map(|_overlay_scene| ())
9512 .map_err(|(error, _overlay_scene)| error)
9513 }
9514
9515 #[allow(clippy::too_many_arguments)]
9516 fn encode_non_effect_segment_commands<C: FrameCommandRecorder>(
9517 &mut self,
9518 frame_encoder: &mut C,
9519 target_view: &wgpu::TextureView,
9520 ordered_items: &[(usize, SegmentDrawItem)],
9521 composites: &[(usize, CompositeBatchItem<'_>)],
9522 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
9523 shapes: &[DrawShape],
9524 brushes: &[Brush],
9525 images: &[ImageDraw],
9526 texts: &[TextDraw],
9527 shadow_draws: &[ShadowDraw],
9528 retained_draws: &[RetainedDraw],
9529 initial_load_op: wgpu::LoadOp<wgpu::Color>,
9530 width: u32,
9531 height: u32,
9532 root_scale: f32,
9533 ) -> Result<SegmentCommandEncodeOutcome, String> {
9534 let mut first_batch = true;
9535 for command in
9536 SegmentCommandIter::new(ordered_items, shapes, images, self.shape_batch_limits)
9537 {
9538 match command {
9539 SegmentRenderCommand::DrawChunk(chunk) => {
9540 let load_op = if first_batch {
9541 initial_load_op
9542 } else {
9543 wgpu::LoadOp::Load
9544 };
9545 let outcome = self.render_segment_draw_chunk(
9546 frame_encoder,
9547 target_view,
9548 ordered_items,
9549 composites,
9550 shader_composites,
9551 shapes,
9552 brushes,
9553 images,
9554 texts,
9555 retained_draws,
9556 chunk,
9557 width,
9558 height,
9559 root_scale,
9560 load_op,
9561 )?;
9562 if outcome.rendered_any {
9563 frame_encoder.record_passes(outcome.pass_count);
9564 first_batch = false;
9565 }
9566 }
9567 SegmentRenderCommand::Shadow(index) => {
9568 if first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
9569 {
9570 let _clear = frame_encoder.encoder().begin_render_pass(
9571 &wgpu::RenderPassDescriptor {
9572 label: Some("Shadow Pre-Clear"),
9573 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
9574 view: target_view,
9575 resolve_target: None,
9576 depth_slice: None,
9577 ops: wgpu::Operations {
9578 load: initial_load_op,
9579 store: wgpu::StoreOp::Store,
9580 },
9581 })],
9582 depth_stencil_attachment: None,
9583 timestamp_writes: None,
9584 occlusion_query_set: None,
9585 multiview_mask: None,
9586 },
9587 );
9588 }
9589 frame_encoder.record_pass();
9590 first_batch = false;
9591 }
9592 let pass_count_before = frame_encoder.recorded_pass_count();
9593 self.encode_shadow_draw(
9594 frame_encoder,
9595 target_view,
9596 &shadow_draws[index],
9597 width,
9598 height,
9599 root_scale,
9600 );
9601 if frame_encoder.recorded_pass_count() > pass_count_before {
9602 first_batch = false;
9603 }
9604 }
9605 }
9606 }
9607 Ok(SegmentCommandEncodeOutcome { first_batch })
9608 }
9609
9610 #[cfg(not(target_arch = "wasm32"))]
9611 #[allow(clippy::too_many_arguments)]
9612 fn render_segment_draw_chunk_fused_native<C: FrameCommandRecorder>(
9613 &mut self,
9614 frame_encoder: &mut C,
9615 target_view: &wgpu::TextureView,
9616 ordered_items: &[(usize, SegmentDrawItem)],
9617 composites: &[(usize, CompositeBatchItem<'_>)],
9618 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
9619 shapes: &[DrawShape],
9620 brushes: &[Brush],
9621 images: &[ImageDraw],
9622 texts: &[TextDraw],
9623 retained_draws: &[RetainedDraw],
9624 chunk: &SegmentDrawChunkPlan,
9625 width: u32,
9626 height: u32,
9627 root_scale: f32,
9628 load_op: wgpu::LoadOp<wgpu::Color>,
9629 ) -> Result<Option<SegmentRenderOutcome>, String> {
9630 let Some(partitions) = native_segment_fusion_partitions(
9631 ordered_items,
9632 shapes,
9633 brushes,
9634 chunk,
9635 self.shape_batch_limits,
9636 )?
9637 else {
9638 return Ok(None);
9639 };
9640
9641 let mut rendered_any = false;
9642 let mut pass_count = 0_u32;
9643 let mut next_load_op = load_op;
9644 let encode_started = Instant::now();
9645 let mut partition_count = 0_u64;
9646 for partition in partitions {
9647 partition_count += 1;
9648 let outcome = self.render_segment_draw_chunk_fused_native_partition(
9649 frame_encoder,
9650 target_view,
9651 ordered_items,
9652 composites,
9653 shader_composites,
9654 shapes,
9655 brushes,
9656 images,
9657 texts,
9658 retained_draws,
9659 &partition.chunk,
9660 partition.budget,
9661 width,
9662 height,
9663 root_scale,
9664 next_load_op,
9665 )?;
9666 if outcome.rendered_any {
9667 rendered_any = true;
9668 pass_count = pass_count.saturating_add(outcome.pass_count);
9669 next_load_op = wgpu::LoadOp::Load;
9670 }
9671 }
9672
9673 self.segment_encode_stats
9674 .note_call(partition_count, encode_started.elapsed().as_micros() as u64);
9675
9676 Ok(Some(SegmentRenderOutcome {
9677 rendered_any,
9678 pass_count,
9679 }))
9680 }
9681
9682 #[cfg(not(target_arch = "wasm32"))]
9683 #[allow(clippy::too_many_arguments)]
9684 fn render_segment_draw_chunk_fused_native_partition<C: FrameCommandRecorder>(
9685 &mut self,
9686 frame_encoder: &mut C,
9687 target_view: &wgpu::TextureView,
9688 ordered_items: &[(usize, SegmentDrawItem)],
9689 composites: &[(usize, CompositeBatchItem<'_>)],
9690 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
9691 shapes: &[DrawShape],
9692 brushes: &[Brush],
9693 images: &[ImageDraw],
9694 texts: &[TextDraw],
9695 retained_draws: &[RetainedDraw],
9696 chunk: &SegmentDrawChunkPlan,
9697 budget: NativeSegmentFusionBudget,
9698 width: u32,
9699 height: u32,
9700 root_scale: f32,
9701 load_op: wgpu::LoadOp<wgpu::Color>,
9702 ) -> Result<SegmentRenderOutcome, String> {
9703 let partition_start = Instant::now();
9704 let mut staged_uploads = self.take_staged_uploads();
9705 staged_uploads.clear();
9706 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
9707 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
9708 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
9709 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
9710 let mut span_cache = std::mem::take(&mut self.static_span);
9713 let mut segment_surfaces = std::mem::take(&mut self.segment_surfaces);
9716
9717 image_vertices.clear();
9718 image_indices.clear();
9719 image_cmds.clear();
9720 glyph_cmds.clear();
9721
9722 let result = (|| {
9723 let viewport = ViewportUniformParams {
9724 width,
9725 height,
9726 offset: [0.0, 0.0],
9727 };
9728 self.prewarm_offscreen_text_glyph_draws_in_chunk(
9729 ordered_items,
9730 texts,
9731 chunk,
9732 viewport,
9733 root_scale,
9734 &mut staged_uploads,
9735 &mut image_vertices,
9736 &mut image_indices,
9737 &mut glyph_cmds,
9738 )?;
9739 let mut shape_refs = Vec::with_capacity(budget.shape_count);
9740 for batch in chunk.iter() {
9741 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
9742 continue;
9743 };
9744 for (_, item) in &ordered_items[start..end] {
9745 let SegmentDrawItem::Shape(shape_index) = item else {
9746 return Err(format!(
9747 "shape batch contains non-shape draw item: {item:?}"
9748 ));
9749 };
9750 shape_refs.push(&shapes[*shape_index]);
9751 }
9752 }
9753 let after_shape_refs = Instant::now();
9754
9755 let mut direct_shape_uploads = StagedBufferUploads::default();
9756 let mut shape_upload_base = 0u64;
9757 if !shape_refs.is_empty() {
9758 let Some((_, upload_base)) = self.prepare_shapes_batch_direct(
9759 frame_encoder,
9760 shape_refs.iter().copied(),
9761 brushes,
9762 root_scale,
9763 viewport,
9764 &mut direct_shape_uploads,
9765 ) else {
9766 return Err(
9767 "native fused segment shape preparation produced no draw batch".to_string(),
9768 );
9769 };
9770 shape_upload_base = upload_base;
9771 }
9772 let after_shape_prepare = Instant::now();
9773
9774 let mut segment_captures: Vec<SegmentCaptureJob> = Vec::new();
9782 let mut segment_composite_plans: Vec<(usize, SegmentCompositePlan)> = Vec::new();
9783 if segment_surfaces.enabled() {
9784 self.plan_segment_surfaces(
9785 &mut segment_surfaces,
9786 ordered_items,
9787 chunk,
9788 retained_draws,
9789 &mut staged_uploads,
9790 &mut segment_captures,
9791 &mut segment_composite_plans,
9792 );
9793 }
9794
9795 let first_batch_info = match chunk.batches.first() {
9801 Some(&SegmentBatchPlan::Shape {
9802 start,
9803 end,
9804 blend_mode,
9805 }) => {
9806 let mut has_gradient = false;
9807 for (_, item) in &ordered_items[start..end] {
9808 if let SegmentDrawItem::Shape(shape_index) = item {
9809 has_gradient |=
9810 shape_gradient_stop_count(&shapes[*shape_index], brushes) > 0;
9811 }
9812 }
9813 Some((end - start, blend_mode, has_gradient))
9814 }
9815 _ => None,
9816 };
9817 let span_decision = span_cache.engage(
9818 load_op,
9819 first_batch_info,
9820 width,
9821 height,
9822 &self.scratch_shape_data,
9823 &self.scratch_gradients,
9824 );
9825 let span_skip = match span_decision {
9826 StaticSpanDecision::Hit { skip } => {
9827 if fill_area_diag_enabled() {
9828 self.fill_area_diag
9833 .note_static_span_skip(&self.scratch_shape_data[..skip]);
9834 }
9835 skip
9836 }
9837 _ => 0,
9838 };
9839
9840 let rim_mesh_on = rim_mesh_enabled();
9848 let mut chunk_rims: Vec<RimDraw> = Vec::new();
9849
9850 let mut fused_batches = Vec::with_capacity(chunk.batches.len());
9851 let mut shape_cursor = 0_u32;
9852 let mut composite_cursor = 0usize;
9853 let mut shader_composite_cursor = 0usize;
9854 for (batch_index, batch) in chunk.iter().enumerate() {
9855 match batch {
9856 SegmentBatchPlan::Shape {
9857 start,
9858 end,
9859 blend_mode,
9860 } => {
9861 let mut has_gradient = false;
9862 for (_, item) in &ordered_items[start..end] {
9863 let SegmentDrawItem::Shape(shape_index) = item else {
9864 return Err(format!(
9865 "shape batch contains non-shape draw item: {item:?}"
9866 ));
9867 };
9868 has_gradient |=
9869 shape_gradient_stop_count(&shapes[*shape_index], brushes) > 0;
9870 }
9871 let skip = if batch_index == 0 { span_skip } else { 0 };
9876 let shape_count = end - start;
9877 if shape_count > 0 {
9878 if rim_mesh_on
9879 && self.instanced_quads.is_some()
9880 && blend_mode == BlendMode::SrcOver
9881 {
9882 for offset in skip..shape_count {
9883 let global_index = shape_cursor + offset as u32;
9888 let converted = &self.scratch_shape_data[global_index as usize];
9889 let Some(band) = rim_mesh_band(converted) else {
9890 continue;
9891 };
9892 let vertex_mark = self.rim_mesh_vertices.len();
9893 let index_mark = self.rim_mesh_indices.len();
9894 if emit_arc_band_mesh(
9895 converted,
9896 global_index,
9897 &band,
9898 &mut self.rim_mesh_vertices,
9899 &mut self.rim_mesh_indices,
9900 )
9901 .is_none()
9902 {
9903 self.rim_mesh_vertices.truncate(vertex_mark);
9906 self.rim_mesh_indices.truncate(index_mark);
9907 continue;
9908 }
9909 if self.rim_mesh_vertices.len() > RIM_MESH_VERTEX_CAPACITY
9910 || self.rim_mesh_indices.len() > RIM_MESH_INDEX_CAPACITY
9911 {
9912 self.rim_mesh_vertices.truncate(vertex_mark);
9916 self.rim_mesh_indices.truncate(index_mark);
9917 rim_mesh_capacity_warn();
9918 continue;
9919 }
9920 chunk_rims.push(RimDraw {
9921 shape_index: global_index,
9922 first_index: index_mark as u32,
9923 index_count: (self.rim_mesh_indices.len() - index_mark)
9924 as u32,
9925 });
9926 if fill_area_diag_enabled() {
9927 self.fill_area_diag.note_rim_mesh(
9928 converted,
9929 triangles_shoelace_area(
9930 &self.rim_mesh_vertices,
9931 &self.rim_mesh_indices[index_mark..],
9932 ),
9933 );
9934 }
9935 self.rim_meshes_emitted += 1;
9936 if self.rim_meshes_emitted % 600 == 1 {
9937 log::debug!(
9938 "[rim-mesh] {} rims meshed lifetime ({} verts live this frame)",
9939 self.rim_meshes_emitted,
9940 self.rim_mesh_vertices.len(),
9941 );
9942 }
9943 }
9944 }
9945 if shape_count > skip {
9946 fused_batches.push(FusedSegmentBatch::Shape {
9947 batch: PreparedShapeBatch {
9948 vertex_start: (shape_cursor + skip as u32) * 6,
9949 vertex_count: (shape_count - skip) as u32 * 6,
9950 has_gradient,
9951 },
9952 blend_mode,
9953 });
9954 }
9955 shape_cursor += shape_count as u32;
9956 }
9957 }
9958 SegmentBatchPlan::Image {
9959 start,
9960 end,
9961 blend_mode,
9962 } => {
9963 let cmd_start = image_cmds.len();
9964 for (_, item) in &ordered_items[start..end] {
9965 let SegmentDrawItem::Image(image_index) = item else {
9966 return Err(format!(
9967 "image batch contains non-image draw item: {item:?}"
9968 ));
9969 };
9970 self.append_image_draw_cmd(
9971 &images[*image_index],
9972 viewport,
9973 root_scale,
9974 &mut image_vertices,
9975 &mut image_indices,
9976 &mut image_cmds,
9977 )?;
9978 }
9979 let cmd_end = image_cmds.len();
9980 if cmd_start < cmd_end {
9981 fused_batches.push(FusedSegmentBatch::Image {
9982 cmd_range: cmd_start..cmd_end,
9983 blend_mode,
9984 });
9985 }
9986 }
9987 SegmentBatchPlan::Text { start, end } => {
9988 let glyph_cmd_start = glyph_cmds.len();
9989 let image_cmd_start = image_cmds.len();
9990 let text_draws =
9991 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
9992 if !self.append_text_glyph_draws(
9993 text_draws,
9994 viewport,
9995 root_scale,
9996 false,
9997 &mut staged_uploads,
9998 &mut image_vertices,
9999 &mut image_indices,
10000 &mut glyph_cmds,
10001 )? {
10002 let text_draws =
10003 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
10004 self.append_text_image_draw_cmds(
10005 text_draws,
10006 viewport,
10007 root_scale,
10008 &mut image_vertices,
10009 &mut image_indices,
10010 &mut image_cmds,
10011 )?;
10012 }
10013 let image_cmd_end = image_cmds.len();
10014 let glyph_cmd_end = glyph_cmds.len();
10015 if image_cmd_start < image_cmd_end || glyph_cmd_start < glyph_cmd_end {
10016 fused_batches.push(FusedSegmentBatch::Text {
10017 image_cmd_range: image_cmd_start..image_cmd_end,
10018 glyph_cmd_range: glyph_cmd_start..glyph_cmd_end,
10019 });
10020 }
10021 }
10022 SegmentBatchPlan::Composite { start, end } => {
10023 for (_, item) in &ordered_items[start..end] {
10024 if !matches!(item, SegmentDrawItem::Composite(_)) {
10025 return Err(format!(
10026 "composite batch contains non-composite draw item: {item:?}"
10027 ));
10028 }
10029 }
10030 let draw_count = end - start;
10031 if draw_count > 0 {
10032 let draw_start = composite_cursor;
10033 composite_cursor += draw_count;
10034 fused_batches.push(FusedSegmentBatch::Composite {
10035 draw_range: draw_start..composite_cursor,
10036 });
10037 }
10038 }
10039 SegmentBatchPlan::ShaderComposite { start, end } => {
10040 for (_, item) in &ordered_items[start..end] {
10041 if !matches!(item, SegmentDrawItem::ShaderComposite(_)) {
10042 return Err(format!(
10043 "shader composite batch contains non-shader-composite draw item: {item:?}"
10044 ));
10045 }
10046 }
10047 let draw_count = end - start;
10048 if draw_count > 0 {
10049 let draw_start = shader_composite_cursor;
10050 shader_composite_cursor += draw_count;
10051 fused_batches.push(FusedSegmentBatch::ShaderComposite {
10052 draw_range: draw_start..shader_composite_cursor,
10053 });
10054 }
10055 }
10056 SegmentBatchPlan::Retained { start, end } => {
10057 self.stage_replay_patches(&mut staged_uploads);
10058 for (_, item) in &ordered_items[start..end] {
10059 let SegmentDrawItem::Retained(index) = item else {
10060 return Err(format!(
10061 "retained batch contains non-retained draw item: {item:?}"
10062 ));
10063 };
10064 let retained = retained_draws.get(*index).ok_or_else(|| {
10065 format!("retained draw index {index} out of bounds")
10066 })?;
10067 if (*index as u32) < MAX_REPLAY_SLOTS
10068 && self.replay_slots.slots.contains_key(&retained.slot)
10069 {
10070 let transform = retained.transform.with_retained_paint();
10071 staged_uploads.stage_at(
10072 UploadTarget::ReplayTransform,
10073 *index as u64 * REPLAY_TRANSFORM_STRIDE,
10074 bytemuck::bytes_of(&transform),
10075 );
10076 }
10077 }
10078 if end > start {
10079 fused_batches.push(FusedSegmentBatch::Retained {
10080 item_range: start..end,
10081 });
10082 }
10083 }
10084 }
10085 }
10086 if !chunk_rims.is_empty() {
10087 self.upload_transient_rim_meshes();
10088 }
10089 let after_batch_prepare = Instant::now();
10090
10091 if !image_indices.is_empty() {
10092 self.stage_native_image_buffers(
10093 &mut staged_uploads,
10094 viewport,
10095 &image_vertices,
10096 &image_indices,
10097 );
10098 }
10099
10100 let display_clip_depth_view =
10109 self.display_clip_pass_depth_view(target_view, width, height);
10110 let pass_depth = display_clip_depth_view.is_some();
10111
10112 let device = self.device.clone();
10113 let composite_items: Vec<_> = chunk
10114 .iter()
10115 .filter_map(|batch| match batch {
10116 SegmentBatchPlan::Composite { start, end } => Some((start, end)),
10117 _ => None,
10118 })
10119 .flat_map(|(start, end)| {
10120 ordered_items[start..end].iter().filter_map(|(_, item)| {
10121 let SegmentDrawItem::Composite(composite_index) = item else {
10122 return None;
10123 };
10124 composites
10125 .get(*composite_index)
10126 .map(|(_, composite)| *composite)
10127 })
10128 })
10129 .collect();
10130 let prepared_composites = self.effect_renderer.prepare_composite_batch_draws(
10131 frame_encoder,
10132 &device,
10133 load_op,
10134 &composite_items,
10135 pass_depth,
10136 );
10137 let shader_items: Vec<_> = chunk
10138 .iter()
10139 .filter_map(|batch| match batch {
10140 SegmentBatchPlan::ShaderComposite { start, end } => Some((start, end)),
10141 _ => None,
10142 })
10143 .flat_map(|(start, end)| {
10144 ordered_items[start..end].iter().filter_map(|(_, item)| {
10145 let SegmentDrawItem::ShaderComposite(composite_index) = item else {
10146 return None;
10147 };
10148 shader_composites
10149 .get(*composite_index)
10150 .map(|(_, composite)| *composite)
10151 })
10152 })
10153 .collect();
10154 let prepared_shaders = self
10155 .effect_renderer
10156 .prepare_shader_batch_draws(frame_encoder, &device, &shader_items, pass_depth)
10157 .ok_or_else(|| "shader composite batch preparation failed".to_string())?;
10158 if !shader_items.is_empty() {
10159 self.effect_renderer.record_composite_pass();
10160 self.effect_renderer
10161 .debug_effects
10162 .set(self.effect_renderer.debug_effects.get() + shader_items.len() as u32);
10163 }
10164 let span_blit_items =
10171 span_cache
10172 .texture
10173 .as_ref()
10174 .filter(|_| span_skip > 0)
10175 .map(|texture| CompositeBatchItem {
10176 source: texture,
10177 alpha: 1.0,
10178 scissor: None,
10179 rounded_mask: None,
10180 blend_mode: BlendMode::SrcOver,
10181 dest_viewport: None,
10182 source_viewport: None,
10183 sample_mode: CompositeSampleMode::Nearest,
10184 });
10185 let span_blit = match &span_blit_items {
10186 Some(item) => self.effect_renderer.prepare_composite_batch_draws(
10187 frame_encoder,
10188 &device,
10189 load_op,
10190 std::slice::from_ref(item),
10191 pass_depth,
10192 ),
10193 None => Vec::new(),
10194 };
10195 let mut prepared_segment_composites: Vec<(usize, PreparedProjectiveComposite<'_>)> =
10200 Vec::with_capacity(segment_composite_plans.len());
10201 for (index, plan) in &segment_composite_plans {
10202 let Some(entry) = segment_surfaces.entry(&plan.key) else {
10203 continue;
10204 };
10205 let item = ProjectiveCompositeItem {
10206 source: &entry.texture,
10207 viewport: (width, height),
10208 dest_quad: plan.dest_quad,
10209 inverse: plan.inverse,
10210 alpha: 1.0,
10211 blend_mode: BlendMode::SrcOver,
10212 sample_mode: if plan.identity {
10215 CompositeSampleMode::Nearest
10216 } else {
10217 CompositeSampleMode::Linear
10218 },
10219 };
10220 let prepared = self.effect_renderer.prepare_projective_composite_draw(
10221 frame_encoder,
10222 &device,
10223 &item,
10224 pass_depth,
10225 );
10226 prepared_segment_composites.push((*index, prepared));
10227 }
10228 let after_composite_prepare = Instant::now();
10229
10230 if fused_batches.is_empty() && span_blit.is_empty() {
10231 return Ok(SegmentRenderOutcome {
10232 rendered_any: false,
10233 pass_count: 0,
10234 });
10235 }
10236
10237 self.flush_staged_uploads_at(
10242 frame_encoder.encoder(),
10243 &direct_shape_uploads,
10244 shape_upload_base,
10245 );
10246 let upload_offset =
10247 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10248 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
10249 let after_upload = Instant::now();
10250
10251 let mut segment_capture_passes = 0u32;
10260 for job in &segment_captures {
10261 let Some(entry) = segment_surfaces.entry(&job.key) else {
10262 continue;
10263 };
10264 let Some(slot) = self.replay_slots.slots.get(&job.key.slot) else {
10265 continue;
10266 };
10267 let Some(uniform_group) =
10268 segment_surfaces.capture_uniform_bind_group(job.capture_index)
10269 else {
10270 continue;
10271 };
10272 let mut capture_pass =
10273 frame_encoder
10274 .encoder()
10275 .begin_render_pass(&wgpu::RenderPassDescriptor {
10276 label: Some("Segment Surface Capture Pass"),
10277 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10278 view: &entry.texture.view,
10279 resolve_target: None,
10280 depth_slice: None,
10281 ops: wgpu::Operations {
10282 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
10286 store: wgpu::StoreOp::Store,
10287 },
10288 })],
10289 depth_stencil_attachment: None,
10290 timestamp_writes: None,
10291 occlusion_query_set: None,
10292 multiview_mask: None,
10293 });
10294 let draws = self.encode_retained_op(
10295 slot,
10296 job.first,
10297 job.last,
10298 MAX_REPLAY_SLOTS + job.capture_index,
10299 &mut |cmd| match cmd {
10300 RetainedCmd::Uniforms(_) => {
10305 capture_pass.set_bind_group(0, uniform_group, &[])
10306 }
10307 RetainedCmd::Pipeline(pipeline) => capture_pass.set_pipeline(pipeline),
10308 RetainedCmd::SlotBindings(group, offset) => {
10309 capture_pass.set_bind_group(1, group, &[offset])
10310 }
10311 RetainedCmd::MeshVertices(buffer) => {
10312 capture_pass.set_vertex_buffer(0, buffer.slice(..))
10313 }
10314 RetainedCmd::Index(buffer, format) => {
10315 capture_pass.set_index_buffer(buffer.slice(..), format)
10316 }
10317 RetainedCmd::Draw(vertices) => capture_pass.draw(vertices, 0..1),
10318 RetainedCmd::DrawIndexed(indices, instances) => {
10319 capture_pass.draw_indexed(indices, 0, instances)
10320 }
10321 },
10322 );
10323 self.frame_stats.add_draw_calls(draws);
10324 segment_capture_passes += 1;
10325 }
10326
10327 let use_retained_bundles = retained_bundles_enabled();
10328 let mut retained_encode_ms = 0.0_f64;
10329 {
10330 let mut render_pass =
10331 frame_encoder
10332 .encoder()
10333 .begin_render_pass(&wgpu::RenderPassDescriptor {
10334 label: Some("Fused Segment Draw Pass"),
10335 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10336 view: target_view,
10337 resolve_target: None,
10338 depth_slice: None,
10339 ops: wgpu::Operations {
10340 load: load_op,
10341 store: wgpu::StoreOp::Store,
10342 },
10343 })],
10344 depth_stencil_attachment: display_clip_depth_view.as_ref().map(
10348 |view| wgpu::RenderPassDepthStencilAttachment {
10349 view,
10350 depth_ops: Some(wgpu::Operations {
10351 load: wgpu::LoadOp::Clear(
10352 crate::display_clip::DISPLAY_CLIP_DEPTH_CLEAR,
10353 ),
10354 store: wgpu::StoreOp::Discard,
10355 }),
10356 stencil_ops: None,
10357 },
10358 ),
10359 timestamp_writes: None,
10360 occlusion_query_set: None,
10361 multiview_mask: None,
10362 });
10363
10364 for draw in &span_blit {
10368 self.effect_renderer.draw_prepared_composite(
10369 &mut render_pass,
10370 (width, height),
10371 draw,
10372 pass_depth,
10373 );
10374 }
10375 if pass_depth {
10376 self.draw_display_clip_occluder(&mut render_pass, width, height);
10379 self.display_clip.pass_depth.set(true);
10380 }
10381
10382 for batch in &fused_batches {
10383 match batch {
10384 FusedSegmentBatch::Shape { batch, blend_mode } => {
10385 self.draw_prepared_shapes(
10386 &mut render_pass,
10387 *blend_mode,
10388 *batch,
10389 width,
10390 height,
10391 &chunk_rims,
10392 );
10393 }
10394 FusedSegmentBatch::Image {
10395 cmd_range,
10396 blend_mode,
10397 } => {
10398 self.draw_native_prepared_image_cmd_range(
10399 &mut render_pass,
10400 &image_cmds,
10401 cmd_range.clone(),
10402 *blend_mode,
10403 )?;
10404 }
10405 FusedSegmentBatch::Text {
10406 image_cmd_range,
10407 glyph_cmd_range,
10408 } => {
10409 if !image_cmd_range.is_empty() {
10410 self.draw_native_prepared_image_cmd_range(
10411 &mut render_pass,
10412 &image_cmds,
10413 image_cmd_range.clone(),
10414 BlendMode::SrcOver,
10415 )?;
10416 self.frame_stats.bump_text();
10417 }
10418 if !glyph_cmd_range.is_empty() {
10419 self.draw_native_prepared_glyph_cmd_range(
10420 &mut render_pass,
10421 &glyph_cmds,
10422 glyph_cmd_range.clone(),
10423 )?;
10424 }
10425 }
10426 FusedSegmentBatch::Composite { draw_range } => {
10427 for draw in
10428 prepared_composites.get(draw_range.clone()).ok_or_else(|| {
10429 "composite draw range is outside the prepared command buffer"
10430 .to_string()
10431 })?
10432 {
10433 self.effect_renderer.draw_prepared_composite(
10434 &mut render_pass,
10435 (width, height),
10436 draw,
10437 pass_depth,
10438 );
10439 }
10440 }
10441 FusedSegmentBatch::ShaderComposite { draw_range } => {
10442 for draw in prepared_shaders.get(draw_range.clone()).ok_or_else(|| {
10443 "shader composite draw range is outside the prepared command buffer"
10444 .to_string()
10445 })? {
10446 self.effect_renderer.draw_prepared_shader_src_over(
10447 &device,
10448 &mut render_pass,
10449 (width, height),
10450 draw,
10451 pass_depth,
10452 );
10453 }
10454 }
10455 FusedSegmentBatch::Retained { item_range } => {
10456 let retained_start = Instant::now();
10462 let stretch_has_composites = !prepared_segment_composites.is_empty()
10468 && ordered_items[item_range.clone()].iter().any(|(_, item)| {
10469 matches!(
10470 item,
10471 SegmentDrawItem::Retained(index)
10472 if prepared_segment_composites
10473 .iter()
10474 .any(|(prepared_index, _)| prepared_index == index)
10475 )
10476 });
10477 if use_retained_bundles && !stretch_has_composites {
10478 self.draw_retained_stretch_bundled(
10479 &mut render_pass,
10480 ordered_items,
10481 retained_draws,
10482 item_range.clone(),
10483 width,
10484 height,
10485 );
10486 } else {
10487 for (_, item) in &ordered_items[item_range.clone()] {
10488 if let SegmentDrawItem::Retained(index) = item {
10489 if let Some((_, prepared)) = prepared_segment_composites
10490 .iter()
10491 .find(|(prepared_index, _)| prepared_index == index)
10492 {
10493 self.effect_renderer
10501 .draw_prepared_projective_composite(
10502 &mut render_pass,
10503 (width, height),
10504 prepared,
10505 pass_depth,
10506 );
10507 self.frame_stats.add_draw_calls(1);
10508 } else if let Some(retained) = retained_draws.get(*index) {
10509 self.draw_retained_batch(
10510 &mut render_pass,
10511 retained,
10512 *index,
10513 width,
10514 height,
10515 );
10516 }
10517 }
10518 }
10519 }
10520 retained_encode_ms += instant_ms(retained_start, Instant::now());
10521 }
10522 }
10523 }
10524 }
10525 self.display_clip.pass_depth.set(false);
10531 let mut capture_passes = 0_u32;
10540 if let StaticSpanDecision::Capture { len, clear } = span_decision {
10541 let texture = match span_cache.texture.take() {
10542 Some(existing) if existing.width == width && existing.height == height => {
10543 existing
10544 }
10545 other => {
10546 if let Some(stale) = other {
10547 self.defer_offscreen_release(stale);
10548 }
10549 self.acquire_offscreen(width, height)
10550 }
10551 };
10552 {
10553 let mut capture_pass =
10554 frame_encoder
10555 .encoder()
10556 .begin_render_pass(&wgpu::RenderPassDescriptor {
10557 label: Some("Static Span Capture Pass"),
10558 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10559 view: &texture.view,
10560 resolve_target: None,
10561 depth_slice: None,
10562 ops: wgpu::Operations {
10563 load: wgpu::LoadOp::Clear(clear),
10564 store: wgpu::StoreOp::Store,
10565 },
10566 })],
10567 depth_stencil_attachment: None,
10568 timestamp_writes: None,
10569 occlusion_query_set: None,
10570 multiview_mask: None,
10571 });
10572 let has_gradient = first_batch_info
10576 .map(|(_, _, has_gradient)| has_gradient)
10577 .unwrap_or(false);
10578 self.draw_prepared_shapes(
10579 &mut capture_pass,
10580 BlendMode::SrcOver,
10581 PreparedShapeBatch {
10582 vertex_start: 0,
10583 vertex_count: len as u32 * 6,
10584 has_gradient,
10585 },
10586 width,
10587 height,
10588 &[],
10589 );
10590 if fill_area_diag_enabled() {
10591 self.fill_area_diag
10595 .add_shape_quads(&self.scratch_shape_data[..len], viewport);
10596 }
10597 span_cache.store_key(
10598 &self.scratch_shape_data[..len],
10599 &self.scratch_gradients,
10600 width,
10601 height,
10602 clear,
10603 has_gradient,
10604 );
10605 }
10606 span_cache.texture = Some(texture);
10607 capture_passes = 1;
10608 }
10609 let after_pass = Instant::now();
10610 if let Some(total_ms) = should_log_wgpu_render_stage(partition_start, after_pass) {
10611 log::warn!(
10612 "[wgpu-render-stage:fused-segment] total_ms={total_ms:.2} shape_refs_ms={:.2} shape_prepare_ms={:.2} batch_prepare_ms={:.2} composite_prepare_ms={:.2} upload_ms={:.2} pass_ms={:.2} retained_encode_ms={retained_encode_ms:.3} batches={} shapes={} image_cmds={} glyph_cmds={} staged_bytes={}",
10613 instant_ms(partition_start, after_shape_refs),
10614 instant_ms(after_shape_refs, after_shape_prepare),
10615 instant_ms(after_shape_prepare, after_batch_prepare),
10616 instant_ms(after_batch_prepare, after_composite_prepare),
10617 instant_ms(after_composite_prepare, after_upload),
10618 instant_ms(after_upload, after_pass),
10619 fused_batches.len(),
10620 budget.shape_count,
10621 image_cmds.len(),
10622 glyph_cmds.len(),
10623 staged_uploads.bytes.len(),
10624 );
10625 }
10626
10627 Ok(SegmentRenderOutcome {
10628 rendered_any: true,
10629 pass_count: 1 + capture_passes + segment_capture_passes,
10630 })
10631 })();
10632
10633 self.display_clip.pass_depth.set(false);
10637 self.scratch_image_vertices = image_vertices;
10638 self.scratch_image_indices = image_indices;
10639 self.scratch_image_cmds = image_cmds;
10640 self.scratch_glyph_cmds = glyph_cmds;
10641 self.restore_staged_uploads(staged_uploads);
10642 self.static_span = span_cache;
10643 if result.is_err() {
10644 segment_surfaces.clear();
10647 }
10648 self.segment_surfaces = segment_surfaces;
10649 result
10650 }
10651
10652 #[allow(clippy::too_many_arguments)]
10653 fn render_segment_draw_chunk<C: FrameCommandRecorder>(
10654 &mut self,
10655 frame_encoder: &mut C,
10656 target_view: &wgpu::TextureView,
10657 ordered_items: &[(usize, SegmentDrawItem)],
10658 composites: &[(usize, CompositeBatchItem<'_>)],
10659 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
10660 shapes: &[DrawShape],
10661 brushes: &[Brush],
10662 images: &[ImageDraw],
10663 texts: &[TextDraw],
10664 retained_draws: &[RetainedDraw],
10665 chunk: SegmentDrawChunkPlan,
10666 width: u32,
10667 height: u32,
10668 root_scale: f32,
10669 load_op: wgpu::LoadOp<wgpu::Color>,
10670 ) -> Result<SegmentRenderOutcome, String> {
10671 #[cfg(target_arch = "wasm32")]
10672 let _ = retained_draws;
10673 #[cfg(not(target_arch = "wasm32"))]
10674 if let Some(outcome) = self.render_segment_draw_chunk_fused_native(
10675 frame_encoder,
10676 target_view,
10677 ordered_items,
10678 composites,
10679 shader_composites,
10680 shapes,
10681 brushes,
10682 images,
10683 texts,
10684 retained_draws,
10685 &chunk,
10686 width,
10687 height,
10688 root_scale,
10689 load_op,
10690 )? {
10691 return Ok(outcome);
10692 }
10693
10694 let mut staged_uploads = self.take_staged_uploads();
10695 let result = (|| {
10696 let mut rendered_any = false;
10697 let mut pass_count = 0_u32;
10698 let mut next_load_op = load_op;
10699 for batch in chunk.iter() {
10700 staged_uploads.clear();
10701 match batch {
10702 SegmentBatchPlan::Shape {
10703 start,
10704 end,
10705 blend_mode,
10706 } => {
10707 let slice = &ordered_items[start..end];
10708 if slice.len() > self.shape_batch_limits.max_shapes_per_batch {
10709 return Err(format!(
10710 "shape batch contains {} shapes, exceeding the renderer limit of {}",
10711 slice.len(),
10712 self.shape_batch_limits.max_shapes_per_batch
10713 ));
10714 }
10715 let viewport = ViewportUniformParams {
10716 width,
10717 height,
10718 offset: [0.0, 0.0],
10719 };
10720 for (_, item) in slice {
10721 if !matches!(item, SegmentDrawItem::Shape(_)) {
10722 return Err(format!(
10723 "shape batch contains non-shape draw item: {item:?}"
10724 ));
10725 }
10726 }
10727 let Some(prepared) = self.prepare_shapes_batch(
10728 slice.iter().filter_map(|(_, item)| match item {
10729 SegmentDrawItem::Shape(shape_index) => Some(&shapes[*shape_index]),
10730 _ => None,
10731 }),
10732 brushes,
10733 root_scale,
10734 viewport,
10735 &mut staged_uploads,
10736 ) else {
10737 continue;
10738 };
10739 let upload_offset = frame_encoder
10740 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10741 self.flush_staged_uploads_at(
10742 frame_encoder.encoder(),
10743 &staged_uploads,
10744 upload_offset,
10745 );
10746 {
10747 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10748 &wgpu::RenderPassDescriptor {
10749 label: Some("Segment Shape Pass"),
10750 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10751 view: target_view,
10752 resolve_target: None,
10753 depth_slice: None,
10754 ops: wgpu::Operations {
10755 load: next_load_op,
10756 store: wgpu::StoreOp::Store,
10757 },
10758 })],
10759 depth_stencil_attachment: None,
10760 timestamp_writes: None,
10761 occlusion_query_set: None,
10762 multiview_mask: None,
10763 },
10764 );
10765 self.draw_prepared_shapes(
10766 &mut render_pass,
10767 blend_mode,
10768 prepared,
10769 width,
10770 height,
10771 &[],
10772 );
10773 }
10774 pass_count = pass_count.saturating_add(1);
10775 rendered_any = true;
10776 next_load_op = wgpu::LoadOp::Load;
10777 }
10778 SegmentBatchPlan::Image {
10779 start,
10780 end,
10781 blend_mode,
10782 } => {
10783 let viewport = ViewportUniformParams {
10784 width,
10785 height,
10786 offset: [0.0, 0.0],
10787 };
10788 for (_, item) in &ordered_items[start..end] {
10789 if !matches!(item, SegmentDrawItem::Image(_)) {
10790 return Err(format!(
10791 "image batch contains non-image draw item: {item:?}"
10792 ));
10793 }
10794 }
10795 let prepared_images = self.prepare_image_draw_cmds(
10796 ordered_items[start..end]
10797 .iter()
10798 .filter_map(|(_, item)| match item {
10799 SegmentDrawItem::Image(image_index) => {
10800 Some(&images[*image_index])
10801 }
10802 _ => None,
10803 }),
10804 viewport,
10805 root_scale,
10806 &mut staged_uploads,
10807 )?;
10808 if prepared_images.is_empty() {
10809 self.scratch_image_cmds = prepared_images.into_cmds();
10810 continue;
10811 }
10812 let upload_offset = frame_encoder
10813 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10814 self.flush_staged_uploads_at(
10815 frame_encoder.encoder(),
10816 &staged_uploads,
10817 upload_offset,
10818 );
10819 let draw_result = {
10820 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10821 &wgpu::RenderPassDescriptor {
10822 label: Some("Segment Image Pass"),
10823 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10824 view: target_view,
10825 resolve_target: None,
10826 depth_slice: None,
10827 ops: wgpu::Operations {
10828 load: next_load_op,
10829 store: wgpu::StoreOp::Store,
10830 },
10831 })],
10832 depth_stencil_attachment: None,
10833 timestamp_writes: None,
10834 occlusion_query_set: None,
10835 multiview_mask: None,
10836 },
10837 );
10838 self.draw_prepared_images(
10839 &mut render_pass,
10840 &prepared_images,
10841 blend_mode,
10842 )
10843 };
10844 pass_count = pass_count.saturating_add(1);
10845 self.scratch_image_cmds = prepared_images.into_cmds();
10846 draw_result?;
10847 rendered_any = true;
10848 next_load_op = wgpu::LoadOp::Load;
10849 }
10850 SegmentBatchPlan::Text { start, end } => {
10851 let viewport = ViewportUniformParams {
10852 width,
10853 height,
10854 offset: [0.0, 0.0],
10855 };
10856 let text_draws =
10857 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
10858 if let Some(prepared_glyphs) = self.prepare_text_glyph_draw_cmds(
10859 text_draws,
10860 viewport,
10861 root_scale,
10862 &mut staged_uploads,
10863 )? {
10864 if prepared_glyphs.is_empty() {
10865 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
10866 continue;
10867 }
10868 let upload_offset = frame_encoder
10869 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10870 self.flush_staged_uploads_at(
10871 frame_encoder.encoder(),
10872 &staged_uploads,
10873 upload_offset,
10874 );
10875 {
10876 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10877 &wgpu::RenderPassDescriptor {
10878 label: Some("Segment Text Glyph Atlas Pass"),
10879 color_attachments: &[Some(
10880 wgpu::RenderPassColorAttachment {
10881 view: target_view,
10882 resolve_target: None,
10883 depth_slice: None,
10884 ops: wgpu::Operations {
10885 load: next_load_op,
10886 store: wgpu::StoreOp::Store,
10887 },
10888 },
10889 )],
10890 depth_stencil_attachment: None,
10891 timestamp_writes: None,
10892 occlusion_query_set: None,
10893 multiview_mask: None,
10894 },
10895 );
10896 self.draw_prepared_glyphs(&mut render_pass, &prepared_glyphs)?;
10897 }
10898 pass_count = pass_count.saturating_add(1);
10899 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
10900 rendered_any = true;
10901 next_load_op = wgpu::LoadOp::Load;
10902 } else {
10903 let text_draws =
10904 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
10905 let prepared_images = self.prepare_text_image_draw_cmds(
10906 text_draws,
10907 viewport,
10908 root_scale,
10909 &mut staged_uploads,
10910 )?;
10911 if prepared_images.is_empty() {
10912 self.scratch_image_cmds = prepared_images.into_cmds();
10913 continue;
10914 }
10915 let upload_offset = frame_encoder
10916 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10917 self.flush_staged_uploads_at(
10918 frame_encoder.encoder(),
10919 &staged_uploads,
10920 upload_offset,
10921 );
10922 {
10923 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10924 &wgpu::RenderPassDescriptor {
10925 label: Some("Segment Text Pass"),
10926 color_attachments: &[Some(
10927 wgpu::RenderPassColorAttachment {
10928 view: target_view,
10929 resolve_target: None,
10930 depth_slice: None,
10931 ops: wgpu::Operations {
10932 load: next_load_op,
10933 store: wgpu::StoreOp::Store,
10934 },
10935 },
10936 )],
10937 depth_stencil_attachment: None,
10938 timestamp_writes: None,
10939 occlusion_query_set: None,
10940 multiview_mask: None,
10941 },
10942 );
10943 self.draw_prepared_images(
10944 &mut render_pass,
10945 &prepared_images,
10946 BlendMode::SrcOver,
10947 )?;
10948 }
10949 self.frame_stats.bump_text();
10950 pass_count = pass_count.saturating_add(1);
10951 self.scratch_image_cmds = prepared_images.into_cmds();
10952 rendered_any = true;
10953 next_load_op = wgpu::LoadOp::Load;
10954 }
10955 }
10956 SegmentBatchPlan::Composite { start, end } => {
10957 let batch_items: Vec<_> = ordered_items[start..end]
10958 .iter()
10959 .map(|(_, item)| match item {
10960 SegmentDrawItem::Composite(composite_index) => composites
10961 .get(*composite_index)
10962 .map(|(_, composite)| *composite)
10963 .ok_or_else(|| {
10964 "composite item index is outside the composite buffer"
10965 .to_string()
10966 }),
10967 other => Err(format!(
10968 "composite batch contains non-composite draw item: {other:?}"
10969 )),
10970 })
10971 .collect::<Result<_, _>>()?;
10972 let device = self.device.clone();
10973 self.effect_renderer.encode_composite_batch_to_view_pass(
10974 frame_encoder,
10975 &device,
10976 target_view,
10977 (width, height),
10978 next_load_op,
10979 &batch_items,
10980 );
10981 self.effect_renderer.record_composite_pass();
10982 pass_count = pass_count.saturating_add(1);
10983 rendered_any = true;
10984 next_load_op = wgpu::LoadOp::Load;
10985 }
10986 SegmentBatchPlan::ShaderComposite { start, end } => {
10987 let batch_items: Vec<_> = ordered_items[start..end]
10988 .iter()
10989 .map(|(_, item)| match item {
10990 SegmentDrawItem::ShaderComposite(composite_index) => {
10991 shader_composites
10992 .get(*composite_index)
10993 .map(|(_, composite)| *composite)
10994 .ok_or_else(|| {
10995 "shader composite item index is outside the shader composite buffer"
10996 .to_string()
10997 })
10998 }
10999 other => Err(format!(
11000 "shader composite batch contains non-shader-composite draw item: {other:?}"
11001 )),
11002 })
11003 .collect::<Result<Vec<_>, _>>()?;
11004 let device = self.device.clone();
11005 let encoded = self.effect_renderer.encode_shader_batch_src_over_to_view(
11006 frame_encoder,
11007 &device,
11008 target_view,
11009 (width, height),
11010 next_load_op,
11011 &batch_items,
11012 );
11013 if !encoded {
11014 return Err("shader composite batch failed to encode".to_string());
11015 }
11016 self.effect_renderer.record_composite_pass();
11017 self.effect_renderer.debug_effects.set(
11018 self.effect_renderer.debug_effects.get() + batch_items.len() as u32,
11019 );
11020 pass_count = pass_count.saturating_add(1);
11021 rendered_any = true;
11022 next_load_op = wgpu::LoadOp::Load;
11023 }
11024 SegmentBatchPlan::Retained { start, end } => {
11025 #[cfg(target_arch = "wasm32")]
11034 {
11035 let _ = (start, end);
11036 return Err("retained shape batches are native-only".to_string());
11037 }
11038 #[cfg(not(target_arch = "wasm32"))]
11039 {
11040 self.stage_replay_patches(&mut staged_uploads);
11041 for (_, item) in &ordered_items[start..end] {
11042 let SegmentDrawItem::Retained(index) = item else {
11043 return Err(format!(
11044 "retained batch contains non-retained draw item: {item:?}"
11045 ));
11046 };
11047 let retained = retained_draws.get(*index).ok_or_else(|| {
11048 format!("retained draw index {index} out of bounds")
11049 })?;
11050 if (*index as u32) < MAX_REPLAY_SLOTS
11051 && self.replay_slots.slots.contains_key(&retained.slot)
11052 {
11053 let transform = retained.transform.with_retained_paint();
11054 staged_uploads.stage_at(
11055 UploadTarget::ReplayTransform,
11056 *index as u64 * REPLAY_TRANSFORM_STRIDE,
11057 bytemuck::bytes_of(&transform),
11058 );
11059 }
11060 }
11061 let upload_offset = frame_encoder
11062 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11063 self.flush_staged_uploads_at(
11064 frame_encoder.encoder(),
11065 &staged_uploads,
11066 upload_offset,
11067 );
11068 {
11069 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11070 &wgpu::RenderPassDescriptor {
11071 label: Some("Segment Retained Pass"),
11072 color_attachments: &[Some(
11073 wgpu::RenderPassColorAttachment {
11074 view: target_view,
11075 resolve_target: None,
11076 depth_slice: None,
11077 ops: wgpu::Operations {
11078 load: next_load_op,
11079 store: wgpu::StoreOp::Store,
11080 },
11081 },
11082 )],
11083 depth_stencil_attachment: None,
11084 timestamp_writes: None,
11085 occlusion_query_set: None,
11086 multiview_mask: None,
11087 },
11088 );
11089 for (_, item) in &ordered_items[start..end] {
11090 if let SegmentDrawItem::Retained(index) = item {
11091 if let Some(retained) = retained_draws.get(*index) {
11092 self.draw_retained_batch(
11093 &mut render_pass,
11094 retained,
11095 *index,
11096 width,
11097 height,
11098 );
11099 }
11100 }
11101 }
11102 }
11103 pass_count = pass_count.saturating_add(1);
11104 rendered_any = true;
11105 next_load_op = wgpu::LoadOp::Load;
11106 }
11107 }
11108 }
11109 }
11110 Ok(SegmentRenderOutcome {
11111 rendered_any,
11112 pass_count,
11113 })
11114 })();
11115 self.restore_staged_uploads(staged_uploads);
11116 result
11117 }
11118
11119 fn viewport_uniforms(params: ViewportUniformParams) -> Uniforms {
11120 Uniforms {
11121 viewport: [params.width as f32, params.height as f32],
11122 viewport_offset: params.offset,
11123 }
11124 }
11125
11126 #[cfg(not(target_arch = "wasm32"))]
11127 fn stage_viewport_uniforms(
11128 &self,
11129 staged_uploads: &mut StagedBufferUploads,
11130 params: ViewportUniformParams,
11131 ) {
11132 let uniforms = Self::viewport_uniforms(params);
11133 staged_uploads.stage(UploadTarget::Uniform, bytemuck::bytes_of(&uniforms));
11134 }
11135
11136 #[cfg(not(target_arch = "wasm32"))]
11137 fn stage_retained_glyph_viewport_uniforms(
11138 &mut self,
11139 staged_uploads: &mut StagedBufferUploads,
11140 params: ViewportUniformParams,
11141 ) -> usize {
11142 let slot = self.claim_retained_glyph_uniform_slot();
11143 let uniforms = Self::viewport_uniforms(params);
11144 staged_uploads.stage_at(
11145 UploadTarget::RetainedGlyphUniform,
11146 self.retained_glyph_uniform_offset(slot),
11147 bytemuck::bytes_of(&uniforms),
11148 );
11149 slot
11150 }
11151
11152 #[cfg(not(target_arch = "wasm32"))]
11153 fn claim_retained_glyph_uniform_slot(&mut self) -> usize {
11154 let slot = self.retained_glyph_uniform_cursor;
11155 self.retained_glyph_uniform_cursor = self.retained_glyph_uniform_cursor.saturating_add(1);
11156 self.ensure_retained_glyph_uniform_capacity(slot.saturating_add(1));
11157 slot
11158 }
11159
11160 #[cfg(not(target_arch = "wasm32"))]
11161 fn retained_glyph_uniform_offset(&self, slot: usize) -> u64 {
11162 self.retained_glyph_uniform_stride * slot as u64
11163 }
11164
11165 #[cfg(not(target_arch = "wasm32"))]
11166 fn retained_glyph_uniform_dynamic_offset(&self, slot: usize) -> Result<u32, String> {
11167 let offset = self.retained_glyph_uniform_offset(slot);
11168 u32::try_from(offset).map_err(|_| {
11169 "retained glyph uniform offset exceeded WGPU dynamic offset range".to_string()
11170 })
11171 }
11172
11173 #[cfg(not(target_arch = "wasm32"))]
11174 fn ensure_retained_glyph_uniform_capacity(&mut self, required_slots: usize) {
11175 if required_slots <= self.retained_glyph_uniform_capacity {
11176 return;
11177 }
11178 let new_capacity = required_slots
11179 .next_power_of_two()
11180 .max(INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS);
11181 self.retained_glyph_uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11182 label: Some("Retained Glyph Uniform Buffer"),
11183 size: self.retained_glyph_uniform_stride * new_capacity as u64,
11184 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
11185 mapped_at_creation: false,
11186 });
11187 self.retained_glyph_uniform_bind_group =
11188 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
11189 label: Some("Retained Glyph Uniform Bind Group"),
11190 layout: &self.retained_glyph_uniform_bind_group_layout,
11191 entries: &[wgpu::BindGroupEntry {
11192 binding: 0,
11193 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11194 buffer: &self.retained_glyph_uniform_buffer,
11195 offset: 0,
11196 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
11197 }),
11198 }],
11199 });
11200 self.retained_glyph_uniform_capacity = new_capacity;
11201 }
11202
11203 #[cfg(target_arch = "wasm32")]
11204 fn prepare_wasm_viewport_uniforms(&mut self, params: ViewportUniformParams) -> usize {
11205 let slot = self.claim_wasm_uniform_batch();
11206 let uniforms = Self::viewport_uniforms(params);
11207 let bytes = bytemuck::bytes_of(&uniforms);
11208 let upload_stats = self.frame_graph_executor.upload_buffer(
11209 &self.queue,
11210 &self.wasm_uniform_batches[slot].buffer,
11211 0,
11212 bytes,
11213 );
11214 self.frame_stats.record_command_stats(upload_stats);
11215 slot
11216 }
11217
11218 #[cfg(target_arch = "wasm32")]
11219 fn claim_wasm_uniform_batch(&mut self) -> usize {
11220 let slot = self.wasm_uniform_batch_cursor;
11221 self.wasm_uniform_batch_cursor += 1;
11222 while self.wasm_uniform_batches.len() <= slot {
11223 self.wasm_uniform_batches.push(UniformBatchBuffer::new(
11224 &self.device,
11225 &self.uniform_bind_group_layout,
11226 ));
11227 }
11228 slot
11229 }
11230
11231 #[cfg(target_arch = "wasm32")]
11232 fn claim_wasm_shape_batch(&mut self) -> usize {
11233 let slot = self.wasm_shape_batch_cursor;
11234 self.wasm_shape_batch_cursor += 1;
11235 while self.wasm_shape_batches.len() <= slot {
11236 self.wasm_shape_batches.push(ShapeBatchBuffers::new(
11237 &self.device,
11238 &self.shape_bind_group_layout,
11239 &self.identity_similarity_buffer,
11240 self.dummy_paint_buffer.as_ref(),
11241 self.shape_batch_limits,
11242 ));
11243 }
11244 slot
11245 }
11246
11247 #[cfg(target_arch = "wasm32")]
11248 fn claim_wasm_image_batch(&mut self) -> usize {
11249 let slot = self.wasm_image_batch_cursor;
11250 self.wasm_image_batch_cursor += 1;
11251 while self.wasm_image_batches.len() <= slot {
11252 self.wasm_image_batches
11253 .push(ImageBatchBuffers::new(&self.device));
11254 }
11255 slot
11256 }
11257
11258 #[cfg(target_arch = "wasm32")]
11259 fn write_wasm_buffer(&self, buffer: &wgpu::Buffer, bytes: &[u8]) {
11260 let upload_stats = self
11261 .frame_graph_executor
11262 .upload_buffer(&self.queue, buffer, 0, bytes);
11263 self.frame_stats.record_command_stats(upload_stats);
11264 }
11265
11266 fn take_staged_uploads(&mut self) -> StagedBufferUploads {
11267 let mut staged_uploads = std::mem::take(&mut self.staged_uploads);
11268 debug_assert!(
11269 staged_uploads.is_empty(),
11270 "renderer-owned staged uploads should be restored as empty scratch storage"
11271 );
11272 staged_uploads.clear();
11273 staged_uploads
11274 }
11275
11276 fn restore_staged_uploads(&mut self, mut staged_uploads: StagedBufferUploads) {
11277 staged_uploads.clear();
11278 self.staged_uploads = staged_uploads;
11279 }
11280
11281 #[cfg(not(target_arch = "wasm32"))]
11282 fn ensure_upload_buffer_capacity(&mut self, required_bytes: u64) {
11283 if required_bytes <= self.upload_buffer.size() {
11284 return;
11285 }
11286
11287 let new_size = required_bytes
11288 .next_power_of_two()
11289 .max(INITIAL_UPLOAD_BUFFER_BYTES);
11290 self.upload_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11291 label: Some("Frame Upload Buffer"),
11292 size: new_size,
11293 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
11294 mapped_at_creation: false,
11295 });
11296 }
11297
11298 fn flush_staged_uploads_at(
11299 &mut self,
11300 encoder: &mut wgpu::CommandEncoder,
11301 staged_uploads: &StagedBufferUploads,
11302 upload_buffer_offset: u64,
11303 ) {
11304 if staged_uploads.is_empty() {
11305 return;
11306 }
11307 debug_assert_eq!(
11308 upload_buffer_offset % wgpu::COPY_BUFFER_ALIGNMENT,
11309 0,
11310 "upload-buffer base offset must satisfy copy alignment"
11311 );
11312
11313 #[cfg(target_arch = "wasm32")]
11314 {
11315 let _ = upload_buffer_offset;
11316 let _ = encoder;
11317 debug_assert!(
11318 staged_uploads.is_empty(),
11319 "wasm draw uploads use retained per-batch resource slots"
11320 );
11321 return;
11322 }
11323
11324 #[cfg(not(target_arch = "wasm32"))]
11325 {
11326 self.ensure_upload_buffer_capacity(
11327 upload_buffer_offset + staged_uploads.bytes.len() as u64,
11328 );
11329 let upload_stats = self.frame_graph_executor.upload_buffer(
11330 &self.queue,
11331 &self.upload_buffer,
11332 upload_buffer_offset,
11333 &staged_uploads.bytes,
11334 );
11335 self.frame_stats.record_command_stats(upload_stats);
11336
11337 for copy in &staged_uploads.copies {
11338 let target_buffer = match copy.target {
11339 UploadTarget::Uniform => &self.uniform_buffer,
11340 UploadTarget::ShapeData => &self.shape_buffers.shape_buffer,
11341 UploadTarget::ShapeGradient => &self.shape_buffers.gradient_buffer,
11342 UploadTarget::ImageVertex => &self.image_vertex_buffer,
11343 UploadTarget::ImageIndex => &self.image_index_buffer,
11344 UploadTarget::RetainedGlyphUniform => &self.retained_glyph_uniform_buffer,
11345 UploadTarget::ReplayTransform => &self.replay_slots.transform_buffer,
11346 UploadTarget::ReplayPaintData(slot) => {
11347 let Some(entry) = self.replay_slots.slots.get(&slot) else {
11350 continue;
11351 };
11352 &entry.paint_buffer
11353 }
11354 };
11355 encoder.copy_buffer_to_buffer(
11356 &self.upload_buffer,
11357 upload_buffer_offset + copy.source_offset,
11358 target_buffer,
11359 copy.target_offset,
11360 copy.size,
11361 );
11362 }
11363 }
11364 }
11365
11366 #[allow(clippy::too_many_arguments)]
11367 fn encode_shadow_draw<C: FrameCommandRecorder>(
11368 &mut self,
11369 frame_encoder: &mut C,
11370 target_view: &wgpu::TextureView,
11371 shadow: &ShadowDraw,
11372 width: u32,
11373 height: u32,
11374 root_scale: f32,
11375 ) {
11376 if shadow.shapes.is_empty() && shadow.texts.is_empty() {
11377 return;
11378 }
11379
11380 let shape_bounds_opt = shadow
11381 .shapes
11382 .iter()
11383 .map(|(shape, _)| shape.rect)
11384 .reduce(|a, b| Rect {
11385 x: a.x.min(b.x),
11386 y: a.y.min(b.y),
11387 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
11388 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
11389 });
11390
11391 let text_bounds_opt = shadow
11392 .texts
11393 .iter()
11394 .map(|text| text.rect)
11395 .reduce(|a, b| Rect {
11396 x: a.x.min(b.x),
11397 y: a.y.min(b.y),
11398 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
11399 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
11400 });
11401
11402 let combined_bounds = match (shape_bounds_opt, text_bounds_opt) {
11403 (Some(s), Some(t)) => Some(Rect {
11404 x: s.x.min(t.x),
11405 y: s.y.min(t.y),
11406 width: (s.x + s.width).max(t.x + t.width) - s.x.min(t.x),
11407 height: (s.y + s.height).max(t.y + t.height) - s.y.min(t.y),
11408 }),
11409 (Some(s), None) => Some(s),
11410 (None, Some(t)) => Some(t),
11411 (None, None) => None,
11412 };
11413
11414 let Some(shape_bounds) = combined_bounds else {
11415 return;
11416 };
11417
11418 let blur_margin = blur_extent_margin(shadow.blur_radius);
11419 let source_blur_bounds = Rect {
11420 x: shape_bounds.x - blur_margin,
11421 y: shape_bounds.y - blur_margin,
11422 width: shape_bounds.width + blur_margin * 2.0,
11423 height: shape_bounds.height + blur_margin * 2.0,
11424 };
11425 let mut visible_blur_bounds = source_blur_bounds;
11426 if let Some(clip) = shadow.clip {
11427 let clip_expanded = Rect {
11428 x: clip.x - blur_margin,
11429 y: clip.y - blur_margin,
11430 width: clip.width + blur_margin * 2.0,
11431 height: clip.height + blur_margin * 2.0,
11432 };
11433 let Some(intersection) = visible_blur_bounds.intersect(clip_expanded) else {
11434 return;
11435 };
11436 visible_blur_bounds = intersection;
11437 }
11438 let processing_scissor =
11439 scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
11440 if processing_scissor.is_none() {
11441 return;
11442 }
11443
11444 if shadow.blur_radius <= 0.0 {
11446 for (shape, blend_mode) in &shadow.shapes {
11447 self.encode_shapes_pass(
11448 frame_encoder,
11449 target_view,
11450 std::iter::once(shape),
11451 &shadow.brushes,
11452 *blend_mode,
11453 width,
11454 height,
11455 root_scale,
11456 wgpu::LoadOp::Load,
11457 [0.0, 0.0],
11458 );
11459 frame_encoder.record_pass();
11460 }
11461 if !shadow.texts.is_empty() {
11462 let mut staged_uploads = self.take_staged_uploads();
11463 let viewport = ViewportUniformParams {
11464 width,
11465 height,
11466 offset: [0.0, 0.0],
11467 };
11468 match self.prepare_text_image_draw_cmds(
11469 shadow.texts.iter(),
11470 viewport,
11471 root_scale,
11472 &mut staged_uploads,
11473 ) {
11474 Ok(prepared_images) if !prepared_images.is_empty() => {
11475 let upload_offset = frame_encoder
11476 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11477 self.flush_staged_uploads_at(
11478 frame_encoder.encoder(),
11479 &staged_uploads,
11480 upload_offset,
11481 );
11482 let draw_result = {
11483 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11484 &wgpu::RenderPassDescriptor {
11485 label: Some("Zero Blur Shadow Text Image Pass"),
11486 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11487 view: target_view,
11488 resolve_target: None,
11489 depth_slice: None,
11490 ops: wgpu::Operations {
11491 load: wgpu::LoadOp::Load,
11492 store: wgpu::StoreOp::Store,
11493 },
11494 })],
11495 depth_stencil_attachment: None,
11496 timestamp_writes: None,
11497 occlusion_query_set: None,
11498 multiview_mask: None,
11499 },
11500 );
11501 self.draw_prepared_images(
11502 &mut render_pass,
11503 &prepared_images,
11504 BlendMode::SrcOver,
11505 )
11506 };
11507 self.scratch_image_cmds = prepared_images.into_cmds();
11508 if let Err(e) = draw_result {
11509 eprintln!("Failed to draw text for zero-blur shadow: {}", e);
11510 } else {
11511 self.frame_stats.bump_text();
11512 frame_encoder.record_pass();
11513 }
11514 }
11515 Ok(prepared_images) => {
11516 self.scratch_image_cmds = prepared_images.into_cmds();
11517 }
11518 Err(e) => {
11519 eprintln!("Failed to prepare text image for zero-blur shadow: {}", e);
11520 }
11521 }
11522 self.restore_staged_uploads(staged_uploads);
11523 }
11524 return;
11525 }
11526
11527 let Some(device_bounds) =
11529 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)
11530 else {
11531 return;
11532 };
11533 let bounds_x = device_bounds.x;
11534 let bounds_y = device_bounds.y;
11535 let bounds_w = device_bounds.width;
11536 let bounds_h = device_bounds.height;
11537 let pixel_radius = shadow.blur_radius * root_scale;
11538
11539 if shadow.texts.is_empty() && !shadow.shapes.is_empty() {
11540 if let Some(plan) = shape_shadow_surface_plan(
11541 &shadow.shapes,
11542 shadow.clip,
11543 shadow.blur_radius,
11544 width,
11545 height,
11546 root_scale,
11547 self.max_texture_dim(),
11548 ) {
11549 if self.encode_shape_only_blurred_shadow_draw(
11550 frame_encoder,
11551 target_view,
11552 shadow,
11553 plan.source_device_bounds,
11554 plan.pixel_radius,
11555 plan.processing_scissor,
11556 width,
11557 height,
11558 root_scale,
11559 ) {
11560 return;
11561 }
11562 }
11563 }
11564
11565 if !shadow.texts.is_empty() {
11566 self.frame_stats.record_shadow_text_blur_fallback();
11567 }
11568
11569 let device = self.device.clone();
11570 let source_descriptor =
11571 self.transient_offscreen_descriptor("Shadow Source", bounds_w, bounds_h);
11572 let source = frame_encoder.acquire_transient_offscreen(&device, source_descriptor);
11573 let viewport_offset = [bounds_x, bounds_y];
11574 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
11575 let source_outcome = self.encode_shadow_shape_source_passes(
11576 frame_encoder,
11577 &source.view,
11578 &shadow.shapes,
11579 &shadow.brushes,
11580 bounds_w,
11581 bounds_h,
11582 viewport_offset,
11583 root_scale,
11584 &mut next_load_op,
11585 );
11586 frame_encoder.record_passes(source_outcome.pass_count);
11587 let mut rendered_any = source_outcome.rendered_any;
11588
11589 if !shadow.texts.is_empty() {
11590 let mut shifted_texts = shadow.texts.clone();
11591 for text in &mut shifted_texts {
11592 text.rect.x -= viewport_offset[0] / root_scale;
11593 text.rect.y -= viewport_offset[1] / root_scale;
11594 if let Some(clip) = text.clip.as_mut() {
11595 clip.x -= viewport_offset[0] / root_scale;
11596 clip.y -= viewport_offset[1] / root_scale;
11597 }
11598 }
11599
11600 let mut staged_uploads = self.take_staged_uploads();
11601 let viewport = ViewportUniformParams {
11602 width: bounds_w,
11603 height: bounds_h,
11604 offset: [0.0, 0.0],
11605 };
11606 match self.prepare_text_image_draw_cmds(
11607 shifted_texts.iter(),
11608 viewport,
11609 root_scale,
11610 &mut staged_uploads,
11611 ) {
11612 Ok(prepared_images) if !prepared_images.is_empty() => {
11613 let upload_offset = frame_encoder
11614 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11615 self.flush_staged_uploads_at(
11616 frame_encoder.encoder(),
11617 &staged_uploads,
11618 upload_offset,
11619 );
11620 let draw_result = {
11621 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11622 &wgpu::RenderPassDescriptor {
11623 label: Some("Shadow Source Text Image Pass"),
11624 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11625 view: &source.view,
11626 resolve_target: None,
11627 depth_slice: None,
11628 ops: wgpu::Operations {
11629 load: next_load_op,
11630 store: wgpu::StoreOp::Store,
11631 },
11632 })],
11633 depth_stencil_attachment: None,
11634 timestamp_writes: None,
11635 occlusion_query_set: None,
11636 multiview_mask: None,
11637 },
11638 );
11639 self.draw_prepared_images(
11640 &mut render_pass,
11641 &prepared_images,
11642 BlendMode::SrcOver,
11643 )
11644 };
11645 self.scratch_image_cmds = prepared_images.into_cmds();
11646 if let Err(e) = draw_result {
11647 eprintln!("Failed to draw text for shadow: {}", e);
11648 } else {
11649 self.frame_stats.bump_text();
11650 frame_encoder.record_pass();
11651 rendered_any = true;
11652 }
11653 }
11654 Ok(prepared_images) => {
11655 self.scratch_image_cmds = prepared_images.into_cmds();
11656 }
11657 Err(e) => {
11658 eprintln!("Failed to prepare text image for shadow: {}", e);
11659 }
11660 }
11661 self.restore_staged_uploads(staged_uploads);
11662 }
11663
11664 if !rendered_any {
11665 frame_encoder.release_transient_offscreen(source_descriptor, source);
11666 return;
11667 }
11668
11669 let scratch_descriptor =
11670 self.transient_offscreen_descriptor("Shadow Blur Scratch", bounds_w, bounds_h);
11671 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
11672 {
11673 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
11674 frame_encoder,
11675 &device,
11676 &source,
11677 &scratch,
11678 &source.view,
11679 pixel_radius,
11680 pixel_radius,
11681 TileMode::Decal,
11682 None, );
11684 }
11685 frame_encoder.record_passes(2);
11686
11687 let clip_scissor = shadow
11688 .clip
11689 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
11690 let scissor = clip_scissor.or(processing_scissor);
11691 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
11692 mask.rect[0] -= viewport_offset[0];
11695 mask.rect[1] -= viewport_offset[1];
11696 mask
11697 });
11698 let dest_viewport = Some((
11699 viewport_offset[0],
11700 viewport_offset[1],
11701 bounds_w as f32,
11702 bounds_h as f32,
11703 ));
11704 {
11705 self.effect_renderer
11706 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
11707 frame_encoder,
11708 &device,
11709 &source,
11710 target_view,
11711 1.0,
11712 wgpu::LoadOp::Load,
11713 scissor,
11714 rounded_mask,
11715 BlendMode::SrcOver,
11716 dest_viewport,
11717 CompositeSampleMode::Linear,
11718 );
11719 }
11720 frame_encoder.record_pass();
11721 self.effect_renderer.record_blur_pass();
11722 self.effect_renderer.record_composite_pass();
11723 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
11724 frame_encoder.release_transient_offscreen(source_descriptor, source);
11725 }
11726
11727 #[allow(clippy::too_many_arguments)]
11728 fn encode_shadow_shape_source_passes<C: FrameCommandRecorder>(
11729 &mut self,
11730 frame_encoder: &mut C,
11731 source_view: &wgpu::TextureView,
11732 shapes: &[(DrawShape, BlendMode)],
11733 brushes: &[Brush],
11734 width: u32,
11735 height: u32,
11736 viewport_offset: [f32; 2],
11737 root_scale: f32,
11738 next_load_op: &mut wgpu::LoadOp<wgpu::Color>,
11739 ) -> ShadowSourceRenderOutcome {
11740 if shapes.is_empty() {
11741 return ShadowSourceRenderOutcome {
11742 rendered_any: false,
11743 pass_count: 0,
11744 };
11745 }
11746
11747 let mut staged_uploads = self.take_staged_uploads();
11748 let mut rendered_any = false;
11749 let mut pass_count = 0_u32;
11750 let mut start = 0usize;
11751 while start < shapes.len() {
11752 let blend_mode = supported_blend_mode(shapes[start].1);
11753 let mut end = start + 1;
11754 while end < shapes.len()
11755 && end - start < self.shape_batch_limits.max_shapes_per_batch
11756 && supported_blend_mode(shapes[end].1) == blend_mode
11757 {
11758 end += 1;
11759 }
11760
11761 staged_uploads.clear();
11762 let viewport = ViewportUniformParams {
11763 width,
11764 height,
11765 offset: viewport_offset,
11766 };
11767 let viewport_rect_logical = viewport_rect_in_logical(viewport, root_scale);
11768 let Some(prepared_shape) = self.prepare_shapes_batch(
11769 shapes[start..end]
11770 .iter()
11771 .map(|(shape, _blend_mode)| shape)
11772 .filter(|shape| match viewport_rect_logical {
11773 Some(rect) => shape_draw_is_visible_in_rect(shape, rect, root_scale),
11774 None => false,
11775 }),
11776 brushes,
11777 root_scale,
11778 viewport,
11779 &mut staged_uploads,
11780 ) else {
11781 start = end;
11782 continue;
11783 };
11784
11785 let upload_offset =
11786 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11787 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
11788
11789 {
11790 let mut render_pass =
11791 frame_encoder
11792 .encoder()
11793 .begin_render_pass(&wgpu::RenderPassDescriptor {
11794 label: Some("Shadow Source Shape Pass"),
11795 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11796 view: source_view,
11797 resolve_target: None,
11798 depth_slice: None,
11799 ops: wgpu::Operations {
11800 load: *next_load_op,
11801 store: wgpu::StoreOp::Store,
11802 },
11803 })],
11804 depth_stencil_attachment: None,
11805 timestamp_writes: None,
11806 occlusion_query_set: None,
11807 multiview_mask: None,
11808 });
11809 self.draw_prepared_shapes(
11810 &mut render_pass,
11811 blend_mode,
11812 prepared_shape,
11813 width,
11814 height,
11815 &[],
11816 );
11817 }
11818
11819 #[cfg(not(target_arch = "wasm32"))]
11820 {
11821 if fill_area_diag_enabled() {
11822 self.fill_area_diag
11828 .add_offscreen_target_fill(f64::from(width) * f64::from(height));
11829 }
11830 }
11831
11832 pass_count = pass_count.saturating_add(1);
11833 rendered_any = true;
11834 *next_load_op = wgpu::LoadOp::Load;
11835 start = end;
11836 }
11837
11838 self.restore_staged_uploads(staged_uploads);
11839 ShadowSourceRenderOutcome {
11840 rendered_any,
11841 pass_count,
11842 }
11843 }
11844
11845 #[allow(clippy::too_many_arguments)]
11846 fn encode_shape_only_blurred_shadow_draw<C: FrameCommandRecorder>(
11847 &mut self,
11848 frame_encoder: &mut C,
11849 target_view: &wgpu::TextureView,
11850 shadow: &ShadowDraw,
11851 device_bounds: DevicePixelBounds,
11852 pixel_radius: f32,
11853 processing_scissor: Option<(u32, u32, u32, u32)>,
11854 width: u32,
11855 height: u32,
11856 root_scale: f32,
11857 ) -> bool {
11858 let bounds_w = device_bounds.width;
11859 let bounds_h = device_bounds.height;
11860 let viewport_offset = [device_bounds.x, device_bounds.y];
11861 let cache_key = shape_shadow_surface_cache_key(
11862 &shadow.shapes,
11863 &shadow.brushes,
11864 device_bounds,
11865 pixel_radius,
11866 root_scale,
11867 );
11868
11869 if let Some(key) = cache_key {
11870 if let Some(cached) = self.cached_shadow_surface(&key) {
11871 self.frame_stats
11872 .record_shadow_shape_cache_hit(bounds_w, bounds_h);
11873 let clip_scissor = shadow
11874 .clip
11875 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
11876 let scissor = clip_scissor.or(processing_scissor);
11877 let rounded_mask =
11878 inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
11879 mask.rect[0] -= viewport_offset[0];
11880 mask.rect[1] -= viewport_offset[1];
11881 mask
11882 });
11883 let dest_viewport = Some((
11884 viewport_offset[0],
11885 viewport_offset[1],
11886 bounds_w as f32,
11887 bounds_h as f32,
11888 ));
11889 {
11890 self.effect_renderer
11891 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
11892 frame_encoder,
11893 &self.device,
11894 &cached,
11895 target_view,
11896 1.0,
11897 wgpu::LoadOp::Load,
11898 scissor,
11899 rounded_mask,
11900 BlendMode::SrcOver,
11901 dest_viewport,
11902 CompositeSampleMode::Nearest,
11903 );
11904 }
11905 frame_encoder.record_pass();
11906 self.effect_renderer.record_composite_pass();
11907 return true;
11908 }
11909 self.frame_stats
11910 .record_shadow_shape_cache_miss(bounds_w, bounds_h);
11911 self.frame_stats.maybe_print_shadow_shape_cache_miss(
11912 bounds_w,
11913 bounds_h,
11914 key.content_hash,
11915 pixel_radius,
11916 viewport_offset,
11917 shadow.shapes.len(),
11918 shadow.clip,
11919 );
11920 }
11921
11922 let device = self.device.clone();
11923 let source_descriptor =
11924 self.transient_offscreen_descriptor("Shape Shadow Source", bounds_w, bounds_h);
11925 let source_is_cacheable = cache_key.is_some();
11926 let source = if source_is_cacheable {
11927 self.acquire_retained_surface(bounds_w, bounds_h)
11928 } else {
11929 frame_encoder.acquire_transient_offscreen(&device, source_descriptor)
11930 };
11931 let scratch_descriptor =
11932 self.transient_offscreen_descriptor("Shape Shadow Blur Scratch", bounds_w, bounds_h);
11933 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
11934 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
11935 let source_outcome = self.encode_shadow_shape_source_passes(
11936 frame_encoder,
11937 &source.view,
11938 &shadow.shapes,
11939 &shadow.brushes,
11940 bounds_w,
11941 bounds_h,
11942 viewport_offset,
11943 root_scale,
11944 &mut next_load_op,
11945 );
11946 frame_encoder.record_passes(source_outcome.pass_count);
11947
11948 if !source_outcome.rendered_any {
11949 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
11950 if source_is_cacheable {
11951 self.defer_offscreen_release(source);
11952 } else {
11953 frame_encoder.release_transient_offscreen(source_descriptor, source);
11954 }
11955 return true;
11956 }
11957
11958 {
11959 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
11960 frame_encoder,
11961 &device,
11962 &source,
11963 &scratch,
11964 &source.view,
11965 pixel_radius,
11966 pixel_radius,
11967 TileMode::Decal,
11968 None,
11969 );
11970 }
11971 frame_encoder.record_passes(2);
11972
11973 let clip_scissor = shadow
11974 .clip
11975 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
11976 let scissor = clip_scissor.or(processing_scissor);
11977 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
11978 mask.rect[0] -= viewport_offset[0];
11979 mask.rect[1] -= viewport_offset[1];
11980 mask
11981 });
11982 let dest_viewport = Some((
11983 viewport_offset[0],
11984 viewport_offset[1],
11985 bounds_w as f32,
11986 bounds_h as f32,
11987 ));
11988 {
11989 self.effect_renderer
11990 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
11991 frame_encoder,
11992 &device,
11993 &source,
11994 target_view,
11995 1.0,
11996 wgpu::LoadOp::Load,
11997 scissor,
11998 rounded_mask,
11999 BlendMode::SrcOver,
12000 dest_viewport,
12001 CompositeSampleMode::Nearest,
12002 );
12003 }
12004 frame_encoder.record_pass();
12005
12006 self.effect_renderer.record_blur_pass();
12007 self.effect_renderer.record_composite_pass();
12008 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
12009 if let Some(key) = cache_key {
12010 self.insert_cached_shadow_surface(key, source);
12011 } else {
12012 frame_encoder.release_transient_offscreen(source_descriptor, source);
12013 }
12014 true
12015 }
12016
12017 fn prepare_shapes_batch<'a, I>(
12018 &mut self,
12019 layer_shapes: I,
12020 brushes: &[Brush],
12021 root_scale: f32,
12022 viewport: ViewportUniformParams,
12023 staged_uploads: &mut StagedBufferUploads,
12024 ) -> Option<PreparedShapeBatch>
12025 where
12026 I: Iterator<Item = &'a DrawShape>,
12027 {
12028 #[cfg(target_arch = "wasm32")]
12029 let _ = staged_uploads;
12030
12031 let shape_refs: Vec<&DrawShape> = layer_shapes
12036 .take(self.shape_batch_limits.max_shapes_per_batch)
12037 .collect();
12038 let shape_count = shape_refs.len();
12039 if shape_count == 0 {
12040 return None;
12041 }
12042
12043 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
12047 let mut total_gradient_stops = 0u32;
12048 gradient_offsets.push(0);
12049 for shape in &shape_refs {
12050 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
12051 gradient_offsets.push(total_gradient_stops);
12052 }
12053
12054 self.scratch_shape_data.clear();
12055 self.scratch_shape_data
12056 .resize(shape_count, ShapeData::zeroed());
12057 self.scratch_gradients.clear();
12058 self.scratch_gradients
12059 .resize(total_gradient_stops as usize, GradientStop::zeroed());
12060
12061 convert_shapes_into_outputs(
12062 &shape_refs,
12063 brushes,
12064 &gradient_offsets,
12065 root_scale,
12066 &mut self.scratch_shape_data,
12067 &mut self.scratch_gradients,
12068 );
12069 #[cfg(not(target_arch = "wasm32"))]
12070 {
12071 if fill_area_diag_enabled() {
12072 self.fill_area_diag
12073 .add_shape_quads(&self.scratch_shape_data, viewport);
12074 }
12075 }
12076
12077 #[cfg(not(target_arch = "wasm32"))]
12078 {
12079 self.shape_buffers.ensure_capacity(
12080 &self.device,
12081 &self.shape_bind_group_layout,
12082 &self.identity_similarity_buffer,
12083 self.dummy_paint_buffer.as_ref(),
12084 shape_count,
12085 self.scratch_gradients.len().max(1),
12086 );
12087 self.stage_viewport_uniforms(staged_uploads, viewport);
12088 staged_uploads.stage(
12089 UploadTarget::ShapeData,
12090 bytemuck::cast_slice(&self.scratch_shape_data),
12091 );
12092 if !self.scratch_gradients.is_empty() {
12093 staged_uploads.stage(
12094 UploadTarget::ShapeGradient,
12095 bytemuck::cast_slice(&self.scratch_gradients),
12096 );
12097 }
12098 }
12099
12100 #[cfg(target_arch = "wasm32")]
12101 let shape_slot = {
12102 let slot = self.claim_wasm_shape_batch();
12103 {
12104 let buffers = &mut self.wasm_shape_batches[slot];
12105 buffers.ensure_capacity(
12106 &self.device,
12107 &self.shape_bind_group_layout,
12108 &self.identity_similarity_buffer,
12109 self.dummy_paint_buffer.as_ref(),
12110 shape_count,
12111 self.scratch_gradients.len().max(1),
12112 );
12113 }
12114 let buffers = &self.wasm_shape_batches[slot];
12115 self.write_wasm_buffer(
12116 &buffers.shape_buffer,
12117 bytemuck::cast_slice(&self.scratch_shape_data),
12118 );
12119 if !self.scratch_gradients.is_empty() {
12120 self.write_wasm_buffer(
12121 &buffers.gradient_buffer,
12122 bytemuck::cast_slice(&self.scratch_gradients),
12123 );
12124 }
12125 slot
12126 };
12127
12128 #[cfg(target_arch = "wasm32")]
12129 let uniform_slot = self.prepare_wasm_viewport_uniforms(viewport);
12130
12131 Some(PreparedShapeBatch {
12132 vertex_start: 0,
12133 vertex_count: shape_count as u32 * 6,
12134 has_gradient: total_gradient_stops > 0,
12135 #[cfg(target_arch = "wasm32")]
12136 shape_slot,
12137 #[cfg(target_arch = "wasm32")]
12138 uniform_slot,
12139 })
12140 }
12141
12142 #[cfg(not(target_arch = "wasm32"))]
12149 fn prepare_shapes_batch_direct<'a, I, C: FrameCommandRecorder>(
12150 &mut self,
12151 frame_encoder: &mut C,
12152 layer_shapes: I,
12153 brushes: &[Brush],
12154 root_scale: f32,
12155 viewport: ViewportUniformParams,
12156 staged_uploads: &mut StagedBufferUploads,
12157 ) -> Option<(PreparedShapeBatch, u64)>
12158 where
12159 I: Iterator<Item = &'a DrawShape>,
12160 {
12161 let shape_refs: Vec<&DrawShape> = layer_shapes
12162 .take(self.shape_batch_limits.max_shapes_per_batch)
12163 .collect();
12164 let shape_count = shape_refs.len();
12165 if shape_count == 0 {
12166 return None;
12167 }
12168
12169 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
12170 let mut total_gradient_stops = 0u32;
12171 gradient_offsets.push(0);
12172 for shape in &shape_refs {
12173 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
12174 gradient_offsets.push(total_gradient_stops);
12175 }
12176
12177 self.shape_buffers.ensure_capacity(
12178 &self.device,
12179 &self.shape_bind_group_layout,
12180 &self.identity_similarity_buffer,
12181 self.dummy_paint_buffer.as_ref(),
12182 shape_count,
12183 (total_gradient_stops as usize).max(1),
12184 );
12185
12186 self.scratch_shape_data.clear();
12187 self.scratch_shape_data
12188 .resize(shape_count, ShapeData::zeroed());
12189 self.scratch_gradients.clear();
12190 self.scratch_gradients
12191 .resize(total_gradient_stops as usize, GradientStop::zeroed());
12192 convert_shapes_into_outputs(
12193 &shape_refs,
12194 brushes,
12195 &gradient_offsets,
12196 root_scale,
12197 &mut self.scratch_shape_data,
12198 &mut self.scratch_gradients,
12199 );
12200 if fill_area_diag_enabled() {
12201 self.fill_area_diag
12202 .add_shape_quads(&self.scratch_shape_data, viewport);
12203 }
12204
12205 let uniform_len = std::mem::size_of::<Uniforms>() as u64;
12212 let shape_len = (shape_count * std::mem::size_of::<ShapeData>()) as u64;
12213 let gradient_len = total_gradient_stops as u64 * std::mem::size_of::<GradientStop>() as u64;
12214 let total_len = uniform_len + shape_len + gradient_len;
12215 let upload_base = frame_encoder.allocate_staged_upload_bytes(total_len);
12216 self.ensure_upload_buffer_capacity(upload_base + total_len);
12217
12218 let shape_off = uniform_len;
12219 let gradient_off = shape_off + shape_len;
12220
12221 let uniforms = Self::viewport_uniforms(viewport);
12222 let mut upload_stats = self.frame_graph_executor.upload_buffer(
12223 &self.queue,
12224 &self.upload_buffer,
12225 upload_base,
12226 bytemuck::bytes_of(&uniforms),
12227 );
12228 upload_stats.upload_bytes += self
12229 .frame_graph_executor
12230 .upload_buffer(
12231 &self.queue,
12232 &self.upload_buffer,
12233 upload_base + shape_off,
12234 bytemuck::cast_slice(&self.scratch_shape_data),
12235 )
12236 .upload_bytes;
12237 if !self.scratch_gradients.is_empty() {
12238 upload_stats.upload_bytes += self
12239 .frame_graph_executor
12240 .upload_buffer(
12241 &self.queue,
12242 &self.upload_buffer,
12243 upload_base + gradient_off,
12244 bytemuck::cast_slice(&self.scratch_gradients),
12245 )
12246 .upload_bytes;
12247 }
12248 self.frame_stats.record_command_stats(upload_stats);
12249
12250 staged_uploads.record_upload_copy(UploadTarget::Uniform, 0, 0, uniform_len);
12251 staged_uploads.record_upload_copy(UploadTarget::ShapeData, shape_off, 0, shape_len);
12252 staged_uploads.record_upload_copy(
12253 UploadTarget::ShapeGradient,
12254 gradient_off,
12255 0,
12256 gradient_len,
12257 );
12258
12259 Some((
12260 PreparedShapeBatch {
12261 vertex_start: 0,
12262 vertex_count: shape_count as u32 * 6,
12263 has_gradient: total_gradient_stops > 0,
12264 },
12265 upload_base,
12266 ))
12267 }
12268
12269 pub(crate) fn replay_supported(&self) -> bool {
12275 #[cfg(target_arch = "wasm32")]
12279 {
12280 false
12281 }
12282 #[cfg(not(target_arch = "wasm32"))]
12283 {
12284 self.shape_batch_limits.storage
12285 }
12286 }
12287
12288 pub(crate) fn restore_replay_ack_confirmations(
12294 &mut self,
12295 confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
12296 ) {
12297 #[cfg(not(target_arch = "wasm32"))]
12298 {
12299 self.replay_ack_confirmations = confirmations;
12300 }
12301 #[cfg(target_arch = "wasm32")]
12302 let _ = confirmations;
12303 }
12304
12305 #[cfg(not(target_arch = "wasm32"))]
12308 pub(crate) fn surface_format(&self) -> wgpu::TextureFormat {
12309 self.surface_format
12310 }
12311
12312 #[cfg(not(target_arch = "wasm32"))]
12315 pub(crate) fn replay_ack_confirmations_capacity(&self) -> usize {
12316 self.replay_ack_confirmations.capacity()
12317 }
12318
12319 #[cfg(not(target_arch = "wasm32"))]
12331 pub(crate) fn take_replay_ack_early(
12332 &mut self,
12333 packet: &mut FramePacket,
12334 ) -> Option<(
12335 crate::frame_packet::ReplayAck,
12336 crate::frame_packet::ReplayFrameOps,
12337 )> {
12338 if packet.replay_preconsumed {
12339 return None;
12340 }
12341 let PacketRoot::Direct(root) = &packet.root else {
12342 return None;
12343 };
12344 let ops = std::mem::take(&mut packet.replay);
12345 let root_scale = packet.root_scale;
12346 let (ack, recycled) =
12347 self.consume_replay_ops(ops, &root.scene.shapes, &root.scene.brushes, root_scale);
12348 packet.replay_preconsumed = true;
12349 Some((ack, recycled))
12350 }
12351
12352 #[cfg(not(target_arch = "wasm32"))]
12367 fn consume_replay_ops(
12368 &mut self,
12369 mut ops: crate::frame_packet::ReplayFrameOps,
12370 shapes: &[DrawShape],
12371 brushes: &[Brush],
12372 root_scale: f32,
12373 ) -> (
12374 crate::frame_packet::ReplayAck,
12375 crate::frame_packet::ReplayFrameOps,
12376 ) {
12377 let acked_frame = ops.frame;
12381 if ops.generation < self.store_feed_generation {
12382 self.replay_generation_drops += 1;
12388 log::warn!(
12389 "[command-feed] dropping replay ops of generation {} against store \
12390 generation {} ({} captures, {} patches, {} releases; lifetime drops {})",
12391 ops.generation,
12392 self.store_feed_generation,
12393 ops.captures.len(),
12394 ops.color_patches.len(),
12395 ops.releases.len(),
12396 self.replay_generation_drops,
12397 );
12398 ops.captures.clear();
12399 ops.color_patches.clear();
12400 ops.releases.clear();
12401 return (
12402 crate::frame_packet::ReplayAck {
12403 generation: self.store_feed_generation,
12404 frame: acked_frame,
12405 confirmations: Vec::new(),
12406 },
12407 ops,
12408 );
12409 }
12410 if ops.generation > self.store_feed_generation {
12411 self.store_feed_generation = ops.generation;
12418 }
12419 let generation = ops.generation;
12420 for slot in ops.releases.drain(..) {
12423 self.release_replay_slot(slot);
12424 }
12425 let mut confirmations = std::mem::take(&mut self.replay_ack_confirmations);
12428 debug_assert!(confirmations.is_empty());
12429 let mut refs: Vec<&DrawShape> = Vec::new();
12433 for capture in ops.captures.drain(..) {
12434 if capture.frame != ops.frame {
12435 log::warn!(
12442 "[command-feed] dropping stale capture for slot {} of {:?} \
12443 (queued frame {}, ops frame {})",
12444 capture.key.1,
12445 capture.key.0,
12446 capture.frame,
12447 ops.frame,
12448 );
12449 continue;
12450 }
12451 let end = capture.shape_start + capture.shape_count;
12452 let Some(slice) = shapes.get(capture.shape_start..end) else {
12453 continue;
12454 };
12455 refs.clear();
12456 refs.extend(slice.iter());
12457 let Some(gpu_slot) = self.capture_replay_slot(&refs, brushes, root_scale) else {
12458 continue;
12459 };
12460 confirmations.push((capture.key, gpu_slot));
12461 }
12462 self.replay_color_patches.clear();
12470 std::mem::swap(&mut self.replay_color_patches, &mut ops.color_patches);
12471 (
12472 crate::frame_packet::ReplayAck {
12473 generation,
12474 frame: acked_frame,
12475 confirmations,
12476 },
12477 ops,
12478 )
12479 }
12480
12481 #[cfg(not(target_arch = "wasm32"))]
12486 pub(crate) fn replay_generation_drops(&self) -> u64 {
12487 self.replay_generation_drops
12488 }
12489
12490 #[cfg(not(target_arch = "wasm32"))]
12498 pub(crate) fn replay_ops_roundtrip_for_tests(&mut self, generation_skew: u64) -> usize {
12499 let generation = self.store_feed_generation.wrapping_add(generation_skew);
12500 let ops = crate::shape_replay::SHAPE_REPLAY
12501 .with(|state| state.borrow_mut().take_frame_ops(generation));
12502 let (ack, recycled) = self.consume_replay_ops(ops, &[], &[], 1.0);
12503 let confirmed = ack.confirmations.len();
12504 self.replay_ack_confirmations = crate::shape_replay::SHAPE_REPLAY
12505 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
12506 confirmed
12507 }
12508
12509 #[cfg(not(target_arch = "wasm32"))]
12516 fn stage_replay_patches(&mut self, staged_uploads: &mut StagedBufferUploads) {
12517 std::mem::swap(
12525 &mut self.replay_color_patches,
12526 &mut self.color_patch_scratch,
12527 );
12528 let total_patches = self.color_patch_scratch.len();
12529 if total_patches == 0 {
12530 self.replay_upload_stats.note_frame(0, 0, 0, 0, 0);
12531 return;
12532 }
12533
12534 #[derive(Clone, Copy)]
12540 struct DirtySpan {
12541 paint_min: u32,
12542 paint_max: u32,
12543 }
12544 const CLEAN: DirtySpan = DirtySpan {
12545 paint_min: u32::MAX,
12546 paint_max: 0,
12547 };
12548 let mut dirty: std::collections::HashMap<
12549 u32,
12550 DirtySpan,
12551 cranpose_ui_graphics::FxBuildHasher,
12552 > = std::collections::HashMap::default();
12553
12554 for patch in &self.color_patch_scratch {
12556 let Some(slot) = self.replay_slots.slots.get_mut(&patch.slot) else {
12557 continue;
12558 };
12559 let Some(paint) = slot.paint_mirror.get_mut(patch.shape_index as usize) else {
12560 continue;
12561 };
12562 *paint = patch.color;
12563 let span = dirty.entry(patch.slot).or_insert(CLEAN);
12564 span.paint_min = span.paint_min.min(patch.shape_index);
12565 span.paint_max = span.paint_max.max(patch.shape_index);
12566 }
12567
12568 let mut uploaded_records = 0u64;
12569 let mut uploaded_bytes = 0u64;
12570 let slots_touched = dirty.len() as u64;
12571 for (slot_id, span) in dirty {
12572 let Some(slot) = self.replay_slots.slots.get(&slot_id) else {
12573 continue;
12574 };
12575 if span.paint_min <= span.paint_max {
12576 let range = span.paint_min as usize..span.paint_max as usize + 1;
12577 uploaded_records += range.len() as u64;
12578 uploaded_bytes += (range.len() * std::mem::size_of::<[f32; 4]>()) as u64;
12579 staged_uploads.stage_at(
12580 UploadTarget::ReplayPaintData(slot_id),
12581 range.start as u64 * std::mem::size_of::<[f32; 4]>() as u64,
12582 bytemuck::cast_slice(&slot.paint_mirror[range]),
12583 );
12584 }
12585 }
12586 let ideal_bytes = total_patches as u64 * 16;
12589 self.replay_upload_stats.note_frame(
12590 total_patches as u64,
12591 slots_touched,
12592 uploaded_records,
12593 uploaded_bytes,
12594 ideal_bytes,
12595 );
12596 if cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG") {
12597 log::warn!(
12598 "[replay-upload] frame: {} patches -> {} records / {:.1} KB staged \
12599 across {} slots (color-only {:.1} KB)",
12600 total_patches,
12601 uploaded_records,
12602 uploaded_bytes as f64 / 1024.0,
12603 slots_touched,
12604 ideal_bytes as f64 / 1024.0,
12605 );
12606 }
12607 self.color_patch_scratch.clear();
12608 }
12609
12610 #[cfg(not(target_arch = "wasm32"))]
12613 pub(crate) fn capture_replay_slot(
12614 &mut self,
12615 shape_refs: &[&DrawShape],
12616 brushes: &[Brush],
12617 root_scale: f32,
12618 ) -> Option<u32> {
12619 if !self.shape_batch_limits.storage || shape_refs.is_empty() {
12620 return None;
12621 }
12622 let id = self.replay_slots.free_ids.pop()?;
12623 let shape_count = shape_refs.len();
12624
12625 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
12626 let mut total_gradient_stops = 0u32;
12627 gradient_offsets.push(0);
12628 for shape in shape_refs {
12629 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
12630 gradient_offsets.push(total_gradient_stops);
12631 }
12632
12633 let mut shape_data = std::mem::take(&mut self.replay_capture_shape_scratch);
12636 shape_data.clear();
12637 shape_data.resize(shape_count, ShapeData::zeroed());
12638 let mut gradients = std::mem::take(&mut self.replay_capture_gradient_scratch);
12639 gradients.clear();
12640 gradients.resize(
12641 (total_gradient_stops as usize).max(1),
12642 GradientStop::zeroed(),
12643 );
12644 convert_shapes_into_outputs(
12645 shape_refs,
12646 brushes,
12647 &gradient_offsets,
12648 root_scale,
12649 &mut shape_data,
12650 &mut gradients,
12651 );
12652
12653 let shape_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12654 label: Some("Replay Shape Buffer"),
12655 size: (std::mem::size_of::<ShapeData>() * shape_count) as u64,
12656 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
12657 mapped_at_creation: true,
12658 });
12659 shape_buffer
12660 .slice(..)
12661 .get_mapped_range_mut()
12662 .copy_from_slice(bytemuck::cast_slice(&shape_data));
12663 shape_buffer.unmap();
12664
12665 let gradient_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12666 label: Some("Replay Gradient Buffer"),
12667 size: (std::mem::size_of::<GradientStop>() * gradients.len()) as u64,
12668 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
12669 mapped_at_creation: true,
12670 });
12671 gradient_buffer
12672 .slice(..)
12673 .get_mapped_range_mut()
12674 .copy_from_slice(bytemuck::cast_slice(&gradients));
12675 gradient_buffer.unmap();
12676
12677 let mut mesh_fill_records: Option<Vec<FillDiagShapeRecord>> = None;
12681 let mut submitted_area_scale = 1.0f32;
12682 let mesh = if arc_mesh_enabled() {
12683 match build_arc_mesh_vertices(&shape_data, retained_mesh_min_px2()) {
12684 Some(build) => {
12685 let meshed_shapes = build.meshed_arcs + build.meshed_rims;
12686 let within_stretch_cap = build.meshed_stretches <= MESH_SLOT_MAX_STRETCHES;
12691 let cut = if build.quad_area > 0.0 {
12692 (1.0 - build.mesh_area / build.quad_area) * 100.0
12693 } else {
12694 0.0
12695 };
12696 log::warn!(
12703 "[arc-mesh] slot {id}: {} arcs + {} rims meshed ({} segs, \
12704 {} stretches), {} instanced; {} unique verts / {} indices; \
12705 quad_px {:.0} -> submit_px {:.0} (-{:.1}%)",
12706 build.meshed_arcs,
12707 build.meshed_rims,
12708 build.meshed_segments,
12709 build.meshed_stretches,
12710 build.passthrough,
12711 build.vertices.len(),
12712 build.indices.len(),
12713 build.quad_area,
12714 build.mesh_area,
12715 cut,
12716 );
12717 if !within_stretch_cap {
12718 log::warn!(
12719 "[arc-mesh] slot {id}: {} meshed stretches exceed the \
12720 {MESH_SLOT_MAX_STRETCHES}-stretch switch cap; slot stays instanced",
12721 build.meshed_stretches,
12722 );
12723 }
12724 let keep_mesh = meshed_shapes > 0 && within_stretch_cap;
12725 if keep_mesh && build.quad_area > 0.0 {
12726 submitted_area_scale =
12732 (build.mesh_area / build.quad_area).clamp(0.05, 1.0) as f32;
12733 }
12734 if keep_mesh && fill_area_diag_enabled() {
12735 mesh_fill_records = Some(fill_diag_capture_records(
12736 &shape_data,
12737 Some((&build.vertices, &build.indices, &build.index_prefix)),
12738 ));
12739 }
12740 keep_mesh.then(|| {
12743 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12744 label: Some("Replay Mesh Vertex Buffer"),
12745 size: (std::mem::size_of::<MeshVertex>() * build.vertices.len()) as u64,
12746 usage: wgpu::BufferUsages::VERTEX,
12747 mapped_at_creation: true,
12748 });
12749 vertex_buffer
12750 .slice(..)
12751 .get_mapped_range_mut()
12752 .copy_from_slice(bytemuck::cast_slice(&build.vertices));
12753 vertex_buffer.unmap();
12754 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12755 label: Some("Replay Mesh Index Buffer"),
12756 size: (std::mem::size_of::<u32>() * build.indices.len()) as u64,
12757 usage: wgpu::BufferUsages::INDEX,
12758 mapped_at_creation: true,
12759 });
12760 index_buffer
12761 .slice(..)
12762 .get_mapped_range_mut()
12763 .copy_from_slice(bytemuck::cast_slice(&build.indices));
12764 index_buffer.unmap();
12765 ReplaySlotMesh {
12766 vertex_buffer,
12767 index_buffer,
12768 index_prefix: build.index_prefix,
12769 meshed_arcs: build.meshed_arcs,
12770 meshed_rims: build.meshed_rims,
12771 passthrough: build.passthrough,
12772 }
12773 })
12774 }
12775 None => {
12776 log::warn!(
12777 "[arc-mesh] slot {id}: geometry byte budget overflowed for \
12778 {shape_count} shapes; whole slot stays instanced"
12779 );
12780 None
12781 }
12782 }
12783 } else {
12784 None
12785 };
12786
12787 let fill_diag_shapes = if fill_area_diag_enabled() {
12788 let records =
12789 mesh_fill_records.unwrap_or_else(|| fill_diag_capture_records(&shape_data, None));
12790 self.fill_area_diag.note_retained_capture(id, &records);
12793 records
12794 } else {
12795 Vec::new()
12796 };
12797
12798 let mut shape_aabbs = Vec::with_capacity(shape_count);
12803 let mut area_prefix = Vec::with_capacity(shape_count + 1);
12804 area_prefix.push(0.0f32);
12805 for shape in &shape_data {
12806 let corners = [
12807 [shape.quad01[0], shape.quad01[1]],
12808 [shape.quad01[2], shape.quad01[3]],
12809 [shape.quad23[0], shape.quad23[1]],
12810 [shape.quad23[2], shape.quad23[3]],
12811 ];
12812 let mut min_x = f32::INFINITY;
12813 let mut min_y = f32::INFINITY;
12814 let mut max_x = f32::NEG_INFINITY;
12815 let mut max_y = f32::NEG_INFINITY;
12816 for corner in corners {
12817 min_x = min_x.min(corner[0]);
12818 min_y = min_y.min(corner[1]);
12819 max_x = max_x.max(corner[0]);
12820 max_y = max_y.max(corner[1]);
12821 }
12822 shape_aabbs.push([min_x, min_y, max_x, max_y]);
12823 let ring = [corners[0], corners[1], corners[3], corners[2]];
12826 let mut doubled = 0.0f32;
12827 for i in 0..4 {
12828 let a = ring[i];
12829 let b = ring[(i + 1) % 4];
12830 doubled += a[0] * b[1] - b[0] * a[1];
12831 }
12832 let area = (doubled * 0.5).abs();
12833 let running = *area_prefix.last().expect("prefix seeded with 0.0");
12834 area_prefix.push(running + area);
12835 }
12836
12837 let paint: Vec<[f32; 4]> = shape_data.iter().map(|shape| shape.color).collect();
12840 let paint_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12841 label: Some("Replay Paint Buffer"),
12842 size: (std::mem::size_of::<[f32; 4]>() * shape_count) as u64,
12843 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
12844 mapped_at_creation: true,
12845 });
12846 paint_buffer
12847 .slice(..)
12848 .get_mapped_range_mut()
12849 .copy_from_slice(bytemuck::cast_slice(&paint));
12850 paint_buffer.unmap();
12851
12852 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
12853 label: Some("Replay Shape Bind Group"),
12854 layout: &self.shape_bind_group_layout,
12855 entries: &[
12856 wgpu::BindGroupEntry {
12857 binding: 0,
12858 resource: shape_buffer.as_entire_binding(),
12859 },
12860 wgpu::BindGroupEntry {
12861 binding: 1,
12862 resource: gradient_buffer.as_entire_binding(),
12863 },
12864 wgpu::BindGroupEntry {
12868 binding: 2,
12869 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
12870 buffer: &self.replay_slots.transform_buffer,
12871 offset: 0,
12872 size: Some(
12873 std::num::NonZeroU64::new(
12874 std::mem::size_of::<SimilarityTransform>() as u64
12875 )
12876 .expect("similarity transform is non-empty"),
12877 ),
12878 }),
12879 },
12880 wgpu::BindGroupEntry {
12881 binding: 3,
12882 resource: paint_buffer.as_entire_binding(),
12883 },
12884 ],
12885 });
12886
12887 let capture_epoch = self.replay_slots.next_capture_epoch;
12888 self.replay_slots.next_capture_epoch += 1;
12889 self.replay_slots.slots.insert(
12890 id,
12891 ReplaySlot {
12892 paint_buffer,
12893 bind_group,
12894 shape_count: shape_count as u32,
12895 paint_mirror: paint,
12896 mesh,
12897 capture_epoch,
12898 has_gradient: total_gradient_stops > 0,
12899 fill_diag_shapes,
12900 shape_aabbs,
12901 area_prefix,
12902 submitted_area_scale,
12903 },
12904 );
12905 self.replay_capture_shape_scratch = shape_data;
12908 self.replay_capture_gradient_scratch = gradients;
12909 Some(id)
12910 }
12911
12912 #[cfg(not(target_arch = "wasm32"))]
12914 pub(crate) fn release_replay_slot(&mut self, id: u32) {
12915 if self.replay_slots.slots.remove(&id).is_some() {
12916 self.replay_slots.free_ids.push(id);
12917 self.retained_bundle_cache.clear();
12923 self.segment_surfaces.drop_slot(id);
12926 }
12927 }
12928
12929 #[cfg(not(target_arch = "wasm32"))]
12933 #[doc(hidden)]
12934 pub fn segment_surface_stats(&self) -> (u64, u64, u64, u64, u64) {
12935 let stats = &self.segment_surfaces.stats;
12936 (
12937 stats.captures,
12938 stats.composites,
12939 stats.dirty_recaptures,
12940 stats.rejected_churn,
12941 stats.rejected_economics,
12942 )
12943 }
12944
12945 #[cfg(not(target_arch = "wasm32"))]
12948 #[doc(hidden)]
12949 pub fn instanced_quads_active(&self) -> bool {
12950 self.instanced_quads.is_some()
12951 }
12952
12953 #[cfg(not(target_arch = "wasm32"))]
12956 #[doc(hidden)]
12957 pub fn replay_slot_mesh_stats(&self) -> (usize, usize) {
12958 let meshed = self
12959 .replay_slots
12960 .slots
12961 .values()
12962 .filter(|slot| slot.mesh.is_some())
12963 .count();
12964 (meshed, self.replay_slots.slots.len())
12965 }
12966
12967 #[cfg(not(target_arch = "wasm32"))]
12971 #[doc(hidden)]
12972 pub fn replay_slot_mesh_engagement(&self) -> (usize, usize, usize) {
12973 self.replay_slots
12974 .slots
12975 .values()
12976 .filter_map(|slot| slot.mesh.as_ref())
12977 .fold((0, 0, 0), |(arcs, rims, passthrough), mesh| {
12978 (
12979 arcs + mesh.meshed_arcs,
12980 rims + mesh.meshed_rims,
12981 passthrough + mesh.passthrough,
12982 )
12983 })
12984 }
12985
12986 #[cfg(not(target_arch = "wasm32"))]
12992 #[allow(clippy::too_many_arguments)]
12993 fn plan_segment_surfaces(
12994 &mut self,
12995 segment_surfaces: &mut SegmentSurfaceCache,
12996 ordered_items: &[(usize, SegmentDrawItem)],
12997 chunk: &SegmentDrawChunkPlan,
12998 retained_draws: &[RetainedDraw],
12999 staged_uploads: &mut StagedBufferUploads,
13000 captures: &mut Vec<SegmentCaptureJob>,
13001 composites: &mut Vec<(usize, SegmentCompositePlan)>,
13002 ) {
13003 segment_surfaces.ensure_dirty_map(
13004 self.replay_color_patches
13005 .iter()
13006 .map(|patch| (patch.slot, patch.shape_index)),
13007 );
13008 let max_texture_dim = self.effect_renderer.max_texture_dim();
13009 for batch in chunk.iter() {
13010 let SegmentBatchPlan::Retained { start, end } = batch else {
13011 continue;
13012 };
13013 for (_, item) in &ordered_items[start..end] {
13014 let SegmentDrawItem::Retained(index) = item else {
13015 continue;
13016 };
13017 if (*index as u32) >= MAX_REPLAY_SLOTS {
13020 continue;
13021 }
13022 let Some(retained) = retained_draws.get(*index) else {
13023 continue;
13024 };
13025 let transform = retained.transform;
13026 let (key, capture_epoch, dirty) = {
13027 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
13028 continue;
13029 };
13030 let first = retained.first_shape.min(slot.shape_count);
13031 let last = retained
13032 .first_shape
13033 .saturating_add(retained.shape_count)
13034 .min(slot.shape_count);
13035 if first >= last {
13036 continue;
13037 }
13038 (
13039 SegmentSurfaceKey {
13040 slot: retained.slot,
13041 first_shape: first,
13042 shape_count: last - first,
13043 },
13044 slot.capture_epoch,
13045 segment_surfaces.range_dirty(retained.slot, first, last),
13046 )
13047 };
13048 let first = key.first_shape;
13049 let last = key.first_shape + key.shape_count;
13050 let slots = &self.replay_slots.slots;
13051 let decision =
13052 segment_surfaces.decide(key, capture_epoch, dirty, transform.scale, || {
13053 let slot = slots.get(&key.slot)?;
13054 plan_segment_capture_geometry(slot, first, last, transform, max_texture_dim)
13055 });
13056 let SegmentSurfaceDecision::Composite { capture } = decision else {
13057 continue;
13058 };
13059 if let Some(plan) = capture {
13060 let texture = segment_surfaces
13061 .take_texture_for_recapture(&key, &plan.rect)
13062 .unwrap_or_else(|| {
13063 let device = self.device.clone();
13064 self.effect_renderer.acquire_offscreen(
13065 &device,
13066 plan.rect.width,
13067 plan.rect.height,
13068 Some(&self.frame_stats),
13069 )
13070 });
13071 segment_surfaces.install_entry(
13072 key,
13073 capture_epoch,
13074 transform.center,
13075 transform.rot,
13076 transform.scale,
13077 plan.rect,
13078 texture,
13079 );
13080 staged_uploads.stage_at(
13084 UploadTarget::ReplayTransform,
13085 (MAX_REPLAY_SLOTS + plan.index) as u64 * REPLAY_TRANSFORM_STRIDE,
13086 bytemuck::bytes_of(&transform.with_retained_paint()),
13087 );
13088 let uniforms = Self::viewport_uniforms(ViewportUniformParams {
13095 width: plan.rect.width,
13096 height: plan.rect.height,
13097 offset: plan.rect.origin,
13098 });
13099 let device = self.device.clone();
13100 let capture_uniforms =
13101 segment_surfaces.capture_uniforms(&device, &self.uniform_bind_group_layout);
13102 let upload_stats = self.frame_graph_executor.upload_buffer(
13103 &self.queue,
13104 &capture_uniforms.buffer,
13105 plan.index as u64 * SEGMENT_CAPTURE_UNIFORM_STRIDE,
13106 bytemuck::bytes_of(&uniforms),
13107 );
13108 self.frame_stats.record_command_stats(upload_stats);
13109 captures.push(SegmentCaptureJob {
13110 key,
13111 first,
13112 last,
13113 capture_index: plan.index,
13114 });
13115 composites.push((
13119 *index,
13120 SegmentCompositePlan {
13121 key,
13122 dest_quad: segment_identity_quad(&plan.rect),
13123 inverse: segment_identity_inverse(&plan.rect),
13124 identity: true,
13125 },
13126 ));
13127 } else {
13128 let Some(entry) = segment_surfaces.entry(&key) else {
13129 continue;
13130 };
13131 let t_now =
13132 Affine2::from_similarity(transform.center, transform.rot, transform.scale);
13133 let t_cap =
13134 Affine2::from_similarity(entry.cap_center, entry.cap_rot, entry.cap_scale);
13135 let Some(cap_inverse) = t_cap.invert() else {
13136 segment_surfaces.remove(&key);
13137 continue;
13138 };
13139 let effective = t_now.compose(&cap_inverse);
13140 let rect = entry.rect;
13141 let plan = if effective.is_identity_for_sampling() {
13142 SegmentCompositePlan {
13145 key,
13146 dest_quad: segment_identity_quad(&rect),
13147 inverse: segment_identity_inverse(&rect),
13148 identity: true,
13149 }
13150 } else {
13151 let Some(inverse) = effective.invert() else {
13152 segment_surfaces.remove(&key);
13153 continue;
13154 };
13155 SegmentCompositePlan {
13156 key,
13157 dest_quad: segment_identity_quad(&rect).map(|c| effective.apply(c)),
13158 inverse: [
13159 [
13160 inverse.l[0][0],
13161 inverse.l[0][1],
13162 inverse.t[0] - rect.origin[0],
13163 ],
13164 [
13165 inverse.l[1][0],
13166 inverse.l[1][1],
13167 inverse.t[1] - rect.origin[1],
13168 ],
13169 [0.0, 0.0, 1.0],
13170 ],
13171 identity: false,
13172 }
13173 };
13174 composites.push((*index, plan));
13175 }
13176 }
13177 }
13178 }
13179
13180 #[cfg(not(target_arch = "wasm32"))]
13184 fn draw_retained_batch(
13185 &self,
13186 render_pass: &mut wgpu::RenderPass<'_>,
13187 retained: &RetainedDraw,
13188 retained_index: usize,
13189 width: u32,
13190 height: u32,
13191 ) {
13192 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
13193 return;
13194 };
13195 if retained_index as u32 >= MAX_REPLAY_SLOTS {
13196 return;
13197 }
13198 let first = retained.first_shape.min(slot.shape_count);
13199 let last = retained
13200 .first_shape
13201 .saturating_add(retained.shape_count)
13202 .min(slot.shape_count);
13203 if first >= last {
13204 return;
13205 }
13206 if fill_area_diag_enabled() {
13207 self.fill_area_diag.add_retained_range(
13208 &slot.fill_diag_shapes,
13209 first,
13210 last,
13211 &retained.transform,
13212 );
13213 }
13214 self.frame_stats.bump_shapes();
13215 render_pass.set_scissor_rect(0, 0, width, height);
13216 let draws =
13217 self.encode_retained_op(
13218 slot,
13219 first,
13220 last,
13221 retained_index as u32,
13222 &mut |cmd| match cmd {
13223 RetainedCmd::Pipeline(pipeline) => render_pass.set_pipeline(pipeline),
13224 RetainedCmd::Uniforms(group) => render_pass.set_bind_group(0, group, &[]),
13225 RetainedCmd::SlotBindings(group, offset) => {
13226 render_pass.set_bind_group(1, group, &[offset])
13227 }
13228 RetainedCmd::MeshVertices(buffer) => {
13229 render_pass.set_vertex_buffer(0, buffer.slice(..))
13230 }
13231 RetainedCmd::Index(buffer, format) => {
13232 render_pass.set_index_buffer(buffer.slice(..), format)
13233 }
13234 RetainedCmd::Draw(vertices) => render_pass.draw(vertices, 0..1),
13235 RetainedCmd::DrawIndexed(indices, instances) => {
13236 render_pass.draw_indexed(indices, 0, instances)
13237 }
13238 },
13239 );
13240 self.frame_stats.add_draw_calls(draws);
13241 }
13242
13243 #[cfg(not(target_arch = "wasm32"))]
13269 fn encode_retained_op<'r>(
13270 &'r self,
13271 slot: &'r ReplaySlot,
13272 first: u32,
13273 last: u32,
13274 retained_index: u32,
13275 sink: &mut impl FnMut(RetainedCmd<'r>),
13276 ) -> u32 {
13277 sink(RetainedCmd::Uniforms(&self.uniform_bind_group));
13278 sink(RetainedCmd::SlotBindings(
13279 &slot.bind_group,
13280 retained_index * REPLAY_TRANSFORM_STRIDE as u32,
13281 ));
13282 let Some(mesh) = slot.mesh.as_ref() else {
13283 self.encode_retained_instanced(slot, first..last, sink);
13284 return 1;
13285 };
13286 sink(RetainedCmd::MeshVertices(&mesh.vertex_buffer));
13287 let prefix = &mesh.index_prefix;
13288 let meshed_at = |shape: u32| prefix[shape as usize + 1] > prefix[shape as usize];
13289 let mut draws = 0;
13290 let mut cursor = first;
13291 while cursor < last {
13292 let run_meshed = meshed_at(cursor);
13293 let mut end = cursor + 1;
13294 while end < last && meshed_at(end) == run_meshed {
13295 end += 1;
13296 }
13297 if run_meshed {
13298 sink(RetainedCmd::Pipeline(self.mesh_pipeline()));
13299 sink(RetainedCmd::Index(
13300 &mesh.index_buffer,
13301 wgpu::IndexFormat::Uint32,
13302 ));
13303 sink(RetainedCmd::DrawIndexed(
13304 prefix[cursor as usize]..prefix[end as usize],
13305 0..1,
13306 ));
13307 } else {
13308 self.encode_retained_instanced(slot, cursor..end, sink);
13309 }
13310 draws += 1;
13311 cursor = end;
13312 }
13313 draws
13314 }
13315
13316 #[cfg(not(target_arch = "wasm32"))]
13322 fn encode_retained_instanced<'r>(
13323 &'r self,
13324 slot: &ReplaySlot,
13325 range: Range<u32>,
13326 sink: &mut impl FnMut(RetainedCmd<'r>),
13327 ) {
13328 match &self.instanced_quads {
13329 Some(instanced) => {
13330 if slot.has_gradient {
13331 sink(RetainedCmd::Pipeline(
13332 self.instanced_pipeline(instanced, BlendMode::SrcOver),
13333 ));
13334 } else {
13335 sink(RetainedCmd::Pipeline(
13336 self.instanced_pipeline_solid(instanced),
13337 ));
13338 }
13339 sink(RetainedCmd::Index(
13340 &instanced.index_buffer,
13341 wgpu::IndexFormat::Uint16,
13342 ));
13343 sink(RetainedCmd::DrawIndexed(0..6, range));
13344 }
13345 None => {
13346 if slot.has_gradient {
13347 sink(RetainedCmd::Pipeline(
13348 self.shape_pipeline(BlendMode::SrcOver),
13349 ));
13350 } else {
13351 sink(RetainedCmd::Pipeline(self.shape_pipeline_solid()));
13352 }
13353 sink(RetainedCmd::Draw(range.start * 6..range.end * 6));
13354 }
13355 }
13356 }
13357
13358 #[cfg(not(target_arch = "wasm32"))]
13366 fn retained_bundle_key(
13367 &self,
13368 ordered_items: &[(usize, SegmentDrawItem)],
13369 retained_draws: &[RetainedDraw],
13370 item_range: Range<usize>,
13371 ) -> RetainedBundleKey {
13372 let mut ops = Vec::with_capacity(item_range.len());
13373 for (_, item) in &ordered_items[item_range] {
13374 let SegmentDrawItem::Retained(index) = item else {
13375 continue;
13376 };
13377 let Some(retained) = retained_draws.get(*index) else {
13378 continue;
13379 };
13380 let slot = self.replay_slots.slots.get(&retained.slot);
13381 let (first, last) = match slot {
13382 Some(slot) => (
13383 retained.first_shape.min(slot.shape_count),
13384 retained
13385 .first_shape
13386 .saturating_add(retained.shape_count)
13387 .min(slot.shape_count),
13388 ),
13389 None => (
13390 retained.first_shape,
13391 retained.first_shape.saturating_add(retained.shape_count),
13392 ),
13393 };
13394 ops.push(RetainedBundleOpKey {
13395 slot: retained.slot,
13396 capture_epoch: slot.map(|slot| slot.capture_epoch),
13397 first,
13398 last,
13399 retained_index: *index as u32,
13400 has_mesh: slot.is_some_and(|slot| slot.mesh.is_some())
13401 && self.shape_batch_limits.storage,
13402 });
13403 }
13404 RetainedBundleKey {
13405 depth: self.pass_depth(),
13406 ops,
13407 }
13408 }
13409
13410 #[cfg(not(target_arch = "wasm32"))]
13417 fn build_retained_bundle(&self, key: &RetainedBundleKey) -> wgpu::RenderBundle {
13418 let mut encoder =
13419 self.device
13420 .create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
13421 label: Some("Retained Stretch Bundle"),
13422 color_formats: &[Some(self.surface_format)],
13426 depth_stencil: key.depth.then_some(wgpu::RenderBundleDepthStencil {
13431 format: display_clip::DISPLAY_CLIP_DEPTH_FORMAT,
13432 depth_read_only: true,
13433 stencil_read_only: true,
13434 }),
13435 sample_count: 1,
13436 multiview: None,
13437 });
13438 for op in &key.ops {
13439 if op.capture_epoch.is_none()
13440 || op.retained_index >= MAX_REPLAY_SLOTS
13441 || op.first >= op.last
13442 {
13443 continue;
13444 }
13445 let Some(slot) = self.replay_slots.slots.get(&op.slot) else {
13446 continue;
13447 };
13448 self.encode_retained_op(
13453 slot,
13454 op.first,
13455 op.last,
13456 op.retained_index,
13457 &mut |cmd| match cmd {
13458 RetainedCmd::Pipeline(pipeline) => encoder.set_pipeline(pipeline),
13459 RetainedCmd::Uniforms(group) => encoder.set_bind_group(0, group, &[]),
13460 RetainedCmd::SlotBindings(group, offset) => {
13461 encoder.set_bind_group(1, group, &[offset])
13462 }
13463 RetainedCmd::MeshVertices(buffer) => {
13464 encoder.set_vertex_buffer(0, buffer.slice(..))
13465 }
13466 RetainedCmd::Index(buffer, format) => {
13467 encoder.set_index_buffer(buffer.slice(..), format)
13468 }
13469 RetainedCmd::Draw(vertices) => encoder.draw(vertices, 0..1),
13470 RetainedCmd::DrawIndexed(indices, instances) => {
13471 encoder.draw_indexed(indices, 0, instances)
13472 }
13473 },
13474 );
13475 }
13476 encoder.finish(&wgpu::RenderBundleDescriptor {
13477 label: Some("Retained Stretch Bundle"),
13478 })
13479 }
13480
13481 #[cfg(not(target_arch = "wasm32"))]
13489 fn draw_retained_stretch_bundled(
13490 &mut self,
13491 render_pass: &mut wgpu::RenderPass<'_>,
13492 ordered_items: &[(usize, SegmentDrawItem)],
13493 retained_draws: &[RetainedDraw],
13494 item_range: Range<usize>,
13495 width: u32,
13496 height: u32,
13497 ) {
13498 let key = self.retained_bundle_key(ordered_items, retained_draws, item_range);
13499 if !self.retained_bundle_cache.hit(&key) {
13500 let bundle = self.build_retained_bundle(&key);
13501 self.retained_bundle_cache.insert(key.clone(), bundle);
13502 }
13503 for op in &key.ops {
13506 if op.capture_epoch.is_some()
13507 && op.retained_index < MAX_REPLAY_SLOTS
13508 && op.first < op.last
13509 {
13510 self.frame_stats.bump_shapes();
13511 self.frame_stats.add_draw_calls(1);
13512 if fill_area_diag_enabled() {
13513 let slot = self.replay_slots.slots.get(&op.slot);
13515 let retained = retained_draws.get(op.retained_index as usize);
13516 if let (Some(slot), Some(retained)) = (slot, retained) {
13517 self.fill_area_diag.add_retained_range(
13518 &slot.fill_diag_shapes,
13519 op.first,
13520 op.last,
13521 &retained.transform,
13522 );
13523 }
13524 }
13525 }
13526 }
13527 render_pass.set_scissor_rect(0, 0, width, height);
13532 if let Some(bundle) = self.retained_bundle_cache.get(&key) {
13533 render_pass.execute_bundles(std::iter::once(bundle));
13534 }
13535 }
13536
13537 #[cfg(not(target_arch = "wasm32"))]
13540 #[doc(hidden)]
13541 pub fn retained_bundle_stats(&self) -> (u64, u64) {
13542 self.retained_bundle_cache.stats()
13543 }
13544
13545 #[cfg(not(target_arch = "wasm32"))]
13548 #[doc(hidden)]
13549 pub fn rim_meshes_emitted(&self) -> u64 {
13550 self.rim_meshes_emitted
13551 }
13552
13553 #[doc(hidden)]
13557 pub fn device_error_count(&self) -> u64 {
13558 self.device_errors.error_count()
13559 }
13560
13561 pub fn static_span_stats(&self) -> (u64, u64) {
13564 (self.static_span.hits, self.static_span.recaptures)
13565 }
13566
13567 #[cfg(not(target_arch = "wasm32"))]
13574 fn upload_transient_rim_meshes(&mut self) {
13575 let device = self.device.clone();
13576 let mut upload_stats = crate::frame_graph::FrameCommandStats::default();
13577 if self.rim_mesh_vertices.len() > self.rim_mesh_uploaded_vertices {
13578 let vertex_buffer = self.rim_mesh_vertex_buffer.get_or_insert_with(|| {
13579 device.create_buffer(&wgpu::BufferDescriptor {
13580 label: Some("Rim Mesh Vertex Buffer"),
13581 size: (RIM_MESH_VERTEX_CAPACITY * std::mem::size_of::<MeshVertex>()) as u64,
13582 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
13583 mapped_at_creation: false,
13584 })
13585 });
13586 upload_stats.upload_bytes += self
13587 .frame_graph_executor
13588 .upload_buffer(
13589 &self.queue,
13590 vertex_buffer,
13591 (self.rim_mesh_uploaded_vertices * std::mem::size_of::<MeshVertex>()) as u64,
13592 bytemuck::cast_slice(
13593 &self.rim_mesh_vertices[self.rim_mesh_uploaded_vertices..],
13594 ),
13595 )
13596 .upload_bytes;
13597 self.rim_mesh_uploaded_vertices = self.rim_mesh_vertices.len();
13598 }
13599 if self.rim_mesh_indices.len() > self.rim_mesh_uploaded_indices {
13600 let index_buffer = self.rim_mesh_index_buffer.get_or_insert_with(|| {
13601 device.create_buffer(&wgpu::BufferDescriptor {
13602 label: Some("Rim Mesh Index Buffer"),
13603 size: (RIM_MESH_INDEX_CAPACITY * std::mem::size_of::<u32>()) as u64,
13604 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
13605 mapped_at_creation: false,
13606 })
13607 });
13608 upload_stats.upload_bytes += self
13609 .frame_graph_executor
13610 .upload_buffer(
13611 &self.queue,
13612 index_buffer,
13613 (self.rim_mesh_uploaded_indices * std::mem::size_of::<u32>()) as u64,
13614 bytemuck::cast_slice(&self.rim_mesh_indices[self.rim_mesh_uploaded_indices..]),
13615 )
13616 .upload_bytes;
13617 self.rim_mesh_uploaded_indices = self.rim_mesh_indices.len();
13618 }
13619 if upload_stats.upload_bytes > 0 {
13620 self.frame_stats.record_command_stats(upload_stats);
13621 }
13622 }
13623
13624 fn draw_prepared_shapes(
13625 &self,
13626 render_pass: &mut wgpu::RenderPass<'_>,
13627 blend_mode: BlendMode,
13628 batch: PreparedShapeBatch,
13629 width: u32,
13630 height: u32,
13631 rims: &[RimDraw],
13632 ) {
13633 #[cfg(target_arch = "wasm32")]
13634 let _ = rims;
13635 self.frame_stats.bump_shapes();
13636 self.frame_stats.add_draw_calls(1);
13637 render_pass.set_scissor_rect(0, 0, width, height);
13638 #[cfg(not(target_arch = "wasm32"))]
13639 let (uniform_bind_group, shape_buffers) = (&self.uniform_bind_group, &self.shape_buffers);
13640 #[cfg(target_arch = "wasm32")]
13641 let (uniform_bind_group, shape_buffers) = (
13642 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
13643 &self.wasm_shape_batches[batch.shape_slot],
13644 );
13645 #[cfg(not(target_arch = "wasm32"))]
13650 if let Some(instanced) = &self.instanced_quads {
13651 assert!(
13652 batch.vertex_start.is_multiple_of(6) && batch.vertex_count.is_multiple_of(6),
13653 "shape batches are whole shapes: vertex range {}..+{} must be \
13654 six-aligned to convert to an instance range",
13655 batch.vertex_start,
13656 batch.vertex_count,
13657 );
13658 let set_instanced_pipeline = |render_pass: &mut wgpu::RenderPass<'_>| {
13661 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
13662 render_pass.set_pipeline(self.instanced_pipeline_solid(instanced));
13663 } else {
13664 render_pass.set_pipeline(self.instanced_pipeline(instanced, blend_mode));
13665 }
13666 };
13667 set_instanced_pipeline(render_pass);
13668 render_pass.set_bind_group(0, uniform_bind_group, &[]);
13669 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
13672 let first_shape = batch.vertex_start / 6;
13673 let shape_count = batch.vertex_count / 6;
13674 render_pass
13675 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
13676 debug_assert!(
13680 rims.windows(2)
13681 .all(|pair| pair[0].shape_index < pair[1].shape_index),
13682 "rim draws must arrive in ascending shape order"
13683 );
13684 let rim_start = rims.partition_point(|rim| rim.shape_index < first_shape);
13685 let rim_end = rims.partition_point(|rim| rim.shape_index < first_shape + shape_count);
13686 let batch_rims = &rims[rim_start..rim_end];
13687 let rim_buffers = match (&self.rim_mesh_vertex_buffer, &self.rim_mesh_index_buffer) {
13688 (Some(vertex_buffer), Some(index_buffer)) if !batch_rims.is_empty() => {
13689 Some((vertex_buffer, index_buffer))
13690 }
13691 _ => None,
13692 };
13693 let Some((rim_vertex_buffer, rim_index_buffer)) = rim_buffers else {
13694 render_pass.draw_indexed(0..6, 0, first_shape..first_shape + shape_count);
13695 return;
13696 };
13697 let mut draw_calls = 0u32;
13705 let mut cursor = first_shape;
13706 for rim in batch_rims {
13707 if cursor < rim.shape_index {
13708 render_pass.draw_indexed(0..6, 0, cursor..rim.shape_index);
13709 draw_calls += 1;
13710 }
13711 render_pass.set_pipeline(self.mesh_pipeline());
13712 render_pass.set_vertex_buffer(0, rim_vertex_buffer.slice(..));
13713 render_pass.set_index_buffer(rim_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
13714 render_pass.draw_indexed(
13715 rim.first_index..rim.first_index + rim.index_count,
13716 0,
13717 0..1,
13718 );
13719 draw_calls += 1;
13720 set_instanced_pipeline(render_pass);
13721 render_pass
13722 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
13723 cursor = rim.shape_index + 1;
13724 }
13725 if cursor < first_shape + shape_count {
13726 render_pass.draw_indexed(0..6, 0, cursor..first_shape + shape_count);
13727 draw_calls += 1;
13728 }
13729 self.frame_stats
13731 .add_draw_calls(draw_calls.saturating_sub(1));
13732 return;
13733 }
13734 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
13735 render_pass.set_pipeline(self.shape_pipeline_solid());
13736 } else {
13737 render_pass.set_pipeline(self.shape_pipeline(blend_mode));
13738 }
13739 render_pass.set_bind_group(0, uniform_bind_group, &[]);
13740 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
13743 render_pass.draw(
13746 batch.vertex_start..batch.vertex_start + batch.vertex_count,
13747 0..1,
13748 );
13749 }
13750
13751 #[allow(clippy::too_many_arguments)]
13754 fn encode_shapes_pass<'a, I, C: FrameCommandRecorder>(
13755 &mut self,
13756 frame_encoder: &mut C,
13757 target_view: &wgpu::TextureView,
13758 layer_shapes: I,
13759 brushes: &[Brush],
13760 blend_mode: BlendMode,
13761 width: u32,
13762 height: u32,
13763 root_scale: f32,
13764 load_op: wgpu::LoadOp<wgpu::Color>,
13765 viewport_offset: [f32; 2],
13766 ) where
13767 I: Iterator<Item = &'a DrawShape>,
13768 {
13769 let mut staged_uploads = self.take_staged_uploads();
13770 let viewport = ViewportUniformParams {
13771 width,
13772 height,
13773 offset: viewport_offset,
13774 };
13775 let viewport_rect_logical = viewport_rect_in_logical(viewport, root_scale);
13776 let Some(batch) = self.prepare_shapes_batch(
13777 layer_shapes.filter(|shape| match viewport_rect_logical {
13778 Some(rect) => shape_draw_is_visible_in_rect(shape, rect, root_scale),
13779 None => false,
13780 }),
13781 brushes,
13782 root_scale,
13783 viewport,
13784 &mut staged_uploads,
13785 ) else {
13786 self.restore_staged_uploads(staged_uploads);
13787 return;
13788 };
13789 let upload_offset =
13790 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
13791 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
13792 self.restore_staged_uploads(staged_uploads);
13793 let mut render_pass =
13794 frame_encoder
13795 .encoder()
13796 .begin_render_pass(&wgpu::RenderPassDescriptor {
13797 label: Some("Shape Pass"),
13798 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
13799 view: target_view,
13800 resolve_target: None,
13801 depth_slice: None,
13802 ops: wgpu::Operations {
13803 load: load_op,
13804 store: wgpu::StoreOp::Store,
13805 },
13806 })],
13807 depth_stencil_attachment: None,
13808 timestamp_writes: None,
13809 occlusion_query_set: None,
13810 multiview_mask: None,
13811 });
13812 self.draw_prepared_shapes(&mut render_pass, blend_mode, batch, width, height, &[]);
13813 }
13814
13815 fn draw_prepared_images(
13816 &mut self,
13817 render_pass: &mut wgpu::RenderPass<'_>,
13818 batch: &PreparedImageBatch,
13819 blend_mode: BlendMode,
13820 ) -> Result<(), String> {
13821 if batch.cmds.is_empty() {
13822 return Ok(());
13823 }
13824 self.frame_stats.bump_images();
13825 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
13826 render_pass.set_pipeline(self.image_pipeline(blend_mode));
13827 #[cfg(not(target_arch = "wasm32"))]
13828 let (uniform_bind_group, vertex_buffer, index_buffer) = (
13829 &self.uniform_bind_group,
13830 &self.image_vertex_buffer,
13831 &self.image_index_buffer,
13832 );
13833 #[cfg(target_arch = "wasm32")]
13834 let (uniform_bind_group, vertex_buffer, index_buffer) = (
13835 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
13836 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
13837 &self.wasm_image_batches[batch.image_slot].index_buffer,
13838 );
13839 render_pass.set_bind_group(0, uniform_bind_group, &[]);
13840 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
13841 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
13842
13843 for cmd in &batch.cmds {
13844 let (sx, sy, sw, sh) = cmd.scissor;
13845 render_pass.set_scissor_rect(sx, sy, sw, sh);
13846
13847 let cached = self
13848 .image_texture_cache
13849 .get(&cmd.image_id)
13850 .ok_or_else(|| "image texture missing from cache".to_string())?;
13851 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
13852 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
13853 }
13854 Ok(())
13855 }
13856
13857 fn draw_prepared_glyphs(
13858 &mut self,
13859 render_pass: &mut wgpu::RenderPass<'_>,
13860 batch: &PreparedGlyphBatch,
13861 ) -> Result<(), String> {
13862 if batch.cmds.is_empty() {
13863 return Ok(());
13864 }
13865 #[cfg(not(target_arch = "wasm32"))]
13866 {
13867 self.draw_native_prepared_glyph_cmd_range(
13868 render_pass,
13869 &batch.cmds,
13870 0..batch.cmds.len(),
13871 )?;
13872 }
13873 #[cfg(target_arch = "wasm32")]
13874 {
13875 self.frame_stats.bump_text();
13876 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
13877 render_pass.set_pipeline(self.glyph_atlas_pipeline());
13878 let (uniform_bind_group, vertex_buffer, index_buffer) = (
13879 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
13880 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
13881 &self.wasm_image_batches[batch.image_slot].index_buffer,
13882 );
13883 render_pass.set_bind_group(0, uniform_bind_group, &[]);
13884 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
13885 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
13886 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
13887
13888 for cmd in &batch.cmds {
13889 let (sx, sy, sw, sh) = cmd.scissor;
13890 render_pass.set_scissor_rect(sx, sy, sw, sh);
13891 let GlyphDrawSource::Shared {
13892 index_start,
13893 index_count,
13894 } = cmd.source;
13895 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
13896 }
13897 }
13898 Ok(())
13899 }
13900
13901 #[cfg(not(target_arch = "wasm32"))]
13902 fn draw_native_prepared_image_cmd_range(
13903 &mut self,
13904 render_pass: &mut wgpu::RenderPass<'_>,
13905 cmds: &[ImageDrawCmd],
13906 cmd_range: Range<usize>,
13907 blend_mode: BlendMode,
13908 ) -> Result<(), String> {
13909 let Some(cmds) = cmds.get(cmd_range) else {
13910 return Err("image command range is outside the prepared command buffer".to_string());
13911 };
13912 if cmds.is_empty() {
13913 return Ok(());
13914 }
13915
13916 self.frame_stats.bump_images();
13917 self.frame_stats.add_draw_calls(cmds.len() as u32);
13918 render_pass.set_pipeline(self.image_pipeline(blend_mode));
13919 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
13920 render_pass.set_index_buffer(self.image_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
13921 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
13922
13923 for cmd in cmds {
13924 let (sx, sy, sw, sh) = cmd.scissor;
13925 render_pass.set_scissor_rect(sx, sy, sw, sh);
13926
13927 let cached = self
13928 .image_texture_cache
13929 .get(&cmd.image_id)
13930 .ok_or_else(|| "image texture missing from cache".to_string())?;
13931 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
13932 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
13933 }
13934 Ok(())
13935 }
13936
13937 #[cfg(not(target_arch = "wasm32"))]
13938 fn draw_native_prepared_glyph_cmd_range(
13939 &mut self,
13940 render_pass: &mut wgpu::RenderPass<'_>,
13941 cmds: &[GlyphDrawCmd],
13942 cmd_range: Range<usize>,
13943 ) -> Result<(), String> {
13944 let Some(cmds) = cmds.get(cmd_range) else {
13945 return Err("glyph command range is outside the prepared command buffer".to_string());
13946 };
13947 if cmds.is_empty() {
13948 return Ok(());
13949 }
13950
13951 self.frame_stats.bump_text();
13952 self.frame_stats.add_draw_calls(cmds.len() as u32);
13953
13954 let mut shared_buffers_bound = false;
13955 let mut retained_pipeline_bound = false;
13956 for cmd in cmds {
13957 let (sx, sy, sw, sh) = cmd.scissor;
13958 render_pass.set_scissor_rect(sx, sy, sw, sh);
13959 match cmd.source {
13960 GlyphDrawSource::Shared {
13961 index_start,
13962 index_count,
13963 } => {
13964 if retained_pipeline_bound || !shared_buffers_bound {
13965 render_pass.set_pipeline(self.glyph_atlas_pipeline());
13966 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
13967 retained_pipeline_bound = false;
13968 }
13969 if !shared_buffers_bound {
13970 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
13971 render_pass.set_index_buffer(
13972 self.image_index_buffer.slice(..),
13973 wgpu::IndexFormat::Uint32,
13974 );
13975 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
13976 shared_buffers_bound = true;
13977 }
13978 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
13979 }
13980 GlyphDrawSource::Retained {
13981 cache_key,
13982 uniform_slot,
13983 } => {
13984 shared_buffers_bound = false;
13985 if !retained_pipeline_bound {
13986 render_pass.set_pipeline(self.retained_glyph_atlas_pipeline());
13987 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
13988 retained_pipeline_bound = true;
13989 }
13990 let cached = self
13991 .text_glyph_gpu_run_cache
13992 .peek(&cache_key)
13993 .ok_or_else(|| "retained glyph buffer missing from cache".to_string())?;
13994 let dynamic_offset =
13995 self.retained_glyph_uniform_dynamic_offset(uniform_slot)?;
13996 render_pass.set_bind_group(
13997 0,
13998 &self.retained_glyph_uniform_bind_group,
13999 &[dynamic_offset],
14000 );
14001 render_pass
14002 .set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
14003 render_pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
14004 render_pass.draw_indexed(0..cached.index_count, 0, 0..1);
14005 }
14006 }
14007 }
14008 Ok(())
14009 }
14010
14011 fn append_image_draw_cmd(
14012 &mut self,
14013 image_draw: &ImageDraw,
14014 viewport: ViewportUniformParams,
14015 root_scale: f32,
14016 image_vertices: &mut Vec<Vertex>,
14017 image_indices: &mut Vec<u32>,
14018 image_cmds: &mut Vec<ImageDrawCmd>,
14019 ) -> Result<(), String> {
14020 let snap_delta = image_draw
14021 .snap_anchor
14022 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
14023 .unwrap_or_default();
14024 let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
14025 if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
14026 return Ok(());
14027 }
14028
14029 let (tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
14030 if tint[3] <= 0.0 {
14031 return Ok(());
14032 }
14033
14034 let prepared_image = if let Some(filter) = cpu_filter {
14035 apply_filter_to_bitmap(&image_draw.image, filter)?
14036 } else {
14037 image_draw.image.clone()
14038 };
14039 self.ensure_image_cached(&prepared_image)?;
14040
14041 let mut adjusted_image = ImageDraw {
14042 rect,
14043 local_rect: image_draw.local_rect.translate(snap_delta.x, snap_delta.y),
14044 quad: translate_quad(image_draw.quad, snap_delta),
14045 snap_anchor: image_draw.snap_anchor,
14046 image: image_draw.image.clone(),
14047 alpha: image_draw.alpha,
14048 color_filter: image_draw.color_filter,
14049 sampling: image_draw.sampling,
14050 z_index: image_draw.z_index,
14051 clip: image_draw.clip,
14052 blend_mode: image_draw.blend_mode,
14053 src_rect: image_draw.src_rect,
14054 motion_context_animated: image_draw.motion_context_animated,
14055 };
14056 snap_nearest_image_to_device_pixels(&mut adjusted_image, root_scale);
14057 let Some(scissor) =
14058 scissor_rect_for_image(&adjusted_image, root_scale, viewport.width, viewport.height)
14059 else {
14060 return Ok(());
14061 };
14062
14063 let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
14064 return Ok(());
14065 };
14066 let device_quad =
14067 nearest_image_device_quad(&adjusted_image, root_scale).unwrap_or_else(|| {
14068 if adjusted_image.snap_anchor.is_some() {
14069 canonicalized_scaled_quad(adjusted_image.quad, root_scale)
14070 } else {
14071 scaled_quad(adjusted_image.quad, root_scale)
14072 }
14073 });
14074 #[cfg(not(target_arch = "wasm32"))]
14075 {
14076 if fill_area_diag_enabled() {
14077 self.fill_area_diag.add_image_quad(&device_quad);
14078 }
14079 }
14080
14081 let base_vertex = image_vertices.len() as u32;
14082 let index_start = image_indices.len() as u32;
14083 image_indices.extend_from_slice(&[
14084 base_vertex,
14085 base_vertex + 1,
14086 base_vertex + 2,
14087 base_vertex + 2,
14088 base_vertex + 1,
14089 base_vertex + 3,
14090 ]);
14091 image_vertices.extend_from_slice(&[
14092 Vertex {
14093 position: device_quad[0],
14094 color: tint,
14095 uv: [uv_rect.min[0], uv_rect.min[1]],
14096 uv_bounds: uv_rect.sample_bounds,
14097 },
14098 Vertex {
14099 position: device_quad[1],
14100 color: tint,
14101 uv: [uv_rect.max[0], uv_rect.min[1]],
14102 uv_bounds: uv_rect.sample_bounds,
14103 },
14104 Vertex {
14105 position: device_quad[2],
14106 color: tint,
14107 uv: [uv_rect.min[0], uv_rect.max[1]],
14108 uv_bounds: uv_rect.sample_bounds,
14109 },
14110 Vertex {
14111 position: device_quad[3],
14112 color: tint,
14113 uv: [uv_rect.max[0], uv_rect.max[1]],
14114 uv_bounds: uv_rect.sample_bounds,
14115 },
14116 ]);
14117
14118 image_cmds.push(ImageDrawCmd {
14119 index_start,
14120 scissor,
14121 image_id: prepared_image.id(),
14122 sampling: image_draw.sampling,
14123 });
14124 Ok(())
14125 }
14126
14127 #[cfg(not(target_arch = "wasm32"))]
14128 fn stage_native_image_buffers(
14129 &mut self,
14130 staged_uploads: &mut StagedBufferUploads,
14131 viewport: ViewportUniformParams,
14132 image_vertices: &[Vertex],
14133 image_indices: &[u32],
14134 ) {
14135 if image_indices.is_empty() {
14136 return;
14137 }
14138
14139 self.stage_viewport_uniforms(staged_uploads, viewport);
14140 let needed_bytes = std::mem::size_of_val(image_vertices) as u64;
14145 if needed_bytes > self.image_vertex_buffer.size() {
14146 self.image_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14147 label: Some("Image Vertex Buffer"),
14148 size: needed_bytes.next_power_of_two(),
14149 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
14150 mapped_at_creation: false,
14151 });
14152 }
14153 let needed_index_bytes = std::mem::size_of_val(image_indices) as u64;
14154 if needed_index_bytes > self.image_index_buffer.size() {
14155 self.image_index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14156 label: Some("Image Index Buffer"),
14157 size: needed_index_bytes.next_power_of_two(),
14158 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
14159 mapped_at_creation: false,
14160 });
14161 }
14162
14163 staged_uploads.stage(
14164 UploadTarget::ImageVertex,
14165 bytemuck::cast_slice(image_vertices),
14166 );
14167 staged_uploads.stage(
14168 UploadTarget::ImageIndex,
14169 bytemuck::cast_slice(image_indices),
14170 );
14171 }
14172
14173 fn prepare_image_draw_cmds<'a, I>(
14176 &mut self,
14177 layer_images: I,
14178 viewport: ViewportUniformParams,
14179 root_scale: f32,
14180 staged_uploads: &mut StagedBufferUploads,
14181 ) -> Result<PreparedImageBatch, String>
14182 where
14183 I: Iterator<Item = &'a ImageDraw>,
14184 {
14185 #[cfg(target_arch = "wasm32")]
14186 let _ = staged_uploads;
14187
14188 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
14189 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
14190 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
14191 image_vertices.clear();
14192 image_indices.clear();
14193 image_cmds.clear();
14194
14195 for image_draw in layer_images {
14196 self.append_image_draw_cmd(
14197 image_draw,
14198 viewport,
14199 root_scale,
14200 &mut image_vertices,
14201 &mut image_indices,
14202 &mut image_cmds,
14203 )?;
14204 }
14205
14206 #[cfg(not(target_arch = "wasm32"))]
14207 if !image_cmds.is_empty() {
14208 self.stage_native_image_buffers(
14209 staged_uploads,
14210 viewport,
14211 &image_vertices,
14212 &image_indices,
14213 );
14214 }
14215
14216 #[cfg(target_arch = "wasm32")]
14217 let image_slot = if image_cmds.is_empty() {
14218 0
14219 } else {
14220 let slot = self.claim_wasm_image_batch();
14221 {
14222 let buffers = &mut self.wasm_image_batches[slot];
14223 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
14224 }
14225 let buffers = &self.wasm_image_batches[slot];
14226 self.write_wasm_buffer(
14227 &buffers.vertex_buffer,
14228 bytemuck::cast_slice(&image_vertices),
14229 );
14230 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
14231 slot
14232 };
14233
14234 #[cfg(target_arch = "wasm32")]
14235 let uniform_slot = if image_cmds.is_empty() {
14236 0
14237 } else {
14238 self.prepare_wasm_viewport_uniforms(viewport)
14239 };
14240
14241 self.scratch_image_vertices = image_vertices;
14242 self.scratch_image_indices = image_indices;
14243 Ok(PreparedImageBatch {
14244 cmds: image_cmds,
14245 #[cfg(target_arch = "wasm32")]
14246 image_slot,
14247 #[cfg(target_arch = "wasm32")]
14248 uniform_slot,
14249 })
14250 }
14251
14252 fn glyph_atlas_entry_for(
14253 &mut self,
14254 glyph: &SoftwareGlyphAtlasGlyph,
14255 ) -> Result<GlyphAtlasEntry, String> {
14256 if let Some(entry) = self.text_glyph_atlas.upload_glyph(
14257 glyph.key,
14258 glyph,
14259 &self.queue,
14260 &mut self.frame_graph_executor,
14261 &mut self.frame_stats,
14262 ) {
14263 return Ok(entry);
14264 }
14265
14266 self.text_glyph_atlas.reset(
14267 &self.device,
14268 &self.image_bind_group_layout,
14269 &self.image_nearest_sampler,
14270 );
14271 Err("text glyph atlas filled and was reset".to_string())
14272 }
14273
14274 fn glyph_atlas_entry_for_cached(
14275 &mut self,
14276 glyph: &SoftwareGlyphAtlasPlacement,
14277 ) -> Option<GlyphAtlasEntry> {
14278 let entry = self.text_glyph_atlas.entry(&glyph.key)?;
14279 self.frame_stats.record_text_glyph_atlas_hit();
14280 Some(entry)
14281 }
14282
14283 fn glyph_atlas_entry_for_placement(
14284 &mut self,
14285 glyph: &SoftwareGlyphAtlasPlacement,
14286 ) -> Result<GlyphAtlasEntry, String> {
14287 if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
14288 return Ok(entry);
14289 }
14290
14291 let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
14292 return Err("text glyph placement has no retained raster mask".to_string());
14293 };
14294 self.glyph_atlas_entry_for(&upload_glyph)
14295 }
14296
14297 fn prepare_text_glyph_quads(
14298 &mut self,
14299 run_key: TextGlyphRunCacheKey,
14300 atlas_generation: u64,
14301 cached_glyph_run: Option<&[SoftwareGlyphAtlasPlacement]>,
14302 collected_run: &[SoftwareGlyphAtlasRunGlyph],
14303 generated_quads: &mut Vec<CachedTextGlyphQuad>,
14304 ) -> Result<Rc<[CachedTextGlyphQuad]>, String> {
14305 generated_quads.clear();
14306 if let Some(glyph_run) = cached_glyph_run {
14307 for glyph in glyph_run {
14308 if glyph.width == 0 || glyph.height == 0 || glyph.color.3 <= 0.0 {
14309 continue;
14310 }
14311 let entry = self.glyph_atlas_entry_for_placement(glyph)?;
14312 generated_quads.push(cached_text_glyph_quad(
14317 glyph,
14318 entry,
14319 self.text_glyph_atlas.size(),
14320 ));
14321 }
14322 } else {
14323 for run_glyph in collected_run {
14324 let placement = run_glyph.placement();
14325 if placement.width == 0 || placement.height == 0 || placement.color.3 <= 0.0 {
14326 continue;
14327 }
14328 let entry = match run_glyph {
14329 SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
14330 self.glyph_atlas_entry_for_placement(placement)?
14331 }
14332 SoftwareGlyphAtlasRunGlyph::New(glyph) => self.glyph_atlas_entry_for(glyph)?,
14333 };
14334 generated_quads.push(cached_text_glyph_quad(
14335 &placement,
14336 entry,
14337 self.text_glyph_atlas.size(),
14338 ));
14339 }
14340 }
14341
14342 let quads: Rc<[CachedTextGlyphQuad]> = Rc::from(generated_quads.clone().into_boxed_slice());
14343 if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
14344 cached.quads = Some(Rc::clone(&quads));
14345 cached.atlas_generation = atlas_generation;
14346 }
14347 Ok(quads)
14348 }
14349
14350 #[allow(clippy::too_many_arguments)]
14351 fn append_text_glyph_quad_run(
14352 &mut self,
14353 source_raster_rect: Rect,
14354 quads: &[CachedTextGlyphQuad],
14355 clip: Option<Rect>,
14356 viewport: ViewportUniformParams,
14357 root_scale: f32,
14358 image_vertices: &mut Vec<Vertex>,
14359 image_indices: &mut Vec<u32>,
14360 record_cached_hits: bool,
14361 ) -> usize {
14362 let mut appended = 0usize;
14363 for quad in quads {
14364 if !cached_text_glyph_quad_is_visible_in_viewport(
14365 source_raster_rect,
14366 quad,
14367 clip,
14368 viewport,
14369 root_scale,
14370 ) {
14371 continue;
14372 }
14373 if append_cached_text_glyph_quad(
14374 source_raster_rect,
14375 quad,
14376 image_vertices,
14377 image_indices,
14378 ) {
14379 if record_cached_hits {
14380 self.frame_stats.record_text_glyph_atlas_hit();
14381 }
14382 #[cfg(not(target_arch = "wasm32"))]
14383 {
14384 if fill_area_diag_enabled() {
14385 self.fill_area_diag.add_glyph_quad(quad);
14386 }
14387 }
14388 appended = appended.saturating_add(1);
14389 }
14390 }
14391 appended
14392 }
14393
14394 #[cfg(not(target_arch = "wasm32"))]
14395 fn retained_glyph_viewport(
14396 viewport: ViewportUniformParams,
14397 source_raster_rect: Rect,
14398 ) -> ViewportUniformParams {
14399 ViewportUniformParams {
14400 width: viewport.width,
14401 height: viewport.height,
14402 offset: [
14403 viewport.offset[0] - source_raster_rect.x,
14404 viewport.offset[1] - source_raster_rect.y,
14405 ],
14406 }
14407 }
14408
14409 #[cfg(not(target_arch = "wasm32"))]
14410 fn retained_text_glyph_run_ready(&mut self, cache_key: TextGlyphRunCacheKey) -> bool {
14411 let atlas_generation = self.text_glyph_atlas.generation();
14412 self.text_glyph_gpu_run_cache
14413 .peek(&cache_key)
14414 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
14415 }
14416
14417 #[cfg(not(target_arch = "wasm32"))]
14418 #[allow(clippy::too_many_arguments)]
14419 fn emit_retained_text_glyph_run_if_ready(
14420 &mut self,
14421 cache_key: TextGlyphRunCacheKey,
14422 quads: &[CachedTextGlyphQuad],
14423 clip: Option<Rect>,
14424 viewport: ViewportUniformParams,
14425 source_raster_rect: Rect,
14426 scissor: (u32, u32, u32, u32),
14427 staged_uploads: &mut StagedBufferUploads,
14428 glyph_cmds: &mut Vec<GlyphDrawCmd>,
14429 ) -> bool {
14430 if !should_use_retained_text_glyph_run(quads.len(), clip) {
14431 return false;
14432 }
14433 if !self.retained_text_glyph_run_ready(cache_key)
14434 && !self.ensure_retained_text_glyph_run(cache_key, quads)
14435 {
14436 return false;
14437 }
14438
14439 let uniform_slot = self.stage_retained_glyph_viewport_uniforms(
14440 staged_uploads,
14441 Self::retained_glyph_viewport(viewport, source_raster_rect),
14442 );
14443 if fill_area_diag_enabled() {
14444 for quad in quads {
14447 self.fill_area_diag.add_glyph_quad(quad);
14448 }
14449 }
14450 glyph_cmds.push(GlyphDrawCmd::retained(cache_key, uniform_slot, scissor));
14451 true
14452 }
14453
14454 #[cfg(not(target_arch = "wasm32"))]
14455 fn ensure_retained_text_glyph_run(
14456 &mut self,
14457 cache_key: TextGlyphRunCacheKey,
14458 quads: &[CachedTextGlyphQuad],
14459 ) -> bool {
14460 let atlas_generation = self.text_glyph_atlas.generation();
14461 if self
14462 .text_glyph_gpu_run_cache
14463 .peek(&cache_key)
14464 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
14465 {
14466 return true;
14467 }
14468
14469 let mut vertices = Vec::with_capacity(quads.len().saturating_mul(4));
14470 let mut indices = Vec::with_capacity(quads.len().saturating_mul(6));
14471 let origin = Rect {
14472 x: 0.0,
14473 y: 0.0,
14474 width: 0.0,
14475 height: 0.0,
14476 };
14477 for quad in quads {
14478 append_cached_text_glyph_quad(origin, quad, &mut vertices, &mut indices);
14479 }
14480 if indices.is_empty() {
14481 return false;
14482 }
14483
14484 let vertex_bytes = bytemuck::cast_slice(&vertices);
14485 let index_bytes = bytemuck::cast_slice(&indices);
14486 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14487 label: Some("Retained Text Glyph Vertex Buffer"),
14488 size: vertex_bytes.len() as u64,
14489 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
14490 mapped_at_creation: false,
14491 });
14492 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14493 label: Some("Retained Text Glyph Index Buffer"),
14494 size: index_bytes.len() as u64,
14495 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
14496 mapped_at_creation: false,
14497 });
14498 let vertex_upload =
14499 self.frame_graph_executor
14500 .upload_buffer(&self.queue, &vertex_buffer, 0, vertex_bytes);
14501 self.frame_stats.record_command_stats(vertex_upload);
14502 let index_upload =
14503 self.frame_graph_executor
14504 .upload_buffer(&self.queue, &index_buffer, 0, index_bytes);
14505 self.frame_stats.record_command_stats(index_upload);
14506
14507 self.text_glyph_gpu_run_cache.put(
14508 cache_key,
14509 CachedGpuTextGlyphRun {
14510 vertex_buffer,
14511 index_buffer,
14512 index_count: indices.len() as u32,
14513 atlas_generation,
14514 },
14515 );
14516 true
14517 }
14518
14519 #[allow(clippy::too_many_arguments)]
14520 fn append_text_glyph_draws<'a, I>(
14521 &mut self,
14522 layer_texts: I,
14523 viewport: ViewportUniformParams,
14524 root_scale: f32,
14525 allow_offscreen_prewarm: bool,
14526 staged_uploads: &mut StagedBufferUploads,
14527 image_vertices: &mut Vec<Vertex>,
14528 image_indices: &mut Vec<u32>,
14529 glyph_cmds: &mut Vec<GlyphDrawCmd>,
14530 ) -> Result<bool, String>
14531 where
14532 I: IntoIterator<Item = &'a TextDraw>,
14533 {
14534 let append_start = Instant::now();
14535 let initial_vertex_len = image_vertices.len();
14536 let initial_index_len = image_indices.len();
14537 let initial_cmd_len = glyph_cmds.len();
14538 let initial_staged_bytes_len = staged_uploads.bytes.len();
14539 let initial_staged_copies_len = staged_uploads.copies.len();
14540 let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
14541 let mut collected_placements = std::mem::take(&mut self.scratch_text_glyph_placements);
14542 let mut generated_quads = std::mem::take(&mut self.scratch_text_glyph_quads);
14543 generated_quads.clear();
14544 let mut visited = 0usize;
14545 let mut emitted_glyphs = 0usize;
14546 let mut prewarmed_glyphs = 0usize;
14547 let mut run_hits = 0usize;
14548 let mut run_misses = 0usize;
14549
14550 for text_draw in layer_texts {
14551 visited = visited.saturating_add(1);
14552 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
14553 self.text_raster_geometry(text_draw, root_scale)
14554 else {
14555 continue;
14556 };
14557 if !static_text_motion {
14558 image_vertices.truncate(initial_vertex_len);
14559 image_indices.truncate(initial_index_len);
14560 glyph_cmds.truncate(initial_cmd_len);
14561 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
14562 self.scratch_text_glyph_run = collected_run;
14563 self.scratch_text_glyph_placements = collected_placements;
14564 self.scratch_text_glyph_quads = generated_quads;
14565 return Ok(false);
14566 }
14567 let is_visible =
14568 text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale);
14569 let draw_action = text_glyph_draw_action(
14570 is_visible,
14571 text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale),
14572 allow_offscreen_prewarm,
14573 );
14574 if draw_action == TextGlyphDrawAction::Skip {
14575 continue;
14576 }
14577
14578 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
14579 let source_draw = raster_source.draw.as_ref();
14580 let source_raster_rect = raster_source.raster_rect;
14581
14582 let run_key = Self::text_glyph_run_cache_key(
14583 source_draw,
14584 source_raster_rect,
14585 text_scale,
14586 static_text_motion,
14587 );
14588 let atlas_generation = self.text_glyph_atlas.generation();
14589 let mut cached_quad_run = None;
14590 let mut miss_collect_ms = None;
14591 let mut miss_cached_glyphs = 0usize;
14592 let mut miss_new_glyphs = 0usize;
14593 let cached_glyph_run = if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
14594 run_hits = run_hits.saturating_add(1);
14595 if cached.atlas_generation == atlas_generation {
14596 cached_quad_run = cached.quads.as_ref().map(Rc::clone);
14597 }
14598 Some(Rc::clone(&cached.glyphs))
14599 } else {
14600 run_misses = run_misses.saturating_add(1);
14601 collected_run.clear();
14602 let collect_start = Instant::now();
14603 let collect_result = collect_solid_text_atlas_run(
14604 source_draw.text.as_ref(),
14605 source_raster_rect,
14606 &source_draw.text_style,
14607 source_draw.color,
14608 source_draw.font_size,
14609 text_scale,
14610 &self.text_fonts,
14611 &mut self.text_glyph_mask_cache,
14612 &mut collected_run,
14613 );
14614 miss_collect_ms = Some(instant_ms(collect_start, Instant::now()));
14615 if collect_result.is_none() {
14616 if text_atlas_fallback_diag_enabled() {
14617 let preview: String = source_draw.text.text.chars().take(96).collect();
14618 log::warn!(
14619 "[text-atlas-fallback] node={:?} visible={} prewarm={} spans={} links={} text_len={} preview={:?} span_style={:?} paragraph_style={:?}",
14620 source_draw.node_id,
14621 is_visible,
14622 draw_action == TextGlyphDrawAction::PrewarmOffscreen,
14623 source_draw.text.span_styles.len(),
14624 source_draw.text.links.len(),
14625 source_draw.text.text.len(),
14626 preview,
14627 source_draw.text_style.span_style,
14628 source_draw.text_style.paragraph_style,
14629 );
14630 }
14631 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
14632 continue;
14633 }
14634 image_vertices.truncate(initial_vertex_len);
14635 image_indices.truncate(initial_index_len);
14636 glyph_cmds.truncate(initial_cmd_len);
14637 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
14638 self.scratch_text_glyph_run = collected_run;
14639 self.scratch_text_glyph_placements = collected_placements;
14640 self.scratch_text_glyph_quads = generated_quads;
14641 return Ok(false);
14642 }
14643 if text_glyph_run_diag_enabled() {
14644 miss_cached_glyphs = collected_run
14645 .iter()
14646 .filter(|glyph| matches!(glyph, SoftwareGlyphAtlasRunGlyph::Cached(_)))
14647 .count();
14648 miss_new_glyphs = collected_run.len().saturating_sub(miss_cached_glyphs);
14649 }
14650 collected_placements.clear();
14651 collected_placements.extend(
14652 collected_run
14653 .iter()
14654 .map(SoftwareGlyphAtlasRunGlyph::placement),
14655 );
14656 let glyphs: Rc<[SoftwareGlyphAtlasPlacement]> =
14657 Rc::from(collected_placements.clone().into_boxed_slice());
14658 self.text_glyph_run_cache.put(
14659 run_key,
14660 CachedTextGlyphRun {
14661 glyphs,
14662 quads: None,
14663 atlas_generation: 0,
14664 },
14665 );
14666 None
14667 };
14668
14669 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
14670 let prewarm_quads = if let Some(quad_run) = cached_quad_run {
14671 quad_run
14672 } else {
14673 let prepare_start = Instant::now();
14674 match self.prepare_text_glyph_quads(
14675 run_key,
14676 atlas_generation,
14677 cached_glyph_run.as_deref(),
14678 &collected_run,
14679 &mut generated_quads,
14680 ) {
14681 Ok(quads) => {
14682 if let Some(collect_ms) = miss_collect_ms {
14683 if text_glyph_run_diag_enabled() {
14684 log::warn!(
14685 "[text-glyph-run-diag] visible=false glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
14686 quads.len(),
14687 miss_cached_glyphs,
14688 miss_new_glyphs,
14689 collect_ms,
14690 instant_ms(prepare_start, Instant::now()),
14691 );
14692 }
14693 }
14694 quads
14695 }
14696 Err(_) => continue,
14697 }
14698 };
14699 #[cfg(not(target_arch = "wasm32"))]
14700 if should_use_retained_text_glyph_run(prewarm_quads.len(), source_draw.clip) {
14701 self.ensure_retained_text_glyph_run(run_key, prewarm_quads.as_ref());
14702 }
14703 prewarmed_glyphs = prewarmed_glyphs.saturating_add(prewarm_quads.len());
14704 continue;
14705 }
14706
14707 let draw_rect = Rect {
14708 x: source_raster_rect.x / root_scale,
14709 y: source_raster_rect.y / root_scale,
14710 width: source_raster_rect.width / root_scale,
14711 height: source_raster_rect.height / root_scale,
14712 };
14713 let Some(scissor) = scissor_rect_for_layer(
14714 draw_rect,
14715 source_draw.clip,
14716 root_scale,
14717 viewport.width,
14718 viewport.height,
14719 ) else {
14720 continue;
14721 };
14722
14723 #[cfg(not(target_arch = "wasm32"))]
14724 if let Some(quad_run) = cached_quad_run.as_ref() {
14725 if should_use_retained_text_glyph_run(quad_run.len(), source_draw.clip)
14726 && self.emit_retained_text_glyph_run_if_ready(
14727 run_key,
14728 quad_run.as_ref(),
14729 source_draw.clip,
14730 viewport,
14731 source_raster_rect,
14732 scissor,
14733 staged_uploads,
14734 glyph_cmds,
14735 )
14736 {
14737 emitted_glyphs = emitted_glyphs.saturating_add(quad_run.len());
14738 continue;
14739 }
14740 }
14741
14742 let index_start = image_indices.len() as u32;
14743 if let Some(quad_run) = cached_quad_run {
14744 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
14745 source_raster_rect,
14746 quad_run.as_ref(),
14747 source_draw.clip,
14748 viewport,
14749 root_scale,
14750 image_vertices,
14751 image_indices,
14752 true,
14753 ));
14754 } else {
14755 let prepare_start = Instant::now();
14756 let Ok(quad_run) = self.prepare_text_glyph_quads(
14757 run_key,
14758 atlas_generation,
14759 cached_glyph_run.as_deref(),
14760 &collected_run,
14761 &mut generated_quads,
14762 ) else {
14763 image_vertices.truncate(initial_vertex_len);
14764 image_indices.truncate(initial_index_len);
14765 glyph_cmds.truncate(initial_cmd_len);
14766 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
14767 self.scratch_text_glyph_run = collected_run;
14768 self.scratch_text_glyph_placements = collected_placements;
14769 self.scratch_text_glyph_quads = generated_quads;
14770 return Ok(false);
14771 };
14772 if let Some(collect_ms) = miss_collect_ms {
14773 if text_glyph_run_diag_enabled() {
14774 log::warn!(
14775 "[text-glyph-run-diag] visible=true glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
14776 quad_run.len(),
14777 miss_cached_glyphs,
14778 miss_new_glyphs,
14779 collect_ms,
14780 instant_ms(prepare_start, Instant::now()),
14781 );
14782 }
14783 }
14784 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
14785 source_raster_rect,
14786 quad_run.as_ref(),
14787 source_draw.clip,
14788 viewport,
14789 root_scale,
14790 image_vertices,
14791 image_indices,
14792 false,
14793 ));
14794 }
14795 let index_count = image_indices.len() as u32 - index_start;
14796 if index_count > 0 {
14797 glyph_cmds.push(GlyphDrawCmd::shared(index_start, index_count, scissor));
14798 }
14799 }
14800
14801 self.scratch_text_glyph_run = collected_run;
14802 self.scratch_text_glyph_placements = collected_placements;
14803 self.scratch_text_glyph_quads = generated_quads;
14804 let append_end = Instant::now();
14805 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
14806 log::warn!(
14807 "[wgpu-render-stage:text-glyph-atlas] total_ms={total_ms:.2} visited={} cmds={} glyphs={} prewarmed={} run_hits={} run_misses={}",
14808 visited,
14809 glyph_cmds.len().saturating_sub(initial_cmd_len),
14810 emitted_glyphs,
14811 prewarmed_glyphs,
14812 run_hits,
14813 run_misses,
14814 );
14815 }
14816 Ok(true)
14817 }
14818
14819 #[cfg(not(target_arch = "wasm32"))]
14820 fn text_glyph_prewarm_decision(
14821 &self,
14822 text_draw: &TextDraw,
14823 viewport: ViewportUniformParams,
14824 root_scale: f32,
14825 ) -> TextGlyphPrewarmDecision {
14826 let Some((logical_rect, _, clip, _, static_text_motion)) =
14827 self.text_raster_geometry(text_draw, root_scale)
14828 else {
14829 return TextGlyphPrewarmDecision::MissingGeometry;
14830 };
14831 if !static_text_motion {
14832 return TextGlyphPrewarmDecision::DynamicMotion;
14833 }
14834 if text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
14835 return TextGlyphPrewarmDecision::Visible;
14836 }
14837 if text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale) {
14838 TextGlyphPrewarmDecision::Candidate
14839 } else {
14840 TextGlyphPrewarmDecision::OutsidePrewarmWindow
14841 }
14842 }
14843
14844 #[cfg(not(target_arch = "wasm32"))]
14845 #[allow(clippy::too_many_arguments)]
14846 fn prewarm_offscreen_text_glyph_draws_in_chunk(
14847 &mut self,
14848 ordered_items: &[(usize, SegmentDrawItem)],
14849 texts: &[TextDraw],
14850 chunk: &SegmentDrawChunkPlan,
14851 viewport: ViewportUniformParams,
14852 root_scale: f32,
14853 staged_uploads: &mut StagedBufferUploads,
14854 image_vertices: &mut Vec<Vertex>,
14855 image_indices: &mut Vec<u32>,
14856 glyph_cmds: &mut Vec<GlyphDrawCmd>,
14857 ) -> Result<(), String> {
14858 let prewarm_start = Instant::now();
14859 let diag_enabled = cranpose_core::env_flag!("CRANPOSE_TEXT_PREWARM_DIAG");
14860 let mut text_items = 0usize;
14861 let mut candidates = 0usize;
14862 let mut missing_geometry = 0usize;
14863 let mut dynamic_motion = 0usize;
14864 let mut visible = 0usize;
14865 let mut outside = 0usize;
14866 let mut already_prepared = 0usize;
14867 let mut admitted_candidates = 0usize;
14868 let mut skipped_unbounded = 0usize;
14869 let mut skipped_budget = 0usize;
14870 let initial_vertex_len = image_vertices.len();
14871 let initial_index_len = image_indices.len();
14872 let initial_cmd_len = glyph_cmds.len();
14873 let initial_staged_bytes_len = staged_uploads.bytes.len();
14874 let initial_staged_copies_len = staged_uploads.copies.len();
14875 'batches: for batch in chunk.iter() {
14876 let SegmentBatchPlan::Text { start, end } = batch else {
14877 continue;
14878 };
14879 for (_, item) in &ordered_items[start..end] {
14880 if offscreen_text_glyph_prewarm_budget_exhausted(prewarm_start, admitted_candidates)
14881 {
14882 skipped_budget = skipped_budget.saturating_add(1);
14883 break 'batches;
14884 }
14885 let SegmentDrawItem::Text(text_index) = item else {
14886 return Err(format!(
14887 "text prewarm batch contains non-text draw item: {item:?}"
14888 ));
14889 };
14890 let Some(text_draw) = texts.get(*text_index) else {
14891 continue;
14892 };
14893 text_items = text_items.saturating_add(1);
14894 match self.text_glyph_prewarm_decision(text_draw, viewport, root_scale) {
14895 TextGlyphPrewarmDecision::Candidate => {}
14896 TextGlyphPrewarmDecision::MissingGeometry => {
14897 missing_geometry = missing_geometry.saturating_add(1);
14898 continue;
14899 }
14900 TextGlyphPrewarmDecision::DynamicMotion => {
14901 dynamic_motion = dynamic_motion.saturating_add(1);
14902 continue;
14903 }
14904 TextGlyphPrewarmDecision::Visible => {
14905 visible = visible.saturating_add(1);
14906 continue;
14907 }
14908 TextGlyphPrewarmDecision::OutsidePrewarmWindow => {
14909 outside = outside.saturating_add(1);
14910 continue;
14911 }
14912 }
14913
14914 candidates = candidates.saturating_add(1);
14915 let Some((_, raster_rect, _, text_scale, static_text_motion)) =
14916 self.text_raster_geometry(text_draw, root_scale)
14917 else {
14918 missing_geometry = missing_geometry.saturating_add(1);
14919 continue;
14920 };
14921 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
14922 let source_draw = raster_source.draw.as_ref();
14923 let run_key = Self::text_glyph_run_cache_key(
14924 source_draw,
14925 raster_source.raster_rect,
14926 text_scale,
14927 static_text_motion,
14928 );
14929 let atlas_generation = self.text_glyph_atlas.generation();
14930 let cached_glyphs = if let Some(cached) = self.text_glyph_run_cache.peek(&run_key) {
14931 if cached.atlas_generation == atlas_generation && cached.quads.is_some() {
14932 already_prepared = already_prepared.saturating_add(1);
14933 continue;
14934 }
14935 Some(cached.glyphs.len())
14936 } else {
14937 None
14938 };
14939 if !offscreen_text_glyph_prewarm_work_is_bounded(
14940 cached_glyphs,
14941 source_draw.text.text.len(),
14942 ) {
14943 skipped_unbounded = skipped_unbounded.saturating_add(1);
14944 continue;
14945 }
14946 admitted_candidates = admitted_candidates.saturating_add(1);
14947 self.append_text_glyph_draws(
14948 std::iter::once(text_draw),
14949 viewport,
14950 root_scale,
14951 true,
14952 staged_uploads,
14953 image_vertices,
14954 image_indices,
14955 glyph_cmds,
14956 )?;
14957 image_vertices.truncate(initial_vertex_len);
14958 image_indices.truncate(initial_index_len);
14959 glyph_cmds.truncate(initial_cmd_len);
14960 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
14961 }
14962 }
14963
14964 if diag_enabled && text_items > 0 {
14965 log::warn!(
14966 "[text-glyph-prewarm-diag] texts={text_items} candidates={candidates} admitted={admitted_candidates} cached={already_prepared} skipped_unbounded={skipped_unbounded} skipped_budget={skipped_budget} visible={visible} outside={outside} dynamic={dynamic_motion} missing={missing_geometry}"
14967 );
14968 }
14969 if admitted_candidates > 0 {
14970 if let Some(total_ms) = should_log_wgpu_render_stage(prewarm_start, Instant::now()) {
14971 log::warn!(
14972 "[wgpu-render-stage:text-glyph-prewarm] total_ms={total_ms:.2} candidates={candidates} admitted={admitted_candidates} cached={already_prepared} skipped_unbounded={skipped_unbounded} skipped_budget={skipped_budget}"
14973 );
14974 }
14975 }
14976 Ok(())
14977 }
14978
14979 fn prepare_text_glyph_draw_cmds<'a, I>(
14980 &mut self,
14981 layer_texts: I,
14982 viewport: ViewportUniformParams,
14983 root_scale: f32,
14984 staged_uploads: &mut StagedBufferUploads,
14985 ) -> Result<Option<PreparedGlyphBatch>, String>
14986 where
14987 I: IntoIterator<Item = &'a TextDraw>,
14988 {
14989 #[cfg(target_arch = "wasm32")]
14990 let _ = staged_uploads;
14991
14992 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
14993 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
14994 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
14995 image_vertices.clear();
14996 image_indices.clear();
14997 glyph_cmds.clear();
14998
14999 if !self.append_text_glyph_draws(
15000 layer_texts,
15001 viewport,
15002 root_scale,
15003 false,
15004 staged_uploads,
15005 &mut image_vertices,
15006 &mut image_indices,
15007 &mut glyph_cmds,
15008 )? {
15009 self.scratch_image_vertices = image_vertices;
15010 self.scratch_image_indices = image_indices;
15011 self.scratch_glyph_cmds = glyph_cmds;
15012 return Ok(None);
15013 }
15014
15015 #[cfg(not(target_arch = "wasm32"))]
15016 if !image_indices.is_empty() {
15017 self.stage_native_image_buffers(
15018 staged_uploads,
15019 viewport,
15020 &image_vertices,
15021 &image_indices,
15022 );
15023 }
15024
15025 #[cfg(target_arch = "wasm32")]
15026 let image_slot = if glyph_cmds.is_empty() {
15027 0
15028 } else {
15029 let slot = self.claim_wasm_image_batch();
15030 {
15031 let buffers = &mut self.wasm_image_batches[slot];
15032 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
15033 }
15034 let buffers = &self.wasm_image_batches[slot];
15035 self.write_wasm_buffer(
15036 &buffers.vertex_buffer,
15037 bytemuck::cast_slice(&image_vertices),
15038 );
15039 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
15040 slot
15041 };
15042
15043 #[cfg(target_arch = "wasm32")]
15044 let uniform_slot = if glyph_cmds.is_empty() {
15045 0
15046 } else {
15047 self.prepare_wasm_viewport_uniforms(viewport)
15048 };
15049
15050 self.scratch_image_vertices = image_vertices;
15051 self.scratch_image_indices = image_indices;
15052 Ok(Some(PreparedGlyphBatch {
15053 cmds: glyph_cmds,
15054 #[cfg(target_arch = "wasm32")]
15055 image_slot,
15056 #[cfg(target_arch = "wasm32")]
15057 uniform_slot,
15058 }))
15059 }
15060
15061 #[allow(clippy::too_many_arguments)]
15062 fn append_image_bitmap_draw_cmd(
15063 &mut self,
15064 image: &ImageBitmap,
15065 rect: Rect,
15066 clip: Option<Rect>,
15067 sampling: ImageSampling,
15068 viewport: ViewportUniformParams,
15069 root_scale: f32,
15070 image_vertices: &mut Vec<Vertex>,
15071 image_indices: &mut Vec<u32>,
15072 image_cmds: &mut Vec<ImageDrawCmd>,
15073 ) -> Result<(), String> {
15074 if rect.width <= 0.0 || rect.height <= 0.0 {
15075 return Ok(());
15076 }
15077
15078 self.ensure_image_cached(image)?;
15079
15080 let (device_quad, scissor_rect) =
15081 if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
15082 let left_px = (rect.x * root_scale).round();
15083 let top_px = (rect.y * root_scale).round();
15084 let width_px = (rect.width * root_scale).round().max(1.0);
15085 let height_px = (rect.height * root_scale).round().max(1.0);
15086 let snapped_rect = Rect {
15087 x: left_px / root_scale,
15088 y: top_px / root_scale,
15089 width: width_px / root_scale,
15090 height: height_px / root_scale,
15091 };
15092 let right_px = left_px + width_px;
15093 let bottom_px = top_px + height_px;
15094 (
15095 [
15096 [left_px, top_px],
15097 [right_px, top_px],
15098 [left_px, bottom_px],
15099 [right_px, bottom_px],
15100 ],
15101 snapped_rect,
15102 )
15103 } else {
15104 (
15105 rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
15106 rect,
15107 )
15108 };
15109
15110 let Some(scissor) = scissor_rect_for_layer(
15111 scissor_rect,
15112 clip,
15113 root_scale,
15114 viewport.width,
15115 viewport.height,
15116 ) else {
15117 return Ok(());
15118 };
15119 let Some(uv_rect) = image_uv_rect(image, None) else {
15120 return Ok(());
15121 };
15122 #[cfg(not(target_arch = "wasm32"))]
15123 {
15124 if fill_area_diag_enabled() {
15125 self.fill_area_diag.add_image_quad(&device_quad);
15126 }
15127 }
15128
15129 let base_vertex = image_vertices.len() as u32;
15130 let index_start = image_indices.len() as u32;
15131 image_indices.extend_from_slice(&[
15132 base_vertex,
15133 base_vertex + 1,
15134 base_vertex + 2,
15135 base_vertex + 2,
15136 base_vertex + 1,
15137 base_vertex + 3,
15138 ]);
15139 let color = [1.0, 1.0, 1.0, 1.0];
15140 image_vertices.extend_from_slice(&[
15141 Vertex {
15142 position: device_quad[0],
15143 color,
15144 uv: [uv_rect.min[0], uv_rect.min[1]],
15145 uv_bounds: uv_rect.sample_bounds,
15146 },
15147 Vertex {
15148 position: device_quad[1],
15149 color,
15150 uv: [uv_rect.max[0], uv_rect.min[1]],
15151 uv_bounds: uv_rect.sample_bounds,
15152 },
15153 Vertex {
15154 position: device_quad[2],
15155 color,
15156 uv: [uv_rect.min[0], uv_rect.max[1]],
15157 uv_bounds: uv_rect.sample_bounds,
15158 },
15159 Vertex {
15160 position: device_quad[3],
15161 color,
15162 uv: [uv_rect.max[0], uv_rect.max[1]],
15163 uv_bounds: uv_rect.sample_bounds,
15164 },
15165 ]);
15166 image_cmds.push(ImageDrawCmd {
15167 index_start,
15168 scissor,
15169 image_id: image.id(),
15170 sampling,
15171 });
15172 Ok(())
15173 }
15174
15175 #[allow(clippy::too_many_arguments)]
15176 fn append_text_image_draw_cmds<'a, I>(
15177 &mut self,
15178 layer_texts: I,
15179 viewport: ViewportUniformParams,
15180 root_scale: f32,
15181 image_vertices: &mut Vec<Vertex>,
15182 image_indices: &mut Vec<u32>,
15183 image_cmds: &mut Vec<ImageDrawCmd>,
15184 ) -> Result<(), String>
15185 where
15186 I: Iterator<Item = &'a TextDraw>,
15187 {
15188 let append_start = Instant::now();
15189 let initial_len = image_cmds.len();
15190 let mut visited = 0usize;
15191 let mut hit_count = 0usize;
15192 let mut miss_count = 0usize;
15193 for text_draw in layer_texts {
15194 visited = visited.saturating_add(1);
15195 let _ = text_draw.node_id;
15196 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
15197 self.text_raster_geometry(text_draw, root_scale)
15198 else {
15199 continue;
15200 };
15201 if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
15202 continue;
15203 }
15204
15205 let raster_source = self.text_image_raster_source(
15206 text_draw,
15207 logical_rect,
15208 raster_rect,
15209 clip,
15210 root_scale,
15211 static_text_motion,
15212 );
15213 let source_draw = raster_source.draw.as_ref();
15214 let source_raster_rect = raster_source.raster_rect;
15215
15216 let cache_key = Self::text_image_cache_key(
15217 source_draw,
15218 source_raster_rect,
15219 text_scale,
15220 static_text_motion,
15221 );
15222 let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
15223 self.frame_stats
15224 .record_text_image_cache_hit(cached.image.width(), cached.image.height());
15225 hit_count = hit_count.saturating_add(1);
15226 cached.image.clone()
15227 } else {
15228 let Some(image) =
15229 self.rasterize_text_draw_to_image(source_draw, source_raster_rect, text_scale)
15230 else {
15231 continue;
15232 };
15233 self.frame_stats
15234 .record_text_image_cache_miss(image.width(), image.height());
15235 miss_count = miss_count.saturating_add(1);
15236 self.text_image_cache.put(
15237 cache_key,
15238 CachedTextImage {
15239 image: image.clone(),
15240 },
15241 );
15242 image
15243 };
15244
15245 let draw_origin = if static_text_motion {
15246 Point::new(
15247 source_raster_rect.x / root_scale,
15248 source_raster_rect.y / root_scale,
15249 )
15250 } else {
15251 Point::new(logical_rect.x, logical_rect.y)
15252 };
15253 let draw_rect = Rect {
15254 x: draw_origin.x,
15255 y: draw_origin.y,
15256 width: image.width() as f32 / root_scale,
15257 height: image.height() as f32 / root_scale,
15258 };
15259 self.append_image_bitmap_draw_cmd(
15260 &image,
15261 draw_rect,
15262 clip,
15263 ImageSampling::Nearest,
15264 viewport,
15265 root_scale,
15266 image_vertices,
15267 image_indices,
15268 image_cmds,
15269 )?;
15270 }
15271 let append_end = Instant::now();
15272 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
15273 log::warn!(
15274 "[wgpu-render-stage:text-images] total_ms={total_ms:.2} visited={} emitted={} hits={} misses={}",
15275 visited,
15276 image_cmds.len().saturating_sub(initial_len),
15277 hit_count,
15278 miss_count,
15279 );
15280 }
15281 Ok(())
15282 }
15283
15284 fn text_image_raster_source<'a>(
15285 &mut self,
15286 text_draw: &'a TextDraw,
15287 logical_rect: Rect,
15288 raster_rect: Rect,
15289 clip: Option<Rect>,
15290 root_scale: f32,
15291 static_text_motion: bool,
15292 ) -> TextRasterSource<'a> {
15293 let Some(clip) = clip else {
15294 return TextRasterSource {
15295 draw: Cow::Borrowed(text_draw),
15296 raster_rect,
15297 };
15298 };
15299 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
15300 return TextRasterSource {
15301 draw: Cow::Borrowed(text_draw),
15302 raster_rect,
15303 };
15304 }
15305
15306 let line_starts = self.text_line_index_cache.line_starts(&text_draw.text);
15307 clipped_text_raster_source_with_line_starts(
15308 text_draw,
15309 logical_rect,
15310 raster_rect,
15311 clip,
15312 root_scale,
15313 line_starts.as_ref(),
15314 )
15315 }
15316
15317 fn prepare_text_image_draw_cmds<'a, I>(
15318 &mut self,
15319 layer_texts: I,
15320 viewport: ViewportUniformParams,
15321 root_scale: f32,
15322 staged_uploads: &mut StagedBufferUploads,
15323 ) -> Result<PreparedImageBatch, String>
15324 where
15325 I: Iterator<Item = &'a TextDraw>,
15326 {
15327 #[cfg(target_arch = "wasm32")]
15328 let _ = staged_uploads;
15329
15330 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
15331 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
15332 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
15333 image_vertices.clear();
15334 image_indices.clear();
15335 image_cmds.clear();
15336
15337 self.append_text_image_draw_cmds(
15338 layer_texts,
15339 viewport,
15340 root_scale,
15341 &mut image_vertices,
15342 &mut image_indices,
15343 &mut image_cmds,
15344 )?;
15345
15346 #[cfg(not(target_arch = "wasm32"))]
15347 if !image_cmds.is_empty() {
15348 self.stage_native_image_buffers(
15349 staged_uploads,
15350 viewport,
15351 &image_vertices,
15352 &image_indices,
15353 );
15354 }
15355
15356 #[cfg(target_arch = "wasm32")]
15357 let image_slot = if image_cmds.is_empty() {
15358 0
15359 } else {
15360 let slot = self.claim_wasm_image_batch();
15361 {
15362 let buffers = &mut self.wasm_image_batches[slot];
15363 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
15364 }
15365 let buffers = &self.wasm_image_batches[slot];
15366 self.write_wasm_buffer(
15367 &buffers.vertex_buffer,
15368 bytemuck::cast_slice(&image_vertices),
15369 );
15370 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
15371 slot
15372 };
15373
15374 #[cfg(target_arch = "wasm32")]
15375 let uniform_slot = if image_cmds.is_empty() {
15376 0
15377 } else {
15378 self.prepare_wasm_viewport_uniforms(viewport)
15379 };
15380
15381 self.scratch_image_vertices = image_vertices;
15382 self.scratch_image_indices = image_indices;
15383 Ok(PreparedImageBatch {
15384 cmds: image_cmds,
15385 #[cfg(target_arch = "wasm32")]
15386 image_slot,
15387 #[cfg(target_arch = "wasm32")]
15388 uniform_slot,
15389 })
15390 }
15391
15392 fn text_raster_geometry(
15393 &self,
15394 text_draw: &TextDraw,
15395 root_scale: f32,
15396 ) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
15397 text_raster_geometry_for_draw(text_draw, root_scale)
15398 }
15399
15400 fn text_image_cache_key(
15401 text_draw: &TextDraw,
15402 raster_rect: Rect,
15403 text_scale: f32,
15404 static_text_motion: bool,
15405 ) -> TextImageCacheKey {
15406 let mut state = default_hash::new();
15407 text_draw.text.render_hash().hash(&mut state);
15408 text_draw.text_style.render_hash().hash(&mut state);
15409 text_draw.color.render_hash().hash(&mut state);
15410 hash_text_raster_geometry_for_cache(raster_rect, static_text_motion, &mut state);
15411 text_draw.font_size.to_bits().hash(&mut state);
15412 text_scale.to_bits().hash(&mut state);
15413 text_draw.layout_options.hash(&mut state);
15414 TextImageCacheKey(state.finish())
15415 }
15416
15417 fn text_glyph_run_cache_key(
15418 text_draw: &TextDraw,
15419 raster_rect: Rect,
15420 text_scale: f32,
15421 static_text_motion: bool,
15422 ) -> TextGlyphRunCacheKey {
15423 TextGlyphRunCacheKey(
15424 Self::text_image_cache_key(text_draw, raster_rect, text_scale, static_text_motion).0,
15425 )
15426 }
15427
15428 fn rasterize_text_draw_to_image(
15429 &mut self,
15430 text_draw: &TextDraw,
15431 raster_rect: Rect,
15432 text_scale: f32,
15433 ) -> Option<ImageBitmap> {
15434 if text_draw.text.span_styles.is_empty() {
15435 let font = self.text_fonts.resolve(&text_draw.text_style)?;
15436 return rasterize_text_to_image_with_glyph_cache(
15437 text_draw.text.text.as_str(),
15438 raster_rect,
15439 &text_draw.text_style,
15440 text_draw.color,
15441 text_draw.font_size,
15442 text_scale,
15443 font,
15444 &mut self.text_glyph_mask_cache,
15445 );
15446 }
15447
15448 if let Some(image) = rasterize_annotated_text_to_image_with_glyph_cache(
15449 text_draw.text.as_ref(),
15450 raster_rect,
15451 &text_draw.text_style,
15452 text_draw.color,
15453 text_draw.font_size,
15454 text_scale,
15455 &self.text_fonts,
15456 &mut self.text_glyph_mask_cache,
15457 ) {
15458 return Some(image);
15459 }
15460
15461 rasterize_spanned_text_to_image(
15462 text_draw,
15463 raster_rect,
15464 text_scale,
15465 &self.text_fonts,
15466 &mut self.text_glyph_mask_cache,
15467 )
15468 }
15469}
15470
15471fn rasterize_spanned_text_to_image(
15472 text_draw: &TextDraw,
15473 raster_rect: Rect,
15474 text_scale: f32,
15475 fonts: &SoftwareTextFontSet,
15476 glyph_cache: &mut SoftwareGlyphRasterCache,
15477) -> Option<ImageBitmap> {
15478 let width = raster_rect.width.ceil().max(1.0) as u32;
15479 let height = raster_rect.height.ceil().max(1.0) as u32;
15480 let mut canvas = vec![0_u8; (width as usize) * (height as usize) * 4];
15481 let boundaries = text_draw.text.span_boundaries();
15482 let base_line_height = text_draw
15483 .text_style
15484 .resolve_line_height(14.0, text_draw.font_size)
15485 .max(1.0);
15486 let mut current_line_height = base_line_height;
15487 let mut cursor_x = raster_rect.x;
15488 let mut cursor_y = raster_rect.y;
15489
15490 for window in boundaries.windows(2) {
15491 let start = window[0];
15492 let end = window[1];
15493 if start == end {
15494 continue;
15495 }
15496
15497 let chunk = &text_draw.text.text[start..end];
15498 let mut merged_span = text_draw.text_style.span_style.clone();
15499 for span in &text_draw.text.span_styles {
15500 if span.range.start <= start && span.range.end >= end {
15501 merged_span = merged_span.merge(&span.item);
15502 }
15503 }
15504
15505 let mut chunk_style = text_draw.text_style.clone();
15506 chunk_style.span_style = merged_span;
15507
15508 for part in chunk.split_inclusive('\n') {
15509 let has_newline = part.ends_with('\n');
15510 let content = if has_newline {
15511 &part[..part.len().saturating_sub(1)]
15512 } else {
15513 part
15514 };
15515
15516 if !content.is_empty() {
15517 let chunk_font_size = chunk_style.resolve_font_size(text_draw.font_size);
15518 let Some(font) = fonts.resolve(&chunk_style) else {
15519 continue;
15520 };
15521 let metrics = measure_text_with_font(content, &chunk_style, chunk_font_size, font);
15522 let segment_rect = Rect {
15523 x: cursor_x,
15524 y: cursor_y,
15525 width: (metrics.width * text_scale).ceil().max(1.0),
15526 height: (metrics.height * text_scale).ceil().max(1.0),
15527 };
15528 if let Some(segment_image) = rasterize_text_to_image_with_glyph_cache(
15529 content,
15530 segment_rect,
15531 &chunk_style,
15532 chunk_style.resolve_text_color(text_draw.color),
15533 chunk_font_size,
15534 text_scale,
15535 font,
15536 glyph_cache,
15537 ) {
15538 composite_text_segment(
15539 &mut canvas,
15540 width,
15541 height,
15542 raster_rect,
15543 segment_rect,
15544 &segment_image,
15545 );
15546 }
15547 cursor_x += metrics.width * text_scale;
15548 current_line_height = current_line_height.max(metrics.line_height.max(1.0));
15549 }
15550
15551 if has_newline {
15552 cursor_x = raster_rect.x;
15553 cursor_y += current_line_height * text_scale;
15554 current_line_height = base_line_height;
15555 }
15556 }
15557 }
15558
15559 ImageBitmap::from_rgba8(width, height, canvas).ok()
15560}
15561
15562struct TextRasterSource<'a> {
15563 draw: Cow<'a, TextDraw>,
15564 raster_rect: Rect,
15565}
15566
15567fn text_glyph_raster_source(text_draw: &TextDraw, raster_rect: Rect) -> TextRasterSource<'_> {
15568 TextRasterSource {
15569 draw: Cow::Borrowed(text_draw),
15570 raster_rect,
15571 }
15572}
15573
15574#[cfg(test)]
15575fn clipped_text_raster_source<'a>(
15576 text_draw: &'a TextDraw,
15577 logical_rect: Rect,
15578 raster_rect: Rect,
15579 clip: Option<Rect>,
15580 root_scale: f32,
15581 static_text_motion: bool,
15582) -> TextRasterSource<'a> {
15583 let Some(clip) = clip else {
15584 return TextRasterSource {
15585 draw: Cow::Borrowed(text_draw),
15586 raster_rect,
15587 };
15588 };
15589 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
15590 return TextRasterSource {
15591 draw: Cow::Borrowed(text_draw),
15592 raster_rect,
15593 };
15594 }
15595 let line_starts = line_start_offsets(text_draw.text.text.as_str());
15596 clipped_text_raster_source_with_line_starts(
15597 text_draw,
15598 logical_rect,
15599 raster_rect,
15600 clip,
15601 root_scale,
15602 &line_starts,
15603 )
15604}
15605
15606fn clipped_text_raster_source_with_line_starts<'a>(
15607 text_draw: &'a TextDraw,
15608 logical_rect: Rect,
15609 raster_rect: Rect,
15610 clip: Rect,
15611 root_scale: f32,
15612 line_starts: &[usize],
15613) -> TextRasterSource<'a> {
15614 if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
15615 return TextRasterSource {
15616 draw: Cow::Borrowed(text_draw),
15617 raster_rect,
15618 };
15619 }
15620
15621 let Some(visible_rect) = logical_rect.intersect(clip) else {
15622 return TextRasterSource {
15623 draw: Cow::Borrowed(text_draw),
15624 raster_rect,
15625 };
15626 };
15627
15628 let line_count = line_starts.len().max(1);
15629 let line_height = logical_rect.height / line_count as f32;
15630 if !line_height.is_finite() || line_height <= 0.0 {
15631 return TextRasterSource {
15632 draw: Cow::Borrowed(text_draw),
15633 raster_rect,
15634 };
15635 }
15636
15637 let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
15638 let visible_bottom =
15639 ((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
15640 let start_line = visible_top.saturating_sub(1).max(0) as usize;
15641 let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
15642 let end_line = end_line.min(line_count);
15643 if start_line == 0 && end_line >= line_count {
15644 return TextRasterSource {
15645 draw: Cow::Borrowed(text_draw),
15646 raster_rect,
15647 };
15648 }
15649
15650 let byte_start = line_starts[start_line];
15651 let byte_end = line_end_offset(text_draw.text.text.as_str(), line_starts, end_line - 1);
15652 if byte_start >= byte_end {
15653 return TextRasterSource {
15654 draw: Cow::Borrowed(text_draw),
15655 raster_rect,
15656 };
15657 }
15658
15659 let slice_y = logical_rect.y + start_line as f32 * line_height;
15660 let slice_height = (end_line - start_line) as f32 * line_height;
15661 let mut slice_raster_rect = Rect {
15662 x: logical_rect.x * root_scale,
15663 y: slice_y * root_scale,
15664 width: logical_rect.width * root_scale,
15665 height: slice_height * root_scale,
15666 };
15667 slice_raster_rect.x = slice_raster_rect.x.round();
15668 slice_raster_rect.y = slice_raster_rect.y.round();
15669 slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
15670 slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
15671
15672 let mut sliced_draw = text_draw.clone();
15673 sliced_draw.rect = Rect {
15674 x: logical_rect.x,
15675 y: slice_y,
15676 width: logical_rect.width,
15677 height: slice_height,
15678 };
15679 sliced_draw.text = Arc::new(text_draw.text.subsequence(byte_start..byte_end));
15680
15681 TextRasterSource {
15682 draw: Cow::Owned(sliced_draw),
15683 raster_rect: slice_raster_rect,
15684 }
15685}
15686
15687fn line_start_offsets(text: &str) -> Vec<usize> {
15688 let mut starts =
15689 Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
15690 starts.push(0);
15691 starts.extend(
15692 text.char_indices()
15693 .filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
15694 );
15695 starts
15696}
15697
15698fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
15699 line_starts.get(line + 1).copied().unwrap_or(text.len())
15700}
15701
15702fn composite_text_segment(
15703 canvas: &mut [u8],
15704 canvas_width: u32,
15705 canvas_height: u32,
15706 canvas_rect: Rect,
15707 segment_rect: Rect,
15708 segment_image: &ImageBitmap,
15709) {
15710 let offset_x = (segment_rect.x - canvas_rect.x).round() as i32;
15711 let offset_y = (segment_rect.y - canvas_rect.y).round() as i32;
15712 let src = segment_image.pixels();
15713 for sy in 0..segment_image.height() as i32 {
15714 let dy = offset_y + sy;
15715 if dy < 0 || dy >= canvas_height as i32 {
15716 continue;
15717 }
15718 for sx in 0..segment_image.width() as i32 {
15719 let dx = offset_x + sx;
15720 if dx < 0 || dx >= canvas_width as i32 {
15721 continue;
15722 }
15723 let src_index = ((sy as u32 * segment_image.width() + sx as u32) * 4) as usize;
15724 let dst_index = ((dy as u32 * canvas_width + dx as u32) * 4) as usize;
15725 blend_rgba_pixel(
15726 &mut canvas[dst_index..dst_index + 4],
15727 &src[src_index..src_index + 4],
15728 );
15729 }
15730 }
15731}
15732
15733fn blend_rgba_pixel(dst: &mut [u8], src: &[u8]) {
15734 let src_alpha = src[3] as f32 / 255.0;
15735 if src_alpha <= 0.0 {
15736 return;
15737 }
15738 let dst_alpha = dst[3] as f32 / 255.0;
15739 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
15740 if out_alpha <= f32::EPSILON {
15741 dst.copy_from_slice(&[0, 0, 0, 0]);
15742 return;
15743 }
15744
15745 for channel in 0..3 {
15746 let src_channel = src[channel] as f32 / 255.0;
15747 let dst_channel = dst[channel] as f32 / 255.0;
15748 let src_premult = src_channel * src_alpha;
15749 let dst_premult = dst_channel * dst_alpha;
15750 dst[channel] =
15751 (((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0) * 255.0)
15752 .round() as u8;
15753 }
15754 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
15755}
15756
15757fn align_to(value: u32, alignment: u32) -> u32 {
15758 debug_assert!(alignment > 0);
15759 value.div_ceil(alignment) * alignment
15760}
15761
15762#[cfg(not(target_arch = "wasm32"))]
15763fn align_usize_to(value: usize, alignment: usize) -> usize {
15764 debug_assert!(alignment > 0);
15765 value.div_ceil(alignment) * alignment
15766}
15767
15768impl GpuRenderer {
15769 fn convert_surface_pixels_to_rgba(&self, pixels: &mut [u8]) -> Result<(), String> {
15770 match self.surface_format {
15771 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => Ok(()),
15772 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
15773 for pixel in pixels.as_chunks_mut::<4>().0 {
15774 pixel.swap(0, 2);
15775 }
15776 Ok(())
15777 }
15778 format => Err(format!(
15779 "Screenshot readback unsupported for texture format: {format:?}"
15780 )),
15781 }
15782 }
15783}
15784
15785fn is_in_effect_range(z_index: usize, effect_z_ranges: &[Range<usize>]) -> bool {
15786 effect_z_ranges.iter().any(|range| range.contains(&z_index))
15787}
15788
15789#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15790enum SegmentDrawItem {
15791 Shape(usize),
15792 Image(usize),
15793 Text(usize),
15794 Shadow(usize),
15795 Composite(usize),
15796 ShaderComposite(usize),
15797 Retained(usize),
15798}
15799
15800#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15801enum SegmentBatchPlan {
15802 Shape {
15803 start: usize,
15804 end: usize,
15805 blend_mode: BlendMode,
15806 },
15807 Image {
15808 start: usize,
15809 end: usize,
15810 blend_mode: BlendMode,
15811 },
15812 Text {
15813 start: usize,
15814 end: usize,
15815 },
15816 Composite {
15817 start: usize,
15818 end: usize,
15819 },
15820 ShaderComposite {
15821 start: usize,
15822 end: usize,
15823 },
15824 Retained {
15827 start: usize,
15828 end: usize,
15829 },
15830}
15831
15832#[derive(Clone, Debug, Default, PartialEq, Eq)]
15833struct SegmentDrawChunkPlan {
15834 batches: Vec<SegmentBatchPlan>,
15835}
15836
15837struct SegmentRenderOutcome {
15838 rendered_any: bool,
15839 pass_count: u32,
15840}
15841
15842struct SegmentCommandEncodeOutcome {
15843 first_batch: bool,
15844}
15845
15846#[cfg(not(target_arch = "wasm32"))]
15847#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15848enum TextGlyphPrewarmDecision {
15849 Candidate,
15850 MissingGeometry,
15851 DynamicMotion,
15852 Visible,
15853 OutsidePrewarmWindow,
15854}
15855
15856#[cfg(not(target_arch = "wasm32"))]
15857#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15858struct NativeSegmentFusionBudget {
15859 shape_count: usize,
15860 gradient_stop_count: usize,
15861}
15862
15863#[cfg(not(target_arch = "wasm32"))]
15864#[derive(Clone, Debug, PartialEq, Eq)]
15865struct NativeSegmentFusionPartition {
15866 chunk: SegmentDrawChunkPlan,
15867 budget: NativeSegmentFusionBudget,
15868}
15869
15870#[cfg(not(target_arch = "wasm32"))]
15871#[derive(Clone, Debug, PartialEq, Eq)]
15872enum FusedSegmentBatch {
15873 Shape {
15874 batch: PreparedShapeBatch,
15875 blend_mode: BlendMode,
15876 },
15877 Image {
15878 cmd_range: Range<usize>,
15879 blend_mode: BlendMode,
15880 },
15881 Text {
15882 image_cmd_range: Range<usize>,
15883 glyph_cmd_range: Range<usize>,
15884 },
15885 Composite {
15886 draw_range: Range<usize>,
15887 },
15888 ShaderComposite {
15889 draw_range: Range<usize>,
15890 },
15891 Retained {
15892 item_range: Range<usize>,
15893 },
15894}
15895
15896struct ShadowSourceRenderOutcome {
15897 rendered_any: bool,
15898 pass_count: u32,
15899}
15900
15901#[cfg(not(target_arch = "wasm32"))]
15905struct SegmentCaptureJob {
15906 key: SegmentSurfaceKey,
15907 first: u32,
15908 last: u32,
15909 capture_index: u32,
15910}
15911
15912#[cfg(not(target_arch = "wasm32"))]
15916struct SegmentCompositePlan {
15917 key: SegmentSurfaceKey,
15918 dest_quad: [[f32; 2]; 4],
15919 inverse: [[f32; 3]; 3],
15920 identity: bool,
15921}
15922
15923#[cfg(not(target_arch = "wasm32"))]
15926fn segment_identity_quad(rect: &CaptureRect) -> [[f32; 2]; 4] {
15927 let [x, y] = rect.origin;
15928 let width = rect.width as f32;
15929 let height = rect.height as f32;
15930 [
15931 [x, y],
15932 [x + width, y],
15933 [x, y + height],
15934 [x + width, y + height],
15935 ]
15936}
15937
15938#[cfg(not(target_arch = "wasm32"))]
15941fn segment_identity_inverse(rect: &CaptureRect) -> [[f32; 3]; 3] {
15942 [
15943 [1.0, 0.0, -rect.origin[0]],
15944 [0.0, 1.0, -rect.origin[1]],
15945 [0.0, 0.0, 1.0],
15946 ]
15947}
15948
15949#[cfg(not(target_arch = "wasm32"))]
15954fn plan_segment_capture_geometry(
15955 slot: &ReplaySlot,
15956 first: u32,
15957 last: u32,
15958 transform: SimilarityTransform,
15959 max_texture_dim: u32,
15960) -> Option<(CaptureRect, f32)> {
15961 let range = first as usize..last as usize;
15962 let aabbs = slot.shape_aabbs.get(range)?;
15963 if aabbs.is_empty() {
15964 return None;
15965 }
15966 let affine = Affine2::from_similarity(transform.center, transform.rot, transform.scale);
15967 let mut min = [f32::INFINITY; 2];
15968 let mut max = [f32::NEG_INFINITY; 2];
15969 for aabb in aabbs {
15970 for corner in [
15971 [aabb[0], aabb[1]],
15972 [aabb[2], aabb[1]],
15973 [aabb[0], aabb[3]],
15974 [aabb[2], aabb[3]],
15975 ] {
15976 let p = affine.apply(corner);
15977 min[0] = min[0].min(p[0]);
15978 min[1] = min[1].min(p[1]);
15979 max[0] = max[0].max(p[0]);
15980 max[1] = max[1].max(p[1]);
15981 }
15982 }
15983 let rect = crate::segment_surface::snap_capture_rect(min, max, max_texture_dim)?;
15984 let base_area = slot.area_prefix.get(last as usize).copied()?
15985 - slot.area_prefix.get(first as usize).copied()?;
15986 let member_px = base_area * transform.scale * transform.scale * slot.submitted_area_scale;
15987 Some((rect, member_px))
15988}
15989
15990impl SegmentDrawChunkPlan {
15991 fn is_empty(&self) -> bool {
15992 self.batches.is_empty()
15993 }
15994
15995 fn push(&mut self, batch: SegmentBatchPlan) {
15996 self.batches.push(batch);
15997 }
15998
15999 fn iter(&self) -> impl Iterator<Item = SegmentBatchPlan> + '_ {
16000 self.batches.iter().copied()
16001 }
16002}
16003
16004#[derive(Clone, Debug, PartialEq, Eq)]
16005enum SegmentRenderCommand {
16006 DrawChunk(SegmentDrawChunkPlan),
16007 Shadow(usize),
16008}
16009
16010struct SegmentCommandIter<'a> {
16011 ordered_items: &'a [(usize, SegmentDrawItem)],
16012 shapes: &'a [DrawShape],
16013 images: &'a [ImageDraw],
16014 cursor: usize,
16015 batch_limits: ShapeBatchLimits,
16016}
16017
16018impl<'a> SegmentCommandIter<'a> {
16019 fn new(
16020 ordered_items: &'a [(usize, SegmentDrawItem)],
16021 shapes: &'a [DrawShape],
16022 images: &'a [ImageDraw],
16023 batch_limits: ShapeBatchLimits,
16024 ) -> Self {
16025 Self {
16026 ordered_items,
16027 shapes,
16028 images,
16029 cursor: 0,
16030 batch_limits,
16031 }
16032 }
16033}
16034
16035impl Iterator for SegmentCommandIter<'_> {
16036 type Item = SegmentRenderCommand;
16037
16038 fn next(&mut self) -> Option<Self::Item> {
16039 if self.cursor >= self.ordered_items.len() {
16040 return None;
16041 }
16042
16043 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
16044 self.cursor += 1;
16045 return Some(SegmentRenderCommand::Shadow(index));
16046 }
16047
16048 let mut chunk = SegmentDrawChunkPlan::default();
16049 while self.cursor < self.ordered_items.len() {
16050 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
16051 if chunk.is_empty() {
16052 self.cursor += 1;
16053 return Some(SegmentRenderCommand::Shadow(index));
16054 }
16055 break;
16056 }
16057
16058 let Some((batch, next_cursor)) = segment_batch_plan_at_cursor(
16059 self.ordered_items,
16060 self.shapes,
16061 self.images,
16062 self.cursor,
16063 self.batch_limits,
16064 ) else {
16065 break;
16066 };
16067 chunk.push(batch);
16068 self.cursor = next_cursor;
16069 }
16070
16071 Some(SegmentRenderCommand::DrawChunk(chunk))
16072 }
16073}
16074
16075#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16076struct PreparedShapeBatch {
16077 vertex_start: u32,
16080 vertex_count: u32,
16081 has_gradient: bool,
16084 #[cfg(target_arch = "wasm32")]
16085 shape_slot: usize,
16086 #[cfg(target_arch = "wasm32")]
16087 uniform_slot: usize,
16088}
16089
16090struct PreparedImageBatch {
16091 cmds: Vec<ImageDrawCmd>,
16092 #[cfg(target_arch = "wasm32")]
16093 image_slot: usize,
16094 #[cfg(target_arch = "wasm32")]
16095 uniform_slot: usize,
16096}
16097
16098impl PreparedImageBatch {
16099 fn is_empty(&self) -> bool {
16100 self.cmds.is_empty()
16101 }
16102
16103 fn into_cmds(self) -> Vec<ImageDrawCmd> {
16104 self.cmds
16105 }
16106}
16107
16108struct PreparedGlyphBatch {
16109 cmds: Vec<GlyphDrawCmd>,
16110 #[cfg(target_arch = "wasm32")]
16111 image_slot: usize,
16112 #[cfg(target_arch = "wasm32")]
16113 uniform_slot: usize,
16114}
16115
16116impl PreparedGlyphBatch {
16117 fn is_empty(&self) -> bool {
16118 self.cmds.is_empty()
16119 }
16120
16121 fn into_cmds(self) -> Vec<GlyphDrawCmd> {
16122 self.cmds
16123 }
16124}
16125
16126#[cfg(not(target_arch = "wasm32"))]
16127fn gradient_stop_count_for_shape(shape: &DrawShape, brushes: &[Brush]) -> usize {
16128 match shape.brush {
16129 SceneBrush::Solid(_) => 0,
16130 SceneBrush::Gradient(index) => match &brushes[index as usize] {
16131 Brush::Solid(_) => 0,
16132 Brush::LinearGradient { colors, .. }
16133 | Brush::RadialGradient { colors, .. }
16134 | Brush::SweepGradient { colors, .. } => colors.len(),
16135 },
16136 }
16137}
16138
16139#[cfg(not(target_arch = "wasm32"))]
16140fn native_segment_fusion_budget(
16141 ordered_items: &[(usize, SegmentDrawItem)],
16142 shapes: &[DrawShape],
16143 brushes: &[Brush],
16144 chunk: &SegmentDrawChunkPlan,
16145 batch_limits: ShapeBatchLimits,
16146) -> Result<Option<NativeSegmentFusionBudget>, String> {
16147 let mut shape_count = 0usize;
16148 let mut gradient_stop_count = 0usize;
16149
16150 for batch in chunk.iter() {
16151 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
16152 continue;
16153 };
16154 for (_, item) in &ordered_items[start..end] {
16155 let SegmentDrawItem::Shape(shape_index) = item else {
16156 return Err(format!(
16157 "shape batch contains non-shape draw item: {item:?}"
16158 ));
16159 };
16160 let shape = &shapes[*shape_index];
16161 shape_count = shape_count.saturating_add(1);
16162 gradient_stop_count =
16163 gradient_stop_count.saturating_add(gradient_stop_count_for_shape(shape, brushes));
16164 }
16165 }
16166
16167 if shape_count > batch_limits.max_shapes_per_batch
16168 || gradient_stop_count > batch_limits.max_gradient_stops
16169 {
16170 return Ok(None);
16171 }
16172
16173 Ok(Some(NativeSegmentFusionBudget {
16174 shape_count,
16175 gradient_stop_count,
16176 }))
16177}
16178
16179#[cfg(not(target_arch = "wasm32"))]
16180fn push_native_segment_fusion_partition(
16181 partitions: &mut Vec<NativeSegmentFusionPartition>,
16182 current: &mut SegmentDrawChunkPlan,
16183 current_budget: &mut NativeSegmentFusionBudget,
16184) {
16185 if current.is_empty() {
16186 return;
16187 }
16188
16189 partitions.push(NativeSegmentFusionPartition {
16190 chunk: std::mem::take(current),
16191 budget: *current_budget,
16192 });
16193 *current_budget = NativeSegmentFusionBudget {
16194 shape_count: 0,
16195 gradient_stop_count: 0,
16196 };
16197}
16198
16199#[cfg(not(target_arch = "wasm32"))]
16200fn native_segment_fusion_partitions(
16201 ordered_items: &[(usize, SegmentDrawItem)],
16202 shapes: &[DrawShape],
16203 brushes: &[Brush],
16204 chunk: &SegmentDrawChunkPlan,
16205 batch_limits: ShapeBatchLimits,
16206) -> Result<Option<Vec<NativeSegmentFusionPartition>>, String> {
16207 if let Some(budget) =
16208 native_segment_fusion_budget(ordered_items, shapes, brushes, chunk, batch_limits)?
16209 {
16210 return Ok(Some(vec![NativeSegmentFusionPartition {
16211 chunk: chunk.clone(),
16212 budget,
16213 }]));
16214 }
16215
16216 let mut partitions = Vec::new();
16217 let mut current = SegmentDrawChunkPlan::default();
16218 let mut current_budget = NativeSegmentFusionBudget {
16219 shape_count: 0,
16220 gradient_stop_count: 0,
16221 };
16222
16223 for batch in chunk.iter() {
16224 let SegmentBatchPlan::Shape {
16225 start,
16226 end,
16227 blend_mode,
16228 } = batch
16229 else {
16230 current.push(batch);
16231 continue;
16232 };
16233
16234 let mut run_start = start;
16235 for (item_cursor, (_, item)) in ordered_items.iter().enumerate().take(end).skip(start) {
16236 let SegmentDrawItem::Shape(shape_index) = *item else {
16237 return Err(format!(
16238 "shape batch contains non-shape draw item: {:?}",
16239 item
16240 ));
16241 };
16242 let gradient_stop_count = gradient_stop_count_for_shape(&shapes[shape_index], brushes);
16243 if gradient_stop_count > batch_limits.max_gradient_stops {
16244 return Ok(None);
16245 }
16246
16247 let fits_shape_count =
16248 current_budget.shape_count.saturating_add(1) <= batch_limits.max_shapes_per_batch;
16249 let fits_gradient_count = current_budget
16250 .gradient_stop_count
16251 .saturating_add(gradient_stop_count)
16252 <= batch_limits.max_gradient_stops;
16253 if !fits_shape_count || !fits_gradient_count {
16254 if run_start < item_cursor {
16255 current.push(SegmentBatchPlan::Shape {
16256 start: run_start,
16257 end: item_cursor,
16258 blend_mode,
16259 });
16260 }
16261 push_native_segment_fusion_partition(
16262 &mut partitions,
16263 &mut current,
16264 &mut current_budget,
16265 );
16266 run_start = item_cursor;
16267 }
16268
16269 current_budget.shape_count = current_budget.shape_count.saturating_add(1);
16270 current_budget.gradient_stop_count = current_budget
16271 .gradient_stop_count
16272 .saturating_add(gradient_stop_count);
16273 }
16274
16275 if run_start < end {
16276 current.push(SegmentBatchPlan::Shape {
16277 start: run_start,
16278 end,
16279 blend_mode,
16280 });
16281 }
16282 }
16283
16284 push_native_segment_fusion_partition(&mut partitions, &mut current, &mut current_budget);
16285 Ok(Some(partitions))
16286}
16287
16288fn segment_batch_plan_at_cursor(
16289 ordered_items: &[(usize, SegmentDrawItem)],
16290 shapes: &[DrawShape],
16291 images: &[ImageDraw],
16292 start: usize,
16293 batch_limits: ShapeBatchLimits,
16294) -> Option<(SegmentBatchPlan, usize)> {
16295 match ordered_items[start].1 {
16296 SegmentDrawItem::Shape(index) => {
16297 let blend_mode = supported_blend_mode(shapes[index].blend_mode);
16298 let mut end = start + 1;
16299 let shape_limit = (start + batch_limits.max_shapes_per_batch).min(ordered_items.len());
16300 while end < shape_limit {
16301 match ordered_items[end].1 {
16302 SegmentDrawItem::Shape(next_index)
16303 if supported_blend_mode(shapes[next_index].blend_mode) == blend_mode =>
16304 {
16305 end += 1;
16306 }
16307 _ => break,
16308 }
16309 }
16310 Some((
16311 SegmentBatchPlan::Shape {
16312 start,
16313 end,
16314 blend_mode,
16315 },
16316 end,
16317 ))
16318 }
16319 SegmentDrawItem::Image(index) => {
16320 let blend_mode = supported_blend_mode(images[index].blend_mode);
16321 let mut end = start + 1;
16322 while end < ordered_items.len() {
16323 match ordered_items[end].1 {
16324 SegmentDrawItem::Image(next_index)
16325 if supported_blend_mode(images[next_index].blend_mode) == blend_mode =>
16326 {
16327 end += 1;
16328 }
16329 _ => break,
16330 }
16331 }
16332 Some((
16333 SegmentBatchPlan::Image {
16334 start,
16335 end,
16336 blend_mode,
16337 },
16338 end,
16339 ))
16340 }
16341 SegmentDrawItem::Text(_) => {
16342 let mut end = start + 1;
16343 while end < ordered_items.len() {
16344 if matches!(ordered_items[end].1, SegmentDrawItem::Text(_)) {
16345 end += 1;
16346 } else {
16347 break;
16348 }
16349 }
16350 Some((SegmentBatchPlan::Text { start, end }, end))
16351 }
16352 SegmentDrawItem::Composite(_) => {
16353 let mut end = start + 1;
16354 while end < ordered_items.len() {
16355 if matches!(ordered_items[end].1, SegmentDrawItem::Composite(_)) {
16356 end += 1;
16357 } else {
16358 break;
16359 }
16360 }
16361 Some((SegmentBatchPlan::Composite { start, end }, end))
16362 }
16363 SegmentDrawItem::ShaderComposite(_) => {
16364 let mut end = start + 1;
16365 while end < ordered_items.len() {
16366 if matches!(ordered_items[end].1, SegmentDrawItem::ShaderComposite(_)) {
16367 end += 1;
16368 } else {
16369 break;
16370 }
16371 }
16372 Some((SegmentBatchPlan::ShaderComposite { start, end }, end))
16373 }
16374 SegmentDrawItem::Retained(_) => {
16375 let mut end = start + 1;
16376 while end < ordered_items.len() {
16377 if matches!(ordered_items[end].1, SegmentDrawItem::Retained(_)) {
16378 end += 1;
16379 } else {
16380 break;
16381 }
16382 }
16383 Some((SegmentBatchPlan::Retained { start, end }, end))
16384 }
16385 SegmentDrawItem::Shadow(_) => None,
16386 }
16387}
16388
16389#[allow(clippy::too_many_arguments)]
16390fn collect_non_effect_segment_items(
16391 shapes: &[DrawShape],
16392 _images: &[ImageDraw],
16393 _texts: &[TextDraw],
16394 _shadow_draws: &[ShadowDraw],
16395 draw_ops: &[DrawOp],
16396 z_start: usize,
16397 z_end: usize,
16398 effect_z_ranges: &[Range<usize>],
16399 width: u32,
16400 height: u32,
16401 root_scale: f32,
16402 scratch: &mut Vec<(usize, SegmentDrawItem)>,
16403) {
16404 scratch.clear();
16405 let viewport = ViewportUniformParams {
16406 width,
16407 height,
16408 offset: [0.0, 0.0],
16409 };
16410
16411 scratch.extend(draw_ops.iter().filter_map(|op| {
16412 if op.z_index < z_start
16413 || op.z_index >= z_end
16414 || is_in_effect_range(op.z_index, effect_z_ranges)
16415 {
16416 return None;
16417 }
16418 let item = match op.kind {
16419 DrawOpKind::Shape(index) => {
16420 let shape = shapes.get(index)?;
16421 if !shape_draw_is_visible_in_viewport(shape, viewport, root_scale) {
16422 return None;
16423 }
16424 SegmentDrawItem::Shape(index)
16425 }
16426 DrawOpKind::Image(index) => SegmentDrawItem::Image(index),
16427 DrawOpKind::Text(index) => SegmentDrawItem::Text(index),
16428 DrawOpKind::Shadow(index) => SegmentDrawItem::Shadow(index),
16429 DrawOpKind::Retained(index) => SegmentDrawItem::Retained(index),
16430 };
16431 Some((op.z_index, item))
16432 }));
16433}
16434
16435fn retain_renderable_shadow_items(
16436 ordered_items: &mut Vec<(usize, SegmentDrawItem)>,
16437 shadow_draws: &[ShadowDraw],
16438 width: u32,
16439 height: u32,
16440 root_scale: f32,
16441 max_texture_dim: u32,
16442) -> usize {
16443 let original_len = ordered_items.len();
16444 ordered_items.retain(|(_, item)| match item {
16445 SegmentDrawItem::Shadow(index) => shadow_draws.get(*index).is_some_and(|shadow| {
16446 shadow_draw_may_render(shadow, width, height, root_scale, max_texture_dim)
16447 }),
16448 _ => true,
16449 });
16450 original_len.saturating_sub(ordered_items.len())
16451}
16452
16453#[cfg(not(target_arch = "wasm32"))]
16454#[derive(Clone, Copy)]
16455struct SegmentDiagCounts {
16456 raw_shadow_items: usize,
16457 culled_shadow_items: usize,
16458 cached_shadow_composites: usize,
16459 composite_items: usize,
16460 shader_composite_items: usize,
16461}
16462
16463#[cfg(not(target_arch = "wasm32"))]
16464fn maybe_print_segment_diag(
16465 z_range: Range<usize>,
16466 ordered_items: &[(usize, SegmentDrawItem)],
16467 shapes: &[DrawShape],
16468 brushes: &[Brush],
16469 images: &[ImageDraw],
16470 counts: SegmentDiagCounts,
16471 batch_limits: ShapeBatchLimits,
16472) {
16473 if !cranpose_core::env_flag!("CRANPOSE_SEGMENT_DIAG") {
16474 return;
16475 }
16476 let line = SEGMENT_DIAG_LINES.fetch_add(1, Ordering::Relaxed);
16477 if line >= 64 {
16478 return;
16479 }
16480
16481 let remaining_shadow_items = ordered_items
16482 .iter()
16483 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
16484 .count();
16485 let commands: Vec<_> =
16486 SegmentCommandIter::new(ordered_items, shapes, images, batch_limits).collect();
16487 let draw_chunks = commands
16488 .iter()
16489 .filter(|command| matches!(command, SegmentRenderCommand::DrawChunk(_)))
16490 .count();
16491 let shadow_commands = commands
16492 .iter()
16493 .filter(|command| matches!(command, SegmentRenderCommand::Shadow(_)))
16494 .count();
16495 let mut native_partitions = 0usize;
16496 let mut native_unfused_chunks = 0usize;
16497 for command in &commands {
16498 let SegmentRenderCommand::DrawChunk(chunk) = command else {
16499 continue;
16500 };
16501 match native_segment_fusion_partitions(ordered_items, shapes, brushes, chunk, batch_limits)
16502 {
16503 Ok(Some(partitions)) => native_partitions += partitions.len(),
16504 Ok(None) | Err(_) => native_unfused_chunks += 1,
16505 }
16506 }
16507
16508 eprintln!(
16509 "[segment-diag #{line}] z={}..{} items={} raw_shadows={} culled_shadows={} cached_shadows={} remaining_shadows={} composites={} shader_composites={} draw_chunks={} shadow_commands={} native_partitions={} native_unfused_chunks={}",
16510 z_range.start,
16511 z_range.end,
16512 ordered_items.len(),
16513 counts.raw_shadow_items,
16514 counts.culled_shadow_items,
16515 counts.cached_shadow_composites,
16516 remaining_shadow_items,
16517 counts.composite_items,
16518 counts.shader_composite_items,
16519 draw_chunks,
16520 shadow_commands,
16521 native_partitions,
16522 native_unfused_chunks,
16523 );
16524}
16525
16526pub(crate) fn has_backdrop_layer_in_range(
16527 backdrop_layers: &[BackdropLayer],
16528 z_start: usize,
16529 z_end: usize,
16530) -> bool {
16531 backdrop_layers
16532 .iter()
16533 .any(|layer| layer.z_index >= z_start && layer.z_index < z_end)
16534}
16535
16536pub(crate) fn scissor_rect_for_rect(
16537 rect: Rect,
16538 root_scale: f32,
16539 width: u32,
16540 height: u32,
16541) -> Option<(u32, u32, u32, u32)> {
16542 let mut left = canonicalize_device_coordinate(rect.x * root_scale);
16543 let mut top = canonicalize_device_coordinate(rect.y * root_scale);
16544 let mut right = canonicalize_device_coordinate((rect.x + rect.width) * root_scale);
16545 let mut bottom = canonicalize_device_coordinate((rect.y + rect.height) * root_scale);
16546
16547 left = left.max(0.0).min(width as f32).floor();
16548 top = top.max(0.0).min(height as f32).floor();
16549 right = right.max(0.0).min(width as f32).ceil();
16550 bottom = bottom.max(0.0).min(height as f32).ceil();
16551
16552 if right <= left || bottom <= top {
16553 return None;
16554 }
16555
16556 Some((
16557 left as u32,
16558 top as u32,
16559 (right - left) as u32,
16560 (bottom - top) as u32,
16561 ))
16562}
16563
16564fn scissor_rect_for_layer(
16565 rect: Rect,
16566 clip: Option<Rect>,
16567 root_scale: f32,
16568 width: u32,
16569 height: u32,
16570) -> Option<(u32, u32, u32, u32)> {
16571 let clipped_rect = match clip {
16572 Some(clip_rect) => rect.intersect(clip_rect)?,
16573 None => rect,
16574 };
16575
16576 scissor_rect_for_rect(clipped_rect, root_scale, width, height)
16577}
16578
16579fn tint_for_image(
16580 color_filter: Option<ColorFilter>,
16581 alpha: f32,
16582) -> ([f32; 4], Option<ColorFilter>) {
16583 let alpha = alpha.clamp(0.0, 1.0);
16584 match color_filter {
16585 Some(filter) if filter.supports_gpu_vertex_modulation() => {
16586 let Some(tint) = filter.gpu_vertex_tint() else {
16587 return ([1.0, 1.0, 1.0, alpha], Some(filter));
16588 };
16589 (
16590 [
16591 tint[0].clamp(0.0, 1.0),
16592 tint[1].clamp(0.0, 1.0),
16593 tint[2].clamp(0.0, 1.0),
16594 (tint[3] * alpha).clamp(0.0, 1.0),
16595 ],
16596 None,
16597 )
16598 }
16599 Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
16600 None => ([1.0, 1.0, 1.0, alpha], None),
16601 }
16602}
16603
16604fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
16605 let Some(src) = src_rect else {
16606 return Some(ImageUvRect {
16607 min: [0.0, 0.0],
16608 max: [1.0, 1.0],
16609 sample_bounds: [0.0, 0.0, 1.0, 1.0],
16610 });
16611 };
16612
16613 let (u_min, u_max, u_bound_min, u_bound_max) =
16614 source_axis_uv(src.x, src.width, image.width() as f32)?;
16615 let (v_min, v_max, v_bound_min, v_bound_max) =
16616 source_axis_uv(src.y, src.height, image.height() as f32)?;
16617
16618 Some(ImageUvRect {
16619 min: [u_min, v_min],
16620 max: [u_max, v_max],
16621 sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
16622 })
16623}
16624
16625fn glyph_atlas_uv_rect(entry: GlyphAtlasEntry, atlas_size: u32) -> ImageUvRect {
16630 let atlas_width = atlas_size as f32;
16631 let atlas_height = atlas_size as f32;
16632 let min = [entry.x as f32 / atlas_width, entry.y as f32 / atlas_height];
16633 let max = [
16634 (entry.x + entry.width) as f32 / atlas_width,
16635 (entry.y + entry.height) as f32 / atlas_height,
16636 ];
16637 let center_min = [
16638 (entry.x as f32 + 0.5) / atlas_width,
16639 (entry.y as f32 + 0.5) / atlas_height,
16640 ];
16641 let center_max = [
16642 (entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) / atlas_width,
16643 (entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) / atlas_height,
16644 ];
16645 ImageUvRect {
16646 min,
16647 max,
16648 sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
16649 }
16650}
16651
16652fn snap_nearest_image_to_device_pixels(image: &mut ImageDraw, root_scale: f32) {
16653 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
16654 return;
16655 }
16656
16657 let Some(rect) = axis_aligned_quad_rect(image.quad) else {
16658 return;
16659 };
16660
16661 let left_px = (rect.x * root_scale).round();
16662 let top_px = (rect.y * root_scale).round();
16663 let width_px = (rect.width * root_scale).round().max(1.0);
16664 let height_px = (rect.height * root_scale).round().max(1.0);
16665 let snapped = Rect {
16666 x: left_px / root_scale,
16667 y: top_px / root_scale,
16668 width: width_px / root_scale,
16669 height: height_px / root_scale,
16670 };
16671
16672 image.rect = snapped;
16673 image.local_rect = Rect {
16674 x: image.local_rect.x + snapped.x - rect.x,
16675 y: image.local_rect.y + snapped.y - rect.y,
16676 width: snapped.width,
16677 height: snapped.height,
16678 };
16679 image.quad = crate::rect_to_quad(snapped);
16680}
16681
16682fn nearest_image_device_quad(image: &ImageDraw, root_scale: f32) -> Option<[[f32; 2]; 4]> {
16683 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
16684 return None;
16685 }
16686
16687 let rect = axis_aligned_quad_rect(image.quad)?;
16688 let left_px = (rect.x * root_scale).round();
16689 let top_px = (rect.y * root_scale).round();
16690 let width_px = (rect.width * root_scale).round().max(1.0);
16691 let height_px = (rect.height * root_scale).round().max(1.0);
16692 let right_px = left_px + width_px;
16693 let bottom_px = top_px + height_px;
16694 Some([
16695 [left_px, top_px],
16696 [right_px, top_px],
16697 [left_px, bottom_px],
16698 [right_px, bottom_px],
16699 ])
16700}
16701
16702fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
16703 if !start.is_finite()
16704 || !extent.is_finite()
16705 || !image_extent.is_finite()
16706 || extent == 0.0
16707 || image_extent <= 0.0
16708 {
16709 return None;
16710 }
16711
16712 let end = start + extent;
16713 let edge_min = start.min(end).clamp(0.0, image_extent);
16714 let edge_max = start.max(end).clamp(0.0, image_extent);
16715 if edge_max <= edge_min {
16716 return None;
16717 }
16718
16719 let center_min = edge_min + 0.5;
16720 let center_max = edge_max - 0.5;
16721 let (bound_min, bound_max) = if center_min <= center_max {
16722 (center_min, center_max)
16723 } else {
16724 let center = (edge_min + edge_max) * 0.5;
16725 (center, center)
16726 };
16727
16728 Some((
16729 edge_min / image_extent,
16730 edge_max / image_extent,
16731 bound_min / image_extent,
16732 bound_max / image_extent,
16733 ))
16734}
16735
16736fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
16737 let mut filtered = Vec::with_capacity(image.pixels().len());
16738 for pixel in image.pixels().as_chunks::<4>().0 {
16739 let rgba = [
16740 pixel[0] as f32 / 255.0,
16741 pixel[1] as f32 / 255.0,
16742 pixel[2] as f32 / 255.0,
16743 pixel[3] as f32 / 255.0,
16744 ];
16745 let out = filter.apply_rgba(rgba);
16746 filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
16747 filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
16748 filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
16749 filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
16750 }
16751 ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
16752 .map_err(|error| format!("failed to build filtered bitmap: {error}"))
16753}
16754
16755fn scissor_rect_for_image(
16756 image: &ImageDraw,
16757 root_scale: f32,
16758 width: u32,
16759 height: u32,
16760) -> Option<(u32, u32, u32, u32)> {
16761 scissor_rect_for_layer(image.rect, image.clip, root_scale, width, height)
16762}
16763
16764fn inner_shadow_composite_mask(
16765 shadow: &ShadowDraw,
16766 root_scale: f32,
16767) -> Option<RoundedCompositeMask> {
16768 if !shadow
16769 .shapes
16770 .iter()
16771 .any(|(_, mode)| *mode == BlendMode::DstOut)
16772 {
16773 return None;
16774 }
16775 let (fill, _) = shadow.shapes.first()?;
16776 let rect = fill.local_rect;
16777 if rect.width <= 0.0 || rect.height <= 0.0 {
16778 return None;
16779 }
16780
16781 let radii = fill.shape.map_or([0.0; 4], |rounded| {
16782 let resolved = rounded.resolve(rect.width, rect.height);
16783 [
16784 resolved.top_left * root_scale,
16785 resolved.top_right * root_scale,
16786 resolved.bottom_left * root_scale,
16787 resolved.bottom_right * root_scale,
16788 ]
16789 });
16790
16791 Some(RoundedCompositeMask {
16792 rect: [
16793 rect.x * root_scale,
16794 rect.y * root_scale,
16795 rect.width * root_scale,
16796 rect.height * root_scale,
16797 ],
16798 radii,
16799 })
16800}
16801
16802#[cfg(test)]
16803mod shape_batch_limits_tests {
16804 use super::*;
16805
16806 fn generous_limits() -> wgpu::Limits {
16809 wgpu::Limits {
16810 max_storage_buffers_per_shader_stage: 8,
16811 max_storage_buffer_binding_size: 128 << 20,
16812 max_uniform_buffer_binding_size: 16 << 10,
16813 ..wgpu::Limits::default()
16814 }
16815 }
16816
16817 #[test]
16818 fn a_device_without_vertex_storage_takes_the_uniform_path() {
16819 let limits = ShapeBatchLimits::select(&generous_limits(), wgpu::DownlevelFlags::empty());
16825 assert!(
16826 !limits.storage,
16827 "no VERTEX_STORAGE must mean uniform mode, whatever the limit says"
16828 );
16829 }
16830
16831 #[test]
16832 fn a_device_with_vertex_storage_still_takes_the_storage_path() {
16833 let limits = ShapeBatchLimits::select(&generous_limits(), wgpu::DownlevelFlags::all());
16834 assert!(
16835 limits.storage,
16836 "the flag must not cost storage mode on a device that has it"
16837 );
16838 }
16839
16840 #[test]
16841 fn the_limit_still_gates_storage_when_the_flag_is_present() {
16842 let mut limits = generous_limits();
16843 limits.max_storage_buffers_per_shader_stage = 1;
16844 let limits = ShapeBatchLimits::select(&limits, wgpu::DownlevelFlags::all());
16845 assert!(!limits.storage, "two bindings are needed, not one");
16846 }
16847}
16848
16849#[cfg(test)]
16850mod tests {
16851 use super::*;
16852 use crate::normalized_scene::visible_draw_rect;
16853 use cranpose_foundation::lazy::{remember_lazy_list_state, LazyListScope, LazyListState};
16854 use cranpose_render_common::graph::{DrawPrimitiveNode, IsolationReasons, TextPrimitiveNode};
16855 use cranpose_render_common::raster_cache::LayerRasterCacheHashes;
16856 use cranpose_render_common::scene_builder::build_graph_from_applier;
16857 use cranpose_ui::text::{
16858 AnnotatedString, BaselineShift, RangeStyle, Shadow, SpanStyle, TextDecoration,
16859 TextDrawStyle, TextGeometricTransform, TextMotion, TextUnit,
16860 };
16861 use cranpose_ui::{
16862 LayoutEngine, LazyColumn, LazyColumnSpec, Modifier, Size, Text, TextLayoutOptions,
16863 TextStyle,
16864 };
16865 use cranpose_ui_graphics::{
16866 Brush, Color, CornerRadii, DrawPrimitive, Rect, RenderEffect, RoundedCornerShape,
16867 RuntimeShader,
16868 };
16869
16870 fn chunk(batches: &[SegmentBatchPlan]) -> SegmentDrawChunkPlan {
16871 let mut chunk = SegmentDrawChunkPlan::default();
16872 for batch in batches {
16873 chunk.push(*batch);
16874 }
16875 chunk
16876 }
16877
16878 fn with_test_app_context<R>(block: impl FnOnce() -> R) -> R {
16879 let app_context = cranpose_ui::AppContext::new();
16880 app_context.enter(block)
16881 }
16882
16883 fn assert_snap_anchor_close(actual: Option<SnapAnchor>, expected_origin: Point, message: &str) {
16884 let Some(actual) = actual else {
16885 panic!("{message}: missing snap anchor");
16886 };
16887 let expected = SnapAnchor::rigid(expected_origin);
16888 assert_eq!(
16889 actual.device_pixel_step, expected.device_pixel_step,
16890 "{message}: device pixel step changed"
16891 );
16892 assert!(
16893 (actual.origin.x - expected.origin.x).abs() <= 1e-4
16894 && (actual.origin.y - expected.origin.y).abs() <= 1e-4,
16895 "{message}: expected origin {:?}, got {:?}",
16896 expected.origin,
16897 actual.origin
16898 );
16899 }
16900
16901 fn effect_layer(z_start: usize, z_end: usize) -> EffectLayer {
16902 EffectLayer {
16903 rect: Rect {
16904 x: 0.0,
16905 y: 0.0,
16906 width: 10.0,
16907 height: 10.0,
16908 },
16909 clip: None,
16910 snap_anchor: None,
16911 effect: Some(RenderEffect::blur(4.0)),
16912 blend_mode: BlendMode::SrcOver,
16913 composite_alpha: 1.0,
16914 z_start,
16915 z_end,
16916 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
16917 }
16918 }
16919
16920 #[test]
16921 fn direct_shader_composite_accepts_box4_when_viewport_preserves_source_pixels() {
16922 assert_eq!(
16923 direct_shader_composite_viewport(
16924 1.0,
16925 BlendMode::SrcOver,
16926 Some((12.0, 18.0, 64.0, 32.0)),
16927 CompositeSampleMode::Box4,
16928 (64, 32),
16929 ),
16930 Some((12.0, 18.0, 64.0, 32.0))
16931 );
16932 }
16933
16934 #[test]
16935 fn direct_shader_composite_rejects_box4_when_viewport_resamples_source() {
16936 assert_eq!(
16937 direct_shader_composite_viewport(
16938 1.0,
16939 BlendMode::SrcOver,
16940 Some((12.0, 18.0, 64.5, 32.0)),
16941 CompositeSampleMode::Box4,
16942 (64, 32),
16943 ),
16944 None
16945 );
16946 assert_eq!(
16947 direct_shader_composite_viewport(
16948 1.0,
16949 BlendMode::SrcOver,
16950 Some((12.25, 18.0, 64.0, 32.0)),
16951 CompositeSampleMode::Box4,
16952 (64, 32),
16953 ),
16954 None
16955 );
16956 }
16957
16958 fn test_text_draw(rect: Rect, text_motion: TextMotion) -> TextDraw {
16959 let mut text_style = TextStyle::default();
16960 text_style.paragraph_style.text_motion = Some(text_motion);
16961 TextDraw {
16962 node_id: 42,
16963 rect,
16964 snap_anchor: None,
16965 translated_content_context: false,
16966 text: Arc::new(AnnotatedString::new("stable markdown row".to_string()).render_string()),
16967 color: Color::WHITE,
16968 text_style,
16969 font_size: 14.0,
16970 scale: 1.0,
16971 layout_options: TextLayoutOptions::default(),
16972 z_index: 0,
16973 clip: None,
16974 }
16975 }
16976
16977 #[test]
16978 fn static_text_image_cache_key_ignores_absolute_scroll_position() {
16979 let base = test_text_draw(
16980 Rect {
16981 x: 12.25,
16982 y: 40.75,
16983 width: 220.0,
16984 height: 24.0,
16985 },
16986 TextMotion::Static,
16987 );
16988 let scrolled = test_text_draw(
16989 Rect {
16990 x: 12.75,
16991 y: -318.5,
16992 width: 220.0,
16993 height: 24.0,
16994 },
16995 TextMotion::Static,
16996 );
16997
16998 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, true);
16999 let scrolled_key = GpuRenderer::text_image_cache_key(&scrolled, scrolled.rect, 1.0, true);
17000
17001 assert_eq!(
17002 base_key, scrolled_key,
17003 "scrolling static text must reuse the same raster cache entry"
17004 );
17005 }
17006
17007 #[test]
17008 fn static_text_glyph_run_cache_key_ignores_absolute_scroll_position() {
17009 let base = test_text_draw(
17010 Rect {
17011 x: 12.25,
17012 y: 40.75,
17013 width: 220.0,
17014 height: 24.0,
17015 },
17016 TextMotion::Static,
17017 );
17018 let scrolled = test_text_draw(
17019 Rect {
17020 x: 12.75,
17021 y: -318.5,
17022 width: 220.0,
17023 height: 24.0,
17024 },
17025 TextMotion::Static,
17026 );
17027
17028 let base_key = GpuRenderer::text_glyph_run_cache_key(&base, base.rect, 1.0, true);
17029 let scrolled_key =
17030 GpuRenderer::text_glyph_run_cache_key(&scrolled, scrolled.rect, 1.0, true);
17031
17032 assert_eq!(
17033 base_key, scrolled_key,
17034 "scrolling static text must reuse the same retained glyph run"
17035 );
17036 }
17037
17038 #[test]
17039 fn static_multiline_text_glyph_source_keeps_full_text_when_image_source_slices() {
17040 let rect = Rect {
17041 x: 8.0,
17042 y: 100.0,
17043 width: 240.0,
17044 height: 1_000.0,
17045 };
17046 let mut draw = test_text_draw(rect, TextMotion::Static);
17047 let lines = (0..100)
17048 .map(|line| format!("line-{line:03}"))
17049 .collect::<Vec<_>>()
17050 .join("\n");
17051 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
17052
17053 let raster_rect = Rect {
17054 x: 16.0,
17055 y: 200.0,
17056 width: 480.0,
17057 height: 2_000.0,
17058 };
17059 let clipped = clipped_text_raster_source(
17060 &draw,
17061 rect,
17062 raster_rect,
17063 Some(Rect {
17064 x: 0.0,
17065 y: 610.0,
17066 width: 800.0,
17067 height: 40.0,
17068 }),
17069 2.0,
17070 true,
17071 );
17072 let glyph = text_glyph_raster_source(&draw, raster_rect);
17073
17074 assert!(
17075 matches!(clipped.draw, Cow::Owned(_)),
17076 "the image source should still slice large clipped multiline text"
17077 );
17078 assert!(
17079 matches!(glyph.draw, Cow::Borrowed(_)),
17080 "the glyph source must keep a stable full-text run key while scrolling"
17081 );
17082
17083 let clipped_key = GpuRenderer::text_glyph_run_cache_key(
17084 clipped.draw.as_ref(),
17085 clipped.raster_rect,
17086 2.0,
17087 true,
17088 );
17089 let glyph_key = GpuRenderer::text_glyph_run_cache_key(
17090 glyph.draw.as_ref(),
17091 glyph.raster_rect,
17092 2.0,
17093 true,
17094 );
17095
17096 assert_ne!(
17097 clipped_key, glyph_key,
17098 "image slicing must not force glyph rendering onto per-scroll line-window cache keys"
17099 );
17100 }
17101
17102 #[cfg(not(target_arch = "wasm32"))]
17103 #[test]
17104 fn retained_glyph_viewport_offsets_relative_vertices_by_source_origin() {
17105 let viewport = ViewportUniformParams {
17106 width: 800,
17107 height: 600,
17108 offset: [10.0, 20.0],
17109 };
17110 let source = Rect {
17111 x: 40.0,
17112 y: 90.0,
17113 width: 120.0,
17114 height: 48.0,
17115 };
17116
17117 let retained = GpuRenderer::retained_glyph_viewport(viewport, source);
17118
17119 assert_eq!(retained.width, viewport.width);
17120 assert_eq!(retained.height, viewport.height);
17121 assert_eq!(retained.offset, [-30.0, -70.0]);
17122 }
17123
17124 #[cfg(not(target_arch = "wasm32"))]
17125 #[test]
17126 fn tiny_text_glyph_runs_stay_in_shared_uploads() {
17127 assert!(
17128 !should_use_retained_text_glyph_run(8, None),
17129 "tiny labels must stay in the shared fused batch"
17130 );
17131 }
17132
17133 #[cfg(not(target_arch = "wasm32"))]
17134 #[test]
17135 fn line_sized_text_glyph_runs_stay_in_shared_uploads() {
17136 assert!(
17137 !should_use_retained_text_glyph_run(64, None),
17138 "Markdown scroll frames contain many line-sized text runs; retaining each one creates per-run buffer binds instead of one shared glyph batch"
17139 );
17140 }
17141
17142 #[cfg(not(target_arch = "wasm32"))]
17143 #[test]
17144 fn large_clipped_text_glyph_runs_stay_in_shared_uploads() {
17145 assert!(
17146 !should_use_retained_text_glyph_run(
17147 MIN_RETAINED_TEXT_GLYPH_QUADS.saturating_mul(2),
17148 Some(Rect {
17149 x: 0.0,
17150 y: 0.0,
17151 width: 200.0,
17152 height: 100.0,
17153 }),
17154 ),
17155 "clipped lazy-list text must not draw a full retained run outside the viewport"
17156 );
17157 }
17158
17159 #[test]
17160 fn normal_text_glyph_draw_skips_offscreen_prewarm_candidates() {
17161 assert_eq!(
17162 text_glyph_draw_action(false, true, false),
17163 TextGlyphDrawAction::Skip,
17164 "normal draw traversal must not prepare offscreen text"
17165 );
17166 }
17167
17168 #[test]
17169 fn bounded_text_glyph_prewarm_admits_offscreen_candidates() {
17170 assert_eq!(
17171 text_glyph_draw_action(false, true, true),
17172 TextGlyphDrawAction::PrewarmOffscreen,
17173 "only the bounded prewarm path may prepare offscreen text"
17174 );
17175 }
17176
17177 #[test]
17178 fn visible_text_glyph_draws_are_always_admitted() {
17179 assert_eq!(
17180 text_glyph_draw_action(true, false, false),
17181 TextGlyphDrawAction::DrawVisible
17182 );
17183 assert_eq!(
17184 text_glyph_draw_action(true, true, true),
17185 TextGlyphDrawAction::DrawVisible
17186 );
17187 }
17188
17189 #[cfg(not(target_arch = "wasm32"))]
17190 #[test]
17191 fn offscreen_text_prewarm_skips_large_uncached_text_runs() {
17192 assert!(
17193 !offscreen_text_glyph_prewarm_work_is_bounded(
17194 None,
17195 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS + 1,
17196 ),
17197 "offscreen prewarm must not collect large uncached text runs in an input frame"
17198 );
17199 }
17200
17201 #[cfg(not(target_arch = "wasm32"))]
17202 #[test]
17203 fn offscreen_text_prewarm_admits_small_uncached_text_runs() {
17204 assert!(
17205 offscreen_text_glyph_prewarm_work_is_bounded(
17206 None,
17207 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
17208 ),
17209 "small labels can be warmed without risking a frame-budget spike"
17210 );
17211 }
17212
17213 #[cfg(not(target_arch = "wasm32"))]
17214 #[test]
17215 fn offscreen_text_prewarm_skips_large_cached_runs_without_quads() {
17216 assert!(
17217 !offscreen_text_glyph_prewarm_work_is_bounded(
17218 Some(MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS + 1),
17219 0,
17220 ),
17221 "cached glyph placements can still be too large to prepare during input frames"
17222 );
17223 }
17224
17225 #[cfg(not(target_arch = "wasm32"))]
17226 #[test]
17227 fn offscreen_text_prewarm_stops_after_candidate_budget() {
17228 assert!(
17229 offscreen_text_glyph_prewarm_budget_exhausted(
17230 Instant::now(),
17231 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES,
17232 ),
17233 "prewarm must be bounded by candidate count even when each candidate is cheap"
17234 );
17235 }
17236
17237 #[test]
17238 fn clipped_cached_glyph_quads_are_filtered_to_viewport() {
17239 fn quad(y: i32) -> CachedTextGlyphQuad {
17240 CachedTextGlyphQuad {
17241 x: 8,
17242 y,
17243 width: 20,
17244 height: 10,
17245 color: (1.0, 1.0, 1.0, 1.0),
17246 uv: ImageUvRect {
17247 min: [0.0, 0.0],
17248 max: [1.0, 1.0],
17249 sample_bounds: [0.0, 0.0, 1.0, 1.0],
17250 },
17251 }
17252 }
17253
17254 let source = Rect {
17255 x: 0.0,
17256 y: 0.0,
17257 width: 320.0,
17258 height: 400.0,
17259 };
17260 let clip = Some(Rect {
17261 x: 0.0,
17262 y: 0.0,
17263 width: 320.0,
17264 height: 80.0,
17265 });
17266 let viewport = ViewportUniformParams {
17267 width: 320,
17268 height: 80,
17269 offset: [0.0, 0.0],
17270 };
17271
17272 assert!(cached_text_glyph_quad_is_visible_in_viewport(
17273 source,
17274 &quad(40),
17275 clip,
17276 viewport,
17277 1.0,
17278 ));
17279 assert!(
17280 !cached_text_glyph_quad_is_visible_in_viewport(source, &quad(140), clip, viewport, 1.0,),
17281 "glyphs outside the effective clip should not enter the frame command stream"
17282 );
17283 }
17284
17285 #[test]
17286 fn small_scene_range_cache_miss_observes_first_render() {
17287 let key = LayerRasterCacheKey::scene_range(
17288 0xCACE,
17289 Rect {
17290 x: 0.0,
17291 y: 0.0,
17292 width: 120.0,
17293 height: 80.0,
17294 },
17295 (120, 80),
17296 ScaleBucket::from_scale(1.0),
17297 );
17298
17299 assert!(
17300 !first_cache_miss_admission(&key),
17301 "a small scene-range miss should render directly first instead of materializing a tiny one-frame retained target"
17302 );
17303 assert!(
17304 repeated_cache_miss_admission(&key),
17305 "a repeated small scene-range miss is stable enough to materialize into the retained cache"
17306 );
17307 }
17308
17309 #[test]
17310 fn large_scene_range_cache_miss_requires_repeated_stable_key() {
17311 let key = LayerRasterCacheKey::scene_range(
17312 0xCACE,
17313 Rect {
17314 x: 0.0,
17315 y: 0.0,
17316 width: 1200.0,
17317 height: 900.0,
17318 },
17319 (1200, 900),
17320 ScaleBucket::from_scale(1.0),
17321 );
17322
17323 assert!(
17324 !first_cache_miss_admission(&key),
17325 "a large first scene-range miss should render directly instead of materializing a multi-MB one-frame cache entry"
17326 );
17327 assert!(
17328 repeated_cache_miss_admission(&key),
17329 "a repeated scene-range miss is stable enough to materialize into the retained cache"
17330 );
17331 }
17332
17333 #[test]
17334 fn renderer_warmup_frame_is_requested_for_cache_miss_stats_only() {
17335 let stats = gpu_stats::FrameStats::default();
17336 let mut snapshot = stats.snapshot();
17337 assert!(
17338 !frame_stats_need_warmup_frame(&snapshot),
17339 "a clean frame must not keep a static scene redrawing"
17340 );
17341
17342 snapshot.layer_cache_misses = 1;
17343 assert!(frame_stats_need_warmup_frame(&snapshot));
17344 snapshot.layer_cache_misses = 0;
17345
17346 snapshot.shadow_shape_cache_misses = 1;
17347 assert!(frame_stats_need_warmup_frame(&snapshot));
17348 snapshot.shadow_shape_cache_misses = 0;
17349
17350 snapshot.text_image_cache_misses = 1;
17351 assert!(frame_stats_need_warmup_frame(&snapshot));
17352 snapshot.text_image_cache_misses = 0;
17353
17354 snapshot.text_glyph_atlas_misses = 1;
17355 assert!(frame_stats_need_warmup_frame(&snapshot));
17356 }
17357
17358 #[test]
17359 fn renderer_warmup_budget_is_consumed_by_a_repeated_cache_miss() {
17360 let stats = gpu_stats::FrameStats::default();
17361 let mut snapshot = stats.snapshot();
17362 snapshot.layer_cache_misses = 1;
17363 let mut pending_frames = 0;
17364
17365 update_frame_warmup_budget(&mut pending_frames, &snapshot);
17366 assert_eq!(pending_frames, CACHE_MISS_WARMUP_FRAMES);
17367
17368 update_frame_warmup_budget(&mut pending_frames, &snapshot);
17369 assert_eq!(
17370 pending_frames, 0,
17371 "a cache miss during the warmup frame must not replenish its budget"
17372 );
17373 }
17374
17375 #[test]
17376 fn non_scene_layer_surface_cache_miss_admits_first_render() {
17377 let key = LayerRasterCacheKey::new(
17378 Some(77),
17379 0xC0FFEE,
17380 0,
17381 Rect {
17382 x: 0.0,
17383 y: 0.0,
17384 width: 120.0,
17385 height: 80.0,
17386 },
17387 (120, 80),
17388 ScaleBucket::from_scale(1.0),
17389 );
17390
17391 assert!(
17392 first_cache_miss_admission(&key),
17393 "ordinary retained layer surfaces should still cache on first miss"
17394 );
17395 }
17396
17397 #[test]
17398 fn text_image_cache_key_is_content_addressed_not_node_addressed() {
17399 let first = test_text_draw(
17400 Rect {
17401 x: 12.25,
17402 y: 40.75,
17403 width: 220.0,
17404 height: 24.0,
17405 },
17406 TextMotion::Static,
17407 );
17408 let mut second = first.clone();
17409 second.node_id = first.node_id + 1;
17410
17411 let first_key = GpuRenderer::text_image_cache_key(&first, first.rect, 1.0, true);
17412 let second_key = GpuRenderer::text_image_cache_key(&second, second.rect, 1.0, true);
17413
17414 assert_eq!(
17415 first_key, second_key,
17416 "text raster cache keys must be based on rendered pixels, not node identity"
17417 );
17418 }
17419
17420 #[test]
17421 fn animated_text_image_cache_key_keeps_fractional_phase_only() {
17422 let base = test_text_draw(
17423 Rect {
17424 x: 12.25,
17425 y: 40.75,
17426 width: 220.0,
17427 height: 24.0,
17428 },
17429 TextMotion::Animated,
17430 );
17431 let integer_translated = test_text_draw(
17432 Rect {
17433 x: 44.25,
17434 y: 88.75,
17435 width: 220.0,
17436 height: 24.0,
17437 },
17438 TextMotion::Animated,
17439 );
17440 let phase_shifted = test_text_draw(
17441 Rect {
17442 x: 44.5,
17443 y: 88.75,
17444 width: 220.0,
17445 height: 24.0,
17446 },
17447 TextMotion::Animated,
17448 );
17449
17450 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, false);
17451 let translated_key = GpuRenderer::text_image_cache_key(
17452 &integer_translated,
17453 integer_translated.rect,
17454 1.0,
17455 false,
17456 );
17457 let phase_shifted_key =
17458 GpuRenderer::text_image_cache_key(&phase_shifted, phase_shifted.rect, 1.0, false);
17459
17460 assert_eq!(
17461 base_key, translated_key,
17462 "integer translation should not invalidate animated text raster cache entries"
17463 );
17464 assert_ne!(
17465 base_key, phase_shifted_key,
17466 "fractional phase affects animated text rasterization and must stay in the key"
17467 );
17468 }
17469
17470 #[test]
17471 fn animated_translated_text_raster_geometry_applies_snap_anchor() {
17472 let mut base = test_text_draw(
17473 Rect {
17474 x: 14.25,
17475 y: 16.50,
17476 width: 220.0,
17477 height: 24.0,
17478 },
17479 TextMotion::Animated,
17480 );
17481 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.50)));
17482
17483 let mut scrolled = test_text_draw(
17484 Rect {
17485 x: 14.25,
17486 y: 15.80,
17487 width: 220.0,
17488 height: 24.0,
17489 },
17490 TextMotion::Animated,
17491 );
17492 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 15.80)));
17493
17494 let (base_logical, base_raster, _, _, base_static) =
17495 text_raster_geometry_for_draw(&base, 1.0).expect("base text geometry");
17496 let (scrolled_logical, scrolled_raster, _, _, scrolled_static) =
17497 text_raster_geometry_for_draw(&scrolled, 1.0).expect("scrolled text geometry");
17498
17499 assert!(!base_static);
17500 assert!(!scrolled_static);
17501 assert!((base_logical.x - 14.0).abs() < f32::EPSILON);
17502 assert!((base_logical.y - 17.0).abs() < f32::EPSILON);
17503 assert!((scrolled_logical.x - 14.0).abs() < f32::EPSILON);
17504 assert!((scrolled_logical.y - 16.0).abs() < f32::EPSILON);
17505 assert_eq!(base_raster.x.fract(), 0.0);
17506 assert_eq!(base_raster.y.fract(), 0.0);
17507 assert_eq!(scrolled_raster.x.fract(), 0.0);
17508 assert_eq!(scrolled_raster.y.fract(), 0.0);
17509
17510 let base_key = GpuRenderer::text_image_cache_key(&base, base_raster, 1.0, false);
17511 let scrolled_key =
17512 GpuRenderer::text_image_cache_key(&scrolled, scrolled_raster, 1.0, false);
17513 assert_eq!(
17514 base_key, scrolled_key,
17515 "translated animated text should keep a stable raster phase while scrolling"
17516 );
17517 }
17518
17519 #[test]
17520 fn translated_static_text_moves_one_device_pixel_at_half_pixel_phase() {
17521 let root_scale = 1.25;
17522 let mut base = test_text_draw(
17523 Rect {
17524 x: 14.0,
17525 y: 276.0,
17526 width: 220.0,
17527 height: 24.0,
17528 },
17529 TextMotion::Static,
17530 );
17531 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.600_006)));
17532
17533 let mut scrolled = test_text_draw(
17534 Rect {
17535 x: 14.0,
17536 y: 275.2,
17537 width: 220.0,
17538 height: 24.0,
17539 },
17540 TextMotion::Static,
17541 );
17542 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 126.799_99)));
17543
17544 let (_, base_raster, _, _, _) =
17545 text_raster_geometry_for_draw(&base, root_scale).expect("base text geometry");
17546 let (_, scrolled_raster, _, _, _) =
17547 text_raster_geometry_for_draw(&scrolled, root_scale).expect("scrolled text geometry");
17548
17549 assert_eq!(
17550 base_raster.y - scrolled_raster.y,
17551 1.0,
17552 "one physical pixel of rigid scrolling must move static text by one raster pixel"
17553 );
17554 }
17555
17556 #[test]
17557 fn translated_text_snap_does_not_move_its_fixed_ancestor_clip() {
17558 let root_scale = 1.25;
17559 let fixed_clip = Rect {
17560 x: 8.0,
17561 y: 20.0,
17562 width: 300.0,
17563 height: 680.0,
17564 };
17565 let mut draw = test_text_draw(
17566 Rect {
17567 x: 14.0,
17568 y: 276.0,
17569 width: 220.0,
17570 height: 24.0,
17571 },
17572 TextMotion::Static,
17573 );
17574 draw.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.4)));
17575 draw.clip = Some(fixed_clip);
17576
17577 let (_, _, clip, _, _) =
17578 text_raster_geometry_for_draw(&draw, root_scale).expect("clipped text geometry");
17579
17580 assert_eq!(
17581 clip,
17582 Some(fixed_clip),
17583 "content pixel snapping must not translate a fixed ancestor clip"
17584 );
17585 }
17586
17587 #[test]
17588 fn clipped_static_multiline_text_raster_source_limits_visible_line_window() {
17589 let rect = Rect {
17590 x: 8.0,
17591 y: 100.0,
17592 width: 240.0,
17593 height: 1_000.0,
17594 };
17595 let mut draw = test_text_draw(rect, TextMotion::Static);
17596 let lines = (0..100)
17597 .map(|line| format!("line-{line:03}"))
17598 .collect::<Vec<_>>()
17599 .join("\n");
17600 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
17601
17602 let raster_rect = Rect {
17603 x: 16.0,
17604 y: 200.0,
17605 width: 480.0,
17606 height: 2_000.0,
17607 };
17608 let source = clipped_text_raster_source(
17609 &draw,
17610 rect,
17611 raster_rect,
17612 Some(Rect {
17613 x: 0.0,
17614 y: 610.0,
17615 width: 800.0,
17616 height: 40.0,
17617 }),
17618 2.0,
17619 true,
17620 );
17621
17622 let Cow::Owned(sliced_draw) = source.draw else {
17623 panic!("clipped static multiline text should rasterize only the visible line window");
17624 };
17625 let sliced_text = sliced_draw.text.text.as_str();
17626 assert!(sliced_text.contains("line-050"));
17627 assert!(sliced_text.contains("line-055"));
17628 assert!(!sliced_text.contains("line-000"));
17629 assert!(!sliced_text.contains("line-099"));
17630 assert_eq!(source.raster_rect.x, raster_rect.x);
17631 assert!(source.raster_rect.y > raster_rect.y);
17632 assert!(source.raster_rect.height < raster_rect.height);
17633 }
17634
17635 #[test]
17636 fn clipped_static_multiline_text_raster_source_slices_short_multiline_text() {
17637 let rect = Rect {
17638 x: 8.0,
17639 y: 100.0,
17640 width: 240.0,
17641 height: 320.0,
17642 };
17643 let mut draw = test_text_draw(rect, TextMotion::Static);
17644 let lines = (0..24)
17645 .map(|line| format!("code-line-{line:02}"))
17646 .collect::<Vec<_>>()
17647 .join("\n");
17648 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
17649
17650 let raster_rect = Rect {
17651 x: 16.0,
17652 y: 200.0,
17653 width: 480.0,
17654 height: 640.0,
17655 };
17656 let source = clipped_text_raster_source(
17657 &draw,
17658 rect,
17659 raster_rect,
17660 Some(Rect {
17661 x: 0.0,
17662 y: 190.0,
17663 width: 800.0,
17664 height: 120.0,
17665 }),
17666 2.0,
17667 true,
17668 );
17669
17670 let Cow::Owned(sliced_draw) = source.draw else {
17671 panic!("clipped multiline text should rasterize only the visible line window");
17672 };
17673 assert!(sliced_draw.text.text.as_str().contains("code-line-06"));
17674 assert!(!sliced_draw.text.text.as_str().contains("code-line-00"));
17675 assert!(!sliced_draw.text.text.as_str().contains("code-line-23"));
17676 assert_eq!(source.raster_rect.x, raster_rect.x);
17677 assert!(source.raster_rect.y > raster_rect.y);
17678 assert!(source.raster_rect.height < raster_rect.height);
17679 }
17680
17681 #[test]
17682 fn text_line_index_cache_reuses_retained_index_for_same_text_instance() {
17683 let mut cache = TextLineIndexCache::new(4);
17684 let text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
17685
17686 let first = cache.line_starts(&text);
17687 let second = cache.line_starts(&text);
17688
17689 assert_eq!(first.as_ref(), &[0, 2, 4]);
17690 assert!(
17691 Rc::ptr_eq(&first, &second),
17692 "retained text should not rebuild its line index on every clipped frame"
17693 );
17694 }
17695
17696 #[test]
17697 fn text_line_index_cache_is_retained_text_instance_local() {
17698 let mut cache = TextLineIndexCache::new(4);
17699 let first_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
17700 let second_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
17701
17702 let first = cache.line_starts(&first_text);
17703 let second = cache.line_starts(&second_text);
17704
17705 assert_eq!(first.as_ref(), second.as_ref());
17706 assert!(
17707 !Rc::ptr_eq(&first, &second),
17708 "line index lookup should not hash large text contents to find unrelated retained nodes"
17709 );
17710 }
17711
17712 #[test]
17713 fn device_pixel_bounds_for_rect_snaps_origin_and_extents() {
17714 let bounds = device_pixel_bounds_for_rect(
17715 Rect {
17716 x: 10.25,
17717 y: 14.6,
17718 width: 20.1,
17719 height: 9.2,
17720 },
17721 200,
17722 120,
17723 2.0,
17724 )
17725 .expect("rect should intersect the viewport");
17726
17727 assert_eq!(
17728 bounds,
17729 DevicePixelBounds {
17730 x: 20.0,
17731 y: 29.0,
17732 width: 41,
17733 height: 19,
17734 }
17735 );
17736 }
17737
17738 #[test]
17739 fn visible_layer_rect_intersects_clip_and_viewport() {
17740 let visible = visible_layer_rect(
17741 Rect {
17742 x: -10.0,
17743 y: 5.0,
17744 width: 80.0,
17745 height: 40.0,
17746 },
17747 Some(Rect {
17748 x: 4.0,
17749 y: 8.0,
17750 width: 20.0,
17751 height: 50.0,
17752 }),
17753 2.0,
17754 60,
17755 40,
17756 )
17757 .expect("visible rect");
17758
17759 assert_eq!(
17760 visible,
17761 Rect {
17762 x: 4.0,
17763 y: 8.0,
17764 width: 20.0,
17765 height: 12.0,
17766 }
17767 );
17768 }
17769
17770 #[test]
17771 fn clamp_effect_surface_scale_caps_large_surfaces_but_keeps_base_scale() {
17772 let clamped = clamp_effect_surface_scale(
17773 Rect {
17774 x: 0.0,
17775 y: 0.0,
17776 width: 1200.0,
17777 height: 900.0,
17778 },
17779 1.0,
17780 8.0,
17781 16_384,
17782 );
17783
17784 assert!(
17785 clamped < 8.0,
17786 "large translated effect layers must be capped to avoid OOM, got {clamped}"
17787 );
17788 assert!(
17789 clamped >= 1.0,
17790 "effect surfaces must not fall below destination resolution, got {clamped}"
17791 );
17792 }
17793
17794 #[test]
17795 fn clamp_effect_surface_scale_keeps_decorated_text_capture_scale() {
17796 let clamped = clamp_effect_surface_scale(
17797 Rect {
17798 x: 0.0,
17799 y: 0.0,
17800 width: 446.0,
17801 height: 44.0,
17802 },
17803 1.0,
17804 9.0,
17805 16_384,
17806 );
17807
17808 assert_eq!(
17809 clamped, 9.0,
17810 "decorated text motion-stable captures must keep full scale"
17811 );
17812 }
17813
17814 fn backdrop_layer(z_index: usize) -> BackdropLayer {
17815 BackdropLayer {
17816 node_id: Some(700 + z_index),
17817 rect: Rect {
17818 x: 0.0,
17819 y: 0.0,
17820 width: 10.0,
17821 height: 10.0,
17822 },
17823 clip: None,
17824 snap_anchor: None,
17825 effect: RenderEffect::blur(2.0),
17826 z_index,
17827 }
17828 }
17829
17830 fn test_shape(z_index: usize, blend_mode: BlendMode) -> DrawShape {
17831 DrawShape {
17832 rect: Rect {
17833 x: 0.0,
17834 y: 0.0,
17835 width: 8.0,
17836 height: 8.0,
17837 },
17838 local_rect: Rect {
17839 x: 0.0,
17840 y: 0.0,
17841 width: 8.0,
17842 height: 8.0,
17843 },
17844 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
17845 snap_anchor: None,
17846 brush: SceneBrush::Solid(Color::BLACK),
17847 shape: None,
17848 stroke: None,
17849 arc: None,
17850 z_index,
17851 clip: None,
17852 blend_mode,
17853 motion_context_animated: false,
17854 }
17855 }
17856
17857 #[test]
17858 fn shape_shadow_content_hash_ignores_viewport_translation() {
17859 fn translate_shape(shape: &DrawShape, dx: f32, dy: f32) -> DrawShape {
17860 let mut translated = *shape;
17861 translated.rect.x += dx;
17862 translated.rect.y += dy;
17863 translated.local_rect.x += dx;
17864 translated.local_rect.y += dy;
17865 for point in &mut translated.quad {
17866 point[0] += dx;
17867 point[1] += dy;
17868 }
17869 translated.snap_anchor = translated.snap_anchor.map(|anchor| {
17870 SnapAnchor::rigid(Point::new(anchor.origin.x + dx, anchor.origin.y + dy))
17871 });
17872 translated.clip = translated.clip.map(|mut clip| {
17873 clip.x += dx;
17874 clip.y += dy;
17875 clip
17876 });
17877 translated
17878 }
17879
17880 let mut first = test_shape(1, BlendMode::SrcOver);
17881 first.rect = Rect {
17882 x: 10.0,
17883 y: 20.0,
17884 width: 80.0,
17885 height: 40.0,
17886 };
17887 first.local_rect = first.rect;
17888 first.quad = [[10.0, 20.0], [90.0, 20.0], [10.0, 60.0], [90.0, 60.0]];
17889 first.snap_anchor = Some(SnapAnchor::rigid(Point::new(7.0, 11.0)));
17890 first.shape = Some(RoundedCornerShape::uniform(8.0));
17891 first.clip = Some(Rect {
17892 x: 8.0,
17893 y: 18.0,
17894 width: 86.0,
17895 height: 44.0,
17896 });
17897 let mut cutout = test_shape(2, BlendMode::DstOut);
17898 cutout.rect = Rect {
17899 x: 18.0,
17900 y: 26.0,
17901 width: 62.0,
17902 height: 22.0,
17903 };
17904 cutout.local_rect = cutout.rect;
17905 cutout.quad = [[18.0, 26.0], [80.0, 26.0], [18.0, 48.0], [80.0, 48.0]];
17906 cutout.shape = Some(RoundedCornerShape::uniform(4.0));
17907
17908 let dx = 37.0;
17909 let dy = -11.5;
17910 let translated = translate_shape(&first, dx, dy);
17911 let translated_cutout = translate_shape(&cutout, dx, dy);
17912
17913 let root_scale = 1.25;
17914 let first_shapes = vec![(first, BlendMode::SrcOver), (cutout, BlendMode::DstOut)];
17915 let translated_shapes = vec![
17916 (translated, BlendMode::SrcOver),
17917 (translated_cutout, BlendMode::DstOut),
17918 ];
17919
17920 let first_hash = shape_shadow_content_hash(&first_shapes, &[], root_scale);
17921 let translated_hash = shape_shadow_content_hash(&translated_shapes, &[], root_scale);
17922
17923 assert_eq!(first_hash, translated_hash);
17924
17925 let mut changed_shapes = translated_shapes;
17926 changed_shapes[0].0.rect.width += 1.0;
17927 let changed_hash = shape_shadow_content_hash(&changed_shapes, &[], root_scale);
17928
17929 assert_ne!(first_hash, changed_hash);
17930 }
17931
17932 #[test]
17933 fn shape_shadow_content_hash_is_stable_under_fractional_scale_scroll() {
17934 fn shadow_shapes_at(y: f32) -> Vec<(DrawShape, BlendMode)> {
17940 let mut shape = test_shape(1, BlendMode::SrcOver);
17941 shape.rect = Rect {
17942 x: 24.0,
17943 y,
17944 width: 180.0,
17945 height: 90.0,
17946 };
17947 shape.local_rect = shape.rect;
17948 shape.quad = crate::rect_to_quad(shape.rect);
17949 shape.shape = Some(RoundedCornerShape::uniform(14.0));
17950 vec![(shape, BlendMode::SrcOver)]
17951 }
17952
17953 let root_scale = 130.0f32 / 96.0;
17954 let blur_radius = 18.0f32;
17955 let pixel_radius = blur_radius * root_scale;
17956
17957 let key_at = |y: f32| {
17958 let shapes = shadow_shapes_at(y);
17959 let plan =
17960 shape_shadow_surface_plan(&shapes, None, blur_radius, 1600, 1600, root_scale, 8192)
17961 .expect("surface plan");
17962 shape_shadow_surface_cache_key(
17963 &shapes,
17964 &[],
17965 plan.source_device_bounds,
17966 pixel_radius,
17967 root_scale,
17968 )
17969 .expect("cache key")
17970 };
17971
17972 let base = key_at(640.0);
17977 for step in 1..=12 {
17978 let scrolled = key_at(640.0 - step as f32 * 4.0);
17979 assert_eq!(
17980 base, scrolled,
17981 "scrolled shadow cache key must stay stable at fractional scale (step {step})"
17982 );
17983 }
17984 }
17985
17986 #[test]
17987 fn shape_shadow_cache_key_uses_unclipped_source_bounds_for_scrolled_clip() {
17988 fn translated_card_shadow(y: f32) -> Vec<(DrawShape, BlendMode)> {
17989 let mut shape = test_shape(1, BlendMode::SrcOver);
17990 shape.rect = Rect {
17991 x: 24.0,
17992 y,
17993 width: 280.0,
17994 height: 120.0,
17995 };
17996 shape.local_rect = shape.rect;
17997 shape.quad = [[24.0, y], [304.0, y], [24.0, y + 120.0], [304.0, y + 120.0]];
17998 shape.shape = Some(RoundedCornerShape::uniform(18.0));
17999 vec![(shape, BlendMode::SrcOver)]
18000 }
18001
18002 let root_scale = 1.0;
18003 let blur_radius = 18.0;
18004 let viewport_clip = Rect {
18005 x: 0.0,
18006 y: 96.0,
18007 width: 360.0,
18008 height: 720.0,
18009 };
18010 let key_for = |y: f32| {
18011 let shapes = translated_card_shadow(y);
18012 let plan = shape_shadow_surface_plan(
18013 &shapes,
18014 Some(viewport_clip),
18015 blur_radius,
18016 360,
18017 900,
18018 root_scale,
18019 4096,
18020 )
18021 .expect("surface plan");
18022 shape_shadow_surface_cache_key(
18023 &shapes,
18024 &[],
18025 plan.source_device_bounds,
18026 plan.pixel_radius,
18027 root_scale,
18028 )
18029 .expect("cache key")
18030 };
18031
18032 assert_eq!(key_for(740.0), key_for(756.0));
18035 }
18036
18037 #[test]
18038 fn shape_visibility_uses_nonzero_viewport_offset_for_cropped_offscreen() {
18039 let mut shape = test_shape(1, BlendMode::SrcOver);
18040 shape.rect = Rect {
18041 x: 24.0,
18042 y: 740.0,
18043 width: 280.0,
18044 height: 120.0,
18045 };
18046 shape.local_rect = shape.rect;
18047 shape.quad = [[24.0, 740.0], [304.0, 740.0], [24.0, 860.0], [304.0, 860.0]];
18048 let viewport = ViewportUniformParams {
18049 width: 316,
18050 height: 228,
18051 offset: [6.0, 686.0],
18052 };
18053
18054 assert!(shape_draw_is_visible_in_viewport(&shape, viewport, 1.0));
18055 }
18056
18057 #[test]
18058 fn text_prewarm_uses_nonzero_viewport_offset_for_cropped_offscreen() {
18059 let viewport = ViewportUniformParams {
18060 width: 316,
18061 height: 228,
18062 offset: [6.0, 686.0],
18063 };
18064 let text_rect = Rect {
18065 x: 24.0,
18066 y: 740.0,
18067 width: 280.0,
18068 height: 40.0,
18069 };
18070
18071 assert!(text_draw_is_visible_in_viewport(
18072 text_rect, None, viewport, 1.0
18073 ));
18074 assert!(text_draw_should_prewarm_in_viewport(
18075 text_rect, None, viewport, 1.0
18076 ));
18077 }
18078
18079 fn test_shadow_draw(shapes: Vec<(DrawShape, BlendMode)>) -> ShadowDraw {
18080 ShadowDraw {
18081 shapes,
18082 brushes: vec![],
18083 texts: vec![],
18084 blur_radius: 8.0,
18085 clip: None,
18086 z_index: 0,
18087 }
18088 }
18089
18090 fn test_image(z_index: usize, blend_mode: BlendMode) -> ImageDraw {
18091 ImageDraw {
18092 rect: Rect {
18093 x: 0.0,
18094 y: 0.0,
18095 width: 8.0,
18096 height: 8.0,
18097 },
18098 local_rect: Rect {
18099 x: 0.0,
18100 y: 0.0,
18101 width: 8.0,
18102 height: 8.0,
18103 },
18104 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
18105 snap_anchor: None,
18106 image: ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image"),
18107 alpha: 1.0,
18108 color_filter: None,
18109 sampling: ImageSampling::Nearest,
18110 z_index,
18111 clip: None,
18112 blend_mode,
18113 src_rect: None,
18114 motion_context_animated: false,
18115 }
18116 }
18117
18118 #[test]
18119 fn image_sampler_descriptors_match_requested_sampling() {
18120 let nearest = image_sampler_descriptor(ImageSampling::Nearest);
18121 assert_eq!(nearest.mag_filter, wgpu::FilterMode::Nearest);
18122 assert_eq!(nearest.min_filter, wgpu::FilterMode::Nearest);
18123
18124 let linear = image_sampler_descriptor(ImageSampling::Linear);
18125 assert_eq!(linear.mag_filter, wgpu::FilterMode::Linear);
18126 assert_eq!(linear.min_filter, wgpu::FilterMode::Linear);
18127 }
18128
18129 #[test]
18130 fn image_uv_rect_clamps_source_rect_to_texel_centers() {
18131 let image = ImageBitmap::from_rgba8(24, 16, vec![0; 24 * 16 * 4]).expect("image");
18132 let uv = image_uv_rect(
18133 &image,
18134 Some(Rect {
18135 x: 0.0,
18136 y: 0.0,
18137 width: 16.0,
18138 height: 16.0,
18139 }),
18140 )
18141 .expect("uv rect");
18142
18143 assert_eq!(uv.min, [0.0, 0.0]);
18144 assert_eq!(uv.max, [16.0 / 24.0, 1.0]);
18145 assert_eq!(
18146 uv.sample_bounds,
18147 [0.5 / 24.0, 0.5 / 16.0, 15.5 / 24.0, 15.5 / 16.0]
18148 );
18149 }
18150
18151 #[test]
18152 fn image_uv_rect_keeps_full_image_unclamped() {
18153 let image = ImageBitmap::from_rgba8(2, 2, vec![0; 16]).expect("image");
18154 let uv = image_uv_rect(&image, None).expect("uv rect");
18155
18156 assert_eq!(uv.min, [0.0, 0.0]);
18157 assert_eq!(uv.max, [1.0, 1.0]);
18158 assert_eq!(uv.sample_bounds, [0.0, 0.0, 1.0, 1.0]);
18159 }
18160
18161 fn test_text(z_index: usize) -> TextDraw {
18162 TextDraw {
18163 node_id: 0,
18164 rect: Rect {
18165 x: 0.0,
18166 y: 0.0,
18167 width: 8.0,
18168 height: 8.0,
18169 },
18170 snap_anchor: None,
18171 translated_content_context: false,
18172 text: Arc::new(cranpose_ui::text::AnnotatedString::from("t").render_string()),
18173 color: Color::WHITE,
18174 text_style: cranpose_ui::TextStyle::default(),
18175 font_size: 12.0,
18176 scale: 1.0,
18177 layout_options: cranpose_ui::TextLayoutOptions::default(),
18178 z_index,
18179 clip: None,
18180 }
18181 }
18182
18183 #[test]
18184 fn text_draw_visibility_rejects_text_outside_clip_before_rasterization() {
18185 let viewport = ViewportUniformParams {
18186 width: 320,
18187 height: 240,
18188 offset: [0.0, 0.0],
18189 };
18190 let text_rect = Rect {
18191 x: 0.0,
18192 y: 260.0,
18193 width: 200.0,
18194 height: 40.0,
18195 };
18196 let clip = Some(Rect {
18197 x: 0.0,
18198 y: 0.0,
18199 width: 320.0,
18200 height: 200.0,
18201 });
18202
18203 assert!(
18204 !text_draw_is_visible_in_viewport(text_rect, clip, viewport, 1.0),
18205 "lazy-list beyond-bound text outside the clip must not be rasterized"
18206 );
18207 }
18208
18209 #[test]
18210 fn text_draw_prewarm_accepts_clipped_text_near_viewport() {
18211 let viewport = ViewportUniformParams {
18212 width: 320,
18213 height: 240,
18214 offset: [0.0, 0.0],
18215 };
18216 let text_rect = Rect {
18217 x: 0.0,
18218 y: 260.0,
18219 width: 200.0,
18220 height: 40.0,
18221 };
18222 let clip = Some(Rect {
18223 x: 0.0,
18224 y: 0.0,
18225 width: 320.0,
18226 height: 200.0,
18227 });
18228
18229 assert!(!text_draw_is_visible_in_viewport(
18230 text_rect, clip, viewport, 1.0
18231 ));
18232 assert!(text_draw_should_prewarm_in_viewport(
18233 text_rect, clip, viewport, 1.0
18234 ));
18235 }
18236
18237 #[test]
18238 fn text_draw_prewarm_rejects_far_clipped_text() {
18239 let viewport = ViewportUniformParams {
18240 width: 320,
18241 height: 240,
18242 offset: [0.0, 0.0],
18243 };
18244 let text_rect = Rect {
18245 x: 0.0,
18246 y: 1600.0,
18247 width: 200.0,
18248 height: 40.0,
18249 };
18250 let clip = Some(Rect {
18251 x: 0.0,
18252 y: 0.0,
18253 width: 320.0,
18254 height: 200.0,
18255 });
18256
18257 assert!(!text_draw_should_prewarm_in_viewport(
18258 text_rect, clip, viewport, 1.0
18259 ));
18260 }
18261
18262 #[test]
18263 fn text_draw_visibility_rejects_unclipped_text_outside_viewport() {
18264 let viewport = ViewportUniformParams {
18265 width: 320,
18266 height: 240,
18267 offset: [0.0, 0.0],
18268 };
18269 let text_rect = Rect {
18270 x: 0.0,
18271 y: 241.0,
18272 width: 200.0,
18273 height: 40.0,
18274 };
18275
18276 assert!(
18277 !text_draw_is_visible_in_viewport(text_rect, None, viewport, 1.0),
18278 "unclipped text outside the target viewport must not be rasterized"
18279 );
18280 }
18281
18282 #[test]
18283 fn text_draw_visibility_keeps_partially_visible_text() {
18284 let viewport = ViewportUniformParams {
18285 width: 320,
18286 height: 240,
18287 offset: [0.0, 0.0],
18288 };
18289 let text_rect = Rect {
18290 x: 0.0,
18291 y: 220.0,
18292 width: 200.0,
18293 height: 40.0,
18294 };
18295
18296 assert!(text_draw_is_visible_in_viewport(
18297 text_rect, None, viewport, 1.0
18298 ));
18299 }
18300
18301 fn test_draw_ops(
18302 shapes: &[DrawShape],
18303 images: &[ImageDraw],
18304 texts: &[TextDraw],
18305 shadows: &[ShadowDraw],
18306 ) -> Vec<DrawOp> {
18307 let mut ops = Vec::new();
18308 ops.extend(shapes.iter().enumerate().map(|(index, shape)| DrawOp {
18309 z_index: shape.z_index,
18310 kind: DrawOpKind::Shape(index),
18311 }));
18312 ops.extend(images.iter().enumerate().map(|(index, image)| DrawOp {
18313 z_index: image.z_index,
18314 kind: DrawOpKind::Image(index),
18315 }));
18316 ops.extend(texts.iter().enumerate().map(|(index, text)| DrawOp {
18317 z_index: text.z_index,
18318 kind: DrawOpKind::Text(index),
18319 }));
18320 ops.extend(shadows.iter().enumerate().map(|(index, shadow)| DrawOp {
18321 z_index: shadow.z_index,
18322 kind: DrawOpKind::Shadow(index),
18323 }));
18324 ops.sort_by_key(|op| op.z_index);
18325 ops
18326 }
18327
18328 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
18329 crate::test_support::layer_node(
18330 local_bounds,
18331 ProjectiveTransform::identity(),
18332 GraphicsLayer::default(),
18333 children,
18334 )
18335 }
18336
18337 fn cacheable_layer(
18338 node_id: cranpose_core::NodeId,
18339 local_bounds: Rect,
18340 children: Vec<RenderNode>,
18341 ) -> LayerNode {
18342 let mut layer = test_layer(local_bounds, children);
18343 layer.node_id = Some(node_id);
18344 layer.cache_policy = cranpose_render_common::graph::CachePolicy::Auto;
18345 layer.recompute_raster_cache_hashes();
18346 layer
18347 }
18348
18349 fn text_layer_with_style(text: AnnotatedString, text_style: TextStyle) -> LayerNode {
18350 test_layer(
18351 Rect {
18352 x: 0.0,
18353 y: 0.0,
18354 width: 64.0,
18355 height: 32.0,
18356 },
18357 vec![RenderNode::Primitive(PrimitiveEntry {
18358 phase: PrimitivePhase::BeforeChildren,
18359 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
18360 node_id: 1,
18361 rect: Rect {
18362 x: 2.0,
18363 y: 3.0,
18364 width: 48.0,
18365 height: 18.0,
18366 },
18367 text: std::rc::Rc::new(text),
18368 text_style,
18369 font_size: 14.0,
18370 layout_options: TextLayoutOptions::default(),
18371 clip: None,
18372 })),
18373 })],
18374 )
18375 }
18376
18377 fn snapped_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
18378 LayerNode {
18379 node_id: Some(77),
18380 local_bounds: Rect {
18381 x: 0.0,
18382 y: 0.0,
18383 width: 48.0,
18384 height: 24.0,
18385 },
18386 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
18387 motion_context_animated: animated,
18388 translated_content_context,
18389 translated_content_offset: Point::default(),
18390 content_offset: Point::default(),
18391 scene_children_origin: cranpose_ui_graphics::Point::default(),
18392 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
18393 graphics_layer: GraphicsLayer::default(),
18394 clip_to_bounds: false,
18395 shadow_clip: None,
18396 hit_test: None,
18397 has_hit_targets: false,
18398 isolation: IsolationReasons::default(),
18399 cache_policy: CachePolicy::None,
18400 cache_hashes: LayerRasterCacheHashes::default(),
18401 cache_hashes_valid: false,
18402 children: vec![
18403 RenderNode::Primitive(PrimitiveEntry {
18404 phase: PrimitivePhase::BeforeChildren,
18405 node: PrimitiveNode::Draw(DrawPrimitiveNode {
18406 primitive: DrawPrimitive::RoundRect {
18407 rect: Rect {
18408 x: 0.0,
18409 y: 0.0,
18410 width: 48.0,
18411 height: 24.0,
18412 },
18413 brush: Brush::solid(Color(0.28, 0.30, 0.46, 0.88)),
18414 radii: CornerRadii::uniform(6.0),
18415 stroke: None,
18416 },
18417 clip: None,
18418 }),
18419 }),
18420 RenderNode::Primitive(PrimitiveEntry {
18421 phase: PrimitivePhase::BeforeChildren,
18422 node: PrimitiveNode::Draw(DrawPrimitiveNode {
18423 primitive: DrawPrimitive::Image {
18424 rect: Rect {
18425 x: 2.0,
18426 y: 2.0,
18427 width: 12.0,
18428 height: 12.0,
18429 },
18430 image: ImageBitmap::from_rgba8(
18431 2,
18432 2,
18433 vec![
18434 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255,
18435 255,
18436 ],
18437 )
18438 .expect("image"),
18439 alpha: 1.0,
18440 color_filter: None,
18441 sampling: ImageSampling::Linear,
18442 src_rect: None,
18443 },
18444 clip: None,
18445 }),
18446 }),
18447 RenderNode::Primitive(PrimitiveEntry {
18448 phase: PrimitivePhase::BeforeChildren,
18449 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
18450 node_id: 77,
18451 rect: Rect {
18452 x: 6.0,
18453 y: 4.0,
18454 width: 36.0,
18455 height: 16.0,
18456 },
18457 text: std::rc::Rc::new(AnnotatedString::from("48 px")),
18458 text_style: TextStyle::default(),
18459 font_size: 14.0,
18460 layout_options: TextLayoutOptions::default(),
18461 clip: None,
18462 })),
18463 }),
18464 ],
18465 }
18466 }
18467
18468 fn snapped_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
18469 let text_leaf = snapped_text_leaf(animated, translated_content_context);
18470 test_layer(
18471 Rect {
18472 x: 0.0,
18473 y: 0.0,
18474 width: 96.0,
18475 height: 64.0,
18476 },
18477 vec![RenderNode::Layer(Box::new(text_leaf))],
18478 )
18479 }
18480
18481 fn translated_content_local_surface_root() -> LayerNode {
18482 let mut effectful_text = text_layer_with_style(
18483 AnnotatedString::from("shadow"),
18484 TextStyle::from_span_style(SpanStyle {
18485 shadow: Some(Shadow {
18486 color: Color::BLACK,
18487 offset: Point::new(1.0, 2.0),
18488 blur_radius: 3.0,
18489 }),
18490 ..SpanStyle::default()
18491 }),
18492 );
18493 effectful_text.translated_content_context = true;
18494
18495 let translated_content = LayerNode {
18496 node_id: Some(78),
18497 local_bounds: Rect {
18498 x: 0.0,
18499 y: 0.0,
18500 width: 96.0,
18501 height: 64.0,
18502 },
18503 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
18504 motion_context_animated: false,
18505 translated_content_context: true,
18506 translated_content_offset: Point::default(),
18507 content_offset: Point::default(),
18508 scene_children_origin: cranpose_ui_graphics::Point::default(),
18509 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
18510 graphics_layer: GraphicsLayer::default(),
18511 clip_to_bounds: false,
18512 shadow_clip: None,
18513 hit_test: None,
18514 has_hit_targets: false,
18515 isolation: IsolationReasons::default(),
18516 cache_policy: CachePolicy::None,
18517 cache_hashes: LayerRasterCacheHashes::default(),
18518 cache_hashes_valid: false,
18519 children: vec![RenderNode::Layer(Box::new(effectful_text))],
18520 };
18521
18522 test_layer(
18523 Rect {
18524 x: 0.0,
18525 y: 0.0,
18526 width: 160.0,
18527 height: 120.0,
18528 },
18529 vec![RenderNode::Layer(Box::new(translated_content))],
18530 )
18531 }
18532
18533 #[test]
18534 fn scissor_rect_for_layer_intersects_with_clip() {
18535 let rect = Rect {
18536 x: 10.0,
18537 y: 10.0,
18538 width: 30.0,
18539 height: 20.0,
18540 };
18541 let clip = Rect {
18542 x: 20.0,
18543 y: 15.0,
18544 width: 100.0,
18545 height: 100.0,
18546 };
18547
18548 let scissor = scissor_rect_for_layer(rect, Some(clip), 1.0, 200, 200);
18549 assert_eq!(scissor, Some((20, 15, 20, 15)));
18550 }
18551
18552 #[test]
18553 fn visible_draw_rect_no_clip_returns_original() {
18554 let rect = Rect {
18555 x: 100.0,
18556 y: 200.0,
18557 width: 300.0,
18558 height: 400.0,
18559 };
18560 assert_eq!(visible_draw_rect(rect, None), Some(rect));
18561 }
18562
18563 #[test]
18564 fn visible_draw_rect_with_clip_intersects() {
18565 let rect = Rect {
18566 x: 0.0,
18567 y: 0.0,
18568 width: 2000.0,
18569 height: 5000.0,
18570 };
18571 let clip = Rect {
18572 x: 0.0,
18573 y: 0.0,
18574 width: 800.0,
18575 height: 600.0,
18576 };
18577 let visible = visible_draw_rect(rect, Some(clip)).expect("should have visible area");
18578 assert_eq!(visible.width, 800.0);
18579 assert_eq!(visible.height, 600.0);
18580 }
18581
18582 #[test]
18583 fn visible_draw_rect_fully_clipped_returns_none() {
18584 let rect = Rect {
18585 x: 1000.0,
18586 y: 1000.0,
18587 width: 200.0,
18588 height: 200.0,
18589 };
18590 let clip = Rect {
18591 x: 0.0,
18592 y: 0.0,
18593 width: 800.0,
18594 height: 600.0,
18595 };
18596 assert!(visible_draw_rect(rect, Some(clip)).is_none());
18597 }
18598
18599 #[test]
18600 fn scene_bounds_respects_clip_on_shapes() {
18601 let mut scene = CompositorScene::new();
18602 scene.shapes.push(DrawShape {
18604 rect: Rect {
18605 x: 10.0,
18606 y: 10.0,
18607 width: 100.0,
18608 height: 50.0,
18609 },
18610 clip: Some(Rect {
18611 x: 0.0,
18612 y: 0.0,
18613 width: 800.0,
18614 height: 600.0,
18615 }),
18616 ..test_shape(0, BlendMode::SrcOver)
18617 });
18618 scene.shapes.push(DrawShape {
18620 rect: Rect {
18621 x: 0.0,
18622 y: 3000.0,
18623 width: 100.0,
18624 height: 50.0,
18625 },
18626 clip: Some(Rect {
18627 x: 0.0,
18628 y: 0.0,
18629 width: 800.0,
18630 height: 600.0,
18631 }),
18632 ..test_shape(1, BlendMode::SrcOver)
18633 });
18634 let bounds = scene_bounds(&scene).expect("should have bounds");
18635 assert!(bounds.y + bounds.height <= 600.0);
18638 }
18639
18640 #[test]
18641 fn scene_bounds_scroll_content_clipped_to_viewport() {
18642 let mut scene = CompositorScene::new();
18645 let viewport_clip = Rect {
18646 x: 0.0,
18647 y: 0.0,
18648 width: 800.0,
18649 height: 600.0,
18650 };
18651 for i in 0..20 {
18652 scene.shapes.push(DrawShape {
18653 rect: Rect {
18654 x: 0.0,
18655 y: i as f32 * 300.0,
18656 width: 800.0,
18657 height: 200.0,
18658 },
18659 clip: Some(viewport_clip),
18660 ..test_shape(i, BlendMode::SrcOver)
18661 });
18662 }
18663 let bounds = scene_bounds(&scene).expect("should have bounds");
18664 assert_eq!(bounds.x, 0.0);
18667 assert_eq!(bounds.y, 0.0);
18668 assert!(bounds.width <= 800.0);
18669 assert!(bounds.height <= 600.0);
18670 }
18671
18672 #[test]
18673 fn scene_bounds_stable_across_scroll_offsets() {
18674 let viewport_clip = Rect {
18677 x: 0.0,
18678 y: 0.0,
18679 width: 400.0,
18680 height: 50.0,
18681 };
18682 let compute_bounds_at_offset = |scroll_x: f32| {
18683 let mut scene = CompositorScene::new();
18684 for i in 0..10 {
18685 scene.shapes.push(DrawShape {
18686 rect: Rect {
18687 x: i as f32 * 100.0 - scroll_x,
18688 y: 0.0,
18689 width: 80.0,
18690 height: 40.0,
18691 },
18692 clip: Some(viewport_clip),
18693 ..test_shape(i, BlendMode::SrcOver)
18694 });
18695 }
18696 scene_bounds(&scene).expect("bounds")
18697 };
18698 let bounds_at_0 = compute_bounds_at_offset(0.0);
18699 let bounds_at_300 = compute_bounds_at_offset(300.0);
18700 let bounds_at_600 = compute_bounds_at_offset(600.0);
18701 assert!(
18703 (bounds_at_0.width - bounds_at_300.width).abs() < 1.0,
18704 "bounds width changed with scroll: {} vs {}",
18705 bounds_at_0.width,
18706 bounds_at_300.width
18707 );
18708 assert!(
18709 (bounds_at_0.width - bounds_at_600.width).abs() < 1.0,
18710 "bounds width changed with scroll: {} vs {}",
18711 bounds_at_0.width,
18712 bounds_at_600.width
18713 );
18714 }
18715
18716 #[test]
18717 fn collect_effect_ranges_respects_excluded_effect() {
18718 let layers = vec![effect_layer(10, 40), effect_layer(20, 30)];
18719 let mut ranges = Vec::new();
18720 collect_effect_ranges(&layers, 10, 40, Some(0), &mut ranges);
18721 assert_eq!(ranges.len(), 1);
18722 assert_eq!(ranges[0], 20..30);
18723 }
18724
18725 #[test]
18726 fn collect_layer_events_includes_nested_when_parent_excluded() {
18727 let effects = vec![effect_layer(10, 40), effect_layer(20, 30)];
18728 let backdrops = vec![backdrop_layer(25)];
18729 let mut events = Vec::new();
18730 collect_layer_events(&effects, &backdrops, 10, 40, Some(0), &mut events);
18731 assert_eq!(events.len(), 2);
18732
18733 match events[0].kind {
18734 LayerEventKind::Effect(index) => assert_eq!(index, 1),
18735 LayerEventKind::Backdrop(_) => panic!("expected nested effect as first event"),
18736 }
18737 match events[1].kind {
18738 LayerEventKind::Backdrop(index) => assert_eq!(index, 0),
18739 LayerEventKind::Effect(_) => panic!("expected backdrop as second event"),
18740 }
18741 }
18742
18743 fn pure_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
18744 LayerNode {
18745 node_id: Some(177),
18746 local_bounds: Rect {
18747 x: 0.0,
18748 y: 0.0,
18749 width: 96.0,
18750 height: 32.0,
18751 },
18752 transform_to_parent: ProjectiveTransform::translation(11.4, 23.6),
18753 motion_context_animated: animated,
18754 translated_content_context,
18755 translated_content_offset: Point::default(),
18756 content_offset: Point::default(),
18757 scene_children_origin: cranpose_ui_graphics::Point::default(),
18758 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
18759 graphics_layer: GraphicsLayer::default(),
18760 clip_to_bounds: false,
18761 shadow_clip: None,
18762 hit_test: None,
18763 has_hit_targets: false,
18764 isolation: IsolationReasons::default(),
18765 cache_policy: CachePolicy::None,
18766 cache_hashes: LayerRasterCacheHashes::default(),
18767 cache_hashes_valid: false,
18768 children: vec![RenderNode::Primitive(PrimitiveEntry {
18769 phase: PrimitivePhase::BeforeChildren,
18770 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
18771 node_id: 177,
18772 rect: Rect {
18773 x: 0.0,
18774 y: 0.0,
18775 width: 96.0,
18776 height: 24.0,
18777 },
18778 clip: None,
18779 text: std::rc::Rc::new(AnnotatedString::from("Pure text")),
18780 text_style: TextStyle::default(),
18781 font_size: 14.0,
18782 layout_options: TextLayoutOptions::default(),
18783 })),
18784 })],
18785 }
18786 }
18787
18788 fn pure_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
18789 let text_leaf = pure_text_leaf(animated, translated_content_context);
18790 test_layer(
18791 Rect {
18792 x: 0.0,
18793 y: 0.0,
18794 width: 160.0,
18795 height: 96.0,
18796 },
18797 vec![RenderNode::Layer(Box::new(text_leaf))],
18798 )
18799 }
18800
18801 #[test]
18802 fn collect_layer_events_sorts_backdrop_before_effect_at_same_z() {
18803 let effects = vec![effect_layer(10, 20)];
18804 let backdrops = vec![backdrop_layer(10)];
18805 let mut events = Vec::new();
18806 collect_layer_events(&effects, &backdrops, 0, 30, None, &mut events);
18807 assert_eq!(events.len(), 2);
18808
18809 match events[0].kind {
18810 LayerEventKind::Backdrop(_) => {}
18811 LayerEventKind::Effect(_) => panic!("expected backdrop to run before effect"),
18812 }
18813 match events[1].kind {
18814 LayerEventKind::Effect(_) => {}
18815 LayerEventKind::Backdrop(_) => panic!("expected effect as second event"),
18816 }
18817 }
18818
18819 #[test]
18820 fn collect_layer_events_prefers_outer_effect_when_same_start_z() {
18821 let effects = vec![effect_layer(10, 20), effect_layer(10, 40)];
18824 let mut events = Vec::new();
18825 collect_layer_events(&effects, &[], 0, 50, None, &mut events);
18826
18827 assert_eq!(events.len(), 2);
18828 match events[0].kind {
18829 LayerEventKind::Effect(index) => assert_eq!(index, 1),
18830 LayerEventKind::Backdrop(_) => panic!("expected outer effect first"),
18831 }
18832 match events[1].kind {
18833 LayerEventKind::Effect(index) => assert_eq!(index, 0),
18834 LayerEventKind::Backdrop(_) => panic!("expected child effect second"),
18835 }
18836 }
18837
18838 #[test]
18839 fn collect_layer_events_prefers_later_effect_when_ranges_match() {
18840 let effects = vec![effect_layer(10, 20), effect_layer(10, 20)];
18841 let mut events = Vec::new();
18842 collect_layer_events(&effects, &[], 0, 30, None, &mut events);
18843
18844 assert_eq!(events.len(), 2);
18845 match events[0].kind {
18846 LayerEventKind::Effect(index) => assert_eq!(index, 1),
18847 LayerEventKind::Backdrop(_) => panic!("expected later effect first"),
18848 }
18849 match events[1].kind {
18850 LayerEventKind::Effect(index) => assert_eq!(index, 0),
18851 LayerEventKind::Backdrop(_) => panic!("expected earlier effect second"),
18852 }
18853 }
18854
18855 #[test]
18856 fn has_backdrop_layer_in_range_detects_nested_layers() {
18857 let backdrops = vec![backdrop_layer(5), backdrop_layer(15), backdrop_layer(25)];
18858 assert!(has_backdrop_layer_in_range(&backdrops, 10, 20));
18859 assert!(has_backdrop_layer_in_range(&backdrops, 0, 6));
18860 assert!(!has_backdrop_layer_in_range(&backdrops, 20, 25));
18861 }
18862
18863 #[test]
18864 fn layer_contains_descendant_backdrop_ignores_self_backdrop() {
18865 let mut self_backdrop = test_layer(
18866 Rect {
18867 x: 0.0,
18868 y: 0.0,
18869 width: 10.0,
18870 height: 10.0,
18871 },
18872 vec![],
18873 );
18874 self_backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
18875 assert!(!layer_contains_descendant_backdrop(&self_backdrop));
18876
18877 let mut child = test_layer(
18878 Rect {
18879 x: 0.0,
18880 y: 0.0,
18881 width: 8.0,
18882 height: 8.0,
18883 },
18884 vec![],
18885 );
18886 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
18887
18888 let parent = test_layer(
18889 Rect {
18890 x: 0.0,
18891 y: 0.0,
18892 width: 20.0,
18893 height: 20.0,
18894 },
18895 vec![RenderNode::Layer(Box::new(child))],
18896 );
18897 assert!(layer_contains_descendant_backdrop(&parent));
18898 }
18899
18900 fn child_layer_composite(
18901 layer: &LayerNode,
18902 z_index: usize,
18903 rect: Rect,
18904 needs_nested_underlay: bool,
18905 ) -> crate::normalized_scene::ChildLayerComposite {
18906 let mut requirements_cache = cranpose_core::collections::map::HashMap::new();
18907 let surface_requirements =
18908 crate::surface_plan::layer_surface_requirements_cached(layer, &mut requirements_cache);
18909 crate::normalized_scene::ChildLayerComposite {
18910 z_index,
18911 logical_rect: Rect {
18912 x: 0.0,
18913 y: 0.0,
18914 width: rect.width,
18915 height: rect.height,
18916 },
18917 dest_quad: rect_to_quad(rect),
18918 snap_anchor: None,
18919 composite_snap_origin: None,
18920 backdrop_rect: rect,
18921 visual_clip: None,
18922 surface_clip: None,
18923 shadow_draws: Vec::new(),
18924 needs_nested_underlay,
18925 node_id: layer.node_id,
18926 backdrop: layer.backdrop().cloned(),
18927 has_effect: layer.effect().is_some(),
18928 effect_contains_runtime_shader: layer
18929 .effect()
18930 .is_some_and(|effect| effect.contains_runtime_shader()),
18931 target_content_hash: layer.target_content_hash(),
18932 effect_hash: layer.effect_hash(),
18933 motion_source_content_hash: Some(layer.motion_source_content_hash()),
18934 contains_descendant_backdrop: layer_contains_descendant_backdrop(layer),
18935 cache_policy: layer.cache_policy,
18936 surface_requirements,
18937 rounded_clip: crate::surface_executor::backend::LayerSurfaceRoundedClip::from_layer(
18938 layer,
18939 ),
18940 isolation: cranpose_render_common::layer_composition::effective_layer_isolation(
18941 &layer.graphics_layer,
18942 ),
18943 translated_content_context: layer.translated_content_context,
18944 own_translated_content_axes: crate::surface_plan::translated_content_axes_for_layer(
18945 layer,
18946 ),
18947 clip_rect: layer.clip_rect(),
18948 local_bounds: layer.local_bounds,
18949 surface_scale: crate::surface_plan::layer_surface_scale(layer),
18950 source: crate::normalized_scene::LoweredChildSource::default(),
18951 }
18952 }
18953
18954 #[test]
18955 fn root_direct_preflight_allows_first_translated_child_underlay() {
18956 let child = test_layer(
18957 Rect {
18958 x: 0.0,
18959 y: 0.0,
18960 width: 400.0,
18961 height: 280.0,
18962 },
18963 vec![],
18964 );
18965 let collected = CollectedLayer {
18966 scene: CompositorScene::new(),
18967 child_layers: vec![child_layer_composite(
18968 &child,
18969 3,
18970 Rect {
18971 x: 48.0,
18972 y: 96.0,
18973 width: 400.0,
18974 height: 280.0,
18975 },
18976 true,
18977 )],
18978 };
18979
18980 assert!(direct_root_child_underlays_are_supported(&collected));
18981 }
18982
18983 #[test]
18984 fn root_direct_preflight_allows_axis_aligned_prior_child_underlay() {
18985 let first = test_layer(
18986 Rect {
18987 x: 0.0,
18988 y: 0.0,
18989 width: 80.0,
18990 height: 40.0,
18991 },
18992 vec![],
18993 );
18994 let backdrop_child = test_layer(
18995 Rect {
18996 x: 0.0,
18997 y: 0.0,
18998 width: 400.0,
18999 height: 280.0,
19000 },
19001 vec![],
19002 );
19003 let collected = CollectedLayer {
19004 scene: CompositorScene::new(),
19005 child_layers: vec![
19006 child_layer_composite(
19007 &first,
19008 1,
19009 Rect {
19010 x: 8.0,
19011 y: 16.0,
19012 width: 80.0,
19013 height: 40.0,
19014 },
19015 false,
19016 ),
19017 child_layer_composite(
19018 &backdrop_child,
19019 4,
19020 Rect {
19021 x: 48.0,
19022 y: 96.0,
19023 width: 400.0,
19024 height: 280.0,
19025 },
19026 true,
19027 ),
19028 ],
19029 };
19030
19031 assert!(direct_root_child_underlays_are_supported(&collected));
19032 }
19033
19034 #[test]
19035 fn root_direct_preflight_rejects_effectful_prior_child_underlay() {
19036 let mut first = test_layer(
19037 Rect {
19038 x: 0.0,
19039 y: 0.0,
19040 width: 80.0,
19041 height: 40.0,
19042 },
19043 vec![],
19044 );
19045 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
19046 let backdrop_child = test_layer(
19047 Rect {
19048 x: 0.0,
19049 y: 0.0,
19050 width: 400.0,
19051 height: 280.0,
19052 },
19053 vec![],
19054 );
19055 let collected = CollectedLayer {
19056 scene: CompositorScene::new(),
19057 child_layers: vec![
19058 child_layer_composite(
19059 &first,
19060 1,
19061 Rect {
19062 x: 64.0,
19063 y: 112.0,
19064 width: 80.0,
19065 height: 40.0,
19066 },
19067 false,
19068 ),
19069 child_layer_composite(
19070 &backdrop_child,
19071 4,
19072 Rect {
19073 x: 48.0,
19074 y: 96.0,
19075 width: 400.0,
19076 height: 280.0,
19077 },
19078 true,
19079 ),
19080 ],
19081 };
19082
19083 assert!(!direct_root_child_underlays_are_supported(&collected));
19084 }
19085
19086 #[test]
19087 fn root_direct_preflight_ignores_non_overlapping_effectful_prior_child_underlay() {
19088 let mut first = test_layer(
19089 Rect {
19090 x: 0.0,
19091 y: 0.0,
19092 width: 80.0,
19093 height: 40.0,
19094 },
19095 vec![],
19096 );
19097 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
19098 let backdrop_child = test_layer(
19099 Rect {
19100 x: 0.0,
19101 y: 0.0,
19102 width: 400.0,
19103 height: 280.0,
19104 },
19105 vec![],
19106 );
19107 let collected = CollectedLayer {
19108 scene: CompositorScene::new(),
19109 child_layers: vec![
19110 child_layer_composite(
19111 &first,
19112 1,
19113 Rect {
19114 x: 8.0,
19115 y: 16.0,
19116 width: 80.0,
19117 height: 40.0,
19118 },
19119 false,
19120 ),
19121 child_layer_composite(
19122 &backdrop_child,
19123 4,
19124 Rect {
19125 x: 48.0,
19126 y: 96.0,
19127 width: 400.0,
19128 height: 280.0,
19129 },
19130 true,
19131 ),
19132 ],
19133 };
19134
19135 assert!(direct_root_child_underlays_are_supported(&collected));
19136 }
19137
19138 #[test]
19139 fn root_direct_preflight_rejects_underlay_that_would_replay_prior_scene_effects() {
19140 let backdrop_child = test_layer(
19141 Rect {
19142 x: 0.0,
19143 y: 0.0,
19144 width: 400.0,
19145 height: 280.0,
19146 },
19147 vec![],
19148 );
19149 let mut scene = CompositorScene::new();
19150 scene.next_z = 1;
19151 scene.push_effect_layer(
19152 Rect {
19153 x: 0.0,
19154 y: 0.0,
19155 width: 120.0,
19156 height: 120.0,
19157 },
19158 None,
19159 Some(RenderEffect::blur(2.0)),
19160 BlendMode::SrcOver,
19161 1.0,
19162 0,
19163 1,
19164 );
19165 let collected = CollectedLayer {
19166 scene,
19167 child_layers: vec![child_layer_composite(
19168 &backdrop_child,
19169 4,
19170 Rect {
19171 x: 48.0,
19172 y: 96.0,
19173 width: 400.0,
19174 height: 280.0,
19175 },
19176 true,
19177 )],
19178 };
19179
19180 assert!(!direct_root_child_underlays_are_supported(&collected));
19181 }
19182
19183 #[test]
19184 fn root_direct_eligibility_does_not_reject_descendant_backdrop() {
19185 let mut backdrop = test_layer(
19186 Rect {
19187 x: 0.0,
19188 y: 0.0,
19189 width: 40.0,
19190 height: 40.0,
19191 },
19192 vec![],
19193 );
19194 backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
19195 let child = test_layer(
19196 Rect {
19197 x: 0.0,
19198 y: 0.0,
19199 width: 120.0,
19200 height: 96.0,
19201 },
19202 vec![RenderNode::Layer(Box::new(backdrop))],
19203 );
19204 let root = test_layer(
19205 Rect {
19206 x: 0.0,
19207 y: 0.0,
19208 width: 240.0,
19209 height: 160.0,
19210 },
19211 vec![RenderNode::Layer(Box::new(child))],
19212 );
19213 let mut cache = HashMap::new();
19214
19215 assert!(root_can_render_directly_cached(&root, &mut cache));
19216 }
19217
19218 #[test]
19219 fn root_direct_scene_events_allow_root_local_effects() {
19220 let mut scene = CompositorScene::new();
19221 scene.effect_layers.push(EffectLayer {
19222 rect: Rect {
19223 x: 20.0,
19224 y: 30.0,
19225 width: 120.0,
19226 height: 80.0,
19227 },
19228 clip: None,
19229 snap_anchor: None,
19230 effect: Some(RenderEffect::blur(6.0)),
19231 blend_mode: BlendMode::SrcOver,
19232 composite_alpha: 1.0,
19233 z_start: 0,
19234 z_end: 1,
19235 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
19236 });
19237
19238 assert!(root_direct_scene_events_are_supported(&scene));
19239 }
19240
19241 #[test]
19242 fn root_direct_scene_events_reject_root_local_backdrops() {
19243 let mut scene = CompositorScene::new();
19244 scene.backdrop_layers.push(BackdropLayer {
19245 node_id: Some(99),
19246 rect: Rect {
19247 x: 20.0,
19248 y: 30.0,
19249 width: 120.0,
19250 height: 80.0,
19251 },
19252 clip: None,
19253 snap_anchor: None,
19254 effect: RenderEffect::blur(6.0),
19255 z_index: 1,
19256 });
19257
19258 assert!(!root_direct_scene_events_are_supported(&scene));
19259 }
19260
19261 #[test]
19262 fn estimate_layer_surface_rect_includes_transformed_child_bounds() {
19263 let mut child = test_layer(
19264 Rect {
19265 x: 0.0,
19266 y: 0.0,
19267 width: 10.0,
19268 height: 6.0,
19269 },
19270 vec![RenderNode::Primitive(PrimitiveEntry {
19271 phase: PrimitivePhase::BeforeChildren,
19272 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19273 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19274 rect: Rect {
19275 x: 0.0,
19276 y: 0.0,
19277 width: 10.0,
19278 height: 6.0,
19279 },
19280 brush: Brush::solid(Color::WHITE),
19281 stroke: None,
19282 },
19283 clip: None,
19284 }),
19285 })],
19286 );
19287 child.transform_to_parent = ProjectiveTransform::translation(18.0, 7.0);
19288
19289 let parent = test_layer(
19290 Rect {
19291 x: 0.0,
19292 y: 0.0,
19293 width: 4.0,
19294 height: 4.0,
19295 },
19296 vec![RenderNode::Layer(Box::new(child))],
19297 );
19298
19299 assert_eq!(
19300 estimate_layer_surface_rect(&parent),
19301 Rect {
19302 x: 18.0,
19303 y: 7.0,
19304 width: 10.0,
19305 height: 6.0,
19306 }
19307 );
19308 }
19309
19310 #[test]
19311 fn estimate_layer_surface_rect_clips_translated_clip_layers_without_hidden_leading_content() {
19312 let mut layer = test_layer(
19313 Rect {
19314 x: 0.0,
19315 y: 0.0,
19316 width: 120.0,
19317 height: 72.0,
19318 },
19319 vec![RenderNode::Primitive(PrimitiveEntry {
19320 phase: PrimitivePhase::BeforeChildren,
19321 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19322 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19323 rect: Rect {
19324 x: 24.0,
19325 y: 0.0,
19326 width: 200.0,
19327 height: 480.0,
19328 },
19329 brush: Brush::solid(Color::WHITE),
19330 stroke: None,
19331 },
19332 clip: None,
19333 }),
19334 })],
19335 );
19336 layer.translated_content_context = true;
19337 layer.motion_context_animated = true;
19338 layer.clip_to_bounds = true;
19339
19340 assert_eq!(
19341 estimate_layer_surface_rect(&layer),
19342 Rect {
19343 x: 24.0,
19344 y: 0.0,
19345 width: 96.0,
19346 height: 72.0,
19347 }
19348 );
19349 }
19350
19351 #[test]
19352 fn estimate_layer_surface_rect_clips_active_horizontal_scroll_content() {
19353 let mut layer = test_layer(
19354 Rect {
19355 x: 0.0,
19356 y: 0.0,
19357 width: 120.0,
19358 height: 72.0,
19359 },
19360 vec![RenderNode::Primitive(PrimitiveEntry {
19361 phase: PrimitivePhase::BeforeChildren,
19362 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19363 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19364 rect: Rect {
19365 x: -24.0,
19366 y: 0.0,
19367 width: 200.0,
19368 height: 480.0,
19369 },
19370 brush: Brush::solid(Color::WHITE),
19371 stroke: None,
19372 },
19373 clip: None,
19374 }),
19375 })],
19376 );
19377 layer.translated_content_context = true;
19378 layer.motion_context_animated = true;
19379 layer.clip_to_bounds = true;
19380
19381 assert_eq!(
19382 estimate_layer_surface_rect(&layer),
19383 Rect {
19384 x: 0.0,
19385 y: 0.0,
19386 width: 120.0,
19387 height: 72.0,
19388 }
19389 );
19390 }
19391
19392 #[test]
19393 fn estimate_layer_surface_rect_clips_active_vertical_scroll_content() {
19394 let mut layer = test_layer(
19395 Rect {
19396 x: 0.0,
19397 y: 0.0,
19398 width: 120.0,
19399 height: 72.0,
19400 },
19401 vec![RenderNode::Primitive(PrimitiveEntry {
19402 phase: PrimitivePhase::BeforeChildren,
19403 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19404 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19405 rect: Rect {
19406 x: 0.0,
19407 y: -24.0,
19408 width: 120.0,
19409 height: 200.0,
19410 },
19411 brush: Brush::solid(Color::WHITE),
19412 stroke: None,
19413 },
19414 clip: None,
19415 }),
19416 })],
19417 );
19418 layer.translated_content_context = true;
19419 layer.motion_context_animated = true;
19420 layer.clip_to_bounds = true;
19421
19422 assert_eq!(
19423 estimate_layer_surface_rect(&layer),
19424 Rect {
19425 x: 0.0,
19426 y: 0.0,
19427 width: 120.0,
19428 height: 72.0,
19429 }
19430 );
19431 }
19432
19433 #[test]
19434 fn estimate_layer_surface_rect_keeps_shallow_scroll_capture_origin_stable() {
19435 fn shallow_scroll_surface_rect(content_y: f32) -> Rect {
19436 let mut layer = test_layer(
19437 Rect {
19438 x: 0.0,
19439 y: 0.0,
19440 width: 120.0,
19441 height: 72.0,
19442 },
19443 vec![RenderNode::Primitive(PrimitiveEntry {
19444 phase: PrimitivePhase::BeforeChildren,
19445 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19446 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19447 rect: Rect {
19448 x: 0.0,
19449 y: content_y,
19450 width: 120.0,
19451 height: 200.0,
19452 },
19453 brush: Brush::solid(Color::WHITE),
19454 stroke: None,
19455 },
19456 clip: None,
19457 }),
19458 })],
19459 );
19460 layer.translated_content_context = true;
19461 layer.motion_context_animated = true;
19462 layer.clip_to_bounds = true;
19463 estimate_layer_surface_rect(&layer)
19464 }
19465
19466 assert_eq!(
19467 shallow_scroll_surface_rect(-24.0),
19468 shallow_scroll_surface_rect(-25.0),
19469 "shallow scroll capture bounds must not move the offscreen surface origin on adjacent scroll positions"
19470 );
19471 }
19472
19473 #[test]
19474 fn estimate_layer_surface_rect_clips_active_xy_scroll_content() {
19475 let mut layer = test_layer(
19476 Rect {
19477 x: 0.0,
19478 y: 0.0,
19479 width: 120.0,
19480 height: 72.0,
19481 },
19482 vec![RenderNode::Primitive(PrimitiveEntry {
19483 phase: PrimitivePhase::BeforeChildren,
19484 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19485 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19486 rect: Rect {
19487 x: -16.0,
19488 y: -24.0,
19489 width: 180.0,
19490 height: 240.0,
19491 },
19492 brush: Brush::solid(Color::WHITE),
19493 stroke: None,
19494 },
19495 clip: None,
19496 }),
19497 })],
19498 );
19499 layer.translated_content_context = true;
19500 layer.motion_context_animated = true;
19501 layer.clip_to_bounds = true;
19502
19503 assert_eq!(
19504 estimate_layer_surface_rect(&layer),
19505 Rect {
19506 x: 0.0,
19507 y: 0.0,
19508 width: 120.0,
19509 height: 72.0,
19510 }
19511 );
19512 }
19513
19514 #[test]
19515 fn estimate_layer_surface_rect_clips_deep_hidden_active_scroll_content() {
19516 let mut layer = test_layer(
19517 Rect {
19518 x: 0.0,
19519 y: 0.0,
19520 width: 120.0,
19521 height: 72.0,
19522 },
19523 vec![RenderNode::Primitive(PrimitiveEntry {
19524 phase: PrimitivePhase::BeforeChildren,
19525 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19526 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19527 rect: Rect {
19528 x: 0.0,
19529 y: -1200.0,
19530 width: 120.0,
19531 height: 1400.0,
19532 },
19533 brush: Brush::solid(Color::WHITE),
19534 stroke: None,
19535 },
19536 clip: None,
19537 }),
19538 })],
19539 );
19540 layer.translated_content_context = true;
19541 layer.motion_context_animated = true;
19542 layer.clip_to_bounds = true;
19543
19544 assert_eq!(
19545 estimate_layer_surface_rect(&layer),
19546 Rect {
19547 x: 0.0,
19548 y: 0.0,
19549 width: 120.0,
19550 height: 72.0,
19551 }
19552 );
19553 }
19554
19555 #[test]
19556 fn estimate_layer_surface_rect_keeps_deep_scroll_capture_origin_stable() {
19557 fn deep_scroll_surface_rect(content_y: f32) -> Rect {
19558 let mut layer = test_layer(
19559 Rect {
19560 x: 0.0,
19561 y: 0.0,
19562 width: 120.0,
19563 height: 72.0,
19564 },
19565 vec![RenderNode::Primitive(PrimitiveEntry {
19566 phase: PrimitivePhase::BeforeChildren,
19567 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19568 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19569 rect: Rect {
19570 x: 0.0,
19571 y: content_y,
19572 width: 120.0,
19573 height: 1400.0,
19574 },
19575 brush: Brush::solid(Color::WHITE),
19576 stroke: None,
19577 },
19578 clip: None,
19579 }),
19580 })],
19581 );
19582 layer.translated_content_context = true;
19583 layer.motion_context_animated = true;
19584 layer.clip_to_bounds = true;
19585 estimate_layer_surface_rect(&layer)
19586 }
19587
19588 assert_eq!(
19589 deep_scroll_surface_rect(-1200.0),
19590 deep_scroll_surface_rect(-1201.0),
19591 "deep scroll capture bounds must not re-phase the offscreen surface origin on adjacent scroll positions"
19592 );
19593 }
19594
19595 #[test]
19596 fn motion_stable_capture_bounds_bounds_shadows_for_clipped_effect_layer() {
19597 let mut layer = test_layer(
19598 Rect {
19599 x: 0.0,
19600 y: 0.0,
19601 width: 120.0,
19602 height: 72.0,
19603 },
19604 vec![],
19605 );
19606 layer.clip_to_bounds = true;
19607 layer.graphics_layer.clip = true;
19608 layer.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
19609
19610 let mut shadow_shape = test_shape(0, BlendMode::SrcOver);
19611 shadow_shape.rect = Rect {
19612 x: -24.0,
19613 y: -1200.0,
19614 width: 180.0,
19615 height: 1400.0,
19616 };
19617 let mut scene = CompositorScene::new();
19618 scene
19619 .shadow_draws
19620 .push(test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]));
19621
19622 let requirements = SurfaceRequirementSet::default()
19623 .with(SurfaceRequirement::RenderEffect)
19624 .with(SurfaceRequirement::MotionStableCapture);
19625
19626 assert_eq!(
19627 motion_stable_capture_bounds(
19628 &layer,
19629 &scene,
19630 &[],
19631 requirements,
19632 TranslatedContentAxes::default(),
19633 None,
19634 ),
19635 Some(Rect {
19636 x: -360.0,
19637 y: -216.0,
19638 width: 480.0,
19639 height: 288.0,
19640 })
19641 );
19642 }
19643
19644 #[test]
19645 fn vertical_motion_stable_capture_uses_viewport_cross_axis_bounds() {
19646 let mut layer = test_layer(
19647 Rect {
19648 x: 0.0,
19649 y: 0.0,
19650 width: 200.0,
19651 height: 100.0,
19652 },
19653 vec![],
19654 );
19655 layer.clip_to_bounds = true;
19656 layer.graphics_layer.clip = true;
19657
19658 let mut shape = test_shape(0, BlendMode::SrcOver);
19659 shape.rect = Rect {
19660 x: 60.0,
19661 y: -80.0,
19662 width: 80.0,
19663 height: 220.0,
19664 };
19665 let mut scene = CompositorScene::new();
19666 scene.shapes.push(shape);
19667
19668 let requirements =
19669 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
19670
19671 assert_eq!(
19672 motion_stable_capture_bounds(
19673 &layer,
19674 &scene,
19675 &[],
19676 requirements,
19677 TranslatedContentAxes { x: false, y: true },
19678 None,
19679 ),
19680 Some(Rect {
19681 x: -96.0,
19682 y: -64.0,
19683 width: 296.0,
19684 height: 164.0,
19685 })
19686 );
19687 }
19688
19689 #[test]
19690 fn vertical_motion_stable_capture_uses_external_surface_clip() {
19691 let layer = test_layer(
19692 Rect {
19693 x: 0.0,
19694 y: 0.0,
19695 width: 200.0,
19696 height: 100.0,
19697 },
19698 vec![],
19699 );
19700
19701 let mut shape = test_shape(0, BlendMode::SrcOver);
19702 shape.rect = Rect {
19703 x: 60.0,
19704 y: -80.0,
19705 width: 80.0,
19706 height: 220.0,
19707 };
19708 let mut scene = CompositorScene::new();
19709 scene.shapes.push(shape);
19710
19711 let requirements =
19712 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
19713
19714 assert_eq!(
19715 motion_stable_capture_bounds(
19716 &layer,
19717 &scene,
19718 &[],
19719 requirements,
19720 TranslatedContentAxes { x: false, y: true },
19721 Some(Rect {
19722 x: 0.0,
19723 y: 0.0,
19724 width: 200.0,
19725 height: 100.0,
19726 }),
19727 ),
19728 Some(Rect {
19729 x: -96.0,
19730 y: -64.0,
19731 width: 296.0,
19732 height: 164.0,
19733 })
19734 );
19735 }
19736
19737 #[test]
19738 fn estimate_layer_surface_rect_expands_for_child_layer_shadow() {
19739 let mut child = test_layer(
19740 Rect {
19741 x: 0.0,
19742 y: 0.0,
19743 width: 12.0,
19744 height: 8.0,
19745 },
19746 vec![],
19747 );
19748 child.transform_to_parent = ProjectiveTransform::translation(20.0, 9.0);
19749 child.graphics_layer.shadow_elevation = 6.0;
19750
19751 let parent = test_layer(
19752 Rect {
19753 x: 0.0,
19754 y: 0.0,
19755 width: 4.0,
19756 height: 4.0,
19757 },
19758 vec![RenderNode::Layer(Box::new(child))],
19759 );
19760
19761 let rect = estimate_layer_surface_rect(&parent);
19762 assert!(rect.x < 20.0);
19763 assert!(rect.y < 9.0);
19764 assert!(rect.width > 12.0);
19765 assert!(rect.height > 8.0);
19766 }
19767
19768 #[test]
19769 fn estimate_layer_surface_rect_respects_local_bounds_for_effect_layers() {
19770 let mut layer = test_layer(
19771 Rect {
19772 x: 0.0,
19773 y: 0.0,
19774 width: 28.0,
19775 height: 28.0,
19776 },
19777 vec![RenderNode::Primitive(PrimitiveEntry {
19778 phase: PrimitivePhase::BeforeChildren,
19779 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19780 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19781 rect: Rect {
19782 x: 10.0,
19783 y: 10.0,
19784 width: 10.0,
19785 height: 10.0,
19786 },
19787 brush: Brush::solid(Color::WHITE),
19788 stroke: None,
19789 },
19790 clip: None,
19791 }),
19792 })],
19793 );
19794 layer.graphics_layer.render_effect = Some(RenderEffect::blur(12.0));
19795
19796 assert_eq!(
19797 estimate_layer_surface_rect(&layer),
19798 Rect {
19799 x: 0.0,
19800 y: 0.0,
19801 width: 28.0,
19802 height: 28.0,
19803 }
19804 );
19805 }
19806
19807 #[test]
19808 fn layer_raster_cache_candidate_ignores_parent_transform() {
19809 let primitive = PrimitiveEntry {
19810 phase: PrimitivePhase::BeforeChildren,
19811 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19812 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19813 rect: Rect {
19814 x: 2.0,
19815 y: 3.0,
19816 width: 6.0,
19817 height: 4.0,
19818 },
19819 brush: Brush::solid(Color::BLACK),
19820 stroke: None,
19821 },
19822 clip: None,
19823 }),
19824 };
19825 let base = cacheable_layer(
19826 41,
19827 Rect {
19828 x: 0.0,
19829 y: 0.0,
19830 width: 20.0,
19831 height: 20.0,
19832 },
19833 vec![RenderNode::Primitive(primitive.clone())],
19834 );
19835 let mut moved = base.clone();
19836 moved.transform_to_parent = ProjectiveTransform::translation(32.0, 18.0);
19837
19838 assert_eq!(
19839 layer_raster_cache_candidate(&base, 1.25, false, false),
19840 layer_raster_cache_candidate(&moved, 1.25, false, false)
19841 );
19842 }
19843
19844 #[test]
19845 fn layer_raster_cache_candidate_changes_for_translated_content_offset() {
19846 let primitive = PrimitiveEntry {
19847 phase: PrimitivePhase::BeforeChildren,
19848 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19849 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19850 rect: Rect {
19851 x: 2.0,
19852 y: 3.0,
19853 width: 6.0,
19854 height: 4.0,
19855 },
19856 brush: Brush::solid(Color::BLACK),
19857 stroke: None,
19858 },
19859 clip: None,
19860 }),
19861 };
19862 let mut base = cacheable_layer(
19863 42,
19864 Rect {
19865 x: 0.0,
19866 y: 0.0,
19867 width: 20.0,
19868 height: 20.0,
19869 },
19870 vec![RenderNode::Primitive(primitive)],
19871 );
19872 base.translated_content_context = true;
19873 base.translated_content_offset = Point::new(0.0, -8.0);
19874 base.recompute_raster_cache_hashes();
19875
19876 let mut moved = base.clone();
19877 moved.translated_content_offset = Point::new(0.0, -16.0);
19878 moved.recompute_raster_cache_hashes();
19879
19880 assert_ne!(
19881 layer_raster_cache_candidate(&base, 1.25, false, false),
19882 layer_raster_cache_candidate(&moved, 1.25, false, false),
19883 "full-surface layer cache candidates must not alias different scroll offsets"
19884 );
19885 }
19886
19887 #[test]
19888 fn layer_raster_cache_candidate_changes_for_child_transform() {
19889 let mut child = cacheable_layer(
19890 8,
19891 Rect {
19892 x: 0.0,
19893 y: 0.0,
19894 width: 12.0,
19895 height: 10.0,
19896 },
19897 vec![],
19898 );
19899 child.transform_to_parent = ProjectiveTransform::translation(4.0, 6.0);
19900 let base = cacheable_layer(
19901 7,
19902 Rect {
19903 x: 0.0,
19904 y: 0.0,
19905 width: 20.0,
19906 height: 20.0,
19907 },
19908 vec![RenderNode::Layer(Box::new(child.clone()))],
19909 );
19910 let mut moved_child = child;
19911 moved_child.transform_to_parent = ProjectiveTransform::translation(9.0, 6.0);
19912 let moved = cacheable_layer(
19913 7,
19914 Rect {
19915 x: 0.0,
19916 y: 0.0,
19917 width: 20.0,
19918 height: 20.0,
19919 },
19920 vec![RenderNode::Layer(Box::new(moved_child))],
19921 );
19922
19923 assert_ne!(
19924 layer_raster_cache_candidate(&base, 1.0, false, false),
19925 layer_raster_cache_candidate(&moved, 1.0, false, false)
19926 );
19927 }
19928
19929 #[test]
19930 fn layer_raster_cache_candidate_rejects_external_backdrop_dependency() {
19931 let mut child = cacheable_layer(
19932 12,
19933 Rect {
19934 x: 0.0,
19935 y: 0.0,
19936 width: 8.0,
19937 height: 8.0,
19938 },
19939 vec![],
19940 );
19941 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
19942 let parent = cacheable_layer(
19943 11,
19944 Rect {
19945 x: 0.0,
19946 y: 0.0,
19947 width: 16.0,
19948 height: 16.0,
19949 },
19950 vec![RenderNode::Layer(Box::new(child))],
19951 );
19952
19953 assert!(layer_raster_cache_candidate(&parent, 1.0, false, false).is_some());
19954 assert!(layer_raster_cache_candidate(&parent, 1.0, true, false).is_none());
19955 }
19956
19957 #[test]
19958 fn layer_raster_cache_candidate_does_not_force_translation_only_text_surfaces() {
19959 let text = RenderNode::Primitive(PrimitiveEntry {
19960 phase: PrimitivePhase::BeforeChildren,
19961 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
19962 node_id: 77,
19963 rect: Rect {
19964 x: 2.0,
19965 y: 3.0,
19966 width: 48.0,
19967 height: 18.0,
19968 },
19969 text: std::rc::Rc::new(AnnotatedString::from("runtime cache")),
19970 text_style: TextStyle::default(),
19971 font_size: 14.0,
19972 layout_options: TextLayoutOptions::default(),
19973 clip: None,
19974 })),
19975 });
19976 let mut layer = test_layer(
19977 Rect {
19978 x: 0.0,
19979 y: 0.0,
19980 width: 64.0,
19981 height: 32.0,
19982 },
19983 vec![text],
19984 );
19985 layer.node_id = Some(77);
19986 layer.recompute_raster_cache_hashes();
19987
19988 assert!(
19989 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
19990 "root path should not isolate plain translation-only text layers"
19991 );
19992 assert!(
19993 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
19994 "child path should also render plain translation-only text layers directly"
19995 );
19996 }
19997
19998 #[test]
19999 fn layer_raster_cache_candidate_allows_stable_runtime_child_effect_surfaces() {
20000 let mut layer = test_layer(
20001 Rect {
20002 x: 0.0,
20003 y: 0.0,
20004 width: 64.0,
20005 height: 32.0,
20006 },
20007 vec![RenderNode::Primitive(PrimitiveEntry {
20008 phase: PrimitivePhase::BeforeChildren,
20009 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20010 primitive: DrawPrimitive::Rect {
20011 rect: Rect {
20012 x: 0.0,
20013 y: 0.0,
20014 width: 64.0,
20015 height: 32.0,
20016 },
20017 brush: Brush::solid(Color::WHITE),
20018 stroke: None,
20019 },
20020 clip: None,
20021 }),
20022 })],
20023 );
20024 layer.node_id = Some(78);
20025 layer.graphics_layer.render_effect = Some(RenderEffect::blur(4.0));
20026 layer.recompute_raster_cache_hashes();
20027
20028 assert!(
20029 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
20030 "root direct path should not force-cache ordinary stable effects"
20031 );
20032 assert!(
20033 layer_raster_cache_candidate(&layer, 1.0, false, true).is_some(),
20034 "child surface rendering should retain stable non-runtime effects"
20035 );
20036 }
20037
20038 #[test]
20039 fn layer_raster_cache_candidate_rejects_runtime_shader_child_effect_surfaces() {
20040 let mut layer = test_layer(
20041 Rect {
20042 x: 0.0,
20043 y: 0.0,
20044 width: 64.0,
20045 height: 32.0,
20046 },
20047 vec![],
20048 );
20049 layer.node_id = Some(79);
20050 layer.graphics_layer.render_effect = Some(RenderEffect::runtime_shader(
20051 RuntimeShader::new("runtime shader"),
20052 ));
20053 layer.recompute_raster_cache_hashes();
20054
20055 assert!(
20056 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
20057 "runtime shaders must not fill the retained layer cache with per-frame uniform variants"
20058 );
20059 }
20060
20061 #[test]
20062 fn layer_surface_requirements_keep_plain_text_on_direct_path() {
20063 let layer = text_layer_with_style(AnnotatedString::from("plain"), TextStyle::default());
20064
20065 let requirements = layer_surface_requirements(&layer);
20066
20067 assert_eq!(requirements.direct_translation, Some(Point::default()));
20068 assert!(requirements
20069 .surface_requirements
20070 .contains(SurfaceRequirement::PixelStableComposite));
20071 assert!(!requirements
20072 .surface_requirements
20073 .has_isolating_requirement());
20074 }
20075
20076 #[test]
20077 fn layer_surface_requirements_keep_translated_plain_text_leaf_on_direct_path() {
20078 let layer = pure_text_leaf(false, true);
20079
20080 let requirements = layer_surface_requirements(&layer);
20081
20082 assert_eq!(
20083 requirements.direct_translation,
20084 Some(Point::new(11.4, 23.6))
20085 );
20086 assert!(
20087 requirements
20088 .surface_requirements
20089 .contains(SurfaceRequirement::PixelStableComposite)
20090 && !requirements
20091 .surface_requirements
20092 .has_isolating_requirement(),
20093 "translated plain text should stay on the direct path and isolate only the glyph draw"
20094 );
20095 }
20096
20097 #[test]
20098 fn layer_surface_requirements_keep_translated_text_leaf_with_background_on_direct_path() {
20099 let layer = snapped_text_leaf(false, true);
20100
20101 let requirements = layer_surface_requirements(&layer);
20102
20103 assert_eq!(
20104 requirements.direct_translation,
20105 Some(Point::new(14.25, 16.5))
20106 );
20107 assert!(
20108 requirements
20109 .surface_requirements
20110 .contains(SurfaceRequirement::PixelStableComposite)
20111 && !requirements
20112 .surface_requirements
20113 .has_isolating_requirement(),
20114 "translated text with direct sibling decoration/background should keep the layer direct"
20115 );
20116 }
20117
20118 #[test]
20119 fn translated_plain_text_uses_bounded_snap_surface() {
20120 let root = pure_text_leaf_root(true, true);
20121 let mut rect_cache = HashMap::new();
20122 let mut requirements_cache = HashMap::new();
20123 let collected =
20124 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20125
20126 assert_eq!(collected.child_layers.len(), 1);
20127 assert!(collected.scene.texts.is_empty());
20128 assert!(collected.scene.effect_layers.is_empty());
20129 assert_snap_anchor_close(
20130 collected.child_layers[0].snap_anchor,
20131 Point::new(11.4, 23.6),
20132 "translated plain text's bounded local surface should composite at the content-origin snap phase",
20133 );
20134 }
20135
20136 #[test]
20140 #[ignore]
20141 fn shape_run_collect_timing_harness() {
20142 use cranpose_render_common::graph::DrawPrimitiveNode;
20143 use cranpose_render_common::layer_composition::local_content_layer_for;
20144 use cranpose_ui_graphics::Stroke;
20145
20146 let bounds = Rect {
20147 x: 0.0,
20148 y: 0.0,
20149 width: 1080.0,
20150 height: 2244.0,
20151 };
20152 let graphics_layer = GraphicsLayer::default();
20153
20154 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
20157 for i in 0..3000u32 {
20158 let f = i as f32;
20159 let brush = if i % 8 == 0 {
20160 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
20161 } else {
20162 Brush::Solid(Color(0.5, 0.2, 0.8, 1.0))
20163 };
20164 let center = Point::new(540.0 + (f % 400.0), 1122.0 + (f % 350.0));
20165 let radius = 8.0 + (i % 23) as f32;
20166 let half = radius + 4.0;
20167 nodes.push(DrawPrimitiveNode {
20168 primitive: DrawPrimitive::Arc {
20169 rect: Rect {
20170 x: center.x - half,
20171 y: center.y - half,
20172 width: half * 2.0,
20173 height: half * 2.0,
20174 },
20175 brush,
20176 center,
20177 radius,
20178 start_angle: f * 0.07,
20179 sweep_angle: 0.5 + (i % 5) as f32,
20180 stroke: (i % 3 != 0).then(|| Stroke::new(4.0)),
20181 inner_radius: if i % 3 == 0 { radius * 0.6 } else { 0.0 },
20182 },
20183 clip: None,
20184 });
20185 }
20186
20187 let children: Vec<RenderNode> = nodes
20188 .iter()
20189 .map(|node| {
20190 RenderNode::Primitive(PrimitiveEntry {
20191 phase: PrimitivePhase::BeforeChildren,
20192 node: PrimitiveNode::Draw(node.clone()),
20193 })
20194 })
20195 .collect();
20196 let layer = crate::test_support::layer_node(
20197 bounds,
20198 ProjectiveTransform::identity(),
20199 graphics_layer,
20200 children,
20201 );
20202
20203 const ITERS: usize = 300;
20204
20205 let local_layer = local_content_layer_for(&layer.graphics_layer);
20207 let start = Instant::now();
20208 let mut sink_shapes = 0usize;
20209 for _ in 0..ITERS {
20210 let mut scene = CompositorScene::new();
20211 for node in &nodes {
20212 crate::pipeline::push_draw_primitive(
20213 &node.primitive,
20214 bounds,
20215 &local_layer,
20216 None,
20217 &mut scene,
20218 None,
20219 false,
20220 );
20221 }
20222 sink_shapes = scene.shapes.len();
20223 }
20224 let serial = start.elapsed();
20225
20226 let mut rect_cache = HashMap::new();
20227 let mut requirements_cache = HashMap::new();
20228 let start = Instant::now();
20229 let mut run_shapes = 0usize;
20230 for _ in 0..ITERS {
20231 let collected = collect_layer_contents(
20232 &layer,
20233 None,
20234 None,
20235 &mut rect_cache,
20236 &mut requirements_cache,
20237 );
20238 run_shapes = collected.scene.shapes.len();
20239 }
20240 let run = start.elapsed();
20241
20242 println!(
20243 "per-primitive: {:?}/iter ({sink_shapes} shapes) shape-run: {:?}/iter ({run_shapes} shapes)",
20244 serial / ITERS as u32,
20245 run / ITERS as u32,
20246 );
20247 }
20248
20249 fn assert_shape_run_collect_matches_per_primitive_emission() {
20251 use cranpose_render_common::graph::DrawPrimitiveNode;
20252 use cranpose_render_common::layer_composition::local_content_layer_for;
20253 use cranpose_render_common::primitive_emit::{resolve_primitive_clip, PrimitiveClipSpace};
20254 use cranpose_ui_graphics::{CornerRadii, Stroke};
20255
20256 let bounds = Rect {
20257 x: 0.0,
20258 y: 0.0,
20259 width: 800.0,
20260 height: 800.0,
20261 };
20262 let graphics_layer = GraphicsLayer {
20265 scale: 1.25,
20266 translation_x: 3.5,
20267 translation_y: -2.0,
20268 alpha: 0.9,
20269 rotation_z: 0.35,
20270 ..GraphicsLayer::default()
20271 };
20272
20273 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
20274 for i in 0..600u32 {
20275 let f = i as f32;
20276 let brush = if i % 11 == 0 {
20277 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
20278 } else {
20279 Brush::Solid(Color(0.1 + (i % 7) as f32 * 0.1, 0.5, 0.9, 1.0))
20280 };
20281 let stroke = (i % 5 == 0).then(|| Stroke::new(1.0 + (i % 3) as f32));
20282 let primitive = match i % 3 {
20283 0 => DrawPrimitive::Rect {
20284 rect: Rect {
20285 x: f % 37.0,
20286 y: f % 53.0,
20287 width: 8.0 + f % 9.0,
20288 height: 6.0 + f % 5.0,
20289 },
20290 brush,
20291 stroke,
20292 },
20293 1 => DrawPrimitive::RoundRect {
20294 rect: Rect {
20295 x: f % 41.0,
20296 y: f % 43.0,
20297 width: 12.0,
20298 height: 10.0,
20299 },
20300 brush,
20301 radii: CornerRadii::uniform(2.0 + (i % 4) as f32),
20302 stroke,
20303 },
20304 _ => {
20305 let center = Point::new(60.0 + f % 71.0, 60.0 + f % 67.0);
20306 let radius = 5.0 + (i % 13) as f32;
20307 let sweep_angle = if i == 302 { 0.0 } else { 0.4 + (i % 6) as f32 };
20309 let half = radius + 4.0;
20310 DrawPrimitive::Arc {
20311 rect: Rect {
20312 x: center.x - half,
20313 y: center.y - half,
20314 width: half * 2.0,
20315 height: half * 2.0,
20316 },
20317 brush,
20318 center,
20319 radius,
20320 start_angle: f * 0.11,
20321 sweep_angle,
20322 stroke: (i % 2 == 0).then(|| Stroke::new(3.0)),
20323 inner_radius: if i % 4 == 2 { radius * 0.5 } else { 0.0 },
20324 }
20325 }
20326 };
20327 let primitive = if i == 300 {
20328 DrawPrimitive::Blend {
20332 primitive: Box::new(DrawPrimitive::Blend {
20333 primitive: Box::new(primitive),
20334 blend_mode: BlendMode::SrcOver,
20335 }),
20336 blend_mode: BlendMode::DstOut,
20337 }
20338 } else if i % 7 == 3 {
20339 DrawPrimitive::Blend {
20340 primitive: Box::new(primitive),
20341 blend_mode: BlendMode::DstOut,
20342 }
20343 } else {
20344 primitive
20345 };
20346 let clip = (i % 31 == 7).then_some(Rect {
20347 x: 0.0,
20348 y: 0.0,
20349 width: 30.0,
20350 height: 30.0,
20351 });
20352 nodes.push(DrawPrimitiveNode { primitive, clip });
20353 }
20354
20355 let children: Vec<RenderNode> = nodes
20356 .iter()
20357 .map(|node| {
20358 RenderNode::Primitive(PrimitiveEntry {
20359 phase: PrimitivePhase::BeforeChildren,
20360 node: PrimitiveNode::Draw(node.clone()),
20361 })
20362 })
20363 .collect();
20364 let layer = crate::test_support::layer_node(
20365 bounds,
20366 ProjectiveTransform::identity(),
20367 graphics_layer,
20368 children,
20369 );
20370
20371 let mut rect_cache = HashMap::new();
20372 let mut requirements_cache = HashMap::new();
20373 let collected =
20374 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
20375
20376 let local_layer = local_content_layer_for(&layer.graphics_layer);
20379 let mut expected = CompositorScene::new();
20380 for node in &nodes {
20381 let clip = resolve_primitive_clip(
20382 node.clip,
20383 bounds,
20384 &local_layer,
20385 None,
20386 PrimitiveClipSpace::Local,
20387 );
20388 if node.clip.is_some() && clip.is_none() {
20389 continue;
20390 }
20391 crate::pipeline::push_draw_primitive(
20392 &node.primitive,
20393 bounds,
20394 &local_layer,
20395 clip,
20396 &mut expected,
20397 None,
20398 false,
20399 );
20400 }
20401
20402 assert!(
20403 collected.scene.shapes.len() >= 590,
20404 "the runs should engage the parallel branch: got {} shapes",
20405 collected.scene.shapes.len()
20406 );
20407 assert_eq!(collected.scene.shapes.len(), expected.shapes.len());
20408 assert_eq!(collected.scene.draw_ops, expected.draw_ops);
20409 assert_eq!(collected.scene.next_z, expected.next_z);
20410 assert!(
20411 collected
20412 .scene
20413 .shapes
20414 .iter()
20415 .all(|s| s.snap_anchor.is_none()),
20416 "a rotated layer must not rigid-snap; the reference scene assumes it"
20417 );
20418 for (index, (got, want)) in collected
20419 .scene
20420 .shapes
20421 .iter()
20422 .zip(&expected.shapes)
20423 .enumerate()
20424 {
20425 assert_eq!(got.rect, want.rect, "shape {index} rect");
20426 assert_eq!(got.local_rect, want.local_rect, "shape {index} local_rect");
20427 assert_eq!(got.quad, want.quad, "shape {index} quad");
20428 assert_eq!(got.snap_anchor, want.snap_anchor, "shape {index} snap");
20429 assert_eq!(got.brush, want.brush, "shape {index} brush");
20430 assert_eq!(got.shape, want.shape, "shape {index} shape");
20431 assert_eq!(got.stroke, want.stroke, "shape {index} stroke");
20432 assert_eq!(got.arc, want.arc, "shape {index} arc");
20433 assert_eq!(got.z_index, want.z_index, "shape {index} z");
20434 assert_eq!(got.clip, want.clip, "shape {index} clip");
20435 assert_eq!(got.blend_mode, want.blend_mode, "shape {index} blend");
20436 assert_eq!(
20437 got.motion_context_animated, want.motion_context_animated,
20438 "shape {index} motion flag"
20439 );
20440 }
20441 }
20442
20443 #[test]
20448 fn shape_run_collect_matches_per_primitive_emission_exactly() {
20449 assert_shape_run_collect_matches_per_primitive_emission();
20450 crate::normalized_scene::force_shape_run_parallel_for_tests(true);
20451 let outcome =
20452 std::panic::catch_unwind(assert_shape_run_collect_matches_per_primitive_emission);
20453 crate::normalized_scene::force_shape_run_parallel_for_tests(false);
20454 if let Err(payload) = outcome {
20455 std::panic::resume_unwind(payload);
20456 }
20457 }
20458
20459 #[test]
20460 fn non_translated_text_local_surface_keeps_linear_composite_resolve() {
20461 let layer = text_layer_with_style(
20462 AnnotatedString::from("gradient"),
20463 TextStyle::from_span_style(SpanStyle {
20464 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
20465 ..SpanStyle::default()
20466 }),
20467 );
20468 let requirements = layer_surface_requirements(&layer);
20469
20470 assert!(requirements
20471 .surface_requirements
20472 .contains(SurfaceRequirement::TextMaterialMask));
20473 assert_eq!(
20474 composite_sample_mode_for_requirements(false, false, requirements),
20475 CompositeSampleMode::Linear
20476 );
20477 }
20478
20479 #[test]
20480 fn inherited_translated_text_local_surface_uses_box4_layer_surface() {
20481 let layer = text_layer_with_style(
20482 AnnotatedString::from("shadow"),
20483 TextStyle::from_span_style(SpanStyle {
20484 shadow: Some(Shadow {
20485 color: Color::BLACK,
20486 offset: Point::new(1.0, 2.0),
20487 blur_radius: 3.0,
20488 }),
20489 ..SpanStyle::default()
20490 }),
20491 );
20492 let requirements = layer_surface_requirements(&layer);
20493
20494 assert!(requirements
20495 .surface_requirements
20496 .contains(SurfaceRequirement::TextMaterialMask));
20497 assert_eq!(
20498 composite_sample_mode_for_requirements(true, false, requirements),
20499 CompositeSampleMode::Box4
20500 );
20501 assert_eq!(
20502 layer_surface_target_scale(
20503 true,
20504 false,
20505 requirements,
20506 1.25,
20507 layer_surface_scale(&layer)
20508 ),
20509 SurfaceRequirementSet::default()
20510 .with(SurfaceRequirement::TextMaterialMask)
20511 .with(SurfaceRequirement::MotionStableCapture)
20512 .target_scale(1.25, 1.0)
20513 );
20514 }
20515
20516 #[test]
20517 fn translated_text_local_surface_inside_capture_keeps_parent_scale() {
20518 let layer = text_layer_with_style(
20519 AnnotatedString::from("shadow"),
20520 TextStyle::from_span_style(SpanStyle {
20521 shadow: Some(Shadow {
20522 color: Color::BLACK,
20523 offset: Point::new(1.0, 2.0),
20524 blur_radius: 3.0,
20525 }),
20526 ..SpanStyle::default()
20527 }),
20528 );
20529 let requirements = layer_surface_requirements(&layer);
20530
20531 assert_eq!(
20532 composite_sample_mode_for_requirements(true, true, requirements),
20533 CompositeSampleMode::Linear
20534 );
20535 assert_eq!(
20536 layer_surface_target_scale(true, true, requirements, 10.0, layer_surface_scale(&layer)),
20537 SurfaceRequirementSet::default()
20538 .with(SurfaceRequirement::TextMaterialMask)
20539 .target_scale(10.0, 1.0)
20540 );
20541 }
20542
20543 #[test]
20544 fn layer_surface_requirements_use_local_surface_for_gradient_and_stroke_text() {
20545 let cases = [
20546 (
20547 "draw_style",
20548 AnnotatedString::from("draw_style"),
20549 TextStyle::from_span_style(SpanStyle {
20550 draw_style: Some(TextDrawStyle::Stroke { width: 2.0 }),
20551 ..SpanStyle::default()
20552 }),
20553 ),
20554 (
20555 "gradient_brush",
20556 AnnotatedString::from("gradient"),
20557 TextStyle::from_span_style(SpanStyle {
20558 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
20559 ..SpanStyle::default()
20560 }),
20561 ),
20562 ];
20563
20564 for (label, text, text_style) in cases {
20565 let layer = text_layer_with_style(text, text_style);
20566 let requirements = layer_surface_requirements(&layer);
20567 assert!(
20568 requirements
20569 .surface_requirements
20570 .contains(SurfaceRequirement::TextMaterialMask),
20571 "{label} text should use a bounded local surface: {requirements:?}"
20572 );
20573 }
20574 }
20575
20576 #[test]
20577 fn layer_surface_requirements_use_local_surface_for_complex_text_effects() {
20578 let cases = [
20579 (
20580 "shadow",
20581 AnnotatedString::from("shadow"),
20582 TextStyle::from_span_style(SpanStyle {
20583 shadow: Some(Shadow {
20584 color: Color::BLACK,
20585 offset: Point::new(1.0, 2.0),
20586 blur_radius: 3.0,
20587 }),
20588 ..SpanStyle::default()
20589 }),
20590 ),
20591 (
20592 "background",
20593 AnnotatedString::from("background"),
20594 TextStyle::from_span_style(SpanStyle {
20595 background: Some(Color::BLACK),
20596 ..SpanStyle::default()
20597 }),
20598 ),
20599 (
20600 "baseline_shift",
20601 AnnotatedString::from("baseline_shift"),
20602 TextStyle::from_span_style(SpanStyle {
20603 baseline_shift: Some(BaselineShift::SUPERSCRIPT),
20604 ..SpanStyle::default()
20605 }),
20606 ),
20607 (
20608 "geometric_transform",
20609 AnnotatedString::from("geometric_transform"),
20610 TextStyle::from_span_style(SpanStyle {
20611 text_geometric_transform: Some(TextGeometricTransform {
20612 scale_x: 1.2,
20613 skew_x: 0.15,
20614 }),
20615 ..SpanStyle::default()
20616 }),
20617 ),
20618 (
20619 "letter_spacing",
20620 AnnotatedString::from("letter_spacing"),
20621 TextStyle::from_span_style(SpanStyle {
20622 letter_spacing: TextUnit::Em(0.2),
20623 ..SpanStyle::default()
20624 }),
20625 ),
20626 ];
20627
20628 for (label, text, text_style) in cases {
20629 let layer = text_layer_with_style(text, text_style);
20630 let requirements = layer_surface_requirements(&layer);
20631 assert!(
20632 requirements
20633 .surface_requirements
20634 .contains(SurfaceRequirement::TextMaterialMask),
20635 "{label} text should use a bounded local surface: {requirements:?}"
20636 );
20637 assert_eq!(
20638 requirements.direct_translation,
20639 Some(Point::default()),
20640 "{label} text should still classify as a direct translation"
20641 );
20642 }
20643 }
20644
20645 #[test]
20646 fn layer_surface_requirements_color_only_span_styles_use_direct_path() {
20647 let layer = text_layer_with_style(
20648 AnnotatedString {
20649 text: "styled".to_string(),
20650 span_styles: vec![RangeStyle {
20651 item: SpanStyle {
20652 color: Some(Color::BLACK),
20653 ..SpanStyle::default()
20654 },
20655 range: 0..3,
20656 }],
20657 ..AnnotatedString::default()
20658 },
20659 TextStyle::default(),
20660 );
20661 let requirements = layer_surface_requirements(&layer);
20662 assert!(
20663 !requirements
20664 .surface_requirements
20665 .contains(SurfaceRequirement::TextMaterialMask),
20666 "color-only span styles should render directly via software text raster colors"
20667 );
20668 }
20669
20670 #[test]
20671 fn layer_surface_requirements_keep_decoration_only_text_on_direct_path() {
20672 let layer = text_layer_with_style(
20673 AnnotatedString::from("decoration"),
20674 TextStyle::from_span_style(SpanStyle {
20675 text_decoration: Some(TextDecoration::UNDERLINE),
20676 ..SpanStyle::default()
20677 }),
20678 );
20679
20680 let requirements = layer_surface_requirements(&layer);
20681
20682 assert_eq!(requirements.direct_translation, Some(Point::default()));
20683 assert!(
20684 requirements
20685 .surface_requirements
20686 .contains(SurfaceRequirement::PixelStableComposite)
20687 && !requirements
20688 .surface_requirements
20689 .has_isolating_requirement(),
20690 "decoration-only text should not force an isolating layer surface: {requirements:?}"
20691 );
20692 }
20693
20694 #[test]
20695 fn direct_text_leaf_snaps_modifier_background_and_text_with_one_anchor() {
20696 let root = snapped_text_leaf_root(false, false);
20697 let mut rect_cache = HashMap::new();
20698 let mut requirements_cache = HashMap::new();
20699
20700 let collected =
20701 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20702
20703 assert_eq!(collected.scene.shapes.len(), 1);
20704 assert_eq!(collected.scene.images.len(), 1);
20705 assert_eq!(collected.scene.texts.len(), 1);
20706 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
20707 assert_eq!(collected.scene.shapes[0].snap_anchor, expected_anchor);
20708 assert_eq!(collected.scene.images[0].snap_anchor, expected_anchor);
20709 assert_eq!(collected.scene.texts[0].snap_anchor, expected_anchor);
20710 }
20711
20712 #[test]
20713 fn animated_translated_content_text_leaf_uses_bounded_content_snap() {
20714 let root = snapped_text_leaf_root(true, true);
20715 let mut rect_cache = HashMap::new();
20716 let mut requirements_cache = HashMap::new();
20717
20718 let collected =
20719 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20720
20721 assert_eq!(collected.child_layers.len(), 1);
20722 assert!(collected.scene.shapes.is_empty());
20723 assert!(collected.scene.images.is_empty());
20724 assert!(collected.scene.texts.is_empty());
20725 assert!(collected.scene.effect_layers.is_empty());
20726 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
20727 assert_eq!(
20728 collected.child_layers[0].snap_anchor, expected_anchor,
20729 "active translated leaf surface should keep the content-origin snap phase"
20730 );
20731 }
20732
20733 #[test]
20734 fn translated_content_assigns_motion_anchor_to_rotated_child_surface() {
20735 let mut child = snapped_text_leaf(false, false);
20736 child.graphics_layer.rotation_z = 5.0;
20737 child.transform_to_parent =
20738 cranpose_render_common::layer_transform::layer_transform_to_parent(
20739 child.local_bounds,
20740 Point::new(108.0, 3.0),
20741 &child.graphics_layer,
20742 );
20743 child.recompute_raster_cache_hashes();
20744 let mut root = test_layer(
20745 Rect {
20746 x: 0.0,
20747 y: 0.0,
20748 width: 320.0,
20749 height: 180.0,
20750 },
20751 vec![RenderNode::Layer(Box::new(child))],
20752 );
20753 root.translated_content_context = true;
20754 root.translated_content_offset = Point::new(0.0, -80.8);
20755 root.recompute_raster_cache_hashes();
20756 let mut rect_cache = HashMap::new();
20757 let mut requirements_cache = HashMap::new();
20758
20759 let collected =
20760 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20761
20762 assert_eq!(collected.child_layers.len(), 1);
20763 assert!(
20764 collected.child_layers[0].snap_anchor.is_some(),
20765 "a projective child still translates rigidly with its scrolling parent"
20766 );
20767 }
20768
20769 #[test]
20770 fn rested_translated_content_context_text_leaf_snaps_for_crisp_scroll_rest() {
20771 let root = snapped_text_leaf_root(false, true);
20772 let mut rect_cache = HashMap::new();
20773 let mut requirements_cache = HashMap::new();
20774
20775 let collected =
20776 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20777
20778 assert_eq!(collected.child_layers.len(), 0);
20779 assert_eq!(collected.scene.shapes.len(), 1);
20780 assert_eq!(collected.scene.images.len(), 1);
20781 assert_eq!(collected.scene.texts.len(), 1);
20782 assert_eq!(collected.scene.effect_layers.len(), 0);
20783 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
20784 assert_eq!(
20785 collected.scene.shapes[0].snap_anchor, expected_anchor,
20786 "rested scroll content should snap back to device pixels"
20787 );
20788 assert_eq!(
20789 collected.scene.images[0].snap_anchor, expected_anchor,
20790 "rested scroll images should snap back to device pixels"
20791 );
20792 assert_eq!(
20793 collected.scene.texts[0].snap_anchor, expected_anchor,
20794 "rested scroll text should snap back to device pixels"
20795 );
20796 }
20797
20798 #[test]
20799 fn complex_text_uses_local_surface() {
20800 let root = translated_content_local_surface_root();
20801 let mut rect_cache = HashMap::new();
20802 let mut requirements_cache = HashMap::new();
20803
20804 let collected =
20805 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20806
20807 assert!(
20808 !collected.child_layers.is_empty(),
20809 "translated-content effectful text should render through a bounded local surface"
20810 );
20811 assert!(collected.scene.texts.is_empty());
20812 assert!(collected.scene.shadow_draws.is_empty());
20813 }
20814
20815 #[test]
20816 fn translated_content_surface_composite_uses_scroll_content_snap_anchor() {
20817 let mut root = translated_content_local_surface_root();
20818 let scroll_offset = Point::new(0.0, -18.5);
20819 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
20820 panic!("expected translated content layer");
20821 };
20822 translated_content.translated_content_offset = scroll_offset;
20823 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
20824 panic!("expected effectful text layer");
20825 };
20826 effectful_text.transform_to_parent =
20827 effectful_text
20828 .transform_to_parent
20829 .then(ProjectiveTransform::translation(
20830 scroll_offset.x,
20831 scroll_offset.y,
20832 ));
20833
20834 let mut rect_cache = HashMap::new();
20835 let mut requirements_cache = HashMap::new();
20836 let collected =
20837 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20838
20839 assert_eq!(collected.child_layers.len(), 1);
20840 assert_eq!(
20841 collected.child_layers[0].snap_anchor,
20842 Some(SnapAnchor::rigid(Point::new(14.25, -2.0))),
20843 "isolated scrolled descendants must composite with the same content-origin snap phase"
20844 );
20845 }
20846
20847 #[test]
20848 fn animated_translated_content_surface_composite_uses_scroll_content_snap_anchor() {
20849 let mut root = translated_content_local_surface_root();
20850 let scroll_offset = Point::new(0.0, -18.5);
20851 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
20852 panic!("expected translated content layer");
20853 };
20854 translated_content.motion_context_animated = true;
20855 translated_content.translated_content_offset = scroll_offset;
20856 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
20857 panic!("expected effectful text layer");
20858 };
20859 effectful_text.transform_to_parent =
20860 effectful_text
20861 .transform_to_parent
20862 .then(ProjectiveTransform::translation(
20863 scroll_offset.x,
20864 scroll_offset.y,
20865 ));
20866
20867 let mut rect_cache = HashMap::new();
20868 let mut requirements_cache = HashMap::new();
20869 let collected =
20870 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20871
20872 assert_eq!(collected.child_layers.len(), 1);
20873 assert_eq!(
20874 collected.child_layers[0].snap_anchor,
20875 Some(SnapAnchor::rigid(Point::new(14.25, 16.5))),
20876 "animated translated content should composite the stable local surface at the viewport-origin snap phase"
20877 );
20878 }
20879
20880 #[test]
20881 fn translated_text_material_effect_layer_uses_scroll_content_snap_anchor() {
20882 let mut layer = text_layer_with_style(
20883 AnnotatedString::from("gradient"),
20884 TextStyle::from_span_style(SpanStyle {
20885 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
20886 ..SpanStyle::default()
20887 }),
20888 );
20889 layer.translated_content_context = true;
20890 layer.translated_content_offset = Point::new(0.0, -18.5);
20891 let mut rect_cache = HashMap::new();
20892 let mut requirements_cache = HashMap::new();
20893
20894 let collected =
20895 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
20896
20897 assert_eq!(collected.scene.effect_layers.len(), 1);
20898 assert_eq!(
20899 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
20900 CompositeSampleMode::Box4
20901 );
20902 assert_eq!(
20903 collected.scene.effect_layers[0].snap_anchor,
20904 Some(SnapAnchor::rigid(Point::new(0.0, -18.5))),
20905 "text material surfaces must composite with the scroll content-origin snap phase"
20906 );
20907 }
20908
20909 #[test]
20910 fn translated_layer_surface_capture_does_not_restart_local_picture_for_shadow_text() {
20911 let mut layer = text_layer_with_style(
20912 AnnotatedString::from("shadow"),
20913 TextStyle::from_span_style(SpanStyle {
20914 shadow: Some(Shadow {
20915 color: Color::BLACK,
20916 offset: Point::new(1.0, 2.0),
20917 blur_radius: 3.0,
20918 }),
20919 ..SpanStyle::default()
20920 }),
20921 );
20922 layer.translated_content_context = true;
20923 let mut rect_cache = HashMap::new();
20924 let mut requirements_cache = HashMap::new();
20925
20926 let collected = collect_layer_contents_with_translation_context(
20927 &layer,
20928 None,
20929 None,
20930 TranslationRenderContext {
20931 inherited_content_translation: false,
20932 surface_capture_active: true,
20933 local_picture_capture_active: true,
20934 ..TranslationRenderContext::default()
20935 },
20936 &mut rect_cache,
20937 &mut requirements_cache,
20938 );
20939
20940 assert!(
20941 collected.scene.effect_layers.is_empty(),
20942 "a translated layer surface already provides the stable local capture"
20943 );
20944 assert_eq!(collected.scene.shadow_draws.len(), 1);
20945 assert_eq!(collected.scene.texts.len(), 1);
20946 assert!(
20947 !collected.scene.texts[0].translated_content_context,
20948 "text inside an active motion-stable capture must raster in capture-local coordinates"
20949 );
20950 }
20951
20952 #[test]
20953 fn translated_layer_surface_capture_keeps_only_material_effect_layers() {
20954 let mut layer = text_layer_with_style(
20955 AnnotatedString::from("gradient"),
20956 TextStyle::from_span_style(SpanStyle {
20957 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
20958 ..SpanStyle::default()
20959 }),
20960 );
20961 layer.translated_content_context = true;
20962 let mut rect_cache = HashMap::new();
20963 let mut requirements_cache = HashMap::new();
20964
20965 let collected = collect_layer_contents_with_translation_context(
20966 &layer,
20967 None,
20968 None,
20969 TranslationRenderContext {
20970 inherited_content_translation: false,
20971 surface_capture_active: true,
20972 local_picture_capture_active: true,
20973 ..TranslationRenderContext::default()
20974 },
20975 &mut rect_cache,
20976 &mut requirements_cache,
20977 );
20978
20979 assert_eq!(collected.scene.effect_layers.len(), 1);
20980 assert!(
20981 collected.scene.effect_layers[0]
20982 .requirements
20983 .contains(SurfaceRequirement::MotionStableCapture),
20984 "translated text materials still need motion-stable resolve semantics inside a stable capture"
20985 );
20986 assert_eq!(
20987 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
20988 CompositeSampleMode::Box4
20989 );
20990 assert_eq!(
20991 effect_layer_target_scale(&collected.scene.effect_layers[0], 10.0),
20992 10.0
20993 );
20994 assert!(collected.scene.effect_layers[0].effect.is_some());
20995 }
20996
20997 #[test]
20998 fn translated_viewport_surface_does_not_add_plain_local_picture_capture() {
20999 let mut layer = text_layer_with_style(
21000 AnnotatedString::from("shadow"),
21001 TextStyle::from_span_style(SpanStyle {
21002 shadow: Some(Shadow {
21003 color: Color::BLACK,
21004 offset: Point::new(1.0, 2.0),
21005 blur_radius: 3.0,
21006 }),
21007 ..SpanStyle::default()
21008 }),
21009 );
21010 layer.translated_content_context = true;
21011 layer.motion_context_animated = true;
21012 let mut rect_cache = HashMap::new();
21013 let mut requirements_cache = HashMap::new();
21014
21015 let collected = collect_layer_contents_with_translation_context(
21016 &layer,
21017 None,
21018 None,
21019 TranslationRenderContext {
21020 surface_capture_active: true,
21021 ..TranslationRenderContext::default()
21022 },
21023 &mut rect_cache,
21024 &mut requirements_cache,
21025 );
21026
21027 assert_eq!(
21028 collected.scene.effect_layers.len(),
21029 0,
21030 "plain translated content inside a viewport surface should not be captured again"
21031 );
21032 assert_eq!(collected.scene.shadow_draws.len(), 1);
21033 assert_eq!(collected.scene.texts.len(), 1);
21034 }
21035
21036 #[test]
21037 fn static_pure_text_leaf_snaps_without_sibling_draw_primitives() {
21038 let root = pure_text_leaf_root(false, false);
21039 let mut rect_cache = HashMap::new();
21040 let mut requirements_cache = HashMap::new();
21041
21042 let collected =
21043 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21044
21045 assert_eq!(collected.scene.texts.len(), 1);
21046 assert!(
21047 collected.scene.texts[0].snap_anchor.is_some(),
21048 "idle pure text leaves should participate in rigid snap anchoring"
21049 );
21050 }
21051
21052 #[test]
21053 fn animated_pure_text_leaf_stays_unsnapped() {
21054 let root = pure_text_leaf_root(true, false);
21055 let mut rect_cache = HashMap::new();
21056 let mut requirements_cache = HashMap::new();
21057
21058 let collected =
21059 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21060
21061 assert_eq!(collected.scene.texts.len(), 1);
21062 assert_eq!(collected.scene.texts[0].snap_anchor, None);
21063 }
21064
21065 #[test]
21066 fn animated_translated_pure_text_uses_bounded_content_snap() {
21067 let root = pure_text_leaf_root(true, true);
21068 let mut rect_cache = HashMap::new();
21069 let mut requirements_cache = HashMap::new();
21070
21071 let collected =
21072 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21073
21074 assert_eq!(collected.child_layers.len(), 1);
21075 assert!(collected.scene.texts.is_empty());
21076 assert!(collected.scene.effect_layers.is_empty());
21077 assert_snap_anchor_close(
21078 collected.child_layers[0].snap_anchor,
21079 Point::new(11.4, 23.6),
21080 "animated translated pure text should use the bounded content snap phase",
21081 );
21082 }
21083
21084 #[test]
21085 fn rested_translated_pure_text_leaf_snaps_for_crisp_scroll_rest() {
21086 let root = pure_text_leaf_root(false, true);
21087 let mut rect_cache = HashMap::new();
21088 let mut requirements_cache = HashMap::new();
21089
21090 let collected =
21091 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21092
21093 assert_eq!(collected.child_layers.len(), 0);
21094 assert_eq!(collected.scene.texts.len(), 1);
21095 assert_eq!(collected.scene.effect_layers.len(), 0);
21096 assert_snap_anchor_close(
21097 collected.scene.texts[0].snap_anchor,
21098 Point::new(11.4, 23.6),
21099 "rested translated text should snap to device pixels",
21100 );
21101 }
21102
21103 #[test]
21104 fn static_gpu_effect_text_leaf_stays_unsnapped() {
21105 let root = text_layer_with_style(
21106 AnnotatedString::from("Gradient"),
21107 TextStyle::from_span_style(SpanStyle {
21108 brush: Some(Brush::linear_gradient(vec![
21109 Color(0.2, 0.8, 1.0, 1.0),
21110 Color(1.0, 0.7, 0.4, 1.0),
21111 ])),
21112 draw_style: Some(TextDrawStyle::Stroke { width: 2.5 }),
21113 ..SpanStyle::default()
21114 }),
21115 );
21116 let mut rect_cache = HashMap::new();
21117 let mut requirements_cache = HashMap::new();
21118
21119 let collected =
21120 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21121
21122 assert_eq!(collected.scene.texts.len(), 1);
21123 assert_eq!(
21124 collected.scene.texts[0].snap_anchor, None,
21125 "gpu text-effect leaves must not take the rigid text snap path"
21126 );
21127 assert_eq!(
21128 collected.scene.effect_layers.len(),
21129 1,
21130 "gradient stroke text should still emit a runtime shader effect layer"
21131 );
21132 }
21133
21134 #[test]
21135 fn layer_surface_requirements_keep_shape_plus_direct_child_on_direct_path() {
21136 let mut child = test_layer(
21137 Rect {
21138 x: 0.0,
21139 y: 0.0,
21140 width: 40.0,
21141 height: 20.0,
21142 },
21143 vec![RenderNode::Primitive(PrimitiveEntry {
21144 phase: PrimitivePhase::BeforeChildren,
21145 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21146 primitive: DrawPrimitive::Rect {
21147 rect: Rect {
21148 x: 0.0,
21149 y: 0.0,
21150 width: 40.0,
21151 height: 20.0,
21152 },
21153 brush: Brush::solid(Color::WHITE),
21154 stroke: None,
21155 },
21156 clip: None,
21157 }),
21158 })],
21159 );
21160 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
21161
21162 let layer = test_layer(
21163 Rect {
21164 x: 0.0,
21165 y: 0.0,
21166 width: 64.0,
21167 height: 32.0,
21168 },
21169 vec![
21170 RenderNode::Primitive(PrimitiveEntry {
21171 phase: PrimitivePhase::BeforeChildren,
21172 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21173 primitive: DrawPrimitive::Rect {
21174 rect: Rect {
21175 x: 0.0,
21176 y: 0.0,
21177 width: 64.0,
21178 height: 32.0,
21179 },
21180 brush: Brush::solid(Color::BLACK),
21181 stroke: None,
21182 },
21183 clip: None,
21184 }),
21185 }),
21186 RenderNode::Layer(Box::new(child)),
21187 ],
21188 );
21189
21190 let requirements = layer_surface_requirements(&layer);
21191
21192 assert_eq!(requirements.direct_translation, Some(Point::default()));
21193 assert!(!requirements
21194 .surface_requirements
21195 .contains(SurfaceRequirement::MixedDirectContent));
21196 assert!(!requirements
21197 .surface_requirements
21198 .has_isolating_requirement());
21199 }
21200
21201 #[test]
21202 fn collect_layer_contents_translates_direct_text_rects_into_parent_space() {
21203 let mut child = text_layer_with_style(
21204 AnnotatedString::from("direct"),
21205 TextStyle::from_span_style(SpanStyle {
21206 text_decoration: Some(TextDecoration::UNDERLINE),
21207 ..SpanStyle::default()
21208 }),
21209 );
21210 child.transform_to_parent = ProjectiveTransform::translation(9.0, 7.0);
21211
21212 let parent = test_layer(
21213 Rect {
21214 x: 0.0,
21215 y: 0.0,
21216 width: 64.0,
21217 height: 32.0,
21218 },
21219 vec![RenderNode::Layer(Box::new(child))],
21220 );
21221
21222 let mut rect_cache = HashMap::new();
21223 let mut requirements_cache = HashMap::new();
21224 let collected = with_test_app_context(|| {
21225 collect_layer_contents(
21226 &parent,
21227 None,
21228 None,
21229 &mut rect_cache,
21230 &mut requirements_cache,
21231 )
21232 });
21233
21234 assert!(
21235 collected.child_layers.is_empty(),
21236 "decoration-only text child should collapse directly into the parent scene"
21237 );
21238 assert_eq!(collected.scene.texts.len(), 1, "expected one text draw");
21239 let text = &collected.scene.texts[0];
21240 assert!(
21241 text.rect.x >= 9.0 && text.rect.y >= 7.0,
21242 "collapsed text rect should be translated into parent space, got {:?}",
21243 text.rect
21244 );
21245 assert!(
21246 collected
21247 .scene
21248 .shapes
21249 .iter()
21250 .any(|shape| shape.rect.y >= 7.0),
21251 "collapsed underline geometry should also be translated into parent space"
21252 );
21253 }
21254
21255 #[test]
21256 fn normalized_scene_keeps_lazy_after_bound_text_for_prewarm() {
21257 use std::cell::RefCell;
21258
21259 fn collect_graph_text_labels(layer: &LayerNode, labels: &mut Vec<String>) {
21260 for child in &layer.children {
21261 match child {
21262 RenderNode::Primitive(PrimitiveEntry {
21263 node: PrimitiveNode::Text(text),
21264 ..
21265 }) => labels.push(text.text.text.clone()),
21266 RenderNode::Layer(child_layer) => {
21267 collect_graph_text_labels(child_layer, labels)
21268 }
21269 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
21270 }
21271 }
21272 }
21273
21274 let state_holder: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
21275 let state_holder_for_comp = state_holder.clone();
21276 let mut composition = cranpose_ui::run_test_composition(move || {
21277 let list_state = remember_lazy_list_state();
21278 *state_holder_for_comp.borrow_mut() = Some(list_state);
21279 let mut spec = LazyColumnSpec::new()
21280 .vertical_arrangement(cranpose_ui::LinearArrangement::SpacedBy(6.0));
21281 spec.beyond_bounds_item_count = 0;
21282 LazyColumn(Modifier::empty().height(96.0), list_state, spec, |scope| {
21283 scope.items(
21284 12,
21285 None::<fn(usize) -> u64>,
21286 None::<fn(usize) -> u64>,
21287 |index| {
21288 Text(
21289 format!("WarmRow {index}"),
21290 Modifier::empty().height(32.0),
21291 TextStyle::default(),
21292 );
21293 },
21294 );
21295 });
21296 });
21297
21298 let list_state = (*state_holder.borrow()).expect("lazy list state should be captured");
21299 list_state.scroll_to_item(4, 0.0);
21300
21301 let root = composition.root().expect("lazy column root");
21302 let handle = composition.runtime_handle();
21303 let mut applier = composition.applier_mut();
21304 applier.set_runtime_handle(handle);
21305 let _ = applier
21306 .compute_layout(
21307 root,
21308 Size {
21309 width: 240.0,
21310 height: 240.0,
21311 },
21312 )
21313 .expect("lazy column layout");
21314 let graph = build_graph_from_applier(&mut applier, root, 1.0).expect("lazy column graph");
21315 applier.clear_runtime_handle();
21316 let mut graph_labels = Vec::new();
21317 collect_graph_text_labels(&graph.root, &mut graph_labels);
21318
21319 let visible_indices: Vec<_> = list_state
21320 .layout_info()
21321 .visible_items_info
21322 .iter()
21323 .map(|item| item.index)
21324 .collect();
21325 assert_eq!(
21326 visible_indices,
21327 vec![4, 5, 6],
21328 "test setup expects exactly three viewport-visible rows"
21329 );
21330
21331 let mut rect_cache = HashMap::new();
21332 let mut requirements_cache = HashMap::new();
21333 let collected = with_test_app_context(|| {
21334 collect_layer_contents(
21335 &graph.root,
21336 None,
21337 None,
21338 &mut rect_cache,
21339 &mut requirements_cache,
21340 )
21341 });
21342 let root_text_labels: Vec<_> = collected
21343 .scene
21344 .texts
21345 .iter()
21346 .map(|text| text.text.text.clone())
21347 .collect();
21348 let child_layer_count = collected.child_layers.len();
21349 let warm_text = collected
21350 .scene
21351 .texts
21352 .iter()
21353 .find(|text| text.text.text == "WarmRow 7")
21354 .unwrap_or_else(|| {
21355 panic!(
21356 "after-bound lazy text should reach WGPU scene collection; graph_texts={graph_labels:?} root_texts={root_text_labels:?} child_layers={child_layer_count}"
21357 )
21358 });
21359
21360 assert!(
21361 warm_text.rect.y >= 96.0,
21362 "after-bound text should be below the viewport, got {:?}",
21363 warm_text.rect
21364 );
21365 assert_eq!(
21366 visible_draw_rect(warm_text.rect, warm_text.clip),
21367 None,
21368 "after-bound text should remain clipped away for drawing while staying available for glyph prewarm"
21369 );
21370 assert!(
21371 text_draw_should_prewarm_in_viewport(
21372 warm_text.rect,
21373 warm_text.clip,
21374 ViewportUniformParams {
21375 width: 240,
21376 height: 96,
21377 offset: [0.0, 0.0],
21378 },
21379 1.0,
21380 ),
21381 "after-bound text inside the warm window must be selected by WGPU prewarm"
21382 );
21383 }
21384
21385 #[test]
21386 fn direct_translation_accepts_nearly_identity_axis_scale_noise() {
21387 let local_bounds = Rect {
21388 x: 0.0,
21389 y: 0.0,
21390 width: 393.3,
21391 height: 16.8,
21392 };
21393 let quad = [
21394 [10.0, 78.399_994],
21395 [403.3, 78.399_994],
21396 [10.0, 95.2],
21397 [403.3, 95.2],
21398 ];
21399 let transform = ProjectiveTransform::from_rect_to_quad(local_bounds, quad);
21400
21401 assert_eq!(
21402 direct_translation(transform),
21403 Some(Point::new(10.0, 78.399_994)),
21404 );
21405 }
21406
21407 #[test]
21408 fn layer_surface_requirements_keep_shape_plus_isolating_child_as_mixed_content() {
21409 let mut child = test_layer(
21410 Rect {
21411 x: 0.0,
21412 y: 0.0,
21413 width: 24.0,
21414 height: 18.0,
21415 },
21416 vec![RenderNode::Primitive(PrimitiveEntry {
21417 phase: PrimitivePhase::BeforeChildren,
21418 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21419 primitive: DrawPrimitive::Rect {
21420 rect: Rect {
21421 x: 0.0,
21422 y: 0.0,
21423 width: 24.0,
21424 height: 18.0,
21425 },
21426 brush: Brush::solid(Color::WHITE),
21427 stroke: None,
21428 },
21429 clip: None,
21430 }),
21431 })],
21432 );
21433 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
21434 child.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
21435
21436 let layer = test_layer(
21437 Rect {
21438 x: 0.0,
21439 y: 0.0,
21440 width: 64.0,
21441 height: 32.0,
21442 },
21443 vec![
21444 RenderNode::Primitive(PrimitiveEntry {
21445 phase: PrimitivePhase::BeforeChildren,
21446 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21447 primitive: DrawPrimitive::Rect {
21448 rect: Rect {
21449 x: 0.0,
21450 y: 0.0,
21451 width: 64.0,
21452 height: 32.0,
21453 },
21454 brush: Brush::solid(Color::BLACK),
21455 stroke: None,
21456 },
21457 clip: None,
21458 }),
21459 }),
21460 RenderNode::Layer(Box::new(child)),
21461 ],
21462 );
21463
21464 let requirements = layer_surface_requirements(&layer);
21465
21466 assert!(requirements
21467 .surface_requirements
21468 .contains(SurfaceRequirement::MixedDirectContent));
21469 assert!(!requirements
21470 .surface_requirements
21471 .has_isolating_requirement());
21472 }
21473
21474 #[test]
21475 fn build_scene_window_filters_and_translates_items() {
21476 let mut shape = test_shape(6, BlendMode::SrcOver);
21477 shape.rect.x = 12.0;
21478 shape.rect.y = 25.0;
21479 shape.local_rect.x = 12.0;
21480 shape.local_rect.y = 25.0;
21481 shape.quad = [[12.0, 25.0], [20.0, 25.0], [12.0, 33.0], [20.0, 33.0]];
21482 shape.clip = Some(Rect {
21483 x: 11.0,
21484 y: 24.0,
21485 width: 10.0,
21486 height: 10.0,
21487 });
21488
21489 let mut image = test_image(8, BlendMode::SrcOver);
21490 image.rect.x = 18.0;
21491 image.rect.y = 27.0;
21492 image.local_rect.x = 18.0;
21493 image.local_rect.y = 27.0;
21494 image.quad = [[18.0, 27.0], [26.0, 27.0], [18.0, 35.0], [26.0, 35.0]];
21495
21496 let mut text = test_text(9);
21497 text.rect.x = 16.0;
21498 text.rect.y = 29.0;
21499 text.clip = Some(Rect {
21500 x: 15.0,
21501 y: 28.0,
21502 width: 9.0,
21503 height: 6.0,
21504 });
21505
21506 let mut shadow_shape = test_shape(7, BlendMode::SrcOver);
21507 shadow_shape.rect.x = 14.0;
21508 shadow_shape.rect.y = 26.0;
21509 shadow_shape.local_rect.x = 14.0;
21510 shadow_shape.local_rect.y = 26.0;
21511 shadow_shape.quad = [[14.0, 26.0], [22.0, 26.0], [14.0, 34.0], [22.0, 34.0]];
21512 let mut shadow = test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]);
21513 shadow.z_index = 7;
21514
21515 let mut nested_effect = effect_layer(6, 10);
21516 nested_effect.rect.x = 13.0;
21517 nested_effect.rect.y = 24.0;
21518 nested_effect.clip = Some(Rect {
21519 x: 15.0,
21520 y: 25.0,
21521 width: 4.0,
21522 height: 5.0,
21523 });
21524
21525 let mut nested_backdrop = backdrop_layer(8);
21526 nested_backdrop.rect.x = 17.0;
21527 nested_backdrop.rect.y = 26.0;
21528 nested_backdrop.clip = Some(Rect {
21529 x: 18.0,
21530 y: 27.0,
21531 width: 3.0,
21532 height: 4.0,
21533 });
21534
21535 let window = build_scene_window(
21536 SceneWindowSource {
21537 shapes: &[test_shape(4, BlendMode::SrcOver), shape],
21538 brushes: &[],
21539 images: &[image],
21540 texts: &[text],
21541 shadow_draws: &[shadow],
21542 draw_ops: &[],
21543 effect_layers: &[effect_layer(2, 4), nested_effect.clone()],
21544 backdrop_layers: &[backdrop_layer(4), nested_backdrop.clone()],
21545 },
21546 5,
21547 10,
21548 Rect {
21549 x: 10.0,
21550 y: 20.0,
21551 width: 20.0,
21552 height: 20.0,
21553 },
21554 );
21555
21556 assert_eq!(window.shapes.len(), 1);
21557 assert_eq!(
21558 window.shapes[0].rect,
21559 Rect {
21560 x: 2.0,
21561 y: 5.0,
21562 width: 8.0,
21563 height: 8.0,
21564 }
21565 );
21566 assert_eq!(
21567 window.shapes[0].clip,
21568 Some(Rect {
21569 x: 1.0,
21570 y: 4.0,
21571 width: 10.0,
21572 height: 10.0,
21573 })
21574 );
21575 assert_eq!(window.images.len(), 1);
21576 assert_eq!(window.images[0].rect.x, 8.0);
21577 assert_eq!(window.images[0].rect.y, 7.0);
21578 assert_eq!(window.texts.len(), 1);
21579 assert_eq!(window.texts[0].rect.x, 6.0);
21580 assert_eq!(window.texts[0].rect.y, 9.0);
21581 assert_eq!(
21582 window.texts[0].clip,
21583 Some(Rect {
21584 x: 5.0,
21585 y: 8.0,
21586 width: 9.0,
21587 height: 6.0,
21588 })
21589 );
21590 assert_eq!(window.shadow_draws.len(), 1);
21591 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.x, 4.0);
21592 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.y, 6.0);
21593 assert_eq!(window.effect_layers.len(), 1);
21594 assert_eq!(
21595 window.effect_layers[0].rect,
21596 Rect {
21597 x: 3.0,
21598 y: 4.0,
21599 width: 10.0,
21600 height: 10.0,
21601 }
21602 );
21603 assert_eq!(
21604 window.effect_layers[0].clip,
21605 Some(Rect {
21606 x: 5.0,
21607 y: 5.0,
21608 width: 4.0,
21609 height: 5.0,
21610 })
21611 );
21612 assert_eq!(window.backdrop_layers.len(), 1);
21613 assert_eq!(
21614 window.backdrop_layers[0].rect,
21615 Rect {
21616 x: 7.0,
21617 y: 6.0,
21618 width: 10.0,
21619 height: 10.0,
21620 }
21621 );
21622 assert_eq!(
21623 window.backdrop_layers[0].clip,
21624 Some(Rect {
21625 x: 8.0,
21626 y: 7.0,
21627 width: 3.0,
21628 height: 4.0,
21629 })
21630 );
21631 }
21632
21633 #[test]
21634 fn filtered_effect_layer_index_counts_only_window_members() {
21635 let effects = vec![
21636 effect_layer(0, 2),
21637 effect_layer(5, 12),
21638 effect_layer(6, 10),
21639 effect_layer(14, 20),
21640 ];
21641
21642 assert_eq!(filtered_effect_layer_index(&effects, 1, 5, 12), Some(0));
21643 assert_eq!(filtered_effect_layer_index(&effects, 2, 5, 12), Some(1));
21644 assert_eq!(filtered_effect_layer_index(&effects, 3, 5, 12), None);
21645 }
21646
21647 #[test]
21648 fn blend_mode_support_matrix_is_explicit() {
21649 assert!(is_blend_mode_supported(BlendMode::SrcOver));
21650 assert!(is_blend_mode_supported(BlendMode::DstOut));
21651 assert!(!is_blend_mode_supported(BlendMode::Clear));
21652 assert!(!is_blend_mode_supported(BlendMode::Multiply));
21653 }
21654
21655 #[test]
21656 fn collect_non_effect_segment_items_preserves_global_z_order() {
21657 let shapes = vec![
21658 test_shape(3, BlendMode::SrcOver),
21659 test_shape(1, BlendMode::DstOut),
21660 ];
21661 let images = vec![test_image(2, BlendMode::SrcOver)];
21662 let texts = vec![test_text(0)];
21663 let shadows: Vec<ShadowDraw> = Vec::new();
21664 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
21665
21666 let mut scratch = Vec::new();
21667 collect_non_effect_segment_items(
21668 &shapes,
21669 &images,
21670 &texts,
21671 &shadows,
21672 &draw_ops,
21673 0,
21674 4,
21675 &[],
21676 100,
21677 100,
21678 1.0,
21679 &mut scratch,
21680 );
21681 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
21682 assert_eq!(
21683 items,
21684 vec![
21685 SegmentDrawItem::Text(0),
21686 SegmentDrawItem::Shape(1),
21687 SegmentDrawItem::Image(0),
21688 SegmentDrawItem::Shape(0),
21689 ]
21690 );
21691 }
21692
21693 #[test]
21694 fn collect_non_effect_segment_items_filters_effect_ranges() {
21695 let shapes = vec![
21696 test_shape(1, BlendMode::SrcOver),
21697 test_shape(3, BlendMode::DstOut),
21698 ];
21699 let images = vec![test_image(2, BlendMode::SrcOver)];
21700 let texts = vec![test_text(4)];
21701 let shadows: Vec<ShadowDraw> = Vec::new();
21702 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
21703 let effect_ranges = [std::ops::Range { start: 2, end: 4 }];
21704
21705 let mut scratch = Vec::new();
21706 collect_non_effect_segment_items(
21707 &shapes,
21708 &images,
21709 &texts,
21710 &shadows,
21711 &draw_ops,
21712 0,
21713 5,
21714 &effect_ranges,
21715 100,
21716 100,
21717 1.0,
21718 &mut scratch,
21719 );
21720 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
21721 assert_eq!(
21722 items,
21723 vec![SegmentDrawItem::Shape(0), SegmentDrawItem::Text(0)]
21724 );
21725 }
21726
21727 #[test]
21728 fn collect_non_effect_segment_items_culls_offscreen_shapes_but_keeps_text_prewarm() {
21729 let mut shape = test_shape(0, BlendMode::SrcOver);
21730 shape.rect.y = 160.0;
21731 shape.local_rect.y = 160.0;
21732 shape.quad = [[0.0, 160.0], [8.0, 160.0], [0.0, 168.0], [8.0, 168.0]];
21733
21734 let shapes = vec![shape];
21735 let images = Vec::new();
21736 let mut text = test_text(1);
21737 text.rect.y = 160.0;
21738 let texts = vec![text];
21739 let shadows: Vec<ShadowDraw> = Vec::new();
21740 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
21741
21742 let mut scratch = Vec::new();
21743 collect_non_effect_segment_items(
21744 &shapes,
21745 &images,
21746 &texts,
21747 &shadows,
21748 &draw_ops,
21749 0,
21750 2,
21751 &[],
21752 100,
21753 100,
21754 1.0,
21755 &mut scratch,
21756 );
21757
21758 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
21759 assert_eq!(items, vec![SegmentDrawItem::Text(0)]);
21760 }
21761
21762 #[test]
21763 fn segment_command_iter_merges_non_conflicting_batches_into_one_chunk() {
21764 let ordered_items = vec![
21765 (0, SegmentDrawItem::Shape(0)),
21766 (1, SegmentDrawItem::Image(0)),
21767 (2, SegmentDrawItem::Text(0)),
21768 ];
21769 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
21770 let images = vec![test_image(1, BlendMode::DstOut)];
21771
21772 let commands: Vec<_> = SegmentCommandIter::new(
21773 &ordered_items,
21774 &shapes,
21775 &images,
21776 ShapeBatchLimits::desktop(),
21777 )
21778 .collect();
21779
21780 assert_eq!(
21781 commands,
21782 vec![SegmentRenderCommand::DrawChunk(chunk(&[
21783 SegmentBatchPlan::Shape {
21784 start: 0,
21785 end: 1,
21786 blend_mode: BlendMode::SrcOver,
21787 },
21788 SegmentBatchPlan::Image {
21789 start: 1,
21790 end: 2,
21791 blend_mode: BlendMode::DstOut,
21792 },
21793 SegmentBatchPlan::Text { start: 2, end: 3 },
21794 ]))]
21795 );
21796 }
21797
21798 #[test]
21799 fn segment_command_iter_keeps_layer_composites_in_ordered_draw_chunk() {
21800 let ordered_items = vec![
21801 (0, SegmentDrawItem::Shape(0)),
21802 (1, SegmentDrawItem::Composite(0)),
21803 (2, SegmentDrawItem::Image(0)),
21804 (3, SegmentDrawItem::Composite(1)),
21805 (4, SegmentDrawItem::Text(0)),
21806 ];
21807 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
21808 let images = vec![test_image(2, BlendMode::SrcOver)];
21809
21810 let commands: Vec<_> = SegmentCommandIter::new(
21811 &ordered_items,
21812 &shapes,
21813 &images,
21814 ShapeBatchLimits::desktop(),
21815 )
21816 .collect();
21817
21818 assert_eq!(
21819 commands,
21820 vec![SegmentRenderCommand::DrawChunk(chunk(&[
21821 SegmentBatchPlan::Shape {
21822 start: 0,
21823 end: 1,
21824 blend_mode: BlendMode::SrcOver,
21825 },
21826 SegmentBatchPlan::Composite { start: 1, end: 2 },
21827 SegmentBatchPlan::Image {
21828 start: 2,
21829 end: 3,
21830 blend_mode: BlendMode::SrcOver,
21831 },
21832 SegmentBatchPlan::Composite { start: 3, end: 4 },
21833 SegmentBatchPlan::Text { start: 4, end: 5 },
21834 ]))]
21835 );
21836 }
21837
21838 #[test]
21839 fn retain_renderable_shadow_items_culls_invisible_shadow_boundaries() {
21840 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
21841 let images = vec![test_image(2, BlendMode::SrcOver)];
21842 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
21843 shadow_shape.rect = Rect {
21844 x: 500.0,
21845 y: 500.0,
21846 width: 12.0,
21847 height: 12.0,
21848 };
21849 let shadow_draws = vec![ShadowDraw {
21850 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
21851 brushes: vec![],
21852 texts: Vec::new(),
21853 blur_radius: 8.0,
21854 clip: None,
21855 z_index: 1,
21856 }];
21857 let mut ordered_items = vec![
21858 (0, SegmentDrawItem::Shape(0)),
21859 (1, SegmentDrawItem::Shadow(0)),
21860 (2, SegmentDrawItem::Image(0)),
21861 ];
21862
21863 let culled =
21864 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
21865 let commands: Vec<_> = SegmentCommandIter::new(
21866 &ordered_items,
21867 &shapes,
21868 &images,
21869 ShapeBatchLimits::desktop(),
21870 )
21871 .collect();
21872
21873 assert_eq!(culled, 1);
21874 assert_eq!(
21875 commands,
21876 vec![SegmentRenderCommand::DrawChunk(chunk(&[
21877 SegmentBatchPlan::Shape {
21878 start: 0,
21879 end: 1,
21880 blend_mode: BlendMode::SrcOver,
21881 },
21882 SegmentBatchPlan::Image {
21883 start: 1,
21884 end: 2,
21885 blend_mode: BlendMode::SrcOver,
21886 },
21887 ]))]
21888 );
21889 }
21890
21891 #[test]
21892 fn retain_renderable_shadow_items_keeps_visible_shadow_boundaries() {
21893 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
21894 shadow_shape.rect = Rect {
21895 x: 20.0,
21896 y: 20.0,
21897 width: 12.0,
21898 height: 12.0,
21899 };
21900 let shadow_draws = vec![ShadowDraw {
21901 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
21902 brushes: vec![],
21903 texts: Vec::new(),
21904 blur_radius: 8.0,
21905 clip: None,
21906 z_index: 1,
21907 }];
21908 let mut ordered_items = vec![(1, SegmentDrawItem::Shadow(0))];
21909
21910 let culled =
21911 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
21912
21913 assert_eq!(culled, 0);
21914 assert_eq!(ordered_items, vec![(1, SegmentDrawItem::Shadow(0))]);
21915 }
21916
21917 #[test]
21918 fn shape_data_layout_matches_the_wgsl_mirror() {
21919 assert_eq!(std::mem::size_of::<ShapeData>(), 160);
21923 assert_eq!(std::mem::size_of::<ShapeData>() % 16, 0);
21924 assert_eq!(std::mem::size_of::<GradientStop>(), 32);
21925 }
21926
21927 #[test]
21928 fn shape_flags_pack_kind_cap_and_join_without_collision() {
21929 assert_eq!(
21930 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
21931 0.0
21932 );
21933 assert_eq!(
21934 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
21935 1.0
21936 );
21937 assert_eq!(
21938 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter),
21939 2.0
21940 );
21941 assert_eq!(
21943 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Round, StrokeJoin::Miter),
21944 2.0 + 4.0
21945 );
21946 assert_eq!(
21947 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Square, StrokeJoin::Miter),
21948 2.0 + 8.0
21949 );
21950 assert_eq!(
21951 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Round),
21952 1.0 + 16.0
21953 );
21954 assert_eq!(
21955 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Bevel),
21956 1.0 + 32.0
21957 );
21958 for kind in [SHAPE_KIND_FILL, SHAPE_KIND_STROKE, SHAPE_KIND_ARC] {
21960 for cap in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square] {
21961 for join in [StrokeJoin::Miter, StrokeJoin::Round, StrokeJoin::Bevel] {
21962 let packed = pack_shape_flags(kind, cap, join);
21963 let bits = packed as u32;
21964 assert_eq!(bits & 3, kind);
21965 assert_eq!((bits >> 2) & 3, stroke_cap_code(cap));
21966 assert_eq!((bits >> 4) & 3, stroke_join_code(join));
21967 assert_eq!(packed, bits as f32, "flags must be exact in f32");
21968 }
21969 }
21970 }
21971 }
21972
21973 #[cfg(not(target_arch = "wasm32"))]
21974 #[test]
21975 fn mesh_vertex_layout_matches_the_wgsl_input() {
21976 assert_eq!(std::mem::size_of::<MeshVertex>(), 20);
21979 }
21980
21981 #[cfg(not(target_arch = "wasm32"))]
21985 #[allow(clippy::too_many_arguments)]
21986 fn sdf_arc_band_reference(
21987 p: [f32; 2],
21988 center: [f32; 2],
21989 inner: f32,
21990 outer: f32,
21991 mid_sin_cos: [f32; 2],
21992 half_sin_cos: [f32; 2],
21993 cap: u32,
21994 ) -> f32 {
21995 let ra = (outer + inner) * 0.5;
21996 let rb = ((outer - inner) * 0.5).max(0.0);
21997 let sm = mid_sin_cos[0];
21998 let cm = mid_sin_cos[1];
21999 let d = [p[0] - center[0], p[1] - center[1]];
22000 let mut q = [-sm * d[0] + cm * d[1], cm * d[0] + sm * d[1]];
22001 q[0] = q[0].abs();
22002 let sc = half_sin_cos;
22003 let mut dist = if sc[1] * q[0] > sc[0] * q[1] {
22004 let dx = q[0] - sc[0] * ra;
22005 let dy = q[1] - sc[1] * ra;
22006 (dx * dx + dy * dy).sqrt() - rb
22007 } else {
22008 ((q[0] * q[0] + q[1] * q[1]).sqrt() - ra).abs() - rb
22009 };
22010 let plane = sc[1] * q[0] - sc[0] * q[1];
22011 if cap == 0 {
22013 dist = dist.max(plane);
22014 } else if cap == 2 {
22015 dist = dist.max(plane - rb);
22016 }
22017 dist
22018 }
22019
22020 #[cfg(not(target_arch = "wasm32"))]
22021 fn point_in_triangle(p: [f64; 2], tri: &[[f64; 2]; 3]) -> bool {
22022 let side = |a: [f64; 2], b: [f64; 2]| {
22023 (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
22024 };
22025 let d0 = side(tri[0], tri[1]);
22026 let d1 = side(tri[1], tri[2]);
22027 let d2 = side(tri[2], tri[0]);
22028 let has_neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
22029 let has_pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
22030 !(has_neg && has_pos)
22031 }
22032
22033 #[cfg(not(target_arch = "wasm32"))]
22034 fn converted_arc_shape(arc: cranpose_ui_graphics::ArcGeometry, root_scale: f32) -> ShapeData {
22035 let bounds = arc.bounds();
22036 let mut shape = test_shape(0, BlendMode::SrcOver);
22037 shape.rect = bounds;
22038 shape.local_rect = bounds;
22039 shape.quad = [
22040 [bounds.x, bounds.y],
22041 [bounds.x + bounds.width, bounds.y],
22042 [bounds.x, bounds.y + bounds.height],
22043 [bounds.x + bounds.width, bounds.y + bounds.height],
22044 ];
22045 shape.arc = Some(arc);
22046 let mut converted = ShapeData::zeroed();
22047 convert_shape_into_slots(&shape, &[], root_scale, 0, &mut converted, &mut []);
22048 converted
22049 }
22050
22051 #[cfg(not(target_arch = "wasm32"))]
22056 #[test]
22057 fn arc_mesh_contains_every_band_pixel() {
22058 use cranpose_ui_graphics::ArcGeometry;
22059 let tau = cranpose_ui_graphics::TAU;
22060 let center = Point::new(250.0, 250.0);
22061 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
22062 (90.0, 100.0, 0.0, tau, StrokeCap::Round),
22064 (80.0, 100.0, 1.0, 10.0, StrokeCap::Butt),
22066 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
22068 (30.0, 80.0, 0.7, 2.5, StrokeCap::Butt),
22070 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
22071 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
22072 (99.0, 101.0, 3.0, 4.0, StrokeCap::Round),
22074 (0.6, 2.0, 0.3, 1.2, StrokeCap::Butt),
22076 (1900.0, 1904.0, 0.1, 0.35, StrokeCap::Square),
22078 (40.0, 60.0, 5.0, 1e-3, StrokeCap::Round),
22080 (40.0, 60.0, 0.2, tau - 1e-3, StrokeCap::Butt),
22082 (0.0, 3.0, 1.0, 2.0, StrokeCap::Round),
22084 (20.0, 60.0, 4.5, 1.9, StrokeCap::Butt),
22086 ];
22087 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
22088 for root_scale in [1.0f32, 2.0, 2.75] {
22093 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
22094 assert!(!arc.is_degenerate(), "case {case} must be drawable");
22095 let converted = converted_arc_shape(arc, root_scale);
22096 let band = arc_mesh_band(&converted)
22097 .unwrap_or_else(|| panic!("case {case} must qualify for meshing"));
22098 let mut vertices = Vec::new();
22099 let mut indices = Vec::new();
22100 let segments =
22101 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
22102 .unwrap_or_else(|| panic!("case {case} must produce a mesh"));
22103 assert!(segments >= ARC_MESH_MIN_SEGMENTS);
22104 let position = |index: u32| {
22107 let p = vertices[index as usize].position;
22108 [p[0] as f64, p[1] as f64]
22109 };
22110 let triangles: Vec<[[f64; 2]; 3]> = indices
22111 .as_chunks::<3>()
22112 .0
22113 .iter()
22114 .map(|tri| [position(tri[0]), position(tri[1]), position(tri[2])])
22115 .collect();
22116
22117 let [qx, qy, ..] = converted.quad01;
22121 let [_, _, qr, qb] = converted.quad23;
22122 let (rw, rh) = (qr - qx, qb - qy);
22123 let cap_bits = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
22124 let step = (rw.max(rh) / 400.0).clamp(0.25, 2.0);
22125 let mut band_points = 0usize;
22126 let mut y = qy;
22127 while y <= qb {
22128 let mut x = qx;
22129 while x <= qr {
22130 let dist = sdf_arc_band_reference(
22131 [x, y],
22132 [converted.arc_params[0], converted.arc_params[1]],
22133 converted.stroke_params[3],
22134 converted.stroke_params[2],
22135 [converted.radii[0], converted.radii[1]],
22136 [converted.radii[2], converted.radii[3]],
22137 cap_bits,
22138 );
22139 if dist <= 0.5 {
22140 band_points += 1;
22141 let p = [x as f64, y as f64];
22142 assert!(
22143 triangles.iter().any(|tri| point_in_triangle(p, tri)),
22144 "case {case} scale {root_scale}: band point ({x}, {y}) \
22145 dist {dist} escapes the mesh"
22146 );
22147 }
22148 x += step;
22149 }
22150 y += step;
22151 }
22152 assert!(
22153 band_points > 0,
22154 "case {case} scale {root_scale}: the sampling grid never hit the band"
22155 );
22156 }
22157 }
22158 }
22159
22160 #[cfg(not(target_arch = "wasm32"))]
22166 #[test]
22167 fn unmeshed_shapes_leave_no_geometry_and_empty_index_ranges() {
22168 let shape = test_shape(0, BlendMode::SrcOver);
22169 let mut converted = ShapeData::zeroed();
22170 convert_shape_into_slots(&shape, &[], 1.0, 0, &mut converted, &mut []);
22171 let build = build_arc_mesh_vertices(
22172 std::slice::from_ref(&converted),
22173 RETAINED_MESH_MIN_PX2_DEFAULT as f64,
22174 )
22175 .expect("within budget");
22176 assert_eq!(build.meshed_arcs, 0);
22177 assert_eq!(build.meshed_rims, 0);
22178 assert_eq!(build.passthrough, 1);
22179 assert_eq!(build.meshed_stretches, 0);
22180 assert!(build.vertices.is_empty());
22181 assert!(build.indices.is_empty());
22182 assert_eq!(build.index_prefix, vec![0, 0]);
22183 assert_eq!(build.mesh_area, build.quad_area);
22186 }
22187
22188 #[cfg(not(target_arch = "wasm32"))]
22194 #[test]
22195 fn arc_mesh_indices_share_boundary_vertices_and_wrap_closed_rings() {
22196 use cranpose_ui_graphics::ArcGeometry;
22197 let tau = cranpose_ui_graphics::TAU;
22198 for (sweep, closed) in [(tau, true), (1.9f32, false)] {
22201 let arc = ArcGeometry::new(
22202 Point::new(250.0, 250.0),
22203 80.0,
22204 100.0,
22205 0.7,
22206 sweep,
22207 StrokeCap::Round,
22208 );
22209 let mut converted = converted_arc_shape(arc, 1.0);
22210 converted.rect = [0.0, 0.0, 500.0, 500.0];
22214 converted.quad01 = [0.0, 0.0, 500.0, 0.0];
22215 converted.quad23 = [0.0, 500.0, 500.0, 500.0];
22216 let band = arc_mesh_band(&converted).expect("arc must qualify");
22217 let mut vertices = Vec::new();
22218 let mut indices = Vec::new();
22219 let segments = emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
22220 .expect("arc must mesh");
22221 let boundary_count = if closed { segments } else { segments + 1 };
22222 assert_eq!(
22223 vertices.len(),
22224 2 * boundary_count,
22225 "closed={closed}: every boundary owns exactly one (inner, outer) pair"
22226 );
22227 assert_eq!(indices.len(), 6 * segments);
22228 for j in 0..segments {
22232 let jb = (j + 1) % boundary_count;
22233 let (in_a, out_a) = (2 * j as u32, 2 * j as u32 + 1);
22234 let (in_b, out_b) = (2 * jb as u32, 2 * jb as u32 + 1);
22235 assert_eq!(
22236 indices[6 * j..6 * j + 6],
22237 [in_a, out_a, out_b, in_a, out_b, in_b],
22238 "closed={closed}: segment {j} must share its boundary pairs"
22239 );
22240 }
22241 if closed {
22242 assert_eq!(indices[6 * segments - 1], 0);
22245 }
22246 for pair in vertices.as_chunks::<2>().0 {
22250 let radius = |v: &MeshVertex| {
22251 let dx = v.position[0] - 250.0;
22252 let dy = v.position[1] - 250.0;
22253 (dx * dx + dy * dy).sqrt()
22254 };
22255 assert!(radius(&pair[0]) < radius(&pair[1]));
22256 }
22257 }
22258 }
22259
22260 #[cfg(not(target_arch = "wasm32"))]
22266 #[test]
22267 fn arc_mesh_clipped_segments_fan_over_private_vertices() {
22268 use cranpose_ui_graphics::ArcGeometry;
22269 let arc = ArcGeometry::new(
22270 Point::new(250.0, 250.0),
22271 80.0,
22272 100.0,
22273 0.0,
22274 cranpose_ui_graphics::TAU,
22275 StrokeCap::Round,
22276 );
22277 let converted = converted_arc_shape(arc, 1.0);
22278 let band = arc_mesh_band(&converted).expect("ring must qualify");
22279 let mut vertices = Vec::new();
22280 let mut indices = Vec::new();
22281 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
22282 .expect("ring must mesh");
22283 let mut uses = vec![0usize; vertices.len()];
22286 for &index in &indices {
22287 uses[index as usize] += 1;
22288 }
22289 assert!(
22290 uses.iter().any(|&count| count >= 3),
22291 "some boundary vertices must be shared across trapezoids"
22292 );
22293 let [left, top, ..] = converted.quad01;
22299 let [.., right, bottom] = converted.quad23;
22300 let clipped: Vec<&MeshVertex> = vertices
22301 .iter()
22302 .filter(|vertex| {
22303 let [x, y] = vertex.position;
22304 x == left || x == right || y == top || y == bottom
22305 })
22306 .collect();
22307 assert!(
22308 !clipped.is_empty(),
22309 "the tight box must clip the pushed-out chord vertices"
22310 );
22311 assert!(
22314 vertices.len() < indices.len(),
22315 "{} unique vertices should undercut {} triangle corners",
22316 vertices.len(),
22317 indices.len()
22318 );
22319 }
22320
22321 #[cfg(not(target_arch = "wasm32"))]
22322 #[test]
22323 fn arc_mesh_budget_overflow_falls_back_to_whole_slot_passthrough() {
22324 use cranpose_ui_graphics::ArcGeometry;
22325 let arc = ArcGeometry::new(
22330 Point::new(2000.0, 2000.0),
22331 1690.0,
22332 1710.0,
22333 0.0,
22334 cranpose_ui_graphics::TAU,
22335 StrokeCap::Round,
22336 );
22337 let converted = converted_arc_shape(arc, 1.0);
22338 let shapes = vec![converted; 100];
22339 assert!(build_arc_mesh_vertices(&shapes, RETAINED_MESH_MIN_PX2_DEFAULT as f64).is_none());
22340 }
22341
22342 #[cfg(not(target_arch = "wasm32"))]
22347 #[test]
22348 fn retained_mesh_size_gate_engages_exactly_per_threshold() {
22349 use cranpose_ui_graphics::ArcGeometry;
22350 let big_ring = converted_arc_shape(
22354 ArcGeometry::new(
22355 Point::new(204.0, 204.0),
22356 140.0,
22357 160.0,
22358 0.0,
22359 cranpose_ui_graphics::TAU,
22360 StrokeCap::Butt,
22361 ),
22362 1.0,
22363 );
22364 let small_arc = converted_arc_shape(
22365 ArcGeometry::new(
22366 Point::new(204.0, 204.0),
22367 12.0,
22368 18.0,
22369 0.3,
22370 0.5,
22371 StrokeCap::Butt,
22372 ),
22373 1.0,
22374 );
22375 let rim = rim_test_shape_data();
22376 let shapes = [big_ring, small_arc, rim];
22377 let big_px2 = quad_shoelace_area(&shapes[0]);
22378 let small_px2 = quad_shoelace_area(&shapes[1]);
22379 let rim_px2 = quad_shoelace_area(&shapes[2]);
22380 assert!(small_px2 < 1024.0 && big_px2 > rim_px2 && rim_px2 > 16384.0);
22381
22382 let build = build_arc_mesh_vertices(&shapes, RETAINED_MESH_MIN_PX2_DEFAULT as f64)
22386 .expect("within budget");
22387 assert_eq!(
22388 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22389 (1, 1, 1)
22390 );
22391 assert_eq!(build.meshed_stretches, 2);
22392 assert_eq!(build.index_prefix[1], build.index_prefix[2]);
22395 assert!(build.index_prefix[1] > build.index_prefix[0]);
22396 assert!(build.index_prefix[3] > build.index_prefix[2]);
22397
22398 let build = build_arc_mesh_vertices(&shapes, big_px2).expect("within budget");
22401 assert_eq!(
22402 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22403 (1, 0, 2)
22404 );
22405 let build =
22407 build_arc_mesh_vertices(&shapes, big_px2 + big_px2 * f64::EPSILON).expect("budget");
22408 assert_eq!(
22409 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22410 (0, 0, 3)
22411 );
22412
22413 let build = build_arc_mesh_vertices(&shapes, (rim_px2 + big_px2) * 0.5).expect("budget");
22415 assert_eq!(
22416 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22417 (1, 0, 2)
22418 );
22419
22420 let everything_gated =
22423 build_arc_mesh_vertices(&shapes, big_px2 * 2.0).expect("within budget");
22424 assert_eq!(everything_gated.passthrough, 3);
22425 assert_eq!(everything_gated.meshed_stretches, 0);
22426 assert!(everything_gated.vertices.is_empty());
22427 assert_eq!(everything_gated.index_prefix, vec![0, 0, 0, 0]);
22428 }
22429
22430 #[cfg(not(target_arch = "wasm32"))]
22435 #[test]
22436 fn meshed_stretches_count_maximal_runs_of_consecutive_meshed_shapes() {
22437 use cranpose_ui_graphics::ArcGeometry;
22438 let big = converted_arc_shape(
22439 ArcGeometry::new(
22440 Point::new(204.0, 204.0),
22441 140.0,
22442 160.0,
22443 0.0,
22444 cranpose_ui_graphics::TAU,
22445 StrokeCap::Butt,
22446 ),
22447 1.0,
22448 );
22449 let small = converted_arc_shape(
22450 ArcGeometry::new(
22451 Point::new(204.0, 204.0),
22452 12.0,
22453 18.0,
22454 0.3,
22455 0.5,
22456 StrokeCap::Butt,
22457 ),
22458 1.0,
22459 );
22460 let shapes = [big, big, small, big, small, small, big, big];
22462 let build = build_arc_mesh_vertices(&shapes, RETAINED_MESH_MIN_PX2_DEFAULT as f64)
22463 .expect("within budget");
22464 assert_eq!(build.meshed_arcs, 5);
22465 assert_eq!(build.passthrough, 3);
22466 assert_eq!(build.meshed_stretches, 3);
22467 assert!(build.meshed_stretches <= MESH_SLOT_MAX_STRETCHES);
22470 }
22471
22472 #[cfg(not(target_arch = "wasm32"))]
22475 #[test]
22476 fn retained_mesh_px2_override_parses_and_clamps() {
22477 assert_eq!(
22478 parse_retained_mesh_min_px2(None),
22479 RETAINED_MESH_MIN_PX2_DEFAULT as f64
22480 );
22481 assert_eq!(
22482 parse_retained_mesh_min_px2(Some("not a number")),
22483 RETAINED_MESH_MIN_PX2_DEFAULT as f64
22484 );
22485 assert_eq!(
22486 parse_retained_mesh_min_px2(Some("-5")),
22487 RETAINED_MESH_MIN_PX2_DEFAULT as f64
22488 );
22489 assert_eq!(parse_retained_mesh_min_px2(Some(" 40000 ")), 40000.0);
22490 assert_eq!(
22491 parse_retained_mesh_min_px2(Some("0")),
22492 *RETAINED_MESH_MIN_PX2_RANGE.start() as f64
22493 );
22494 assert_eq!(
22495 parse_retained_mesh_min_px2(Some("99999999")),
22496 *RETAINED_MESH_MIN_PX2_RANGE.end() as f64
22497 );
22498 }
22499
22500 #[cfg(not(target_arch = "wasm32"))]
22505 #[test]
22506 fn retained_capture_meshes_big_stroked_circle_rims_as_annuli() {
22507 let rim = rim_test_shape_data();
22508 let build = build_arc_mesh_vertices(
22509 std::slice::from_ref(&rim),
22510 RETAINED_MESH_MIN_PX2_DEFAULT as f64,
22511 )
22512 .expect("within budget");
22513 assert_eq!(
22514 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22515 (0, 1, 0)
22516 );
22517 assert!(build.meshed_segments >= ARC_MESH_MIN_SEGMENTS);
22518 assert!(build.mesh_area < 0.2 * build.quad_area);
22523 let band = rim_band_geometry(&rim).expect("rim must qualify");
22524 for vertex in &build.vertices {
22525 let dx = vertex.position[0] - band.center[0];
22526 let dy = vertex.position[1] - band.center[1];
22527 let radius = (dx * dx + dy * dy).sqrt();
22528 assert!(
22529 radius >= band.inner - ARC_MESH_MARGIN - 1e-3,
22530 "vertex at radius {radius} fell inside the annulus hole"
22531 );
22532 }
22533 }
22534
22535 #[cfg(not(target_arch = "wasm32"))]
22539 fn rim_test_shape_data() -> ShapeData {
22540 let mut shape = ShapeData::zeroed();
22541 shape.rect = [40.0, 40.0, 300.0, 300.0];
22542 shape.radii = [146.0; 4];
22543 shape.stroke_params = [
22544 8.0,
22545 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
22546 0.0,
22547 0.0,
22548 ];
22549 shape.quad01 = [40.0, 40.0, 340.0, 40.0];
22550 shape.quad23 = [40.0, 340.0, 340.0, 340.0];
22551 shape.color = [1.0, 1.0, 1.0, 1.0];
22552 shape
22553 }
22554
22555 #[cfg(not(target_arch = "wasm32"))]
22558 fn offscreen_test_viewport() -> ViewportUniformParams {
22559 ViewportUniformParams {
22560 width: 64,
22561 height: 64,
22562 offset: [7.0, 7.0],
22563 }
22564 }
22565
22566 #[cfg(not(target_arch = "wasm32"))]
22567 #[test]
22568 fn fill_diag_buckets_shape_quads_by_decoded_sdf_class() {
22569 let diag = FillAreaDiag::default();
22570 let mut arc = ShapeData::zeroed();
22571 arc.stroke_params[1] = pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter);
22572 arc.radii = [0.5; 4];
22575 arc.quad01 = [0.0, 0.0, 10.0, 0.0];
22576 arc.quad23 = [0.0, 10.0, 10.0, 10.0];
22577 let mut rounded = ShapeData::zeroed();
22578 rounded.stroke_params[1] =
22579 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
22580 rounded.radii = [2.0; 4];
22581 rounded.quad01 = [0.0, 0.0, 4.0, 0.0];
22582 rounded.quad23 = [0.0, 5.0, 4.0, 5.0];
22583 let mut plain = ShapeData::zeroed();
22584 plain.stroke_params[1] =
22585 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
22586 plain.quad01 = [0.0, 0.0, 2.0, 0.0];
22587 plain.quad23 = [0.0, 3.0, 2.0, 3.0];
22588 diag.add_shape_quads(
22589 &[rim_test_shape_data(), arc, rounded, plain],
22590 offscreen_test_viewport(),
22591 );
22592 assert_eq!(diag.frame[FillAreaDiag::RRECT_STROKE].get(), 300.0 * 300.0);
22593 assert_eq!(diag.frame[FillAreaDiag::ARC].get(), 100.0);
22594 assert_eq!(diag.frame[FillAreaDiag::RRECT_FILL].get(), 20.0);
22595 assert_eq!(diag.frame[FillAreaDiag::RECT].get(), 6.0);
22596 assert_eq!(diag.frame_corner.get(), 0.0);
22598 for (lit, quad) in diag.frame_lit.iter().zip(&diag.frame) {
22600 assert!(lit.get() <= quad.get() + 1e-9);
22601 }
22602 }
22603
22604 #[cfg(not(target_arch = "wasm32"))]
22605 #[test]
22606 fn fill_diag_rim_mesh_moves_quad_area_to_the_mesh_bucket() {
22607 let diag = FillAreaDiag::default();
22608 diag.add_shape_quads(&[rim_test_shape_data()], offscreen_test_viewport());
22609 diag.note_rim_mesh(&rim_test_shape_data(), 1234.5);
22610 assert_eq!(diag.frame[FillAreaDiag::RRECT_STROKE].get(), 0.0);
22611 assert_eq!(diag.frame[FillAreaDiag::MESH].get(), 1234.5);
22612 assert_eq!(diag.frame_lit[FillAreaDiag::RRECT_STROKE].get(), 0.0);
22615 assert!(diag.frame_lit[FillAreaDiag::MESH].get() <= 1234.5);
22616 assert!(diag.frame_lit[FillAreaDiag::MESH].get() > 0.0);
22617 }
22618
22619 #[cfg(not(target_arch = "wasm32"))]
22620 #[test]
22621 fn fill_diag_image_and_glyph_quads_share_one_bucket() {
22622 let diag = FillAreaDiag::default();
22623 diag.add_image_quad(&[[0.0, 0.0], [8.0, 0.0], [0.0, 4.0], [8.0, 4.0]]);
22624 let quad = CachedTextGlyphQuad {
22625 x: 0,
22626 y: 0,
22627 width: 5,
22628 height: 7,
22629 color: (1.0, 1.0, 1.0, 1.0),
22630 uv: ImageUvRect {
22631 min: [0.0, 0.0],
22632 max: [1.0, 1.0],
22633 sample_bounds: [0.0, 0.0, 1.0, 1.0],
22634 },
22635 };
22636 diag.add_glyph_quad(&quad);
22637 assert_eq!(diag.frame[FillAreaDiag::IMAGE_GLYPH].get(), 32.0 + 35.0);
22638 assert_eq!(diag.frame_lit[FillAreaDiag::IMAGE_GLYPH].get(), 32.0 + 35.0);
22640 }
22641
22642 #[cfg(not(target_arch = "wasm32"))]
22645 fn numeric_area(bounds: [f64; 4], steps: usize, inside: impl Fn(f64, f64) -> bool) -> f64 {
22646 let dx = (bounds[2] - bounds[0]) / steps as f64;
22647 let dy = (bounds[3] - bounds[1]) / steps as f64;
22648 let mut area = 0.0;
22649 for column in 0..steps {
22650 let x = bounds[0] + (column as f64 + 0.5) * dx;
22651 for row in 0..steps {
22652 let y = bounds[1] + (row as f64 + 0.5) * dy;
22653 if inside(x, y) {
22654 area += dx * dy;
22655 }
22656 }
22657 }
22658 area
22659 }
22660
22661 #[cfg(not(target_arch = "wasm32"))]
22664 fn sdf_rounded_rect_reference(
22665 p: [f64; 2],
22666 center: [f64; 2],
22667 half: [f64; 2],
22668 radius: f64,
22669 ) -> f64 {
22670 let qx = (p[0] - center[0]).abs() - (half[0] - radius);
22671 let qy = (p[1] - center[1]).abs() - (half[1] - radius);
22672 qx.max(0.0).hypot(qy.max(0.0)) + qx.max(qy).min(0.0) - radius
22673 }
22674
22675 #[cfg(not(target_arch = "wasm32"))]
22676 #[test]
22677 fn fill_truth_arc_lit_matches_the_sdf_covered_area() {
22678 use cranpose_ui_graphics::ArcGeometry;
22679 let tau = cranpose_ui_graphics::TAU;
22680 let center = Point::new(250.0, 250.0);
22681 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
22684 (90.0, 100.0, 0.7, 2.5, StrokeCap::Butt),
22685 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
22686 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
22687 (80.0, 100.0, 0.0, tau, StrokeCap::Round),
22688 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
22689 ];
22690 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
22691 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
22692 let converted = converted_arc_shape(arc, 1.0);
22693 let cap_code = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
22694 let arc_center = [converted.arc_params[0], converted.arc_params[1]];
22695 let mid = [converted.radii[0], converted.radii[1]];
22696 let half = [converted.radii[2], converted.radii[3]];
22697 let aabb = quad_aabb(&converted);
22698 let bounds = [aabb[0] - 2.0, aabb[1] - 2.0, aabb[2] + 2.0, aabb[3] + 2.0];
22701 let numeric = numeric_area(bounds, 1000, |x, y| {
22702 sdf_arc_band_reference(
22703 [x as f32, y as f32],
22704 arc_center,
22705 converted.stroke_params[3],
22706 converted.stroke_params[2],
22707 mid,
22708 half,
22709 cap_code,
22710 ) < 0.0
22711 });
22712 let analytic = analytic_covered_area(&converted);
22713 let error = (analytic - numeric).abs() / numeric.max(1.0);
22714 assert!(
22715 error < 0.02,
22716 "case {case}: analytic {analytic:.1} vs sdf {numeric:.1} \
22717 ({:.2}% off)",
22718 error * 100.0
22719 );
22720 }
22721 }
22722
22723 #[cfg(not(target_arch = "wasm32"))]
22724 #[test]
22725 fn fill_truth_circle_and_rrect_fill_lit_match_references() {
22726 let mut circle = ShapeData::zeroed();
22728 circle.stroke_params[1] =
22729 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
22730 circle.rect = [10.0, 10.0, 200.0, 200.0];
22731 circle.radii = [100.0; 4];
22732 let analytic = analytic_covered_area(&circle);
22733 let exact = std::f64::consts::PI * 100.0 * 100.0;
22734 assert!(
22735 (analytic - exact).abs() / exact < 1e-9,
22736 "circle: {analytic} vs {exact}"
22737 );
22738
22739 let mut rounded = ShapeData::zeroed();
22741 rounded.stroke_params[1] =
22742 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
22743 rounded.rect = [50.0, 80.0, 200.0, 120.0];
22744 rounded.radii = [40.0; 4];
22745 let numeric = numeric_area([48.0, 78.0, 252.0, 202.0], 1000, |x, y| {
22746 sdf_rounded_rect_reference([x, y], [150.0, 140.0], [100.0, 60.0], 40.0) < 0.0
22747 });
22748 let analytic = analytic_covered_area(&rounded);
22749 let error = (analytic - numeric).abs() / numeric;
22750 assert!(
22751 error < 0.02,
22752 "rrect fill: analytic {analytic:.1} vs sdf {numeric:.1}"
22753 );
22754 }
22755
22756 #[cfg(not(target_arch = "wasm32"))]
22757 #[test]
22758 fn fill_truth_stroked_rrect_lit_matches_the_band_area() {
22759 let rim = rim_test_shape_data();
22762 let analytic = analytic_covered_area(&rim);
22763 let exact = std::f64::consts::PI * (150.0 * 150.0 - 142.0 * 142.0);
22764 assert!(
22765 (analytic - exact).abs() / exact < 1e-9,
22766 "circle rim: {analytic} vs {exact}"
22767 );
22768
22769 let mut square_ring = rim_test_shape_data();
22772 square_ring.radii = [60.0; 4];
22773 let numeric = numeric_area([38.0, 38.0, 342.0, 342.0], 1000, |x, y| {
22774 sdf_rounded_rect_reference([x, y], [190.0, 190.0], [146.0, 146.0], 60.0).abs() < 4.0
22775 });
22776 let analytic = analytic_covered_area(&square_ring);
22777 let error = (analytic - numeric).abs() / numeric;
22778 assert!(
22779 error < 0.02,
22780 "square ring: analytic {analytic:.1} vs sdf {numeric:.1}"
22781 );
22782 }
22783
22784 #[cfg(not(target_arch = "wasm32"))]
22785 #[test]
22786 fn fill_truth_corner_counter_prices_the_area_outside_the_inscribed_circle() {
22787 let full = area_outside_inscribed_circle([0.0, 0.0, 454.0, 454.0], (454, 454));
22790 let exact = (1.0 - std::f64::consts::FRAC_PI_4) * 454.0 * 454.0;
22791 assert!(
22792 (full - exact).abs() / exact < 0.01,
22793 "full quad: {full} vs {exact}"
22794 );
22795 assert_eq!(
22797 area_outside_inscribed_circle([127.0, 127.0, 327.0, 327.0], (454, 454)),
22798 0.0
22799 );
22800 let corner = area_outside_inscribed_circle([0.0, 0.0, 40.0, 40.0], (454, 454));
22802 assert!((corner - 1600.0).abs() < 1e-6, "corner box: {corner}");
22803 }
22804
22805 #[cfg(not(target_arch = "wasm32"))]
22806 #[test]
22807 fn fill_truth_opacity_histogram_classifies_solid_alpha_exactly() {
22808 let diag = FillAreaDiag::default();
22809 diag.reset_frame(454, 454);
22810 let full_frame = ViewportUniformParams {
22811 width: 454,
22812 height: 454,
22813 offset: [0.0, 0.0],
22814 };
22815 let mut opaque = ShapeData::zeroed();
22816 opaque.stroke_params[1] =
22817 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
22818 opaque.rect = [0.0, 0.0, 100.0, 50.0];
22819 opaque.quad01 = [0.0, 0.0, 100.0, 0.0];
22820 opaque.quad23 = [0.0, 50.0, 100.0, 50.0];
22821 opaque.color = [1.0, 1.0, 1.0, 1.0];
22822 let mut faded = opaque;
22823 faded.color[3] = 0.82;
22824 let mut gradient = opaque;
22825 gradient.brush_type = 1;
22826 diag.add_shape_quads(&[opaque, faded, gradient], full_frame);
22827 let lit = |class: FillOpacityClass| diag.frame_opacity[class as usize].get();
22829 assert_eq!(lit(FillOpacityClass::Opaque), 5000.0);
22830 assert_eq!(lit(FillOpacityClass::Translucent), 5000.0);
22831 assert_eq!(lit(FillOpacityClass::NonSolid), 5000.0);
22832 assert!(diag.frame_corner.get() > 0.0);
22834
22835 let offscreen = FillAreaDiag::default();
22838 offscreen.reset_frame(454, 454);
22839 offscreen.add_shape_quads(&[opaque], offscreen_test_viewport());
22840 assert_eq!(offscreen.frame_corner.get(), 0.0);
22841 }
22842
22843 #[cfg(not(target_arch = "wasm32"))]
22844 #[test]
22845 fn fill_truth_retained_records_price_ranges_and_identity_corners() {
22846 let mut plain = ShapeData::zeroed();
22847 plain.stroke_params[1] =
22848 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
22849 plain.rect = [200.0, 200.0, 20.0, 10.0];
22850 plain.quad01 = [200.0, 200.0, 220.0, 200.0];
22851 plain.quad23 = [200.0, 210.0, 220.0, 210.0];
22852 plain.color = [1.0, 1.0, 1.0, 1.0];
22853 let shapes = vec![rim_test_shape_data(), plain];
22854 let records = fill_diag_capture_records(&shapes, None);
22855 assert_eq!(records.len(), 2);
22856 assert_eq!(records[0].bucket, FillAreaDiag::RRECT_STROKE);
22857 assert_eq!(records[0].drawn_px2, 300.0 * 300.0);
22858 assert!(records[0].lit_px2 < records[0].drawn_px2, "a rim has slack");
22859 assert_eq!(records[1].bucket, FillAreaDiag::RECT);
22861 assert_eq!(records[1].lit_px2, records[1].drawn_px2);
22862
22863 let diag = FillAreaDiag::default();
22864 diag.reset_frame(454, 454);
22865 let scaled = SimilarityTransform::new([0.0, 0.0], 0.0, 2.0);
22868 diag.add_retained_range(&records, 0, 2, &scaled);
22869 let drawn: f64 = records.iter().map(|record| record.drawn_px2).sum();
22870 assert!((diag.frame[FillAreaDiag::RETAINED].get() - drawn * 4.0).abs() < 1e-6);
22871 assert_eq!(diag.frame_corner.get(), 0.0);
22872
22873 let identity_diag = FillAreaDiag::default();
22876 identity_diag.reset_frame(454, 454);
22877 identity_diag.add_retained_range(&records, 0, 2, &SimilarityTransform::IDENTITY);
22878 assert!(identity_diag.frame_corner.get() > 0.0);
22879 let tail = FillAreaDiag::default();
22881 tail.reset_frame(454, 454);
22882 tail.add_retained_range(&records, 1, 2, &SimilarityTransform::IDENTITY);
22883 assert_eq!(
22884 tail.frame[FillAreaDiag::RETAINED].get(),
22885 records[1].drawn_px2
22886 );
22887 }
22888
22889 #[cfg(not(target_arch = "wasm32"))]
22890 #[test]
22891 fn fill_truth_top_slack_dump_keeps_the_worst_ten() {
22892 let mut diag = FillAreaDiag::default();
22893 let records: Vec<FillDiagShapeRecord> = (0..12)
22894 .map(|index| FillDiagShapeRecord {
22895 drawn_px2: 1000.0 * (index + 1) as f64,
22896 lit_px2: 100.0,
22897 bucket: FillAreaDiag::ARC,
22898 opacity: FillOpacityClass::Opaque,
22899 aabb: [0.0, 0.0, 10.0, 10.0],
22900 })
22901 .collect();
22902 diag.note_retained_capture(3, &records);
22903 assert_eq!(diag.slack_top.len(), FILL_DIAG_SLACK_TOP);
22904 assert_eq!(diag.slack_top[0].drawn_px2, 12000.0);
22906 assert_eq!(diag.slack_top[0].slot, 3);
22907 assert_eq!(diag.slack_top[0].shape, 11);
22908 for pair in diag.slack_top.windows(2) {
22909 assert!(pair[0].drawn_px2 - pair[0].lit_px2 >= pair[1].drawn_px2 - pair[1].lit_px2);
22910 }
22911 assert!(diag
22912 .slack_top
22913 .iter()
22914 .all(|entry| entry.drawn_px2 - entry.lit_px2 > 2000.0 - 100.0));
22915 }
22916
22917 #[cfg(not(target_arch = "wasm32"))]
22918 #[test]
22919 fn rim_mesh_band_accepts_only_huge_solid_unclipped_circle_rims() {
22920 let band = rim_mesh_band(&rim_test_shape_data()).expect("circle rim must qualify");
22921 assert_eq!(band.center, [190.0, 190.0]);
22922 assert_eq!(band.inner, 142.0);
22923 assert_eq!(band.outer, 150.0);
22924 assert_eq!(band.start, 0.0);
22925 assert!(
22926 band.sweep >= cranpose_ui_graphics::TAU,
22927 "a rim band is a closed ring"
22928 );
22929 let mut vertices = Vec::new();
22931 let mut indices = Vec::new();
22932 emit_arc_band_mesh(
22933 &rim_test_shape_data(),
22934 7,
22935 &band,
22936 &mut vertices,
22937 &mut indices,
22938 )
22939 .expect("rim must mesh");
22940 assert!(vertices.iter().all(|vertex| vertex.shape_idx == 7));
22941
22942 let mut square = rim_test_shape_data();
22946 square.radii = [100.0; 4];
22947 assert!(rim_mesh_band(&square).is_none());
22948
22949 let mut oblong = rim_test_shape_data();
22951 oblong.rect = [40.0, 40.0, 300.0, 200.0];
22952 assert!(rim_mesh_band(&oblong).is_none());
22953
22954 let mut gradient = rim_test_shape_data();
22956 gradient.brush_type = 1;
22957 assert!(rim_mesh_band(&gradient).is_none());
22958
22959 let mut clipped = rim_test_shape_data();
22961 clipped.clip_rect = [0.0, 0.0, 400.0, 400.0];
22962 assert!(rim_mesh_band(&clipped).is_none());
22963
22964 let mut small = rim_test_shape_data();
22966 small.rect = [40.0, 40.0, 100.0, 100.0];
22967 small.quad01 = [40.0, 40.0, 140.0, 40.0];
22968 small.quad23 = [40.0, 140.0, 140.0, 140.0];
22969 small.radii = [46.0; 4];
22970 assert!(rim_mesh_band(&small).is_none());
22971
22972 let mut fill = rim_test_shape_data();
22974 fill.stroke_params[1] =
22975 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
22976 assert!(rim_mesh_band(&fill).is_none());
22977
22978 let mut hairline = rim_test_shape_data();
22980 hairline.stroke_params[0] = 0.0;
22981 assert!(rim_mesh_band(&hairline).is_none());
22982
22983 let mut uneven = rim_test_shape_data();
22985 uneven.radii[2] = 145.0;
22986 assert!(rim_mesh_band(&uneven).is_none());
22987 }
22988
22989 #[cfg(not(target_arch = "wasm32"))]
22990 #[test]
22991 fn shape_batch_limits_follow_uniform_binding_size() {
22992 let desktop_shapes = 65536 / std::mem::size_of::<ShapeData>();
22995 assert_eq!(desktop_shapes, 409);
22996 assert_eq!(
22997 ShapeBatchLimits::desktop(),
22998 ShapeBatchLimits {
22999 max_shapes_per_batch: desktop_shapes.min(MAX_SHAPES_PER_BATCH),
23000 max_gradient_stops: MAX_GRADIENT_STOPS,
23001 storage: false,
23002 }
23003 );
23004
23005 let downlevel = ShapeBatchLimits::for_uniform_binding_size(16384);
23008 assert_eq!(downlevel.max_shapes_per_batch, 16384 / 160);
23009 assert_eq!(downlevel.max_shapes_per_batch, 102);
23010 assert_eq!(downlevel.max_gradient_stops, 512.min(MAX_GRADIENT_STOPS));
23011 assert!(downlevel.max_shapes_per_batch * std::mem::size_of::<ShapeData>() <= 16384);
23012 assert!(downlevel.max_gradient_stops * std::mem::size_of::<GradientStop>() <= 16384);
23013
23014 let tiny = ShapeBatchLimits::for_uniform_binding_size(1);
23016 assert_eq!(tiny.max_shapes_per_batch, 1);
23017 assert_eq!(tiny.max_gradient_stops, 1);
23018 }
23019
23020 #[test]
23021 fn storage_shape_batch_limits_uncap_the_batch_and_start_small() {
23022 let storage = ShapeBatchLimits::for_storage_binding_size(128 << 20);
23025 assert!(storage.storage);
23026 assert_eq!(storage.max_shapes_per_batch, MAX_SHAPES_PER_STORAGE_BATCH);
23027 assert_eq!(
23028 storage.max_gradient_stops,
23029 MAX_GRADIENT_STOPS_PER_STORAGE_BATCH
23030 );
23031
23032 assert_eq!(
23035 storage.initial_shape_capacity(),
23036 INITIAL_STORAGE_BATCH_CAPACITY
23037 );
23038 assert_eq!(
23039 storage.initial_gradient_capacity(),
23040 INITIAL_STORAGE_BATCH_CAPACITY
23041 );
23042 assert_eq!(
23043 storage.data_binding_type(),
23044 wgpu::BufferBindingType::Storage { read_only: true }
23045 );
23046 assert!(storage
23047 .data_buffer_usage()
23048 .contains(wgpu::BufferUsages::STORAGE));
23049
23050 let uniform = ShapeBatchLimits::desktop();
23053 assert_eq!(
23054 uniform.initial_shape_capacity(),
23055 uniform.max_shapes_per_batch
23056 );
23057 assert_eq!(
23058 uniform.initial_gradient_capacity(),
23059 uniform.max_gradient_stops
23060 );
23061 assert_eq!(
23062 uniform.data_binding_type(),
23063 wgpu::BufferBindingType::Uniform
23064 );
23065 assert!(uniform
23066 .data_buffer_usage()
23067 .contains(wgpu::BufferUsages::UNIFORM));
23068 }
23069
23070 #[test]
23071 fn storage_shape_shader_swaps_the_arrays_to_runtime_sized_storage() {
23072 let source =
23073 shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20), false);
23074 assert!(
23075 source.contains("var<storage, read> shape_data: array<ShapeData>;"),
23076 "storage-mode shader must declare a runtime-sized shape array"
23077 );
23078 assert!(
23079 source.contains("var<storage, read> gradient_stops: array<GradientStop>;"),
23080 "storage-mode shader must declare a runtime-sized gradient array"
23081 );
23082 assert!(
23083 !source.contains("var<uniform> shape_data"),
23084 "the uniform shape declaration must be fully replaced"
23085 );
23086 assert!(
23087 !source.contains("var<uniform> gradient_stops"),
23088 "the uniform gradient declaration must be fully replaced"
23089 );
23090 assert!(
23091 source.contains("var<storage, read> paint: array<vec4<f32>>;"),
23092 "storage-mode shader must declare the retained paint array"
23093 );
23094 assert!(
23095 source.contains("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)"),
23096 "storage-mode shader must read paint under the paint_select flag"
23097 );
23098 assert!(
23099 source.contains("fn vs_mesh("),
23100 "the storage rewrite must leave the retained-mesh vertex entry intact"
23101 );
23102 assert!(
23103 source.contains("fn vs_shape_instanced("),
23104 "the storage rewrite must leave the instanced-quad vertex entry intact"
23105 );
23106 assert_eq!(
23107 source
23108 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
23109 .count(),
23110 3,
23111 "vs_main, vs_shape_instanced and vs_mesh must all read paint under \
23112 the paint_select flag (meshless retained draws ride the instanced \
23113 entry when the selection is latched on)"
23114 );
23115
23116 let module = naga::front::wgsl::parse_str(&source)
23119 .expect("storage-mode shape shader must parse as WGSL");
23120 naga::valid::Validator::new(
23121 naga::valid::ValidationFlags::all(),
23122 naga::valid::Capabilities::all(),
23123 )
23124 .validate(&module)
23125 .expect("storage-mode shape shader must validate for WebGPU");
23126 }
23127
23128 #[test]
23129 fn solid_trim_keeps_the_full_struct_locations_with_the_dropped_slots_vacant() {
23130 let appendix = shaders::SOLID_TRIM_APPENDIX;
23136 for line in [
23137 "@location(0) color: vec4<f32>,",
23138 "@location(1) uv: vec2<f32>,",
23139 "@location(2) world_pos: vec2<f32>,",
23140 "@location(3) @interpolate(flat) rect: vec4<f32>,",
23141 "@location(4) @interpolate(flat) radii: vec4<f32>,",
23142 "@location(6) @interpolate(flat) clip_rect: vec4<f32>,",
23143 "@location(7) @interpolate(flat) stroke_params: vec4<f32>,",
23144 "@location(8) @interpolate(flat) arc_params: vec4<f32>,",
23145 ] {
23146 assert!(
23147 shaders::SHADER.contains(line),
23148 "`{line}` drifted out of VertexOutput; realign the trimmed \
23149 struct line for line before touching anything else"
23150 );
23151 assert!(
23152 appendix.contains(line),
23153 "`{line}` must appear verbatim in VertexOutputSolid — the \
23154 surviving varyings keep the full struct's location indices"
23155 );
23156 }
23157 assert!(
23158 !appendix.contains("@location(5)"),
23159 "location 5 is gradient_params' slot and must stay VACANT — \
23160 dense renumbering is the reverted attempt's suspect #1"
23161 );
23162 assert!(
23163 !appendix.contains("@location(9)"),
23164 "location 9 is brush's slot and must stay VACANT — dense \
23165 renumbering is the reverted attempt's suspect #1"
23166 );
23167 assert!(
23168 !appendix.contains("output.gradient_params") && !appendix.contains("output.brush"),
23169 "the trimmed vertex entries must not write the dropped varyings"
23170 );
23171 }
23172
23173 #[test]
23174 fn solid_trim_source_reaches_every_injection_and_validates() {
23175 let storage =
23180 shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20), true);
23181 for entry in [
23182 "fn vs_solid(",
23183 "fn vs_solid_instanced(",
23184 "fn fs_solid_trim(",
23185 ] {
23186 assert!(
23187 storage.contains(entry),
23188 "trimmed storage source must carry `{entry}`"
23189 );
23190 }
23191 assert_eq!(
23192 storage
23193 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
23194 .count(),
23195 5,
23196 "vs_main, vs_shape_instanced, vs_mesh, vs_solid and \
23197 vs_solid_instanced must all read paint under the paint_select \
23198 flag"
23199 );
23200
23201 let uniform = shape_shader_source(ShapeBatchLimits::desktop(), true);
23204 for source in [&storage, &uniform] {
23205 for depth in [false, true] {
23206 let text = display_clip::with_content_z(Cow::Owned(source.to_string()), depth);
23207 let module = naga::front::wgsl::parse_str(&text)
23208 .expect("trimmed shape shader must parse as WGSL");
23209 naga::valid::Validator::new(
23210 naga::valid::ValidationFlags::all(),
23211 naga::valid::Capabilities::all(),
23212 )
23213 .validate(&module)
23214 .expect("trimmed shape shader must validate for WebGPU");
23215 }
23216 }
23217 }
23218
23219 #[test]
23220 fn solid_trim_flag_reads_the_documented_variable() {
23221 std::env::remove_var("CRANPOSE_SOLID_TRIM_VARYINGS");
23224 assert!(!solid_trim_varyings_enabled(), "the trim must default OFF");
23225 std::env::set_var("CRANPOSE_SOLID_TRIM_VARYINGS", "1");
23226 assert!(solid_trim_varyings_enabled());
23227 std::env::set_var("CRANPOSE_SOLID_TRIM_VARYINGS", "0");
23228 assert!(!solid_trim_varyings_enabled());
23229 std::env::remove_var("CRANPOSE_SOLID_TRIM_VARYINGS");
23230 }
23231
23232 #[test]
23233 fn uniform_shape_shader_keeps_the_in_record_color_and_no_paint_binding() {
23234 for source in [
23238 Cow::Borrowed(shaders::SHADER),
23239 shape_shader_source(ShapeBatchLimits::desktop(), false),
23240 ] {
23241 assert!(
23242 !source.contains("paint: array"),
23243 "the uniform variant must not declare a paint array"
23244 );
23245 assert!(
23246 source.contains("output.color = shape.color;"),
23247 "the uniform variant must read the color from ShapeData \
23248 (this literal is also what `shape_shader_source` rewrites)"
23249 );
23250 assert!(
23251 source.contains("paint_select: f32"),
23252 "SimilarityTransform must name the flag field in both \
23253 variants; the Rust mirror is Pod and uploads raw bytes"
23254 );
23255 }
23256 }
23257
23258 #[test]
23259 fn shipped_shape_shader_array_length_fits_the_downlevel_uniform_floor() {
23260 assert!(
23264 shaders::SHADER.contains("array<ShapeData, 102>"),
23265 "shape.wgsl array length must stay in sync with \
23266 `shape_shader_source`'s replace string and MAX_SHAPES_PER_BATCH"
23267 );
23268 assert!(102 * std::mem::size_of::<ShapeData>() <= 16384);
23269 assert!(103 * std::mem::size_of::<ShapeData>() > 16384);
23270 }
23271
23272 #[test]
23273 fn glyph_atlas_doubles_on_overflow_and_stops_at_the_device_ceiling() {
23274 assert_eq!(
23277 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MIN_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
23278 1024
23279 );
23280 assert_eq!(
23281 next_glyph_atlas_size(2048, TEXT_GLYPH_ATLAS_MAX_SIZE),
23282 TEXT_GLYPH_ATLAS_MAX_SIZE
23283 );
23284 assert_eq!(
23285 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MAX_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
23286 TEXT_GLYPH_ATLAS_MAX_SIZE
23287 );
23288
23289 assert_eq!(next_glyph_atlas_size(1024, 2048), 2048);
23292 assert_eq!(next_glyph_atlas_size(2048, 2048), 2048);
23293
23294 assert_eq!(next_glyph_atlas_size(u32::MAX, 4096), 4096);
23296 assert_eq!(next_glyph_atlas_size(0, 0), 1);
23297 }
23298
23299 #[test]
23300 fn glyph_atlas_uv_rect_normalizes_against_the_atlas_it_was_placed_in() {
23301 let entry = GlyphAtlasEntry {
23305 x: 128,
23306 y: 256,
23307 width: 16,
23308 height: 32,
23309 };
23310
23311 let small = glyph_atlas_uv_rect(entry, 512);
23312 let large = glyph_atlas_uv_rect(entry, 4096);
23313
23314 assert_eq!(small.min, [128.0 / 512.0, 256.0 / 512.0]);
23315 assert_eq!(large.min, [128.0 / 4096.0, 256.0 / 4096.0]);
23316 assert_eq!(small.max, [144.0 / 512.0, 288.0 / 512.0]);
23317 assert_eq!(large.max, [144.0 / 4096.0, 288.0 / 4096.0]);
23318 }
23319
23320 #[test]
23321 fn native_shape_shader_source_uses_native_batch_limits() {
23322 let limits = ShapeBatchLimits::desktop();
23323 let source = shape_shader_source(limits, false);
23324
23325 assert!(source.contains(&format!(
23326 "array<ShapeData, {}>",
23327 limits.max_shapes_per_batch
23328 )));
23329 assert!(source.contains(&format!(
23330 "array<GradientStop, {}>",
23331 limits.max_gradient_stops
23332 )));
23333 assert!(!source.contains("array<ShapeData, 146>"));
23336 }
23337
23338 #[test]
23339 fn stroked_and_arc_shapes_batch_together_with_fills() {
23340 let fill = test_shape(0, BlendMode::SrcOver);
23346 let mut stroked = test_shape(1, BlendMode::SrcOver);
23347 stroked.stroke = Some(
23348 cranpose_ui_graphics::Stroke::new(3.0)
23349 .with_cap(StrokeCap::Round)
23350 .with_join(StrokeJoin::Bevel),
23351 );
23352 let mut arc = test_shape(2, BlendMode::SrcOver);
23353 arc.arc = Some(cranpose_ui_graphics::ArcGeometry::new(
23354 Point::new(4.0, 4.0),
23355 2.0,
23356 4.0,
23357 0.0,
23358 1.0,
23359 StrokeCap::Round,
23360 ));
23361 let trailing_fill = test_shape(3, BlendMode::SrcOver);
23362
23363 assert!(!fill.has_stroke_or_arc());
23364 assert!(stroked.has_stroke_or_arc());
23365 assert!(arc.has_stroke_or_arc());
23366 assert!(!trailing_fill.has_stroke_or_arc());
23367
23368 let shapes = vec![fill, stroked, arc, trailing_fill];
23369 let ordered_items: Vec<_> = (0..shapes.len())
23370 .map(|index| (index, SegmentDrawItem::Shape(index)))
23371 .collect();
23372 let images = Vec::new();
23373
23374 let commands: Vec<_> = SegmentCommandIter::new(
23375 &ordered_items,
23376 &shapes,
23377 &images,
23378 ShapeBatchLimits::desktop(),
23379 )
23380 .collect();
23381
23382 assert_eq!(
23383 commands,
23384 vec![SegmentRenderCommand::DrawChunk(chunk(&[
23385 SegmentBatchPlan::Shape {
23386 start: 0,
23387 end: 4,
23388 blend_mode: BlendMode::SrcOver,
23389 }
23390 ]))],
23391 "mixed fill/stroke/arc runs must stay one batch"
23392 );
23393 }
23394
23395 #[cfg(not(target_arch = "wasm32"))]
23396 #[test]
23397 fn native_segment_fusion_budget_allows_small_interleaved_chunks() {
23398 let ordered_items = vec![
23399 (0, SegmentDrawItem::Shape(0)),
23400 (1, SegmentDrawItem::Image(0)),
23401 (2, SegmentDrawItem::Text(0)),
23402 (3, SegmentDrawItem::Shape(1)),
23403 ];
23404 let shapes = vec![
23405 test_shape(0, BlendMode::SrcOver),
23406 test_shape(3, BlendMode::DstOut),
23407 ];
23408 let segment = chunk(&[
23409 SegmentBatchPlan::Shape {
23410 start: 0,
23411 end: 1,
23412 blend_mode: BlendMode::SrcOver,
23413 },
23414 SegmentBatchPlan::Image {
23415 start: 1,
23416 end: 2,
23417 blend_mode: BlendMode::SrcOver,
23418 },
23419 SegmentBatchPlan::Text { start: 2, end: 3 },
23420 SegmentBatchPlan::Shape {
23421 start: 3,
23422 end: 4,
23423 blend_mode: BlendMode::DstOut,
23424 },
23425 ]);
23426
23427 let budget = native_segment_fusion_budget(
23428 &ordered_items,
23429 &shapes,
23430 &[],
23431 &segment,
23432 ShapeBatchLimits::desktop(),
23433 )
23434 .expect("budget should be valid")
23435 .expect("chunk should fit native fusion budget");
23436
23437 assert_eq!(
23438 budget,
23439 NativeSegmentFusionBudget {
23440 shape_count: 2,
23441 gradient_stop_count: 0,
23442 }
23443 );
23444 }
23445
23446 #[cfg(not(target_arch = "wasm32"))]
23447 #[test]
23448 fn native_segment_fusion_budget_rejects_shape_uniform_overflow() {
23449 let ordered_items: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
23450 .map(|index| (index, SegmentDrawItem::Shape(index)))
23451 .collect();
23452 let shapes: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
23453 .map(|index| test_shape(index, BlendMode::SrcOver))
23454 .collect();
23455 let segment = chunk(&[
23456 SegmentBatchPlan::Shape {
23457 start: 0,
23458 end: MAX_SHAPES_PER_BATCH,
23459 blend_mode: BlendMode::SrcOver,
23460 },
23461 SegmentBatchPlan::Shape {
23462 start: MAX_SHAPES_PER_BATCH,
23463 end: MAX_SHAPES_PER_BATCH + 1,
23464 blend_mode: BlendMode::SrcOver,
23465 },
23466 ]);
23467
23468 let budget = native_segment_fusion_budget(
23469 &ordered_items,
23470 &shapes,
23471 &[],
23472 &segment,
23473 ShapeBatchLimits::desktop(),
23474 )
23475 .expect("valid plan");
23476
23477 assert_eq!(budget, None);
23478 }
23479
23480 #[cfg(not(target_arch = "wasm32"))]
23481 #[test]
23482 fn native_segment_fusion_budget_rejects_gradient_uniform_overflow() {
23483 let ordered_items = vec![(0, SegmentDrawItem::Shape(0))];
23484 let mut shape = test_shape(0, BlendMode::SrcOver);
23485 let brushes = vec![Brush::linear_gradient(vec![
23486 Color::BLACK;
23487 MAX_GRADIENT_STOPS + 1
23488 ])];
23489 shape.brush = SceneBrush::Gradient(0);
23490 let shapes = vec![shape];
23491 let segment = chunk(&[SegmentBatchPlan::Shape {
23492 start: 0,
23493 end: 1,
23494 blend_mode: BlendMode::SrcOver,
23495 }]);
23496
23497 let budget = native_segment_fusion_budget(
23498 &ordered_items,
23499 &shapes,
23500 &brushes,
23501 &segment,
23502 ShapeBatchLimits::desktop(),
23503 )
23504 .expect("valid plan");
23505
23506 assert_eq!(budget, None);
23507 }
23508
23509 #[cfg(not(target_arch = "wasm32"))]
23510 #[test]
23511 fn native_segment_fusion_partitions_shape_uniform_overflow() {
23512 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
23515 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
23516 .map(|index| (index, SegmentDrawItem::Shape(index)))
23517 .collect();
23518 let shapes: Vec<_> = (0..=desktop_batch_cap)
23519 .map(|index| test_shape(index, BlendMode::SrcOver))
23520 .collect();
23521 let segment = chunk(&[
23522 SegmentBatchPlan::Shape {
23523 start: 0,
23524 end: desktop_batch_cap,
23525 blend_mode: BlendMode::SrcOver,
23526 },
23527 SegmentBatchPlan::Shape {
23528 start: desktop_batch_cap,
23529 end: desktop_batch_cap + 1,
23530 blend_mode: BlendMode::SrcOver,
23531 },
23532 ]);
23533
23534 let partitions = native_segment_fusion_partitions(
23535 &ordered_items,
23536 &shapes,
23537 &[],
23538 &segment,
23539 ShapeBatchLimits::desktop(),
23540 )
23541 .expect("valid plan")
23542 .expect("overflowing segment should be partitionable");
23543
23544 assert_eq!(partitions.len(), 2);
23545 assert_eq!(
23546 partitions[0],
23547 NativeSegmentFusionPartition {
23548 chunk: chunk(&[SegmentBatchPlan::Shape {
23549 start: 0,
23550 end: desktop_batch_cap,
23551 blend_mode: BlendMode::SrcOver,
23552 }]),
23553 budget: NativeSegmentFusionBudget {
23554 shape_count: desktop_batch_cap,
23555 gradient_stop_count: 0,
23556 },
23557 }
23558 );
23559 assert_eq!(
23560 partitions[1],
23561 NativeSegmentFusionPartition {
23562 chunk: chunk(&[SegmentBatchPlan::Shape {
23563 start: desktop_batch_cap,
23564 end: desktop_batch_cap + 1,
23565 blend_mode: BlendMode::SrcOver,
23566 }]),
23567 budget: NativeSegmentFusionBudget {
23568 shape_count: 1,
23569 gradient_stop_count: 0,
23570 },
23571 }
23572 );
23573 }
23574
23575 #[cfg(not(target_arch = "wasm32"))]
23576 #[test]
23577 fn native_segment_fusion_partitions_gradient_uniform_overflow() {
23578 const STOPS_PER_SHAPE: usize = MAX_GRADIENT_STOPS / 2;
23579 let ordered_items = vec![
23580 (0, SegmentDrawItem::Shape(0)),
23581 (1, SegmentDrawItem::Shape(1)),
23582 (2, SegmentDrawItem::Shape(2)),
23583 ];
23584 let mut shapes = Vec::new();
23585 let brushes = vec![Brush::linear_gradient(vec![Color::BLACK; STOPS_PER_SHAPE])];
23586 for index in 0..3 {
23587 let mut shape = test_shape(index, BlendMode::SrcOver);
23588 shape.brush = SceneBrush::Gradient(0);
23589 shapes.push(shape);
23590 }
23591 let segment = chunk(&[SegmentBatchPlan::Shape {
23592 start: 0,
23593 end: 3,
23594 blend_mode: BlendMode::SrcOver,
23595 }]);
23596
23597 let partitions = native_segment_fusion_partitions(
23598 &ordered_items,
23599 &shapes,
23600 &brushes,
23601 &segment,
23602 ShapeBatchLimits::desktop(),
23603 )
23604 .expect("valid plan")
23605 .expect("overflowing gradient segment should be partitionable");
23606
23607 assert_eq!(partitions.len(), 2);
23608 assert_eq!(
23609 partitions[0],
23610 NativeSegmentFusionPartition {
23611 chunk: chunk(&[SegmentBatchPlan::Shape {
23612 start: 0,
23613 end: 2,
23614 blend_mode: BlendMode::SrcOver,
23615 }]),
23616 budget: NativeSegmentFusionBudget {
23617 shape_count: 2,
23618 gradient_stop_count: MAX_GRADIENT_STOPS,
23619 },
23620 }
23621 );
23622 assert_eq!(
23623 partitions[1],
23624 NativeSegmentFusionPartition {
23625 chunk: chunk(&[SegmentBatchPlan::Shape {
23626 start: 2,
23627 end: 3,
23628 blend_mode: BlendMode::SrcOver,
23629 }]),
23630 budget: NativeSegmentFusionBudget {
23631 shape_count: 1,
23632 gradient_stop_count: STOPS_PER_SHAPE,
23633 },
23634 }
23635 );
23636 }
23637
23638 #[cfg(not(target_arch = "wasm32"))]
23639 #[test]
23640 fn native_segment_fusion_accepts_layer_composite_chunks() {
23641 let ordered_items = vec![
23642 (0, SegmentDrawItem::Shape(0)),
23643 (1, SegmentDrawItem::Composite(0)),
23644 (2, SegmentDrawItem::ShaderComposite(0)),
23645 (3, SegmentDrawItem::Shape(1)),
23646 ];
23647 let shapes = vec![
23648 test_shape(0, BlendMode::SrcOver),
23649 test_shape(1, BlendMode::SrcOver),
23650 ];
23651 let segment = chunk(&[
23652 SegmentBatchPlan::Shape {
23653 start: 0,
23654 end: 1,
23655 blend_mode: BlendMode::SrcOver,
23656 },
23657 SegmentBatchPlan::Composite { start: 1, end: 2 },
23658 SegmentBatchPlan::ShaderComposite { start: 2, end: 3 },
23659 SegmentBatchPlan::Shape {
23660 start: 3,
23661 end: 4,
23662 blend_mode: BlendMode::SrcOver,
23663 },
23664 ]);
23665
23666 let partitions = native_segment_fusion_partitions(
23667 &ordered_items,
23668 &shapes,
23669 &[],
23670 &segment,
23671 ShapeBatchLimits::desktop(),
23672 )
23673 .expect("valid plan")
23674 .expect("composites are drawable inside the native fused pass");
23675
23676 assert_eq!(
23677 partitions,
23678 vec![NativeSegmentFusionPartition {
23679 chunk: segment,
23680 budget: NativeSegmentFusionBudget {
23681 shape_count: 2,
23682 gradient_stop_count: 0,
23683 },
23684 }],
23685 "layer composites and shader composites must preserve order without forcing separate render passes"
23686 );
23687 }
23688
23689 #[cfg(not(target_arch = "wasm32"))]
23690 #[test]
23691 fn native_segment_fusion_partitions_preserve_non_shape_order_at_budget_boundary() {
23692 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
23695 let ordered_items: Vec<_> = (0..desktop_batch_cap)
23696 .map(|index| (index, SegmentDrawItem::Shape(index)))
23697 .chain([
23698 (desktop_batch_cap, SegmentDrawItem::Image(0)),
23699 (
23700 desktop_batch_cap + 1,
23701 SegmentDrawItem::Shape(desktop_batch_cap),
23702 ),
23703 ])
23704 .collect();
23705 let shapes: Vec<_> = (0..=desktop_batch_cap)
23706 .map(|index| test_shape(index, BlendMode::SrcOver))
23707 .collect();
23708 let segment = chunk(&[
23709 SegmentBatchPlan::Shape {
23710 start: 0,
23711 end: desktop_batch_cap,
23712 blend_mode: BlendMode::SrcOver,
23713 },
23714 SegmentBatchPlan::Image {
23715 start: desktop_batch_cap,
23716 end: desktop_batch_cap + 1,
23717 blend_mode: BlendMode::SrcOver,
23718 },
23719 SegmentBatchPlan::Shape {
23720 start: desktop_batch_cap + 1,
23721 end: desktop_batch_cap + 2,
23722 blend_mode: BlendMode::SrcOver,
23723 },
23724 ]);
23725
23726 let partitions = native_segment_fusion_partitions(
23727 &ordered_items,
23728 &shapes,
23729 &[],
23730 &segment,
23731 ShapeBatchLimits::desktop(),
23732 )
23733 .expect("valid plan")
23734 .expect("overflowing segment should be partitionable");
23735
23736 assert_eq!(partitions.len(), 2);
23737 assert_eq!(
23738 partitions[0].chunk,
23739 chunk(&[
23740 SegmentBatchPlan::Shape {
23741 start: 0,
23742 end: desktop_batch_cap,
23743 blend_mode: BlendMode::SrcOver,
23744 },
23745 SegmentBatchPlan::Image {
23746 start: desktop_batch_cap,
23747 end: desktop_batch_cap + 1,
23748 blend_mode: BlendMode::SrcOver,
23749 },
23750 ])
23751 );
23752 assert_eq!(
23753 partitions[1].chunk,
23754 chunk(&[SegmentBatchPlan::Shape {
23755 start: desktop_batch_cap + 1,
23756 end: desktop_batch_cap + 2,
23757 blend_mode: BlendMode::SrcOver,
23758 }])
23759 );
23760 }
23761
23762 #[test]
23763 fn segment_command_iter_keeps_repeated_batch_kinds_in_one_chunk() {
23764 let ordered_items = vec![
23765 (0, SegmentDrawItem::Shape(0)),
23766 (1, SegmentDrawItem::Image(0)),
23767 (2, SegmentDrawItem::Shape(1)),
23768 ];
23769 let shapes = vec![
23770 test_shape(0, BlendMode::SrcOver),
23771 test_shape(2, BlendMode::DstOut),
23772 ];
23773 let images = vec![test_image(1, BlendMode::SrcOver)];
23774
23775 let commands: Vec<_> = SegmentCommandIter::new(
23776 &ordered_items,
23777 &shapes,
23778 &images,
23779 ShapeBatchLimits::desktop(),
23780 )
23781 .collect();
23782
23783 assert_eq!(
23784 commands,
23785 vec![SegmentRenderCommand::DrawChunk(chunk(&[
23786 SegmentBatchPlan::Shape {
23787 start: 0,
23788 end: 1,
23789 blend_mode: BlendMode::SrcOver,
23790 },
23791 SegmentBatchPlan::Image {
23792 start: 1,
23793 end: 2,
23794 blend_mode: BlendMode::SrcOver,
23795 },
23796 SegmentBatchPlan::Shape {
23797 start: 2,
23798 end: 3,
23799 blend_mode: BlendMode::DstOut,
23800 },
23801 ]))]
23802 );
23803 }
23804
23805 #[test]
23806 fn segment_command_iter_splits_contiguous_shape_runs_at_uniform_batch_limit() {
23807 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
23810 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
23811 .map(|index| (index, SegmentDrawItem::Shape(index)))
23812 .collect();
23813 let shapes: Vec<_> = (0..=desktop_batch_cap)
23814 .map(|index| test_shape(index, BlendMode::SrcOver))
23815 .collect();
23816 let images = Vec::new();
23817
23818 let commands: Vec<_> = SegmentCommandIter::new(
23819 &ordered_items,
23820 &shapes,
23821 &images,
23822 ShapeBatchLimits::desktop(),
23823 )
23824 .collect();
23825
23826 assert_eq!(
23827 commands,
23828 vec![SegmentRenderCommand::DrawChunk(chunk(&[
23829 SegmentBatchPlan::Shape {
23830 start: 0,
23831 end: desktop_batch_cap,
23832 blend_mode: BlendMode::SrcOver,
23833 },
23834 SegmentBatchPlan::Shape {
23835 start: desktop_batch_cap,
23836 end: desktop_batch_cap + 1,
23837 blend_mode: BlendMode::SrcOver,
23838 },
23839 ]))]
23840 );
23841 }
23842
23843 #[test]
23844 fn segment_command_iter_keeps_shadows_as_explicit_boundaries() {
23845 let ordered_items = vec![
23846 (0, SegmentDrawItem::Shape(0)),
23847 (1, SegmentDrawItem::Shadow(0)),
23848 (2, SegmentDrawItem::Image(0)),
23849 (3, SegmentDrawItem::Text(0)),
23850 ];
23851 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
23852 let images = vec![test_image(2, BlendMode::SrcOver)];
23853
23854 let commands: Vec<_> = SegmentCommandIter::new(
23855 &ordered_items,
23856 &shapes,
23857 &images,
23858 ShapeBatchLimits::desktop(),
23859 )
23860 .collect();
23861
23862 assert_eq!(
23863 commands,
23864 vec![
23865 SegmentRenderCommand::DrawChunk(chunk(&[SegmentBatchPlan::Shape {
23866 start: 0,
23867 end: 1,
23868 blend_mode: BlendMode::SrcOver,
23869 }])),
23870 SegmentRenderCommand::Shadow(0),
23871 SegmentRenderCommand::DrawChunk(chunk(&[
23872 SegmentBatchPlan::Image {
23873 start: 2,
23874 end: 3,
23875 blend_mode: BlendMode::SrcOver,
23876 },
23877 SegmentBatchPlan::Text { start: 3, end: 4 },
23878 ])),
23879 ]
23880 );
23881 }
23882
23883 #[test]
23884 fn staged_buffer_uploads_align_new_copies_to_copy_buffer_alignment() {
23885 let mut uploads = StagedBufferUploads::default();
23886 uploads.bytes.extend_from_slice(&[1, 2]);
23887
23888 uploads.stage(UploadTarget::ImageIndex, &[3, 4, 5, 6]);
23889
23890 assert_eq!(uploads.bytes, vec![1, 2, 0, 0, 3, 4, 5, 6]);
23891 assert_eq!(
23892 uploads.copies,
23893 vec![PendingBufferCopy {
23894 source_offset: 4,
23895 target_offset: 0,
23896 size: 4,
23897 target: UploadTarget::ImageIndex,
23898 }]
23899 );
23900 }
23901
23902 #[test]
23903 fn staged_buffer_uploads_ignore_empty_payloads() {
23904 let mut uploads = StagedBufferUploads::default();
23905
23906 uploads.stage(UploadTarget::Uniform, &[]);
23907
23908 assert!(uploads.is_empty());
23909 assert!(uploads.bytes.is_empty());
23910 }
23911
23912 #[test]
23913 fn staged_buffer_uploads_return_exact_payload_slice_for_copy() {
23914 let mut uploads = StagedBufferUploads::default();
23915 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
23916 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
23917
23918 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
23919 assert_eq!(uploads.payload_for_copy(uploads.copies[1]), &[5, 6, 7, 8]);
23920 }
23921
23922 #[test]
23923 fn staged_buffer_uploads_record_destination_offsets() {
23924 let mut uploads = StagedBufferUploads::default();
23925
23926 uploads.stage_at(UploadTarget::ImageIndex, 256, &[1, 2, 3, 4]);
23927
23928 assert_eq!(uploads.copies[0].target_offset, 256);
23929 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
23930 }
23931
23932 #[test]
23933 fn staged_buffer_uploads_truncate_restores_previous_state() {
23934 let mut uploads = StagedBufferUploads::default();
23935 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
23936 let bytes_len = uploads.bytes.len();
23937 let copies_len = uploads.copies.len();
23938 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
23939
23940 uploads.truncate(bytes_len, copies_len);
23941
23942 assert_eq!(uploads.bytes, vec![1, 2, 3, 4]);
23943 assert_eq!(uploads.copies.len(), 1);
23944 }
23945
23946 #[test]
23947 fn inner_shadow_composite_mask_uses_fill_shape_and_scale() {
23948 let mut fill = test_shape(0, BlendMode::SrcOver);
23949 fill.local_rect = Rect {
23950 x: 10.0,
23951 y: 12.0,
23952 width: 40.0,
23953 height: 20.0,
23954 };
23955 fill.shape = Some(RoundedCornerShape::uniform(6.0));
23956
23957 let cutout = test_shape(1, BlendMode::DstOut);
23958 let shadow = test_shadow_draw(vec![
23959 (fill, BlendMode::SrcOver),
23960 (cutout, BlendMode::DstOut),
23961 ]);
23962
23963 let mask = inner_shadow_composite_mask(&shadow, 1.5).expect("inner mask expected");
23964 assert_eq!(mask.rect, [15.0, 18.0, 60.0, 30.0]);
23965 assert_eq!(mask.radii, [9.0, 9.0, 9.0, 9.0]);
23966 }
23967
23968 #[test]
23969 fn inner_shadow_composite_mask_is_none_without_dst_out() {
23970 let fill = test_shape(0, BlendMode::SrcOver);
23971 let shadow = test_shadow_draw(vec![(fill, BlendMode::SrcOver)]);
23972 assert!(inner_shadow_composite_mask(&shadow, 1.0).is_none());
23973 }
23974
23975 #[test]
23976 fn render_effect_support_matrix_covers_all_variants() {
23977 let blur = RenderEffect::blur(4.0);
23978 let offset = RenderEffect::offset(2.0, 3.0);
23979 let shader = RenderEffect::runtime_shader(cranpose_ui_graphics::RuntimeShader::new(
23980 r#"
23981 @group(0) @binding(0) var input_texture: texture_2d<f32>;
23982 @group(0) @binding(1) var input_sampler: sampler;
23983 @group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
23984 struct VertexOutput {
23985 @builtin(position) position: vec4<f32>,
23986 @location(0) uv: vec2<f32>,
23987 }
23988 @vertex
23989 fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
23990 var output: VertexOutput;
23991 let x = f32(i32(vertex_index & 1u) * 2 - 1);
23992 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
23993 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
23994 output.position = vec4<f32>(x, y, 0.0, 1.0);
23995 return output;
23996 }
23997 @fragment
23998 fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
23999 return textureSample(input_texture, input_sampler, input.uv);
24000 }
24001 "#,
24002 ));
24003 let chain = blur.clone().then(offset.clone());
24004
24005 assert!(is_render_effect_supported(&blur));
24006 assert!(is_render_effect_supported(&offset));
24007 assert!(is_render_effect_supported(&shader));
24008 assert!(is_render_effect_supported(&chain));
24009 }
24010
24011 #[test]
24012 fn clip_to_bounds_propagates_visual_clip_to_all_descendant_shapes() {
24013 let container_local_bounds = Rect {
24016 x: 0.0,
24017 y: 0.0,
24018 width: 800.0,
24019 height: 500.0,
24020 };
24021 let container_clip_in_parent = Rect {
24023 x: 0.0,
24024 y: 50.0,
24025 width: 800.0,
24026 height: 500.0,
24027 };
24028
24029 let shape_above = RenderNode::Primitive(PrimitiveEntry {
24031 phase: PrimitivePhase::BeforeChildren,
24032 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24033 primitive: DrawPrimitive::Rect {
24034 rect: Rect {
24035 x: 10.0,
24036 y: -30.0,
24037 width: 100.0,
24038 height: 40.0,
24039 },
24040 brush: Brush::solid(Color::WHITE),
24041 stroke: None,
24042 },
24043 clip: None,
24044 }),
24045 });
24046
24047 let shape_inside = RenderNode::Primitive(PrimitiveEntry {
24049 phase: PrimitivePhase::BeforeChildren,
24050 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24051 primitive: DrawPrimitive::Rect {
24052 rect: Rect {
24053 x: 10.0,
24054 y: 100.0,
24055 width: 100.0,
24056 height: 40.0,
24057 },
24058 brush: Brush::solid(Color::WHITE),
24059 stroke: None,
24060 },
24061 clip: None,
24062 }),
24063 });
24064
24065 let shape_below = RenderNode::Primitive(PrimitiveEntry {
24067 phase: PrimitivePhase::BeforeChildren,
24068 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24069 primitive: DrawPrimitive::Rect {
24070 rect: Rect {
24071 x: 10.0,
24072 y: 600.0,
24073 width: 100.0,
24074 height: 40.0,
24075 },
24076 brush: Brush::solid(Color::WHITE),
24077 stroke: None,
24078 },
24079 clip: None,
24080 }),
24081 });
24082
24083 let mut content_layer = test_layer(
24085 Rect {
24086 x: 0.0,
24087 y: 0.0,
24088 width: 800.0,
24089 height: 1000.0,
24090 },
24091 vec![shape_above, shape_inside, shape_below],
24092 );
24093 content_layer.transform_to_parent = ProjectiveTransform::translation(0.0, -30.0);
24094 content_layer.translated_content_context = true;
24095
24096 let mut clip_container = test_layer(
24098 container_local_bounds,
24099 vec![RenderNode::Layer(Box::new(content_layer))],
24100 );
24101 clip_container.clip_to_bounds = true;
24102 clip_container.transform_to_parent = ProjectiveTransform::translation(0.0, 50.0);
24103
24104 let root = test_layer(
24106 Rect {
24107 x: 0.0,
24108 y: 0.0,
24109 width: 800.0,
24110 height: 600.0,
24111 },
24112 vec![RenderNode::Layer(Box::new(clip_container))],
24113 );
24114
24115 let mut rect_cache = HashMap::new();
24116 let mut requirements_cache = HashMap::new();
24117 let collected =
24118 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
24119
24120 assert_eq!(
24121 collected.scene.shapes.len(),
24122 3,
24123 "all three shapes should be flattened into the scene"
24124 );
24125
24126 for (i, shape) in collected.scene.shapes.iter().enumerate() {
24127 assert!(
24128 shape.clip.is_some(),
24129 "shape {} at rect {:?} must have a clip from clip_to_bounds container, but clip is None",
24130 i,
24131 shape.rect
24132 );
24133 let clip = shape.clip.unwrap();
24134 assert_eq!(
24135 clip, container_clip_in_parent,
24136 "shape {} clip should match the clip_to_bounds container bounds in parent space",
24137 i
24138 );
24139 }
24140 }
24141
24142 #[test]
24143 fn clip_to_bounds_culls_child_layers_outside_boundary() {
24144 let clip_container_bounds = Rect {
24152 x: 0.0,
24153 y: 0.0,
24154 width: 800.0,
24155 height: 500.0,
24156 };
24157
24158 let shape_in_card = RenderNode::Primitive(PrimitiveEntry {
24159 phase: PrimitivePhase::BeforeChildren,
24160 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24161 primitive: DrawPrimitive::Rect {
24162 rect: Rect {
24163 x: 0.0,
24164 y: 0.0,
24165 width: 300.0,
24166 height: 80.0,
24167 },
24168 brush: Brush::solid(Color::WHITE),
24169 stroke: None,
24170 },
24171 clip: None,
24172 }),
24173 });
24174
24175 let mut card_outside = crate::test_support::layer_node(
24177 Rect {
24178 x: 0.0,
24179 y: 0.0,
24180 width: 300.0,
24181 height: 80.0,
24182 },
24183 ProjectiveTransform::identity(),
24184 GraphicsLayer {
24185 clip: true,
24186 ..GraphicsLayer::default()
24187 },
24188 vec![shape_in_card.clone()],
24189 );
24190 card_outside.transform_to_parent = ProjectiveTransform::translation(10.0, 600.0);
24191
24192 let mut card_inside = crate::test_support::layer_node(
24194 Rect {
24195 x: 0.0,
24196 y: 0.0,
24197 width: 300.0,
24198 height: 80.0,
24199 },
24200 ProjectiveTransform::identity(),
24201 GraphicsLayer {
24202 clip: true,
24203 ..GraphicsLayer::default()
24204 },
24205 vec![shape_in_card],
24206 );
24207 card_inside.transform_to_parent = ProjectiveTransform::translation(10.0, 100.0);
24208
24209 let content = test_layer(
24211 Rect {
24212 x: 0.0,
24213 y: 0.0,
24214 width: 800.0,
24215 height: 1000.0,
24216 },
24217 vec![
24218 RenderNode::Layer(Box::new(card_inside)),
24219 RenderNode::Layer(Box::new(card_outside)),
24220 ],
24221 );
24222
24223 let mut clip_container = test_layer(
24225 clip_container_bounds,
24226 vec![RenderNode::Layer(Box::new(content))],
24227 );
24228 clip_container.clip_to_bounds = true;
24229
24230 let root = test_layer(
24232 Rect {
24233 x: 0.0,
24234 y: 0.0,
24235 width: 800.0,
24236 height: 600.0,
24237 },
24238 vec![RenderNode::Layer(Box::new(clip_container))],
24239 );
24240
24241 let mut rect_cache = HashMap::new();
24242 let mut requirements_cache = HashMap::new();
24243 let collected =
24244 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
24245
24246 assert_eq!(
24247 collected.scene.shapes.len(),
24248 1,
24249 "only the card inside the clip boundary should produce shapes; \
24250 the card outside must be culled entirely"
24251 );
24252
24253 let shape = &collected.scene.shapes[0];
24254 assert!(
24255 shape.clip.is_some(),
24256 "the visible card's shape must have a clip from clip_to_bounds"
24257 );
24258 }
24259
24260 #[test]
24261 fn flattened_layer_shadow_z_index_is_below_content() {
24262 let shape = RenderNode::Primitive(PrimitiveEntry {
24266 phase: PrimitivePhase::BeforeChildren,
24267 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24268 primitive: DrawPrimitive::Rect {
24269 rect: Rect {
24270 x: 0.0,
24271 y: 0.0,
24272 width: 100.0,
24273 height: 100.0,
24274 },
24275 brush: Brush::solid(Color::WHITE),
24276 stroke: None,
24277 },
24278 clip: None,
24279 }),
24280 });
24281
24282 let child_bounds = Rect {
24283 x: 0.0,
24284 y: 0.0,
24285 width: 100.0,
24286 height: 100.0,
24287 };
24288
24289 let child = crate::test_support::layer_node(
24290 child_bounds,
24291 ProjectiveTransform::translation(50.0, 50.0),
24292 GraphicsLayer {
24293 shadow_elevation: 20.0,
24294 ..GraphicsLayer::default()
24295 },
24296 vec![shape],
24297 );
24298
24299 let root = test_layer(
24300 Rect {
24301 x: 0.0,
24302 y: 0.0,
24303 width: 800.0,
24304 height: 600.0,
24305 },
24306 vec![RenderNode::Layer(Box::new(child))],
24307 );
24308
24309 let mut rect_cache = HashMap::new();
24310 let mut requirements_cache = HashMap::new();
24311 let collected =
24312 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
24313
24314 assert!(
24315 !collected.scene.shadow_draws.is_empty(),
24316 "shadow_elevation > 0 must produce shadow draws"
24317 );
24318 let max_shadow_z = collected
24319 .scene
24320 .shadow_draws
24321 .iter()
24322 .map(|s| s.z_index)
24323 .max()
24324 .unwrap();
24325 let min_content_z = collected
24326 .scene
24327 .shapes
24328 .iter()
24329 .map(|s| s.z_index)
24330 .min()
24331 .unwrap();
24332 assert!(
24333 max_shadow_z < min_content_z,
24334 "shadow z-index ({}) must be less than content z-index ({}); \
24335 shadows must render behind their content",
24336 max_shadow_z,
24337 min_content_z
24338 );
24339 }
24340
24341 #[cfg(not(target_arch = "wasm32"))]
24344 fn bundle_op(slot: u32, epoch: Option<u64>, first: u32, last: u32) -> RetainedBundleOpKey {
24345 RetainedBundleOpKey {
24346 slot,
24347 capture_epoch: epoch,
24348 first,
24349 last,
24350 retained_index: slot,
24351 has_mesh: false,
24352 }
24353 }
24354
24355 #[cfg(not(target_arch = "wasm32"))]
24356 fn bundle_key(ops: &[RetainedBundleOpKey]) -> RetainedBundleKey {
24357 RetainedBundleKey {
24358 depth: false,
24359 ops: ops.to_vec(),
24360 }
24361 }
24362
24363 #[cfg(not(target_arch = "wasm32"))]
24366 #[test]
24367 fn retained_bundle_cache_reuses_stable_keys() {
24368 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24369 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
24370 let key = bundle_key(&ops);
24371
24372 assert!(!cache.hit(&key), "empty cache must miss");
24373 cache.insert(key.clone(), 111);
24374 assert_eq!(cache.get(&key), Some(&111));
24375 cache.end_frame();
24376
24377 for _ in 0..3 {
24378 assert!(cache.hit(&bundle_key(&ops)), "stable key must stay cached");
24379 cache.end_frame();
24380 }
24381 assert_eq!(cache.stats(), (1, 3), "one rebuild, three cached executes");
24382 }
24383
24384 #[cfg(not(target_arch = "wasm32"))]
24388 #[test]
24389 fn retained_bundle_cache_invalidates_on_any_op_change() {
24390 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
24391 let variants: [Vec<RetainedBundleOpKey>; 5] = [
24392 vec![bundle_op(3, Some(8), 0, 40), bundle_op(5, Some(9), 4, 12)],
24394 vec![bundle_op(5, Some(9), 4, 12), bundle_op(3, Some(7), 0, 40)],
24396 vec![bundle_op(3, Some(7), 0, 40)],
24398 vec![bundle_op(3, Some(7), 0, 41), bundle_op(5, Some(9), 4, 12)],
24400 vec![bundle_op(3, Some(7), 0, 40), bundle_op(5, None, 4, 12)],
24402 ];
24403 for changed in variants {
24404 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24405 cache.insert(bundle_key(&ops), 111);
24406 cache.end_frame();
24407 assert!(
24408 !cache.hit(&RetainedBundleKey {
24409 depth: false,
24410 ops: changed.clone()
24411 }),
24412 "changed key {changed:?} must not reuse the stale bundle"
24413 );
24414 }
24415 }
24416
24417 #[cfg(not(target_arch = "wasm32"))]
24421 #[test]
24422 fn retained_bundle_cache_keys_depth_variants_apart() {
24423 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24424 let ops = vec![bundle_op(3, Some(7), 0, 40)];
24425 cache.insert(
24426 RetainedBundleKey {
24427 depth: false,
24428 ops: ops.clone(),
24429 },
24430 111,
24431 );
24432 cache.end_frame();
24433 assert!(
24434 !cache.hit(&RetainedBundleKey { depth: true, ops }),
24435 "a flat bundle must not replay into the display-clip culled pass"
24436 );
24437 }
24438
24439 #[cfg(not(target_arch = "wasm32"))]
24443 #[test]
24444 fn retained_bundle_cache_evicts_unused_entries() {
24445 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24446 let stale = bundle_key(&[bundle_op(1, Some(1), 0, 6)]);
24447 let live = bundle_key(&[bundle_op(2, Some(2), 0, 6)]);
24448 cache.insert(stale.clone(), 1);
24449 cache.insert(live.clone(), 2);
24450 cache.end_frame();
24451
24452 assert!(cache.hit(&live));
24453 cache.end_frame();
24454
24455 assert!(
24456 !cache.hit(&stale),
24457 "entry unused for a frame must have been evicted"
24458 );
24459 assert!(cache.hit(&live), "used entry must survive eviction");
24460
24461 cache.clear();
24462 assert!(!cache.hit(&live), "clear must drop every entry");
24463 }
24464}