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::{capture_root_target_reads, 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(
447 collected: &CollectedLayer,
448 root_target_reads: bool,
449) -> bool {
450 for (child_index, child) in collected.child_layers.iter().enumerate() {
451 if child.backdrop.is_some() && !root_target_reads {
452 if root_direct_diag_enabled() {
453 log::warn!(
454 "[root-direct-diag] reject self-backdrop child node={:?}",
455 child.node_id
456 );
457 }
458 return false;
459 }
460 if child.needs_nested_underlay {
461 let Some(dest_rect) = axis_aligned_quad_rect(child.dest_quad) else {
462 if root_direct_diag_enabled() {
463 log::warn!(
464 "[root-direct-diag] reject projective underlay child node={:?}",
465 child.node_id
466 );
467 }
468 return false;
469 };
470 let translation_only = (dest_rect.width - child.logical_rect.width).abs() <= 0.001
471 && (dest_rect.height - child.logical_rect.height).abs() <= 0.001;
472 let unsupported_preceding_child_layer = collected.child_layers[..child_index]
473 .iter()
474 .any(|preceding| {
475 if direct_root_child_can_be_replayed_into_later_underlay(preceding) {
476 return false;
477 }
478 axis_aligned_quad_rect(preceding.dest_quad)
479 .is_none_or(|preceding_rect| rects_overlap(preceding_rect, dest_rect))
480 });
481 let preceding_scene_events =
482 scene_layer_events_precede_z(&collected.scene, child.z_index);
483 if unsupported_preceding_child_layer || preceding_scene_events || !translation_only {
484 if root_direct_diag_enabled() {
485 log::warn!(
486 "[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})",
487 child.node_id,
488 unsupported_preceding_child_layer,
489 preceding_scene_events,
490 translation_only,
491 dest_rect.x,
492 dest_rect.y,
493 dest_rect.width,
494 dest_rect.height,
495 child.logical_rect.x,
496 child.logical_rect.y,
497 child.logical_rect.width,
498 child.logical_rect.height
499 );
500 }
501 return false;
502 }
503 }
504 }
505 true
506}
507
508#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
509struct ShadowSurfaceCacheKey {
510 content_hash: u64,
511 pixel_size: [u32; 2],
512 root_scale_bits: u32,
513 blur_radius_bits: u32,
514}
515
516struct CachedShadowSurface {
517 target: Rc<OffscreenTarget>,
518 byte_size: u64,
519}
520
521struct CachedShadowComposite {
522 source: Rc<OffscreenTarget>,
523 scissor: Option<(u32, u32, u32, u32)>,
524 rounded_mask: Option<RoundedCompositeMask>,
525 dest_viewport: Option<(f32, f32, f32, f32)>,
526}
527
528impl CachedShadowComposite {
529 fn batch_item(&self) -> CompositeBatchItem<'_> {
530 CompositeBatchItem {
531 source: &self.source,
532 alpha: 1.0,
533 scissor: self.scissor,
534 rounded_mask: self.rounded_mask,
535 blend_mode: BlendMode::SrcOver,
536 dest_viewport: self.dest_viewport,
537 source_viewport: None,
538 sample_mode: CompositeSampleMode::Nearest,
539 }
540 }
541}
542
543#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
544struct TextImageCacheKey(u64);
545
546struct CachedTextImage {
547 image: ImageBitmap,
548}
549
550#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
551struct TextGlyphRunCacheKey(u64);
552
553#[derive(Clone, Copy)]
554struct CachedTextGlyphQuad {
555 x: i32,
556 y: i32,
557 width: usize,
558 height: usize,
559 color: (f32, f32, f32, f32),
560 uv: ImageUvRect,
561}
562
563struct CachedTextGlyphRun {
564 glyphs: Rc<[SoftwareGlyphAtlasPlacement]>,
565 quads: Option<Rc<[CachedTextGlyphQuad]>>,
566 atlas_generation: u64,
567}
568
569const TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER: f32 = 2.0;
570
571#[cfg(not(target_arch = "wasm32"))]
572struct CachedGpuTextGlyphRun {
573 vertex_buffer: wgpu::Buffer,
574 index_buffer: wgpu::Buffer,
575 index_count: u32,
576 atlas_generation: u64,
577}
578
579#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
580struct TextLineIndexCacheKey(usize);
581
582struct CachedTextLineIndex {
583 text: std::sync::Weak<cranpose_ui::text::RenderString>,
584 len: usize,
585 starts: Rc<[usize]>,
586}
587
588struct TextLineIndexCache {
589 entries: BoundedLruCache<TextLineIndexCacheKey, CachedTextLineIndex>,
590}
591
592impl TextLineIndexCache {
593 fn new(capacity: usize) -> Self {
594 Self {
595 entries: BoundedLruCache::with_capacity_at_least_one(capacity),
596 }
597 }
598
599 fn line_starts(&mut self, text: &Arc<cranpose_ui::text::RenderString>) -> Rc<[usize]> {
600 let key = TextLineIndexCacheKey(Arc::as_ptr(text) as usize);
601 if let Some(cached) = self.entries.get(&key) {
602 if cached.len == text.text.len()
603 && cached
604 .text
605 .upgrade()
606 .is_some_and(|cached_text| Arc::ptr_eq(&cached_text, text))
607 {
608 return cached.starts.clone();
609 }
610 }
611
612 let starts = Rc::<[usize]>::from(line_start_offsets(text.text.as_str()));
613 self.entries.put(
614 key,
615 CachedTextLineIndex {
616 text: Arc::downgrade(text),
617 len: text.text.len(),
618 starts: starts.clone(),
619 },
620 );
621 starts
622 }
623}
624
625#[derive(Clone, Copy, Debug, PartialEq)]
626struct ShapeShadowSurfacePlan {
627 source_device_bounds: DevicePixelBounds,
628 processing_scissor: Option<(u32, u32, u32, u32)>,
629 pixel_radius: f32,
630}
631
632#[derive(Default)]
648struct DeviceErrorSentry {
649 errors: std::sync::atomic::AtomicU64,
651 poisoned: std::sync::atomic::AtomicBool,
653}
654
655impl DeviceErrorSentry {
656 fn record(&self, error: &wgpu::Error) {
659 use std::sync::atomic::Ordering;
660 self.poisoned.store(true, Ordering::Release);
661 let count = self.errors.fetch_add(1, Ordering::Relaxed) + 1;
662 if count.is_power_of_two() {
668 log::error!("[gpu-device] uncaptured wgpu error #{count}: {error}");
669 }
670 }
671
672 fn take_poison(&self) -> bool {
673 self.poisoned
674 .swap(false, std::sync::atomic::Ordering::AcqRel)
675 }
676
677 fn error_count(&self) -> u64 {
678 self.errors.load(std::sync::atomic::Ordering::Relaxed)
679 }
680}
681
682#[derive(Default)]
683struct RendererWarningState {
684 unsupported_effect_reported: Cell<bool>,
685}
686
687impl RendererWarningState {
688 fn warn_unsupported_effect_once(&self) {
689 if !self.unsupported_effect_reported.replace(true) {
690 log::warn!(
691 "WGPU renderer received an unsupported RenderEffect variant; falling back to passthrough compositing"
692 );
693 }
694 }
695}
696
697fn is_blend_mode_supported(mode: BlendMode) -> bool {
698 matches!(mode, BlendMode::SrcOver | BlendMode::DstOut)
699}
700
701fn blend_state_for_mode(mode: BlendMode) -> wgpu::BlendState {
702 match mode {
703 BlendMode::DstOut => wgpu::BlendState {
704 color: wgpu::BlendComponent {
705 src_factor: wgpu::BlendFactor::Zero,
706 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
707 operation: wgpu::BlendOperation::Add,
708 },
709 alpha: wgpu::BlendComponent {
710 src_factor: wgpu::BlendFactor::Zero,
711 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
712 operation: wgpu::BlendOperation::Add,
713 },
714 },
715 _ => wgpu::BlendState::ALPHA_BLENDING,
716 }
717}
718
719fn supported_blend_mode(mode: BlendMode) -> BlendMode {
720 if is_blend_mode_supported(mode) {
721 return mode;
722 }
723
724 BlendMode::SrcOver
725}
726
727fn direct_shader_composite_viewport(
728 alpha: f32,
729 blend_mode: BlendMode,
730 dest_viewport: Option<(f32, f32, f32, f32)>,
731 sample_mode: CompositeSampleMode,
732 source_size: (u32, u32),
733) -> Option<(f32, f32, f32, f32)> {
734 if alpha != 1.0 || supported_blend_mode(blend_mode) != BlendMode::SrcOver {
735 return None;
736 }
737 let viewport = dest_viewport?;
738 if viewport.2 <= 0.0 || viewport.3 <= 0.0 {
739 return None;
740 }
741 match sample_mode {
742 CompositeSampleMode::Linear | CompositeSampleMode::Nearest => Some(viewport),
743 CompositeSampleMode::Box4
744 if shader_composite_preserves_source_pixel_grid(viewport, source_size) =>
745 {
746 Some(viewport)
747 }
748 CompositeSampleMode::Box4 => None,
749 }
750}
751
752fn shader_composite_preserves_source_pixel_grid(
753 viewport: (f32, f32, f32, f32),
754 source_size: (u32, u32),
755) -> bool {
756 const EPSILON: f32 = 0.01;
757 let (x, y, width, height) = viewport;
758 let (source_width, source_height) = source_size;
759 (x - x.round()).abs() <= EPSILON
760 && (y - y.round()).abs() <= EPSILON
761 && (width - source_width as f32).abs() <= EPSILON
762 && (height - source_height as f32).abs() <= EPSILON
763}
764
765type DirectShaderTailComposite<'a> = (&'a RenderEffect, &'a RuntimeShader, (f32, f32, f32, f32));
766
767fn direct_shader_tail_composite(
768 effect: &RenderEffect,
769 alpha: f32,
770 blend_mode: BlendMode,
771 dest_viewport: Option<(f32, f32, f32, f32)>,
772 sample_mode: CompositeSampleMode,
773 source_size: (u32, u32),
774) -> Option<DirectShaderTailComposite<'_>> {
775 let viewport = direct_shader_composite_viewport(
776 alpha,
777 blend_mode,
778 dest_viewport,
779 sample_mode,
780 source_size,
781 )?;
782 let RenderEffect::Chain { first, second } = effect else {
783 return None;
784 };
785 let RenderEffect::Shader { shader } = second.as_ref() else {
786 return None;
787 };
788 Some((first.as_ref(), shader, viewport))
789}
790
791fn hash_f32_for_cache<H: Hasher>(value: f32, state: &mut H) {
792 value.to_bits().hash(state);
793}
794
795fn hash_text_raster_geometry_for_cache<H: Hasher>(
796 rect: Rect,
797 static_text_motion: bool,
798 state: &mut H,
799) {
800 hash_f32_for_cache(rect.width, state);
801 hash_f32_for_cache(rect.height, state);
802 static_text_motion.hash(state);
803 if !static_text_motion {
804 hash_f32_for_cache(rect.x.fract(), state);
805 hash_f32_for_cache(rect.y.fract(), state);
806 }
807}
808
809fn text_raster_geometry_for_draw(
810 text_draw: &TextDraw,
811 root_scale: f32,
812) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
813 if text_draw.text.is_empty()
814 || text_draw.rect.width <= 0.0
815 || text_draw.rect.height <= 0.0
816 || !root_scale.is_finite()
817 || root_scale <= 0.0
818 {
819 return None;
820 }
821
822 let text_scale = text_draw.scale * root_scale;
823 if !text_scale.is_finite() || text_scale <= 0.0 {
824 return None;
825 }
826
827 let static_text_motion = text_draw
828 .text_style
829 .paragraph_style
830 .text_motion
831 .unwrap_or(cranpose_ui::text::TextMotion::Static)
832 == cranpose_ui::text::TextMotion::Static;
833 let snap_delta = text_draw
834 .snap_anchor
835 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
836 .unwrap_or_default();
837 let logical_rect = text_draw.rect.translate(snap_delta.x, snap_delta.y);
838 let clip = text_draw.clip;
841 let mut raster_rect = Rect {
842 x: logical_rect.x * root_scale,
843 y: logical_rect.y * root_scale,
844 width: logical_rect.width * root_scale,
845 height: logical_rect.height * root_scale,
846 };
847 if text_draw.snap_anchor.is_some() {
848 raster_rect.x = canonicalize_device_coordinate(raster_rect.x);
849 raster_rect.y = canonicalize_device_coordinate(raster_rect.y);
850 }
851 if static_text_motion {
852 raster_rect.x = raster_rect.x.round();
853 raster_rect.y = raster_rect.y.round();
854 }
855 raster_rect.width = raster_rect.width.ceil().max(1.0);
856 raster_rect.height = raster_rect.height.ceil().max(1.0);
857 Some((
858 logical_rect,
859 raster_rect,
860 clip,
861 text_scale,
862 static_text_motion,
863 ))
864}
865
866fn text_draw_is_visible_in_viewport(
867 logical_rect: Rect,
868 clip: Option<Rect>,
869 viewport: ViewportUniformParams,
870 root_scale: f32,
871) -> bool {
872 draw_rect_is_visible_in_viewport(logical_rect, clip, viewport, root_scale)
873}
874
875fn text_draw_should_prewarm_in_viewport(
876 logical_rect: Rect,
877 clip: Option<Rect>,
878 viewport: ViewportUniformParams,
879 root_scale: f32,
880) -> bool {
881 if !root_scale.is_finite() || root_scale <= 0.0 {
882 return false;
883 }
884 let viewport_rect = Rect {
885 x: viewport.offset[0] / root_scale,
886 y: viewport.offset[1] / root_scale,
887 width: viewport.width as f32 / root_scale,
888 height: viewport.height as f32 / root_scale,
889 };
890 let margin_x = viewport_rect.width * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
891 let margin_y = viewport_rect.height * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
892 let prewarm_viewport = expand_rect(viewport_rect, margin_x, margin_y);
893 let prewarm_rect = match clip {
894 Some(clip) => expand_rect(clip, margin_x, margin_y).intersect(prewarm_viewport),
895 None => Some(prewarm_viewport),
896 };
897 prewarm_rect.is_some_and(|rect| logical_rect.intersect(rect).is_some())
898}
899
900fn expand_rect(rect: Rect, margin_x: f32, margin_y: f32) -> Rect {
901 Rect {
902 x: rect.x - margin_x,
903 y: rect.y - margin_y,
904 width: rect.width + margin_x * 2.0,
905 height: rect.height + margin_y * 2.0,
906 }
907}
908
909fn draw_rect_is_visible_in_viewport(
910 rect: Rect,
911 clip: Option<Rect>,
912 viewport: ViewportUniformParams,
913 root_scale: f32,
914) -> bool {
915 if !root_scale.is_finite() || root_scale <= 0.0 {
916 return false;
917 }
918 let viewport_rect = Rect {
919 x: viewport.offset[0] / root_scale,
920 y: viewport.offset[1] / root_scale,
921 width: viewport.width as f32 / root_scale,
922 height: viewport.height as f32 / root_scale,
923 };
924 let visible_rect = match clip {
925 Some(clip) => clip.intersect(viewport_rect),
926 None => Some(viewport_rect),
927 };
928 visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
929}
930
931fn shape_draw_is_visible_in_viewport(
932 shape: &DrawShape,
933 viewport: ViewportUniformParams,
934 root_scale: f32,
935) -> bool {
936 let Some(viewport_rect) = viewport_rect_in_logical(viewport, root_scale) else {
937 return false;
938 };
939 shape_draw_is_visible_in_rect(shape, viewport_rect, root_scale)
940}
941
942fn viewport_rect_in_logical(viewport: ViewportUniformParams, root_scale: f32) -> Option<Rect> {
946 if !root_scale.is_finite() || root_scale <= 0.0 {
947 return None;
948 }
949 Some(Rect {
950 x: viewport.offset[0] / root_scale,
951 y: viewport.offset[1] / root_scale,
952 width: viewport.width as f32 / root_scale,
953 height: viewport.height as f32 / root_scale,
954 })
955}
956
957fn shape_draw_is_visible_in_rect(shape: &DrawShape, viewport_rect: Rect, root_scale: f32) -> bool {
960 let snap_delta = shape
961 .snap_anchor
962 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
963 .unwrap_or_default();
964 let rect = quad_bounds(translate_quad(shape.quad, snap_delta));
965 let visible_rect = match shape.clip {
966 Some(clip) => clip.intersect(viewport_rect),
967 None => Some(viewport_rect),
968 };
969 visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
970}
971
972fn cached_text_glyph_quad(
973 glyph: &SoftwareGlyphAtlasPlacement,
974 entry: GlyphAtlasEntry,
975 atlas_size: u32,
976) -> CachedTextGlyphQuad {
977 CachedTextGlyphQuad {
978 x: glyph.x,
979 y: glyph.y,
980 width: glyph.width,
981 height: glyph.height,
982 color: (
983 glyph.color.0.clamp(0.0, 1.0),
984 glyph.color.1.clamp(0.0, 1.0),
985 glyph.color.2.clamp(0.0, 1.0),
986 glyph.color.3.clamp(0.0, 1.0),
987 ),
988 uv: glyph_atlas_uv_rect(entry, atlas_size),
989 }
990}
991
992fn append_cached_text_glyph_quad(
993 source_raster_rect: Rect,
994 quad: &CachedTextGlyphQuad,
995 image_vertices: &mut Vec<Vertex>,
996 image_indices: &mut Vec<u32>,
997) -> bool {
998 if quad.width == 0 || quad.height == 0 || quad.color.3 <= 0.0 {
999 return false;
1000 }
1001
1002 let base_vertex = image_vertices.len() as u32;
1003 image_indices.extend_from_slice(&[
1004 base_vertex,
1005 base_vertex + 1,
1006 base_vertex + 2,
1007 base_vertex + 2,
1008 base_vertex + 1,
1009 base_vertex + 3,
1010 ]);
1011
1012 let x0 = source_raster_rect.x + quad.x as f32;
1013 let y0 = source_raster_rect.y + quad.y as f32;
1014 let x1 = x0 + quad.width as f32;
1015 let y1 = y0 + quad.height as f32;
1016 let color = [quad.color.0, quad.color.1, quad.color.2, quad.color.3];
1017
1018 image_vertices.extend_from_slice(&[
1019 Vertex {
1020 position: [x0, y0],
1021 color,
1022 uv: [quad.uv.min[0], quad.uv.min[1]],
1023 uv_bounds: quad.uv.sample_bounds,
1024 },
1025 Vertex {
1026 position: [x1, y0],
1027 color,
1028 uv: [quad.uv.max[0], quad.uv.min[1]],
1029 uv_bounds: quad.uv.sample_bounds,
1030 },
1031 Vertex {
1032 position: [x0, y1],
1033 color,
1034 uv: [quad.uv.min[0], quad.uv.max[1]],
1035 uv_bounds: quad.uv.sample_bounds,
1036 },
1037 Vertex {
1038 position: [x1, y1],
1039 color,
1040 uv: [quad.uv.max[0], quad.uv.max[1]],
1041 uv_bounds: quad.uv.sample_bounds,
1042 },
1043 ]);
1044 true
1045}
1046
1047fn cached_text_glyph_quad_logical_rect(
1048 source_raster_rect: Rect,
1049 quad: &CachedTextGlyphQuad,
1050 root_scale: f32,
1051) -> Option<Rect> {
1052 if !root_scale.is_finite() || root_scale <= 0.0 {
1053 return None;
1054 }
1055 Some(Rect {
1056 x: (source_raster_rect.x + quad.x as f32) / root_scale,
1057 y: (source_raster_rect.y + quad.y as f32) / root_scale,
1058 width: quad.width as f32 / root_scale,
1059 height: quad.height as f32 / root_scale,
1060 })
1061}
1062
1063fn cached_text_glyph_quad_is_visible_in_viewport(
1064 source_raster_rect: Rect,
1065 quad: &CachedTextGlyphQuad,
1066 clip: Option<Rect>,
1067 viewport: ViewportUniformParams,
1068 root_scale: f32,
1069) -> bool {
1070 cached_text_glyph_quad_logical_rect(source_raster_rect, quad, root_scale)
1071 .is_some_and(|rect| draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale))
1072}
1073
1074#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1075enum TextGlyphDrawAction {
1076 DrawVisible,
1077 PrewarmOffscreen,
1078 Skip,
1079}
1080
1081fn text_glyph_draw_action(
1082 is_visible: bool,
1083 is_prewarm_candidate: bool,
1084 allow_offscreen_prewarm: bool,
1085) -> TextGlyphDrawAction {
1086 if is_visible {
1087 TextGlyphDrawAction::DrawVisible
1088 } else if allow_offscreen_prewarm && is_prewarm_candidate {
1089 TextGlyphDrawAction::PrewarmOffscreen
1090 } else {
1091 TextGlyphDrawAction::Skip
1092 }
1093}
1094
1095#[cfg(not(target_arch = "wasm32"))]
1096fn should_use_retained_text_glyph_run(quads_len: usize, clip: Option<Rect>) -> bool {
1097 clip.is_none() && quads_len >= MIN_RETAINED_TEXT_GLYPH_QUADS
1098}
1099
1100#[cfg(not(target_arch = "wasm32"))]
1101fn offscreen_text_glyph_prewarm_work_is_bounded(
1102 cached_glyphs: Option<usize>,
1103 text_len: usize,
1104) -> bool {
1105 match cached_glyphs {
1106 Some(glyphs) => glyphs <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS,
1107 None => text_len <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
1108 }
1109}
1110
1111#[cfg(not(target_arch = "wasm32"))]
1112fn offscreen_text_glyph_prewarm_budget_exhausted(
1113 start: Instant,
1114 admitted_candidates: usize,
1115) -> bool {
1116 admitted_candidates >= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES
1117 || instant_ms(start, Instant::now()) >= OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS
1118}
1119
1120fn text_draws_for_ordered_range<'a>(
1121 ordered_items: &'a [(usize, SegmentDrawItem)],
1122 texts: &'a [TextDraw],
1123 start: usize,
1124 end: usize,
1125) -> Result<impl Iterator<Item = &'a TextDraw>, String> {
1126 let range_items = ordered_items
1127 .get(start..end)
1128 .ok_or_else(|| format!("text batch range {start}..{end} is outside ordered draw items"))?;
1129 for (_, item) in range_items {
1130 match item {
1131 SegmentDrawItem::Text(text_index) if *text_index < texts.len() => {}
1132 SegmentDrawItem::Text(text_index) => {
1133 return Err(format!(
1134 "text batch references missing text draw index: {text_index}"
1135 ));
1136 }
1137 _ => return Err(format!("text batch contains non-text draw item: {item:?}")),
1138 }
1139 }
1140
1141 Ok(range_items.iter().filter_map(move |(_, item)| match item {
1142 SegmentDrawItem::Text(text_index) => texts.get(*text_index),
1143 _ => None,
1144 }))
1145}
1146
1147const SHADOW_CACHE_DEVICE_QUANT: f32 = 16.0;
1152
1153fn hash_shadow_device_offset<H: Hasher>(value: f32, origin: f32, root_scale: f32, state: &mut H) {
1154 let quantized = ((value - origin) * root_scale * SHADOW_CACHE_DEVICE_QUANT).round();
1155 (quantized as i64).hash(state);
1156}
1157
1158fn hash_shadow_device_rect<H: Hasher>(
1159 rect: Rect,
1160 origin_x: f32,
1161 origin_y: f32,
1162 root_scale: f32,
1163 state: &mut H,
1164) {
1165 hash_shadow_device_offset(rect.x, origin_x, root_scale, state);
1166 hash_shadow_device_offset(rect.y, origin_y, root_scale, state);
1167 hash_shadow_device_offset(rect.width, 0.0, root_scale, state);
1168 hash_shadow_device_offset(rect.height, 0.0, root_scale, state);
1169}
1170
1171fn hash_shape_shadow_item<H: Hasher>(
1172 shape: &DrawShape,
1173 brushes: &[Brush],
1174 blend_mode: BlendMode,
1175 origin_x: f32,
1176 origin_y: f32,
1177 root_scale: f32,
1178 state: &mut H,
1179) {
1180 hash_shadow_device_rect(shape.rect, origin_x, origin_y, root_scale, state);
1181 hash_shadow_device_rect(shape.local_rect, origin_x, origin_y, root_scale, state);
1182 for point in shape.quad {
1183 hash_shadow_device_offset(point[0], origin_x, root_scale, state);
1184 hash_shadow_device_offset(point[1], origin_y, root_scale, state);
1185 }
1186 match shape.snap_anchor {
1187 Some(anchor) => {
1188 1u8.hash(state);
1189 hash_shadow_device_offset(anchor.origin.x, origin_x, root_scale, state);
1190 hash_shadow_device_offset(anchor.origin.y, origin_y, root_scale, state);
1191 hash_f32_for_cache(anchor.device_pixel_step, state);
1192 }
1193 None => 0u8.hash(state),
1194 }
1195 shape.brush.render_hash(brushes).hash(state);
1196 match shape.shape {
1197 Some(corner_shape) => {
1198 1u8.hash(state);
1199 corner_shape.radii().render_hash().hash(state);
1200 }
1201 None => 0u8.hash(state),
1202 }
1203 match shape.clip {
1204 Some(clip) => {
1205 1u8.hash(state);
1206 hash_shadow_device_rect(clip, origin_x, origin_y, root_scale, state);
1207 }
1208 None => 0u8.hash(state),
1209 }
1210 blend_mode.hash(state);
1211 shape.blend_mode.hash(state);
1212}
1213
1214fn shape_shadow_content_hash(
1215 shapes: &[(DrawShape, BlendMode)],
1216 brushes: &[Brush],
1217 root_scale: f32,
1218) -> u64 {
1219 let mut hasher = FxHasher::default();
1220 let origin = shape_shadow_bounds(shapes).unwrap_or(Rect {
1225 x: 0.0,
1226 y: 0.0,
1227 width: 0.0,
1228 height: 0.0,
1229 });
1230
1231 shapes.len().hash(&mut hasher);
1232 for (shape, blend_mode) in shapes {
1233 hash_shape_shadow_item(
1234 shape,
1235 brushes,
1236 *blend_mode,
1237 origin.x,
1238 origin.y,
1239 root_scale,
1240 &mut hasher,
1241 );
1242 }
1243 hasher.finish()
1244}
1245
1246fn shape_shadow_surface_cache_key(
1247 shapes: &[(DrawShape, BlendMode)],
1248 brushes: &[Brush],
1249 device_bounds: DevicePixelBounds,
1250 pixel_radius: f32,
1251 root_scale: f32,
1252) -> Option<ShadowSurfaceCacheKey> {
1253 (root_scale.is_finite() && root_scale > 0.0).then(|| ShadowSurfaceCacheKey {
1254 content_hash: shape_shadow_content_hash(shapes, brushes, root_scale),
1255 pixel_size: [device_bounds.width, device_bounds.height],
1256 root_scale_bits: root_scale.to_bits(),
1257 blur_radius_bits: pixel_radius.to_bits(),
1258 })
1259}
1260
1261fn shape_shadow_bounds(shapes: &[(DrawShape, BlendMode)]) -> Option<Rect> {
1262 shapes
1263 .iter()
1264 .map(|(shape, _)| shape.rect)
1265 .reduce(|a, b| Rect {
1266 x: a.x.min(b.x),
1267 y: a.y.min(b.y),
1268 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1269 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1270 })
1271}
1272
1273fn shared_shape_shadow_snap_anchor(shapes: &[(DrawShape, BlendMode)]) -> Option<SnapAnchor> {
1274 let anchor = shapes.first()?.0.snap_anchor?;
1275 shapes
1276 .iter()
1277 .all(|(shape, _)| shape.snap_anchor == Some(anchor))
1278 .then_some(anchor)
1279}
1280
1281fn shadow_draw_bounds(shadow: &ShadowDraw) -> Option<Rect> {
1282 shadow
1283 .shapes
1284 .iter()
1285 .map(|(shape, _)| shape.rect)
1286 .chain(shadow.texts.iter().map(|text| text.rect))
1287 .reduce(|a, b| Rect {
1288 x: a.x.min(b.x),
1289 y: a.y.min(b.y),
1290 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1291 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1292 })
1293}
1294
1295fn shadow_draw_may_render(
1296 shadow: &ShadowDraw,
1297 width: u32,
1298 height: u32,
1299 root_scale: f32,
1300 max_texture_dim: u32,
1301) -> bool {
1302 if shadow.texts.is_empty() && !shadow.shapes.is_empty() && shadow.blur_radius > 0.0 {
1303 return shape_shadow_surface_plan(
1304 &shadow.shapes,
1305 shadow.clip,
1306 shadow.blur_radius,
1307 width,
1308 height,
1309 root_scale,
1310 max_texture_dim,
1311 )
1312 .is_some();
1313 }
1314
1315 let Some(bounds) = shadow_draw_bounds(shadow) else {
1316 return false;
1317 };
1318 let blur_margin = blur_extent_margin(shadow.blur_radius);
1319 let mut visible_bounds = Rect {
1320 x: bounds.x - blur_margin,
1321 y: bounds.y - blur_margin,
1322 width: bounds.width + blur_margin * 2.0,
1323 height: bounds.height + blur_margin * 2.0,
1324 };
1325 if let Some(clip) = shadow.clip {
1326 let clip_expanded = Rect {
1327 x: clip.x - blur_margin,
1328 y: clip.y - blur_margin,
1329 width: clip.width + blur_margin * 2.0,
1330 height: clip.height + blur_margin * 2.0,
1331 };
1332 let Some(intersection) = visible_bounds.intersect(clip_expanded) else {
1333 return false;
1334 };
1335 visible_bounds = intersection;
1336 }
1337
1338 scissor_rect_for_rect(visible_bounds, root_scale, width, height).is_some()
1339}
1340
1341fn shape_shadow_surface_plan(
1342 shapes: &[(DrawShape, BlendMode)],
1343 clip: Option<Rect>,
1344 blur_radius: f32,
1345 width: u32,
1346 height: u32,
1347 root_scale: f32,
1348 max_texture_dim: u32,
1349) -> Option<ShapeShadowSurfacePlan> {
1350 let shape_bounds = shape_shadow_bounds(shapes)?;
1351 let blur_margin = blur_extent_margin(blur_radius);
1352 let source_blur_bounds = Rect {
1353 x: shape_bounds.x - blur_margin,
1354 y: shape_bounds.y - blur_margin,
1355 width: shape_bounds.width + blur_margin * 2.0,
1356 height: shape_bounds.height + blur_margin * 2.0,
1357 };
1358
1359 let mut visible_blur_bounds = source_blur_bounds;
1360 if let Some(clip) = clip {
1361 let clip_expanded = Rect {
1362 x: clip.x - blur_margin,
1363 y: clip.y - blur_margin,
1364 width: clip.width + blur_margin * 2.0,
1365 height: clip.height + blur_margin * 2.0,
1366 };
1367 visible_blur_bounds = visible_blur_bounds.intersect(clip_expanded)?;
1368 }
1369
1370 let processing_scissor = scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
1371 processing_scissor?;
1372 let visible_device_bounds =
1373 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)?;
1374 let source_device_bounds = translation_stable_anchored_device_pixel_bounds(
1375 source_blur_bounds,
1376 shared_shape_shadow_snap_anchor(shapes),
1377 root_scale,
1378 max_texture_dim,
1379 )
1380 .unwrap_or(visible_device_bounds);
1381
1382 Some(ShapeShadowSurfacePlan {
1383 source_device_bounds,
1384 processing_scissor,
1385 pixel_radius: blur_radius * root_scale,
1386 })
1387}
1388
1389fn is_render_effect_supported(effect: &RenderEffect) -> bool {
1390 match effect {
1391 RenderEffect::Blur { .. } => true,
1392 RenderEffect::Offset { .. } => true,
1393 RenderEffect::Shader { .. } => true,
1394 RenderEffect::Chain { first, second } => {
1395 is_render_effect_supported(first) && is_render_effect_supported(second)
1396 }
1397 }
1398}
1399
1400fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
1401 if value.is_finite() {
1402 origin + value
1403 } else if value.is_sign_positive() {
1404 origin + extent
1405 } else {
1406 origin
1407 }
1408}
1409
1410fn gradient_tile_mode_value(tile_mode: TileMode) -> u32 {
1411 match tile_mode {
1412 TileMode::Clamp => 0,
1413 TileMode::Repeated => 1,
1414 TileMode::Mirror => 2,
1415 TileMode::Decal => 3,
1416 }
1417}
1418
1419fn shape_shader_base(solid_trim: bool) -> Cow<'static, str> {
1427 if solid_trim {
1428 return Cow::Owned(format!(
1429 "{}\n{}",
1430 shaders::SHADER,
1431 shaders::SOLID_TRIM_APPENDIX
1432 ));
1433 }
1434 Cow::Borrowed(shaders::SHADER)
1435}
1436
1437#[cfg(not(target_arch = "wasm32"))]
1438fn shape_shader_source(batch_limits: ShapeBatchLimits, solid_trim: bool) -> Cow<'static, str> {
1439 let base = shape_shader_base(solid_trim);
1440 if batch_limits.storage {
1444 return Cow::Owned(
1445 base.replace(
1446 "var<uniform> shape_data: array<ShapeData, 102>;",
1447 "var<storage, read> shape_data: array<ShapeData>;",
1448 )
1449 .replace(
1450 "var<uniform> gradient_stops: array<GradientStop, 256>;",
1451 "var<storage, read> gradient_stops: array<GradientStop>;\n\n\
1458 @group(1) @binding(3)\n\
1459 var<storage, read> paint: array<vec4<f32>>;",
1460 )
1461 .replace(
1462 "output.color = shape.color;",
1463 "output.color = \
1464 select(shape.color, paint[shape_idx], similarity.paint_select > 0.5);",
1465 ),
1466 );
1467 }
1468 Cow::Owned(
1469 base.replace(
1470 "array<ShapeData, 102>",
1471 &format!("array<ShapeData, {}>", batch_limits.max_shapes_per_batch),
1472 )
1473 .replace(
1474 "array<GradientStop, 256>",
1475 &format!("array<GradientStop, {}>", batch_limits.max_gradient_stops),
1476 ),
1477 )
1478}
1479
1480#[cfg(target_arch = "wasm32")]
1481fn shape_shader_source(_batch_limits: ShapeBatchLimits, solid_trim: bool) -> Cow<'static, str> {
1482 shape_shader_base(solid_trim)
1486}
1487
1488pub(crate) fn create_render_pipeline_logged<'a>(
1493 device: &wgpu::Device,
1494 cache: Option<&'a wgpu::PipelineCache>,
1495 tag: &str,
1496 mut descriptor: wgpu::RenderPipelineDescriptor<'a>,
1497) -> wgpu::RenderPipeline {
1498 descriptor.cache = cache;
1499 let started = Instant::now();
1500 let pipeline = device.create_render_pipeline(&descriptor);
1501 log::info!(
1502 "[pipeline-create] {tag} {:.1}ms",
1503 instant_ms(started, Instant::now())
1504 );
1505 pipeline
1506}
1507
1508#[cfg(not(target_arch = "wasm32"))]
1511fn pipeline_prewarm_enabled() -> bool {
1512 std::env::var("CRANPOSE_PIPELINE_PREWARM").as_deref() != Ok("0")
1513}
1514
1515#[cfg(not(target_arch = "wasm32"))]
1516struct PipelinePrewarmInputs {
1517 device: Arc<wgpu::Device>,
1518 cache: Option<wgpu::PipelineCache>,
1519 surface_format: wgpu::TextureFormat,
1520 uniform_layout: wgpu::BindGroupLayout,
1521 shape_layout: wgpu::BindGroupLayout,
1522 image_layout: wgpu::BindGroupLayout,
1523 batch_limits: ShapeBatchLimits,
1524 instanced: bool,
1525}
1526
1527#[cfg(not(target_arch = "wasm32"))]
1538fn spawn_pipeline_prewarm(inputs: PipelinePrewarmInputs) {
1539 if !pipeline_prewarm_enabled() {
1540 return;
1541 }
1542 let spawned = std::thread::Builder::new()
1543 .name("cranpose-pl-warm".into())
1544 .spawn(move || {
1545 let started = Instant::now();
1546 let cache = inputs.cache.as_ref();
1547 let device = &inputs.device;
1548 let solid_trim = solid_trim_varyings_enabled();
1549 let mut built = 0_u32;
1550 if inputs.instanced {
1551 let (vertex_entry, fragment_entry) = if solid_trim {
1552 ("vs_solid_instanced", "fs_solid_trim")
1553 } else {
1554 ("vs_shape_instanced", "fs_solid")
1555 };
1556 drop(create_instanced_shape_pipeline(
1557 device,
1558 cache,
1559 inputs.surface_format,
1560 &inputs.uniform_layout,
1561 &inputs.shape_layout,
1562 BlendMode::SrcOver,
1563 inputs.batch_limits,
1564 solid_trim,
1565 vertex_entry,
1566 fragment_entry,
1567 false,
1568 ));
1569 drop(create_instanced_shape_pipeline(
1570 device,
1571 cache,
1572 inputs.surface_format,
1573 &inputs.uniform_layout,
1574 &inputs.shape_layout,
1575 BlendMode::SrcOver,
1576 inputs.batch_limits,
1577 false,
1578 "vs_shape_instanced",
1579 "fs_main",
1580 false,
1581 ));
1582 } else {
1583 let (vertex_entry, fragment_entry) = if solid_trim {
1584 ("vs_solid", "fs_solid_trim")
1585 } else {
1586 ("vs_main", "fs_solid")
1587 };
1588 drop(create_shape_pipeline(
1589 device,
1590 cache,
1591 inputs.surface_format,
1592 &inputs.uniform_layout,
1593 &inputs.shape_layout,
1594 BlendMode::SrcOver,
1595 inputs.batch_limits,
1596 solid_trim,
1597 vertex_entry,
1598 fragment_entry,
1599 false,
1600 ));
1601 drop(create_shape_pipeline(
1602 device,
1603 cache,
1604 inputs.surface_format,
1605 &inputs.uniform_layout,
1606 &inputs.shape_layout,
1607 BlendMode::SrcOver,
1608 inputs.batch_limits,
1609 false,
1610 "vs_main",
1611 "fs_main",
1612 false,
1613 ));
1614 }
1615 built += 2;
1616 if inputs.batch_limits.storage {
1617 drop(create_mesh_shape_pipeline(
1618 device,
1619 cache,
1620 inputs.surface_format,
1621 &inputs.uniform_layout,
1622 &inputs.shape_layout,
1623 inputs.batch_limits,
1624 false,
1625 ));
1626 built += 1;
1627 }
1628 drop(create_glyph_atlas_pipeline(
1629 device,
1630 cache,
1631 inputs.surface_format,
1632 &inputs.uniform_layout,
1633 &inputs.image_layout,
1634 false,
1635 ));
1636 built += 1;
1637 log::info!(
1638 "[pipeline-prewarm] {built} pipelines in {:.1} ms",
1639 instant_ms(started, Instant::now())
1640 );
1641 });
1642 if let Err(error) = spawned {
1643 log::warn!("[pipeline-prewarm] thread failed to spawn: {error}");
1644 }
1645}
1646
1647#[allow(clippy::too_many_arguments)]
1648fn create_shape_pipeline(
1649 device: &wgpu::Device,
1650 cache: Option<&wgpu::PipelineCache>,
1651 surface_format: wgpu::TextureFormat,
1652 uniform_layout: &wgpu::BindGroupLayout,
1653 shape_layout: &wgpu::BindGroupLayout,
1654 blend_mode: BlendMode,
1655 batch_limits: ShapeBatchLimits,
1656 solid_trim: bool,
1657 vertex_entry: &'static str,
1658 fragment_entry: &'static str,
1659 depth: bool,
1660) -> wgpu::RenderPipeline {
1661 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1662 label: Some("Shape Shader"),
1663 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1664 shape_shader_source(batch_limits, solid_trim),
1665 depth,
1666 )),
1667 });
1668
1669 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1670 label: Some("Render Pipeline Layout"),
1671 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1672 immediate_size: 0,
1673 });
1674
1675 create_render_pipeline_logged(
1676 device,
1677 cache,
1678 &format!("shape entry={fragment_entry} blend={blend_mode:?} depth={depth}"),
1679 wgpu::RenderPipelineDescriptor {
1680 label: Some("Render Pipeline"),
1681 layout: Some(&pipeline_layout),
1682 vertex: wgpu::VertexState {
1683 module: &shader,
1684 entry_point: Some(vertex_entry),
1685 compilation_options: wgpu::PipelineCompilationOptions::default(),
1686 buffers: &[],
1689 },
1690 fragment: Some(wgpu::FragmentState {
1691 module: &shader,
1692 entry_point: Some(fragment_entry),
1693 compilation_options: wgpu::PipelineCompilationOptions::default(),
1694 targets: &[Some(wgpu::ColorTargetState {
1695 format: surface_format,
1696 blend: Some(blend_state_for_mode(blend_mode)),
1697 write_mask: wgpu::ColorWrites::ALL,
1698 })],
1699 }),
1700 primitive: wgpu::PrimitiveState {
1701 topology: wgpu::PrimitiveTopology::TriangleList,
1702 strip_index_format: None,
1703 front_face: wgpu::FrontFace::Ccw,
1704 cull_mode: None,
1705 unclipped_depth: false,
1706 polygon_mode: wgpu::PolygonMode::Fill,
1707 conservative: false,
1708 },
1709 depth_stencil: display_clip::content_depth_state(depth),
1710 multisample: wgpu::MultisampleState::default(),
1711 multiview_mask: None,
1712 cache: None,
1713 },
1714 )
1715}
1716
1717#[cfg(not(target_arch = "wasm32"))]
1724fn create_mesh_shape_pipeline(
1725 device: &wgpu::Device,
1726 cache: Option<&wgpu::PipelineCache>,
1727 surface_format: wgpu::TextureFormat,
1728 uniform_layout: &wgpu::BindGroupLayout,
1729 shape_layout: &wgpu::BindGroupLayout,
1730 batch_limits: ShapeBatchLimits,
1731 depth: bool,
1732) -> wgpu::RenderPipeline {
1733 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1734 label: Some("Shape Mesh Shader"),
1735 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1738 shape_shader_source(batch_limits, false),
1739 depth,
1740 )),
1741 });
1742
1743 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1744 label: Some("Mesh Render Pipeline Layout"),
1745 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1746 immediate_size: 0,
1747 });
1748
1749 create_render_pipeline_logged(
1750 device,
1751 cache,
1752 &format!("mesh depth={depth}"),
1753 wgpu::RenderPipelineDescriptor {
1754 label: Some("Retained Mesh Pipeline"),
1755 layout: Some(&pipeline_layout),
1756 vertex: wgpu::VertexState {
1757 module: &shader,
1758 entry_point: Some("vs_mesh"),
1759 compilation_options: wgpu::PipelineCompilationOptions::default(),
1760 buffers: &[MeshVertex::desc()],
1761 },
1762 fragment: Some(wgpu::FragmentState {
1763 module: &shader,
1764 entry_point: Some("fs_main"),
1765 compilation_options: wgpu::PipelineCompilationOptions::default(),
1766 targets: &[Some(wgpu::ColorTargetState {
1767 format: surface_format,
1768 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1769 write_mask: wgpu::ColorWrites::ALL,
1770 })],
1771 }),
1772 primitive: wgpu::PrimitiveState {
1773 topology: wgpu::PrimitiveTopology::TriangleList,
1774 strip_index_format: None,
1775 front_face: wgpu::FrontFace::Ccw,
1776 cull_mode: None,
1777 unclipped_depth: false,
1778 polygon_mode: wgpu::PolygonMode::Fill,
1779 conservative: false,
1780 },
1781 depth_stencil: display_clip::content_depth_state(depth),
1782 multisample: wgpu::MultisampleState::default(),
1783 multiview_mask: None,
1784 cache: None,
1785 },
1786 )
1787}
1788
1789#[cfg(not(target_arch = "wasm32"))]
1797#[allow(clippy::too_many_arguments)]
1798fn create_instanced_shape_pipeline(
1799 device: &wgpu::Device,
1800 cache: Option<&wgpu::PipelineCache>,
1801 surface_format: wgpu::TextureFormat,
1802 uniform_layout: &wgpu::BindGroupLayout,
1803 shape_layout: &wgpu::BindGroupLayout,
1804 blend_mode: BlendMode,
1805 batch_limits: ShapeBatchLimits,
1806 solid_trim: bool,
1807 vertex_entry: &'static str,
1808 fragment_entry: &'static str,
1809 depth: bool,
1810) -> wgpu::RenderPipeline {
1811 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1812 label: Some("Shape Instanced Shader"),
1813 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1814 shape_shader_source(batch_limits, solid_trim),
1815 depth,
1816 )),
1817 });
1818
1819 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1820 label: Some("Instanced Render Pipeline Layout"),
1821 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1822 immediate_size: 0,
1823 });
1824
1825 create_render_pipeline_logged(
1826 device,
1827 cache,
1828 &format!("instanced entry={fragment_entry} blend={blend_mode:?} depth={depth}"),
1829 wgpu::RenderPipelineDescriptor {
1830 label: Some("Instanced Render Pipeline"),
1831 layout: Some(&pipeline_layout),
1832 vertex: wgpu::VertexState {
1833 module: &shader,
1834 entry_point: Some(vertex_entry),
1835 compilation_options: wgpu::PipelineCompilationOptions::default(),
1836 buffers: &[],
1840 },
1841 fragment: Some(wgpu::FragmentState {
1842 module: &shader,
1843 entry_point: Some(fragment_entry),
1844 compilation_options: wgpu::PipelineCompilationOptions::default(),
1845 targets: &[Some(wgpu::ColorTargetState {
1846 format: surface_format,
1847 blend: Some(blend_state_for_mode(blend_mode)),
1848 write_mask: wgpu::ColorWrites::ALL,
1849 })],
1850 }),
1851 primitive: wgpu::PrimitiveState {
1852 topology: wgpu::PrimitiveTopology::TriangleList,
1853 strip_index_format: None,
1854 front_face: wgpu::FrontFace::Ccw,
1855 cull_mode: None,
1856 unclipped_depth: false,
1857 polygon_mode: wgpu::PolygonMode::Fill,
1858 conservative: false,
1859 },
1860 depth_stencil: display_clip::content_depth_state(depth),
1861 multisample: wgpu::MultisampleState::default(),
1862 multiview_mask: None,
1863 cache: None,
1864 },
1865 )
1866}
1867
1868fn create_image_pipeline(
1869 device: &wgpu::Device,
1870 cache: Option<&wgpu::PipelineCache>,
1871 surface_format: wgpu::TextureFormat,
1872 uniform_layout: &wgpu::BindGroupLayout,
1873 image_layout: &wgpu::BindGroupLayout,
1874 blend_mode: BlendMode,
1875 depth: bool,
1876) -> wgpu::RenderPipeline {
1877 let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1878 label: Some("Image Shader"),
1879 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1880 shaders::IMAGE_SHADER.into(),
1881 depth,
1882 )),
1883 });
1884
1885 let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1886 label: Some("Image Pipeline Layout"),
1887 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1888 immediate_size: 0,
1889 });
1890
1891 create_render_pipeline_logged(
1892 device,
1893 cache,
1894 &format!("image blend={blend_mode:?} depth={depth}"),
1895 wgpu::RenderPipelineDescriptor {
1896 label: Some("Image Pipeline"),
1897 layout: Some(&image_pipeline_layout),
1898 vertex: wgpu::VertexState {
1899 module: &image_shader,
1900 entry_point: Some("image_vs_main"),
1901 compilation_options: wgpu::PipelineCompilationOptions::default(),
1902 buffers: &[Vertex::desc()],
1903 },
1904 fragment: Some(wgpu::FragmentState {
1905 module: &image_shader,
1906 entry_point: Some("image_fs_main"),
1907 compilation_options: wgpu::PipelineCompilationOptions::default(),
1908 targets: &[Some(wgpu::ColorTargetState {
1909 format: surface_format,
1910 blend: Some(blend_state_for_mode(blend_mode)),
1911 write_mask: wgpu::ColorWrites::ALL,
1912 })],
1913 }),
1914 primitive: wgpu::PrimitiveState {
1915 topology: wgpu::PrimitiveTopology::TriangleList,
1916 strip_index_format: None,
1917 front_face: wgpu::FrontFace::Ccw,
1918 cull_mode: None,
1919 unclipped_depth: false,
1920 polygon_mode: wgpu::PolygonMode::Fill,
1921 conservative: false,
1922 },
1923 depth_stencil: display_clip::content_depth_state(depth),
1924 multisample: wgpu::MultisampleState::default(),
1925 multiview_mask: None,
1926 cache: None,
1927 },
1928 )
1929}
1930
1931fn create_glyph_atlas_pipeline(
1932 device: &wgpu::Device,
1933 cache: Option<&wgpu::PipelineCache>,
1934 surface_format: wgpu::TextureFormat,
1935 uniform_layout: &wgpu::BindGroupLayout,
1936 image_layout: &wgpu::BindGroupLayout,
1937 depth: bool,
1938) -> wgpu::RenderPipeline {
1939 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1940 label: Some("Glyph Atlas Shader"),
1941 source: wgpu::ShaderSource::Wgsl(display_clip::with_content_z(
1942 shaders::GLYPH_ATLAS_SHADER.into(),
1943 depth,
1944 )),
1945 });
1946
1947 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1948 label: Some("Glyph Atlas Pipeline Layout"),
1949 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1950 immediate_size: 0,
1951 });
1952
1953 create_render_pipeline_logged(
1954 device,
1955 cache,
1956 &format!("glyph-atlas depth={depth}"),
1957 wgpu::RenderPipelineDescriptor {
1958 label: Some("Glyph Atlas Pipeline"),
1959 layout: Some(&pipeline_layout),
1960 vertex: wgpu::VertexState {
1961 module: &shader,
1962 entry_point: Some("glyph_atlas_vs_main"),
1963 compilation_options: wgpu::PipelineCompilationOptions::default(),
1964 buffers: &[Vertex::desc()],
1965 },
1966 fragment: Some(wgpu::FragmentState {
1967 module: &shader,
1968 entry_point: Some("glyph_atlas_fs_main"),
1969 compilation_options: wgpu::PipelineCompilationOptions::default(),
1970 targets: &[Some(wgpu::ColorTargetState {
1971 format: surface_format,
1972 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1973 write_mask: wgpu::ColorWrites::ALL,
1974 })],
1975 }),
1976 primitive: wgpu::PrimitiveState {
1977 topology: wgpu::PrimitiveTopology::TriangleList,
1978 strip_index_format: None,
1979 front_face: wgpu::FrontFace::Ccw,
1980 cull_mode: None,
1981 unclipped_depth: false,
1982 polygon_mode: wgpu::PolygonMode::Fill,
1983 conservative: false,
1984 },
1985 depth_stencil: display_clip::content_depth_state(depth),
1986 multisample: wgpu::MultisampleState::default(),
1987 multiview_mask: None,
1988 cache: None,
1989 },
1990 )
1991}
1992
1993#[cfg(not(target_arch = "wasm32"))]
2001fn create_display_clip_occluder_pipeline(
2002 device: &wgpu::Device,
2003 cache: Option<&wgpu::PipelineCache>,
2004 surface_format: wgpu::TextureFormat,
2005) -> wgpu::RenderPipeline {
2006 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2007 label: Some("Display Clip Occluder Shader"),
2008 source: wgpu::ShaderSource::Wgsl(display_clip::OCCLUDER_SHADER.into()),
2009 });
2010 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2011 label: Some("Display Clip Occluder Pipeline Layout"),
2012 bind_group_layouts: &[],
2013 immediate_size: 0,
2014 });
2015 create_render_pipeline_logged(
2016 device,
2017 cache,
2018 "occluder",
2019 wgpu::RenderPipelineDescriptor {
2020 label: Some("Display Clip Occluder Pipeline"),
2021 layout: Some(&pipeline_layout),
2022 vertex: wgpu::VertexState {
2023 module: &shader,
2024 entry_point: Some("mask_vs"),
2025 compilation_options: wgpu::PipelineCompilationOptions::default(),
2026 buffers: &[wgpu::VertexBufferLayout {
2027 array_stride: (std::mem::size_of::<[f32; 2]>()) as wgpu::BufferAddress,
2028 step_mode: wgpu::VertexStepMode::Vertex,
2029 attributes: &[wgpu::VertexAttribute {
2030 offset: 0,
2031 shader_location: 0,
2032 format: wgpu::VertexFormat::Float32x2,
2033 }],
2034 }],
2035 },
2036 fragment: Some(wgpu::FragmentState {
2037 module: &shader,
2038 entry_point: Some("mask_fs"),
2039 compilation_options: wgpu::PipelineCompilationOptions::default(),
2040 targets: &[Some(wgpu::ColorTargetState {
2041 format: surface_format,
2042 blend: None,
2043 write_mask: wgpu::ColorWrites::empty(),
2044 })],
2045 }),
2046 primitive: wgpu::PrimitiveState {
2047 topology: wgpu::PrimitiveTopology::TriangleList,
2048 strip_index_format: None,
2049 front_face: wgpu::FrontFace::Ccw,
2050 cull_mode: None,
2051 unclipped_depth: false,
2052 polygon_mode: wgpu::PolygonMode::Fill,
2053 conservative: false,
2054 },
2055 depth_stencil: Some(wgpu::DepthStencilState {
2056 format: display_clip::DISPLAY_CLIP_DEPTH_FORMAT,
2057 depth_write_enabled: Some(true),
2058 depth_compare: Some(wgpu::CompareFunction::Always),
2059 stencil: wgpu::StencilState::default(),
2060 bias: wgpu::DepthBiasState::default(),
2061 }),
2062 multisample: wgpu::MultisampleState::default(),
2063 multiview_mask: None,
2064 cache: None,
2065 },
2066 )
2067}
2068
2069#[repr(C)]
2070#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2071struct Vertex {
2072 position: [f32; 2],
2073 color: [f32; 4],
2074 uv: [f32; 2],
2075 uv_bounds: [f32; 4],
2076}
2077
2078impl Vertex {
2079 const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
2080 0 => Float32x2,
2081 1 => Float32x4,
2082 2 => Float32x2,
2083 3 => Float32x4
2084 ];
2085
2086 fn desc() -> wgpu::VertexBufferLayout<'static> {
2087 wgpu::VertexBufferLayout {
2088 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
2089 step_mode: wgpu::VertexStepMode::Vertex,
2090 attributes: &Self::ATTRIBS,
2091 }
2092 }
2093}
2094
2095#[repr(C)]
2096#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2097struct Uniforms {
2098 viewport: [f32; 2],
2099 viewport_offset: [f32; 2],
2100}
2101
2102#[repr(C)]
2108#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2109struct ShapeData {
2110 rect: [f32; 4], radii: [f32; 4],
2115 gradient_params: [f32; 4], clip_rect: [f32; 4], stroke_params: [f32; 4],
2120 arc_params: [f32; 4],
2122 quad01: [f32; 4],
2124 quad23: [f32; 4],
2126 color: [f32; 4],
2128 brush_type: u32, gradient_start: u32, gradient_count: u32, gradient_tile_mode: u32, }
2133
2134const SHAPE_KIND_FILL: u32 = 0;
2136const SHAPE_KIND_STROKE: u32 = 1;
2137const SHAPE_KIND_ARC: u32 = 2;
2138
2139fn stroke_cap_code(cap: StrokeCap) -> u32 {
2140 match cap {
2141 StrokeCap::Butt => 0,
2142 StrokeCap::Round => 1,
2143 StrokeCap::Square => 2,
2144 }
2145}
2146
2147fn stroke_join_code(join: StrokeJoin) -> u32 {
2148 match join {
2149 StrokeJoin::Miter => 0,
2150 StrokeJoin::Round => 1,
2151 StrokeJoin::Bevel => 2,
2152 }
2153}
2154
2155fn pack_shape_flags(kind: u32, cap: StrokeCap, join: StrokeJoin) -> f32 {
2161 ((kind & 3) | (stroke_cap_code(cap) << 2) | (stroke_join_code(join) << 4)) as f32
2162}
2163
2164#[cfg(not(target_arch = "wasm32"))]
2172static SHAPE_CONVERT_TUNER: crate::cost_tuner::CostTuner =
2173 crate::cost_tuner::CostTuner::new("shape-convert", 256, 400_000);
2174
2175#[cfg(not(target_arch = "wasm32"))]
2176pub(crate) fn shape_convert_worker_count() -> usize {
2177 static WORKERS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
2178 *WORKERS.get_or_init(|| {
2179 let cpus = std::thread::available_parallelism()
2180 .map(|count| count.get())
2181 .unwrap_or(1);
2182 let workers = cpus.clamp(1, 4);
2183 log::info!("[shape-convert] fan-out width {workers} (available parallelism {cpus})");
2187 workers
2188 })
2189}
2190
2191#[cfg(target_arch = "wasm32")]
2192pub(crate) fn shape_convert_worker_count() -> usize {
2193 1
2194}
2195
2196fn shape_gradient_stop_count(shape: &DrawShape, brushes: &[Brush]) -> usize {
2197 match shape.brush {
2198 SceneBrush::Solid(_) => 0,
2199 SceneBrush::Gradient(index) => match &brushes[index as usize] {
2200 Brush::Solid(_) => 0,
2201 Brush::LinearGradient { colors, .. }
2202 | Brush::RadialGradient { colors, .. }
2203 | Brush::SweepGradient { colors, .. } => colors.len(),
2204 },
2205 }
2206}
2207
2208fn convert_shape_into_slots(
2213 shape: &DrawShape,
2214 brushes: &[Brush],
2215 root_scale: f32,
2216 gradient_start: u32,
2217 shape_out: &mut ShapeData,
2218 gradient_out: &mut [GradientStop],
2219) {
2220 let snap_delta = shape
2221 .snap_anchor
2222 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
2223 .unwrap_or_default();
2224 let local_rect = shape.local_rect.translate(snap_delta.x, snap_delta.y);
2225 let quad = translate_quad(shape.quad, snap_delta);
2226 let clip = shape.clip;
2229 let canonicalize = shape.snap_anchor.is_some();
2230 let device_local_rect = if canonicalize {
2231 canonicalized_scaled_rect(local_rect, root_scale)
2232 } else {
2233 Rect {
2234 x: local_rect.x * root_scale,
2235 y: local_rect.y * root_scale,
2236 width: local_rect.width * root_scale,
2237 height: local_rect.height * root_scale,
2238 }
2239 };
2240 let device_quad = if canonicalize {
2241 canonicalized_scaled_quad(quad, root_scale)
2242 } else {
2243 scaled_quad(quad, root_scale)
2244 };
2245 let canonicalize_brush_coordinate = |value| {
2246 if canonicalize {
2247 canonicalize_device_coordinate(value)
2248 } else {
2249 value
2250 }
2251 };
2252
2253 let clip_rect = if let Some(clip) = clip {
2255 let device_clip = if canonicalize {
2256 canonicalized_scaled_rect(clip, root_scale)
2257 } else {
2258 Rect {
2259 x: clip.x * root_scale,
2260 y: clip.y * root_scale,
2261 width: clip.width * root_scale,
2262 height: clip.height * root_scale,
2263 }
2264 };
2265 [
2266 device_clip.x,
2267 device_clip.y,
2268 device_clip.width,
2269 device_clip.height,
2270 ]
2271 } else {
2272 [0.0, 0.0, 0.0, 0.0]
2273 };
2274
2275 let mut fill_gradient_entries = |colors: &[Color], stops: Option<&[f32]>| {
2277 let count = colors.len();
2278 let explicit_stops = stops.filter(|values| values.len() == count);
2279 for (index, color) in colors.iter().enumerate() {
2280 let position = explicit_stops
2281 .map(|values| values[index])
2282 .unwrap_or_else(|| {
2283 if count <= 1 {
2284 0.0
2285 } else {
2286 index as f32 / (count - 1) as f32
2287 }
2288 });
2289 gradient_out[index] = GradientStop {
2290 color: [color.r(), color.g(), color.b(), color.a()],
2291 position: [position, 0.0, 0.0, 0.0],
2292 };
2293 }
2294 count as u32
2295 };
2296 let mut gradient_params = [0.0f32; 4];
2297 let (brush_type, gradient_count, gradient_tile_mode) = match &shape.brush {
2298 SceneBrush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
2299 SceneBrush::Gradient(index) => match &brushes[*index as usize] {
2300 Brush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
2301 Brush::LinearGradient {
2302 colors,
2303 stops,
2304 start,
2305 end,
2306 tile_mode,
2307 } => {
2308 let count = fill_gradient_entries(colors, stops.as_deref());
2309 gradient_params = [
2310 canonicalize_brush_coordinate(resolve_gradient_point(
2311 device_local_rect.x,
2312 device_local_rect.width,
2313 start.x * root_scale,
2314 )),
2315 canonicalize_brush_coordinate(resolve_gradient_point(
2316 device_local_rect.y,
2317 device_local_rect.height,
2318 start.y * root_scale,
2319 )),
2320 canonicalize_brush_coordinate(resolve_gradient_point(
2321 device_local_rect.x,
2322 device_local_rect.width,
2323 end.x * root_scale,
2324 )),
2325 canonicalize_brush_coordinate(resolve_gradient_point(
2326 device_local_rect.y,
2327 device_local_rect.height,
2328 end.y * root_scale,
2329 )),
2330 ];
2331 (1u32, count, gradient_tile_mode_value(*tile_mode))
2332 }
2333 Brush::RadialGradient {
2334 colors,
2335 stops,
2336 center,
2337 radius,
2338 tile_mode,
2339 } => {
2340 let count = fill_gradient_entries(colors, stops.as_deref());
2341 gradient_params = [
2342 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
2343 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
2344 (radius * root_scale).max(f32::EPSILON),
2345 0.0,
2346 ];
2347 (2u32, count, gradient_tile_mode_value(*tile_mode))
2348 }
2349 Brush::SweepGradient {
2350 colors,
2351 stops,
2352 center,
2353 } => {
2354 let count = fill_gradient_entries(colors, stops.as_deref());
2355 gradient_params = [
2356 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
2357 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
2358 0.0,
2359 0.0,
2360 ];
2361 (3u32, count, gradient_tile_mode_value(TileMode::Clamp))
2362 }
2363 },
2364 };
2365
2366 let stroke_outset = shape
2371 .stroke
2372 .map(|stroke| stroke.half_width())
2373 .unwrap_or(0.0);
2374 let geometry_width = (local_rect.width - stroke_outset * 2.0).max(0.0);
2375 let geometry_height = (local_rect.height - stroke_outset * 2.0).max(0.0);
2376
2377 let radii = if let Some(arc) = shape.arc {
2378 if arc.sweep_angle >= cranpose_ui_graphics::TAU && arc.start_angle == 0.0 {
2388 [0.0, -1.0, 0.0, -1.0]
2389 } else {
2390 let half_sweep = arc.sweep_angle.clamp(0.0, cranpose_ui_graphics::TAU) * 0.5;
2391 let (mid_sin, mid_cos) = (arc.start_angle + half_sweep).sin_cos();
2392 let (half_sin, half_cos) = half_sweep.sin_cos();
2393 [mid_sin, mid_cos, half_sin.max(0.0), half_cos]
2394 }
2395 } else if let Some(rounded) = shape.shape {
2396 let resolved = rounded.resolve(geometry_width, geometry_height);
2397 [
2398 resolved.top_left * root_scale,
2399 resolved.top_right * root_scale,
2400 resolved.bottom_left * root_scale,
2401 resolved.bottom_right * root_scale,
2402 ]
2403 } else {
2404 [0.0, 0.0, 0.0, 0.0]
2405 };
2406
2407 let device_rect = [
2408 device_local_rect.x,
2409 device_local_rect.y,
2410 device_local_rect.width,
2411 device_local_rect.height,
2412 ];
2413
2414 let (stroke_params, arc_params) = match (shape.arc, shape.stroke) {
2418 (Some(arc), _) => (
2419 [
2420 0.0,
2421 pack_shape_flags(SHAPE_KIND_ARC, arc.cap, StrokeJoin::Miter),
2422 arc.outer_radius * root_scale,
2423 arc.inner_radius * root_scale,
2424 ],
2425 [
2426 (arc.center.x + snap_delta.x) * root_scale,
2427 (arc.center.y + snap_delta.y) * root_scale,
2428 arc.start_angle,
2429 arc.sweep_angle,
2430 ],
2431 ),
2432 (None, Some(stroke)) => (
2433 [
2434 stroke.width.max(0.0) * root_scale,
2435 pack_shape_flags(SHAPE_KIND_STROKE, stroke.cap, stroke.join),
2436 0.0,
2437 0.0,
2438 ],
2439 [0.0; 4],
2440 ),
2441 (None, None) => (
2442 [
2443 0.0,
2444 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
2445 0.0,
2446 0.0,
2447 ],
2448 [0.0; 4],
2449 ),
2450 };
2451
2452 let color = match &shape.brush {
2453 SceneBrush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2454 SceneBrush::Gradient(index) => match &brushes[*index as usize] {
2455 Brush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2456 Brush::LinearGradient { colors, .. } => {
2457 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2458 [first.r(), first.g(), first.b(), first.a()]
2459 }
2460 Brush::RadialGradient { colors, .. } | Brush::SweepGradient { colors, .. } => {
2461 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2462 [first.r(), first.g(), first.b(), first.a()]
2463 }
2464 },
2465 };
2466
2467 *shape_out = ShapeData {
2468 rect: device_rect,
2469 radii,
2470 gradient_params,
2471 clip_rect,
2472 stroke_params,
2473 arc_params,
2474 quad01: [
2475 device_quad[0][0],
2476 device_quad[0][1],
2477 device_quad[1][0],
2478 device_quad[1][1],
2479 ],
2480 quad23: [
2481 device_quad[2][0],
2482 device_quad[2][1],
2483 device_quad[3][0],
2484 device_quad[3][1],
2485 ],
2486 color,
2487 brush_type,
2488 gradient_start,
2489 gradient_count,
2490 gradient_tile_mode,
2491 };
2492}
2493
2494fn quad_area_diag_enabled() -> bool {
2501 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2502 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_QUAD_AREA_DIAG").is_some())
2503}
2504
2505fn convert_shapes_into_outputs(
2511 shape_refs: &[&DrawShape],
2512 brushes: &[Brush],
2513 gradient_offsets: &[u32],
2514 root_scale: f32,
2515 shape_data_out: &mut [ShapeData],
2516 gradients_out: &mut [GradientStop],
2517) {
2518 let shape_count = shape_refs.len();
2519 #[cfg(not(target_arch = "wasm32"))]
2520 let convert_started = Instant::now();
2521 #[cfg(not(target_arch = "wasm32"))]
2522 let parallel =
2523 SHAPE_CONVERT_TUNER.choose_parallel(shape_count) && shape_convert_worker_count() > 1;
2524 if quad_area_diag_enabled() {
2525 let quad_area = |q: [[f32; 2]; 4]| {
2526 let poly = [q[0], q[1], q[3], q[2]];
2528 let mut twice = 0.0f64;
2529 for i in 0..4 {
2530 let a = poly[i];
2531 let b = poly[(i + 1) % 4];
2532 twice += a[0] as f64 * b[1] as f64 - b[0] as f64 * a[1] as f64;
2533 }
2534 twice.abs() * 0.5
2535 };
2536 let mut arc_quad = 0.0f64; let mut arc_band = 0.0f64; let mut arc_count = 0usize;
2539 let mut ring_count = 0usize;
2540 let mut other_quad = 0.0f64;
2541 let mut other_count = 0usize;
2542 let mut top_other: Vec<(f64, usize)> = Vec::new();
2545 for (index, shape) in shape_refs.iter().enumerate() {
2546 let area = quad_area(shape.quad);
2547 if let Some(arc) = shape.arc {
2548 arc_quad += area;
2549 arc_count += 1;
2550 if arc.sweep_angle >= cranpose_ui_graphics::TAU {
2551 ring_count += 1;
2552 }
2553 let ra = arc.mid_radius() as f64;
2554 let rb = arc.half_thickness() as f64;
2555 arc_band +=
2556 arc.sweep_angle as f64 * ra * (2.0 * rb) + std::f64::consts::PI * rb * rb;
2557 } else {
2558 other_quad += area;
2559 other_count += 1;
2560 top_other.push((area, index));
2561 }
2562 }
2563 let scale2 = (root_scale as f64) * (root_scale as f64);
2564 eprintln!(
2565 "[quad-area] arcs={arc_count} (rings={ring_count}) arc_quad_px={:.0} arc_band_px={:.0} | other={other_count} other_px={:.0}",
2566 arc_quad * scale2,
2567 arc_band * scale2,
2568 other_quad * scale2,
2569 );
2570 top_other.sort_by(|a, b| b.0.total_cmp(&a.0));
2571 for &(area, index) in top_other.iter().take(4) {
2572 let shape = shape_refs[index];
2573 let brush = match shape.brush.resolve(brushes).as_ref() {
2574 cranpose_ui_graphics::Brush::Solid(color) => format!("solid a={:.2}", color.3),
2575 cranpose_ui_graphics::Brush::LinearGradient { colors, .. } => {
2576 format!("linear n={}", colors.len())
2577 }
2578 cranpose_ui_graphics::Brush::RadialGradient { colors, .. } => {
2579 format!("radial n={}", colors.len())
2580 }
2581 cranpose_ui_graphics::Brush::SweepGradient { colors, .. } => {
2582 format!("sweep n={}", colors.len())
2583 }
2584 };
2585 eprintln!(
2586 "[quad-area] top other: {:.0}px {}x{} at ({:.0},{:.0}) {} shape={} stroke={} clip={} blend={:?} z={}",
2587 area * scale2,
2588 shape.rect.width.round(),
2589 shape.rect.height.round(),
2590 shape.rect.x,
2591 shape.rect.y,
2592 brush,
2593 shape.shape.is_some(),
2594 shape.stroke.is_some(),
2595 shape.clip.is_some(),
2596 shape.blend_mode,
2597 shape.z_index,
2598 );
2599 }
2600 }
2601 #[cfg(target_arch = "wasm32")]
2602 let parallel = false;
2603 let workers = if parallel {
2604 shape_convert_worker_count()
2605 } else {
2606 1
2607 };
2608 if workers <= 1 {
2609 for (idx, shape) in shape_refs.iter().enumerate() {
2610 let gradient_start = gradient_offsets[idx];
2611 let gradient_end = gradient_offsets[idx + 1];
2612 convert_shape_into_slots(
2613 shape,
2614 brushes,
2615 root_scale,
2616 gradient_start,
2617 &mut shape_data_out[idx],
2618 &mut gradients_out[gradient_start as usize..gradient_end as usize],
2619 );
2620 }
2621 #[cfg(not(target_arch = "wasm32"))]
2622 SHAPE_CONVERT_TUNER.record(
2623 false,
2624 shape_count,
2625 convert_started.elapsed().as_nanos() as u64,
2626 );
2627 return;
2628 }
2629
2630 let chunk_len = shape_count.div_ceil(workers);
2631 let mut shape_data_rest = shape_data_out;
2632 let mut gradients_rest = gradients_out;
2633 std::thread::scope(|scope| {
2634 let mut chunk_start = 0usize;
2635 while chunk_start < shape_count {
2636 let chunk_end = (chunk_start + chunk_len).min(shape_count);
2637 let count = chunk_end - chunk_start;
2638 let gradient_base = gradient_offsets[chunk_start];
2639 let gradient_span = (gradient_offsets[chunk_end] - gradient_base) as usize;
2640 let (shape_data_chunk, rest) = std::mem::take(&mut shape_data_rest).split_at_mut(count);
2641 shape_data_rest = rest;
2642 let (gradient_chunk, rest) =
2643 std::mem::take(&mut gradients_rest).split_at_mut(gradient_span);
2644 gradients_rest = rest;
2645 let chunk_refs = &shape_refs[chunk_start..chunk_end];
2646 let chunk_offsets = &gradient_offsets[chunk_start..=chunk_end];
2647 let mut convert_chunk = move || {
2648 for (j, shape) in chunk_refs.iter().enumerate() {
2649 let gradient_start = chunk_offsets[j];
2650 let local_start = (gradient_start - gradient_base) as usize;
2651 let local_end = (chunk_offsets[j + 1] - gradient_base) as usize;
2652 convert_shape_into_slots(
2653 shape,
2654 brushes,
2655 root_scale,
2656 gradient_start,
2657 &mut shape_data_chunk[j],
2658 &mut gradient_chunk[local_start..local_end],
2659 );
2660 }
2661 };
2662 if chunk_end == shape_count {
2663 convert_chunk();
2667 } else {
2668 scope.spawn(convert_chunk);
2669 }
2670 chunk_start = chunk_end;
2671 }
2672 });
2673 #[cfg(not(target_arch = "wasm32"))]
2674 SHAPE_CONVERT_TUNER.record(
2675 true,
2676 shape_count,
2677 convert_started.elapsed().as_nanos() as u64,
2678 );
2679}
2680
2681#[repr(C)]
2682#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2683struct GradientStop {
2684 color: [f32; 4],
2685 position: [f32; 4],
2686}
2687
2688#[cfg(not(target_arch = "wasm32"))]
2692const MAX_REPLAY_SLOTS: u32 = 128;
2693#[cfg(not(target_arch = "wasm32"))]
2694const REPLAY_TRANSFORM_STRIDE: u64 = 256;
2695
2696#[cfg(not(target_arch = "wasm32"))]
2703struct ReplaySlot {
2704 paint_buffer: wgpu::Buffer,
2709 bind_group: wgpu::BindGroup,
2710 shape_count: u32,
2711 paint_mirror: Vec<[f32; 4]>,
2717 mesh: Option<ReplaySlotMesh>,
2722 capture_epoch: u64,
2727 has_gradient: bool,
2732 fill_diag_shapes: Vec<FillDiagShapeRecord>,
2738 shape_aabbs: Vec<[f32; 4]>,
2744 area_prefix: Vec<f32>,
2748 submitted_area_scale: f32,
2752}
2753
2754#[cfg(not(target_arch = "wasm32"))]
2764struct ReplaySlotMesh {
2765 vertex_buffer: wgpu::Buffer,
2766 index_buffer: wgpu::Buffer,
2772 index_prefix: Vec<u32>,
2778 meshed_arcs: usize,
2783 meshed_rims: usize,
2784 passthrough: usize,
2785}
2786
2787#[cfg(not(target_arch = "wasm32"))]
2791#[repr(C)]
2792#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2793struct MeshVertex {
2794 position: [f32; 2],
2795 uv: [f32; 2],
2796 shape_idx: u32,
2797}
2798
2799#[cfg(not(target_arch = "wasm32"))]
2800impl MeshVertex {
2801 const ATTRIBS: [wgpu::VertexAttribute; 3] =
2802 wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32];
2803
2804 fn desc() -> wgpu::VertexBufferLayout<'static> {
2805 wgpu::VertexBufferLayout {
2806 array_stride: std::mem::size_of::<MeshVertex>() as wgpu::BufferAddress,
2807 step_mode: wgpu::VertexStepMode::Vertex,
2808 attributes: &Self::ATTRIBS,
2809 }
2810 }
2811}
2812
2813#[cfg(not(target_arch = "wasm32"))]
2818fn arc_mesh_enabled() -> bool {
2819 matches!(std::env::var("CRANPOSE_ARC_MESH").as_deref(), Ok("1"))
2835}
2836
2837#[cfg(not(target_arch = "wasm32"))]
2843const ARC_MESH_MARGIN: f32 = 1.0;
2844
2845#[cfg(not(target_arch = "wasm32"))]
2849const ARC_MESH_OVERSHOOT: f32 = 2.0;
2850
2851#[cfg(not(target_arch = "wasm32"))]
2852const ARC_MESH_MIN_SEGMENTS: usize = 4;
2853#[cfg(not(target_arch = "wasm32"))]
2854const ARC_MESH_MAX_SEGMENTS: usize = 64;
2855
2856#[cfg(not(target_arch = "wasm32"))]
2865const ARC_MESH_BUDGET_BYTES_PER_SHAPE: usize = 48 * std::mem::size_of::<MeshVertex>();
2866#[cfg(not(target_arch = "wasm32"))]
2867const ARC_MESH_BUDGET_FLOOR_BYTES: usize = 4096 * std::mem::size_of::<MeshVertex>();
2868
2869#[cfg(not(target_arch = "wasm32"))]
2872fn arc_mesh_bytes(vertices: usize, indices: usize) -> usize {
2873 vertices * std::mem::size_of::<MeshVertex>() + indices * std::mem::size_of::<u32>()
2874}
2875
2876#[cfg(not(target_arch = "wasm32"))]
2887const MESH_SLOT_MAX_STRETCHES: usize = 8;
2888
2889#[cfg(not(target_arch = "wasm32"))]
2912const RETAINED_MESH_MIN_PX2_DEFAULT: usize = 16384;
2913#[cfg(not(target_arch = "wasm32"))]
2918const RETAINED_MESH_MIN_PX2_RANGE: std::ops::RangeInclusive<usize> = 1024..=262144;
2919
2920#[cfg(not(target_arch = "wasm32"))]
2926fn retained_mesh_min_px2() -> f64 {
2927 parse_retained_mesh_min_px2(std::env::var("CRANPOSE_RETAINED_MESH_PX2").ok().as_deref())
2928}
2929
2930#[cfg(not(target_arch = "wasm32"))]
2931fn parse_retained_mesh_min_px2(value: Option<&str>) -> f64 {
2932 value
2933 .and_then(|value| value.trim().parse::<usize>().ok())
2934 .map(|px2| {
2935 px2.clamp(
2936 *RETAINED_MESH_MIN_PX2_RANGE.start(),
2937 *RETAINED_MESH_MIN_PX2_RANGE.end(),
2938 )
2939 })
2940 .unwrap_or(RETAINED_MESH_MIN_PX2_DEFAULT) as f64
2941}
2942
2943#[cfg(not(target_arch = "wasm32"))]
2948struct ArcMeshBand {
2949 center: [f32; 2],
2950 inner: f32,
2951 outer: f32,
2952 start: f32,
2953 sweep: f32,
2954}
2955
2956#[cfg(not(target_arch = "wasm32"))]
2957fn arc_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
2958 let flags = shape.stroke_params[1].max(0.0) as u32;
2960 if flags & 3 != SHAPE_KIND_ARC {
2961 return None;
2962 }
2963 if shape.brush_type != 0 {
2967 return None;
2968 }
2969 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
2974 return None;
2975 }
2976 let [_, _, w, h] = shape.rect;
2977 if !(w > 0.0 && h > 0.0) {
2978 return None;
2979 }
2980 let [left, top, right, _] = shape.quad01;
2988 let [bl_x, bottom, br_x, br_y] = shape.quad23;
2989 let axis_aligned = shape.quad01[3] == top
2990 && bl_x == left
2991 && br_x == right
2992 && br_y == bottom
2993 && left < right
2994 && top < bottom;
2995 if !axis_aligned {
2996 return None;
2997 }
2998 let center = [shape.arc_params[0], shape.arc_params[1]];
2999 let start = shape.arc_params[2];
3000 let sweep = shape.arc_params[3];
3001 let outer = shape.stroke_params[2];
3002 let inner = shape.stroke_params[3];
3003 let finite = center[0].is_finite()
3004 && center[1].is_finite()
3005 && start.is_finite()
3006 && sweep.is_finite()
3007 && outer.is_finite()
3008 && inner.is_finite();
3009 if !finite || outer <= 0.0 || sweep <= 0.0 {
3010 return None;
3011 }
3012 Some(ArcMeshBand {
3013 center,
3014 inner,
3015 outer,
3016 start,
3017 sweep,
3018 })
3019}
3020
3021#[cfg(not(target_arch = "wasm32"))]
3030fn rim_mesh_enabled() -> bool {
3031 !matches!(std::env::var("CRANPOSE_RIM_MESH").as_deref(), Ok("0"))
3032}
3033
3034#[cfg(not(target_arch = "wasm32"))]
3042const RIM_MESH_VERTEX_CAPACITY: usize = 8192;
3043#[cfg(not(target_arch = "wasm32"))]
3046const RIM_MESH_INDEX_CAPACITY: usize = 32768;
3047
3048#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
3054#[derive(Clone, Copy, Debug)]
3055struct RimDraw {
3056 shape_index: u32,
3057 first_index: u32,
3058 index_count: u32,
3059}
3060
3061#[cfg(not(target_arch = "wasm32"))]
3065fn rim_mesh_capacity_warn() {
3066 use std::sync::atomic::{AtomicU64, Ordering};
3067 static OVERFLOWS: AtomicU64 = AtomicU64::new(0);
3068 let count = OVERFLOWS.fetch_add(1, Ordering::Relaxed);
3069 if count.is_multiple_of(512) {
3070 log::warn!(
3071 "[rim-mesh] transient buffers full; rim falls back to quad expansion \
3072 (lifetime overflows {})",
3073 count + 1,
3074 );
3075 }
3076}
3077
3078#[cfg(not(target_arch = "wasm32"))]
3107fn rim_band_geometry(shape: &ShapeData) -> Option<ArcMeshBand> {
3108 let flags = shape.stroke_params[1].max(0.0) as u32;
3110 if flags & 3 != SHAPE_KIND_STROKE {
3111 return None;
3112 }
3113 if shape.brush_type != 0 {
3116 return None;
3117 }
3118 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
3122 return None;
3123 }
3124 let [x, y, w, h] = shape.rect;
3125 if !(w > 0.0 && h > 0.0) {
3126 return None;
3127 }
3128 let [left, top, right, _] = shape.quad01;
3133 let [bl_x, bottom, br_x, br_y] = shape.quad23;
3134 let axis_aligned = shape.quad01[3] == top
3135 && bl_x == left
3136 && br_x == right
3137 && br_y == bottom
3138 && left < right
3139 && top < bottom;
3140 if !axis_aligned {
3141 return None;
3142 }
3143 if w.to_bits() != h.to_bits() {
3145 return None;
3146 }
3147 let [r0, r1, r2, r3] = shape.radii;
3149 if r0.to_bits() != r1.to_bits() || r0.to_bits() != r2.to_bits() || r0.to_bits() != r3.to_bits()
3150 {
3151 return None;
3152 }
3153 if !r0.is_finite() || r0 <= 0.0 {
3154 return None;
3155 }
3156 let sw = shape.stroke_params[0];
3157 if !sw.is_finite() || sw <= 0.0 {
3158 return None;
3159 }
3160 let geom_half = (w - sw) * 0.5;
3163 let center = [x + w * 0.5, y + h * 0.5];
3164 let inner = geom_half - sw * 0.5;
3165 let outer = geom_half + sw * 0.5;
3166 let finite =
3167 center[0].is_finite() && center[1].is_finite() && inner.is_finite() && outer.is_finite();
3168 if !finite || outer <= 0.0 {
3169 return None;
3170 }
3171 if (r0 - geom_half).abs() > 0.01 {
3173 return None;
3174 }
3175 Some(ArcMeshBand {
3176 center,
3177 inner,
3178 outer,
3179 start: 0.0,
3180 sweep: cranpose_ui_graphics::TAU,
3181 })
3182}
3183
3184#[cfg(not(target_arch = "wasm32"))]
3188fn rim_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
3189 let [_, _, w, h] = shape.rect;
3190 if w * h < 65536.0 {
3191 return None;
3192 }
3193 rim_band_geometry(shape)
3194}
3195
3196#[cfg(not(target_arch = "wasm32"))]
3203fn static_span_enabled() -> bool {
3204 !matches!(std::env::var("CRANPOSE_STATIC_SPAN").as_deref(), Ok("0"))
3205}
3206
3207#[cfg(not(target_arch = "wasm32"))]
3211const STATIC_SPAN_MAX_SHAPES: usize = 16;
3212
3213#[cfg(not(target_arch = "wasm32"))]
3217const STATIC_SPAN_UPGRADE_FRAMES: u32 = 30;
3218
3219#[cfg(not(target_arch = "wasm32"))]
3222#[derive(Clone, Copy, Debug, PartialEq)]
3223enum StaticSpanDecision {
3224 Pass,
3226 Hit { skip: usize },
3229 Capture { len: usize, clear: wgpu::Color },
3232}
3233
3234#[cfg(not(target_arch = "wasm32"))]
3282#[derive(Default)]
3283struct StaticSpanCache {
3284 texture: Option<OffscreenTarget>,
3288 key_shapes: Vec<ShapeData>,
3290 key_gradients: Vec<GradientStop>,
3292 key_width: u32,
3293 key_height: u32,
3294 key_clear: [u64; 4],
3298 key_has_gradient: bool,
3303 prev_shapes: Vec<ShapeData>,
3306 prev_gradients: Vec<GradientStop>,
3307 extension_stable_frames: u32,
3310 armed: bool,
3315 hits: u64,
3316 recaptures: u64,
3317}
3318
3319#[cfg(not(target_arch = "wasm32"))]
3320impl StaticSpanCache {
3321 fn engage(
3327 &mut self,
3328 load_op: wgpu::LoadOp<wgpu::Color>,
3329 first_batch: Option<(usize, BlendMode, bool)>,
3330 width: u32,
3331 height: u32,
3332 shapes: &[ShapeData],
3333 gradients: &[GradientStop],
3334 ) -> StaticSpanDecision {
3335 if !self.armed || !static_span_enabled() {
3336 return StaticSpanDecision::Pass;
3337 }
3338 let wgpu::LoadOp::Clear(clear) = load_op else {
3339 return StaticSpanDecision::Pass;
3340 };
3341 if clear.a != 1.0 {
3345 return StaticSpanDecision::Pass;
3346 }
3347 self.armed = false;
3348 let Some((batch_len, blend_mode, has_gradient)) = first_batch else {
3349 self.forget_observation();
3350 return StaticSpanDecision::Pass;
3351 };
3352 if blend_mode != BlendMode::SrcOver || batch_len == 0 {
3355 self.forget_observation();
3356 return StaticSpanDecision::Pass;
3357 }
3358 let leading = &shapes[..batch_len.min(STATIC_SPAN_MAX_SHAPES).min(shapes.len())];
3359 if leading.is_empty() {
3360 self.forget_observation();
3361 return StaticSpanDecision::Pass;
3362 }
3363 if !static_span_fullscreen_opaque(&leading[0], width, height) {
3364 self.forget_observation();
3365 return StaticSpanDecision::Pass;
3366 }
3367 let mut eligible = 1;
3370 while eligible < leading.len() && rim_mesh_band(&leading[eligible]).is_none() {
3371 eligible += 1;
3372 }
3373 let leading = &leading[..eligible];
3374 let clear_key = [
3375 clear.r.to_bits(),
3376 clear.g.to_bits(),
3377 clear.b.to_bits(),
3378 clear.a.to_bits(),
3379 ];
3380
3381 let key_len = self.key_shapes.len();
3382 let valid = self.texture.is_some()
3383 && key_len > 0
3384 && key_len <= leading.len()
3385 && self.key_width == width
3386 && self.key_height == height
3387 && self.key_clear == clear_key
3388 && self.key_has_gradient == has_gradient
3389 && span_records_equal(
3390 &self.key_shapes,
3391 &leading[..key_len],
3392 &self.key_gradients,
3393 gradients,
3394 );
3395
3396 let mut stable = 0;
3400 while stable < leading.len()
3401 && stable < self.prev_shapes.len()
3402 && span_records_equal(
3403 &self.prev_shapes[stable..stable + 1],
3404 &leading[stable..stable + 1],
3405 &self.prev_gradients,
3406 gradients,
3407 )
3408 {
3409 stable += 1;
3410 }
3411 self.remember_observation(leading, gradients);
3412
3413 if valid {
3414 if stable > key_len {
3426 self.extension_stable_frames += 1;
3427 if self.extension_stable_frames >= STATIC_SPAN_UPGRADE_FRAMES {
3428 self.extension_stable_frames = 0;
3429 return StaticSpanDecision::Capture { len: stable, clear };
3430 }
3431 } else {
3432 self.extension_stable_frames = 0;
3433 }
3434 self.hits += 1;
3435 if self.hits.is_multiple_of(600) {
3436 log::debug!(
3437 "[static-span] {} hits / {} recaptures lifetime (span {} shapes, {}x{})",
3438 self.hits,
3439 self.recaptures,
3440 key_len,
3441 width,
3442 height,
3443 );
3444 }
3445 return StaticSpanDecision::Hit { skip: key_len };
3446 }
3447
3448 self.extension_stable_frames = 0;
3449 if stable == 0 || span_gradient_len(&leading[..stable]) == 0 {
3458 return StaticSpanDecision::Pass;
3459 }
3460 StaticSpanDecision::Capture { len: stable, clear }
3461 }
3462
3463 fn remember_observation(&mut self, leading: &[ShapeData], gradients: &[GradientStop]) {
3465 self.prev_shapes.clear();
3466 self.prev_shapes.extend_from_slice(leading);
3467 let stop_len = span_gradient_len(leading);
3468 self.prev_gradients.clear();
3469 self.prev_gradients
3470 .extend_from_slice(&gradients[..stop_len]);
3471 }
3472
3473 fn forget_observation(&mut self) {
3474 self.prev_shapes.clear();
3475 self.prev_gradients.clear();
3476 self.extension_stable_frames = 0;
3477 }
3478
3479 #[allow(clippy::too_many_arguments)]
3482 fn store_key(
3483 &mut self,
3484 span: &[ShapeData],
3485 gradients: &[GradientStop],
3486 width: u32,
3487 height: u32,
3488 clear: wgpu::Color,
3489 has_gradient: bool,
3490 ) {
3491 self.key_shapes.clear();
3492 self.key_shapes.extend_from_slice(span);
3493 let stop_len = span_gradient_len(span);
3494 self.key_gradients.clear();
3495 self.key_gradients.extend_from_slice(&gradients[..stop_len]);
3496 self.key_width = width;
3497 self.key_height = height;
3498 self.key_clear = [
3499 clear.r.to_bits(),
3500 clear.g.to_bits(),
3501 clear.b.to_bits(),
3502 clear.a.to_bits(),
3503 ];
3504 self.key_has_gradient = has_gradient;
3505 self.recaptures += 1;
3506 if self.recaptures.is_multiple_of(64) || self.recaptures == 1 {
3507 log::debug!(
3508 "[static-span] recapture #{} (span {} shapes, {} stops, {}x{}; {} hits lifetime)",
3509 self.recaptures,
3510 self.key_shapes.len(),
3511 self.key_gradients.len(),
3512 width,
3513 height,
3514 self.hits,
3515 );
3516 }
3517 }
3518}
3519
3520#[cfg(not(target_arch = "wasm32"))]
3524fn span_gradient_len(span: &[ShapeData]) -> usize {
3525 span.iter().map(|shape| shape.gradient_count as usize).sum()
3526}
3527
3528#[cfg(not(target_arch = "wasm32"))]
3535fn span_records_equal(
3536 expected: &[ShapeData],
3537 actual: &[ShapeData],
3538 expected_gradients: &[GradientStop],
3539 actual_gradients: &[GradientStop],
3540) -> bool {
3541 if bytemuck::cast_slice::<ShapeData, u8>(expected)
3542 != bytemuck::cast_slice::<ShapeData, u8>(actual)
3543 {
3544 return false;
3545 }
3546 for shape in expected {
3547 let start = shape.gradient_start as usize;
3548 let end = start + shape.gradient_count as usize;
3549 if end > expected_gradients.len() || end > actual_gradients.len() {
3550 return false;
3551 }
3552 if bytemuck::cast_slice::<GradientStop, u8>(&expected_gradients[start..end])
3553 != bytemuck::cast_slice::<GradientStop, u8>(&actual_gradients[start..end])
3554 {
3555 return false;
3556 }
3557 }
3558 true
3559}
3560
3561#[cfg(not(target_arch = "wasm32"))]
3570fn static_span_fullscreen_opaque(shape: &ShapeData, width: u32, height: u32) -> bool {
3571 if shape.brush_type != 0 || shape.gradient_count != 0 {
3572 return false;
3573 }
3574 if shape.color[3] != 1.0 {
3575 return false;
3576 }
3577 if shape.clip_rect != [0.0; 4] || shape.stroke_params != [0.0; 4] || shape.radii != [0.0; 4] {
3578 return false;
3579 }
3580 let [left, top, right, top_right_y] = shape.quad01;
3583 let [bl_x, bottom, br_x, br_y] = shape.quad23;
3584 let axis_aligned = top_right_y == top
3585 && bl_x == left
3586 && br_x == right
3587 && br_y == bottom
3588 && left < right
3589 && top < bottom;
3590 axis_aligned && left <= 0.0 && top <= 0.0 && right >= width as f32 && bottom >= height as f32
3591}
3592
3593#[cfg(not(target_arch = "wasm32"))]
3604fn clip_polygon_axis(
3605 input: &[[f32; 2]],
3606 axis: usize,
3607 bound: f32,
3608 keep_at_most: bool,
3609 output: &mut Vec<[f32; 2]>,
3610) {
3611 output.clear();
3612 let inside = |p: [f32; 2]| {
3613 if keep_at_most {
3614 p[axis] <= bound
3615 } else {
3616 p[axis] >= bound
3617 }
3618 };
3619 let intersect = |a: [f32; 2], b: [f32; 2]| {
3620 let (p, q) = if (b[0], b[1]) < (a[0], a[1]) {
3621 (b, a)
3622 } else {
3623 (a, b)
3624 };
3625 let t = (bound - p[axis]) / (q[axis] - p[axis]);
3626 let mut point = [0.0f32; 2];
3627 point[axis] = bound;
3628 point[1 - axis] = p[1 - axis] + t * (q[1 - axis] - p[1 - axis]);
3629 point
3630 };
3631 for (index, ¤t) in input.iter().enumerate() {
3632 let previous = input[(index + input.len() - 1) % input.len()];
3633 match (inside(previous), inside(current)) {
3634 (true, true) => output.push(current),
3635 (true, false) => output.push(intersect(previous, current)),
3636 (false, true) => {
3637 output.push(intersect(previous, current));
3638 output.push(current);
3639 }
3640 (false, false) => {}
3641 }
3642 }
3643}
3644
3645#[cfg(not(target_arch = "wasm32"))]
3680fn emit_arc_band_mesh(
3681 shape: &ShapeData,
3682 shape_idx: u32,
3683 band: &ArcMeshBand,
3684 vertices: &mut Vec<MeshVertex>,
3685 indices: &mut Vec<u32>,
3686) -> Option<usize> {
3687 let [cx, cy] = band.center;
3688 let ra = (band.outer + band.inner) * 0.5;
3689 let rb = ((band.outer - band.inner) * 0.5).max(0.0);
3690 let rb_m = rb + ARC_MESH_MARGIN;
3691 let ro = ra + rb_m;
3692 let ri = (ra - rb_m).max(0.0);
3693 let tau = cranpose_ui_graphics::TAU;
3694
3695 let (range_start, range) = if band.sweep >= tau {
3696 (0.0, tau)
3697 } else {
3698 let pad = if rb_m < ra {
3699 (rb_m / ra).asin() + 0.05
3700 } else {
3701 std::f32::consts::PI
3704 };
3705 let padded = band.sweep + pad + pad;
3706 if padded >= tau {
3707 (0.0, tau)
3708 } else {
3709 (band.start - pad, padded)
3710 }
3711 };
3712 let closed = range >= tau;
3713
3714 let dtheta = (2.0 * (ro / (ro + ARC_MESH_OVERSHOOT)).acos()).clamp(tau / 64.0, tau / 6.0);
3715 let segments =
3716 ((range / dtheta).ceil() as usize).clamp(ARC_MESH_MIN_SEGMENTS, ARC_MESH_MAX_SEGMENTS);
3717 let step = range / segments as f32;
3718 let rc = ro / (step * 0.5).cos();
3719
3720 let boundary_count = if closed { segments } else { segments + 1 };
3724 let mut boundaries = Vec::with_capacity(boundary_count);
3725 for j in 0..boundary_count {
3726 let (sin, cos) = (range_start + step * j as f32).sin_cos();
3727 boundaries.push((
3728 [cx + cos * ri, cy + sin * ri],
3729 [cx + cos * rc, cy + sin * rc],
3730 ));
3731 }
3732
3733 let quad_min = [shape.quad01[0], shape.quad01[1]];
3734 let quad_max = [shape.quad23[2], shape.quad23[3]];
3735
3736 enum SegmentGeometry {
3741 Shared,
3742 Fan(Vec<[f32; 2]>),
3743 Empty,
3744 }
3745
3746 let mut polygon: Vec<[f32; 2]> = Vec::with_capacity(8);
3748 let mut scratch: Vec<[f32; 2]> = Vec::with_capacity(8);
3749 let mut segment_geometry = Vec::with_capacity(segments);
3750 let mut boundary_used = vec![false; boundary_count];
3751 for j in 0..segments {
3752 let jb = (j + 1) % boundary_count;
3753 let (inner_a, outer_a) = boundaries[j];
3754 let (inner_b, outer_b) = boundaries[jb];
3755 polygon.clear();
3756 polygon.extend_from_slice(&[inner_a, outer_a, outer_b, inner_b]);
3757 clip_polygon_axis(&polygon, 0, quad_min[0], false, &mut scratch);
3758 clip_polygon_axis(&scratch, 0, quad_max[0], true, &mut polygon);
3759 clip_polygon_axis(&polygon, 1, quad_min[1], false, &mut scratch);
3760 clip_polygon_axis(&scratch, 1, quad_max[1], true, &mut polygon);
3761 scratch.clear();
3764 for &point in polygon.iter() {
3765 if scratch.last() != Some(&point) {
3766 scratch.push(point);
3767 }
3768 }
3769 while scratch.len() > 1 && scratch.first() == scratch.last() {
3770 scratch.pop();
3771 }
3772 if scratch.len() < 3 {
3773 segment_geometry.push(SegmentGeometry::Empty);
3774 } else if scratch[..] == [inner_a, outer_a, outer_b, inner_b] {
3775 boundary_used[j] = true;
3776 boundary_used[jb] = true;
3777 segment_geometry.push(SegmentGeometry::Shared);
3778 } else {
3779 segment_geometry.push(SegmentGeometry::Fan(scratch.clone()));
3780 }
3781 }
3782
3783 let push_vertex = |vertices: &mut Vec<MeshVertex>, position: [f32; 2]| -> u32 {
3784 let index = vertices.len() as u32;
3785 vertices.push(MeshVertex {
3786 position,
3787 uv: [
3788 (position[0] - shape.rect[0]) / shape.rect[2],
3789 (position[1] - shape.rect[1]) / shape.rect[3],
3790 ],
3791 shape_idx,
3792 });
3793 index
3794 };
3795
3796 let mut boundary_vertex = vec![[0u32; 2]; boundary_count];
3799 for (j, used) in boundary_used.iter().enumerate() {
3800 if *used {
3801 let (inner, outer) = boundaries[j];
3802 boundary_vertex[j] = [push_vertex(vertices, inner), push_vertex(vertices, outer)];
3803 }
3804 }
3805
3806 let start_len = indices.len();
3809 for (j, geometry) in segment_geometry.iter().enumerate() {
3810 match geometry {
3811 SegmentGeometry::Empty => {}
3812 SegmentGeometry::Shared => {
3813 let jb = (j + 1) % boundary_count;
3814 let [in_a, out_a] = boundary_vertex[j];
3815 let [in_b, out_b] = boundary_vertex[jb];
3816 indices.extend_from_slice(&[in_a, out_a, out_b, in_a, out_b, in_b]);
3820 }
3821 SegmentGeometry::Fan(points) => {
3822 let base = vertices.len() as u32;
3823 for &point in points {
3824 push_vertex(vertices, point);
3825 }
3826 for i in 1..points.len() as u32 - 1 {
3827 indices.extend_from_slice(&[base, base + i, base + i + 1]);
3828 }
3829 }
3830 }
3831 }
3832 if indices.len() == start_len {
3833 return None;
3834 }
3835 Some(segments)
3836}
3837
3838#[cfg(not(target_arch = "wasm32"))]
3841fn triangles_shoelace_area(vertices: &[MeshVertex], indices: &[u32]) -> f64 {
3842 indices
3843 .as_chunks::<3>()
3844 .0
3845 .iter()
3846 .map(|tri| {
3847 let [a, b, c] = [
3848 vertices[tri[0] as usize].position,
3849 vertices[tri[1] as usize].position,
3850 vertices[tri[2] as usize].position,
3851 ];
3852 let cross = (b[0] as f64 - a[0] as f64) * (c[1] as f64 - a[1] as f64)
3853 - (b[1] as f64 - a[1] as f64) * (c[0] as f64 - a[0] as f64);
3854 cross.abs() * 0.5
3855 })
3856 .sum()
3857}
3858
3859#[cfg(not(target_arch = "wasm32"))]
3862fn quad_shoelace_area(shape: &ShapeData) -> f64 {
3863 let corners = [
3864 [shape.quad01[0] as f64, shape.quad01[1] as f64],
3865 [shape.quad01[2] as f64, shape.quad01[3] as f64],
3866 [shape.quad23[0] as f64, shape.quad23[1] as f64],
3867 [shape.quad23[2] as f64, shape.quad23[3] as f64],
3868 ];
3869 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
3870 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
3871 };
3872 tri(corners[0], corners[1], corners[2]) + tri(corners[2], corners[1], corners[3])
3873}
3874
3875#[cfg(not(target_arch = "wasm32"))]
3880pub(crate) fn fill_area_diag_enabled() -> bool {
3881 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3882 *ENABLED.get_or_init(
3883 || matches!(std::env::var("CRANPOSE_FILL_DIAG").as_deref(), Ok(value) if value != "0"),
3884 )
3885}
3886
3887#[cfg(not(target_arch = "wasm32"))]
3889const FILL_DIAG_WINDOW_FRAMES: u32 = 120;
3890
3891#[cfg(not(target_arch = "wasm32"))]
3892const FILL_DIAG_BUCKETS: usize = 9;
3893
3894#[cfg(not(target_arch = "wasm32"))]
3901#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3902enum FillOpacityClass {
3903 Opaque = 0,
3904 Translucent = 1,
3905 NonSolid = 2,
3906}
3907
3908#[cfg(not(target_arch = "wasm32"))]
3909fn fill_opacity_class(shape: &ShapeData) -> FillOpacityClass {
3910 if shape.brush_type != 0 {
3911 FillOpacityClass::NonSolid
3912 } else if shape.color[3] == 1.0 {
3913 FillOpacityClass::Opaque
3914 } else {
3915 FillOpacityClass::Translucent
3916 }
3917}
3918
3919#[cfg(not(target_arch = "wasm32"))]
3924fn fill_diag_bucket(shape: &ShapeData) -> usize {
3925 match shape.stroke_params[1].max(0.0) as u32 & 3 {
3926 SHAPE_KIND_ARC => FillAreaDiag::ARC,
3927 SHAPE_KIND_STROKE => FillAreaDiag::RRECT_STROKE,
3928 _ if shape.radii.iter().any(|radius| *radius > 0.0) => FillAreaDiag::RRECT_FILL,
3929 _ => FillAreaDiag::RECT,
3930 }
3931}
3932
3933#[cfg(not(target_arch = "wasm32"))]
3934fn fill_diag_bucket_name(bucket: usize) -> &'static str {
3935 match bucket {
3936 FillAreaDiag::ARC => "arc",
3937 FillAreaDiag::RRECT_STROKE => "rrect-stroke",
3938 FillAreaDiag::RRECT_FILL => "rrect-fill",
3939 FillAreaDiag::RECT => "rect",
3940 FillAreaDiag::MESH => "mesh",
3941 FillAreaDiag::RETAINED => "retained",
3942 FillAreaDiag::IMAGE_GLYPH => "img+glyph",
3943 FillAreaDiag::EFFECT_COMPOSITE => "effect-comp",
3944 FillAreaDiag::OFFSCREEN_SOURCE => "offscr-src",
3945 _ => "?",
3946 }
3947}
3948
3949#[cfg(not(target_arch = "wasm32"))]
3970fn analytic_covered_area(shape: &ShapeData) -> f64 {
3971 let flags = shape.stroke_params[1].max(0.0) as u32;
3972 match flags & 3 {
3973 SHAPE_KIND_ARC => {
3974 let outer = f64::from(shape.stroke_params[2]).max(0.0);
3975 let inner = f64::from(shape.stroke_params[3]).clamp(0.0, outer);
3976 let tau = f64::from(cranpose_ui_graphics::TAU);
3977 let sweep = f64::from(shape.arc_params[3]).clamp(0.0, tau);
3978 let thickness = outer - inner;
3979 let band = sweep * 0.5 * (outer + inner) * thickness;
3980 let caps = if sweep >= tau {
3981 0.0
3982 } else {
3983 match (flags >> 2) & 3 {
3984 1 | 2 => std::f64::consts::PI * (thickness * 0.5) * (thickness * 0.5),
3988 _ => 0.0,
3989 }
3990 };
3991 band + caps
3992 }
3993 SHAPE_KIND_STROKE => {
3994 let stroke_width = f64::from(shape.stroke_params[0]).max(0.0);
3995 let geom_w = (f64::from(shape.rect[2]) - stroke_width).max(0.0);
3997 let geom_h = (f64::from(shape.rect[3]) - stroke_width).max(0.0);
3998 let max_radius = geom_w.min(geom_h) * 0.5;
3999 let radii_sum: f64 = shape
4000 .radii
4001 .iter()
4002 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
4003 .sum();
4004 let perimeter =
4005 2.0 * (geom_w + geom_h) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum;
4006 perimeter.max(0.0) * stroke_width
4007 }
4008 _ => {
4009 let width = f64::from(shape.rect[2]).max(0.0);
4010 let height = f64::from(shape.rect[3]).max(0.0);
4011 let max_radius = width.min(height) * 0.5;
4012 let radii_sq: f64 = shape
4013 .radii
4014 .iter()
4015 .map(|radius| {
4016 let radius = f64::from(*radius).clamp(0.0, max_radius);
4017 radius * radius
4018 })
4019 .sum();
4020 width * height - (1.0 - std::f64::consts::FRAC_PI_4) * radii_sq
4021 }
4022 }
4023}
4024
4025#[cfg(not(target_arch = "wasm32"))]
4031fn aa_perimeter_allowance(shape: &ShapeData) -> f64 {
4032 let flags = shape.stroke_params[1].max(0.0) as u32;
4033 match flags & 3 {
4034 SHAPE_KIND_ARC => {
4035 let outer = f64::from(shape.stroke_params[2]).max(0.0);
4036 let inner = f64::from(shape.stroke_params[3]).clamp(0.0, outer);
4037 let tau = f64::from(cranpose_ui_graphics::TAU);
4038 let sweep = f64::from(shape.arc_params[3]).clamp(0.0, tau);
4039 let ends = if sweep >= tau {
4040 0.0
4041 } else {
4042 2.0 * (outer - inner)
4043 };
4044 sweep * (outer + inner) + ends
4045 }
4046 SHAPE_KIND_STROKE => {
4047 let stroke_width = f64::from(shape.stroke_params[0]).max(0.0);
4048 let geom_w = (f64::from(shape.rect[2]) - stroke_width).max(0.0);
4049 let geom_h = (f64::from(shape.rect[3]) - stroke_width).max(0.0);
4050 let max_radius = geom_w.min(geom_h) * 0.5;
4051 let radii_sum: f64 = shape
4052 .radii
4053 .iter()
4054 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
4055 .sum();
4056 let perimeter =
4057 2.0 * (geom_w + geom_h) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum;
4058 2.0 * perimeter.max(0.0)
4059 }
4060 _ if shape.radii.iter().any(|radius| *radius > 0.0) => {
4061 let width = f64::from(shape.rect[2]).max(0.0);
4062 let height = f64::from(shape.rect[3]).max(0.0);
4063 let max_radius = width.min(height) * 0.5;
4064 let radii_sum: f64 = shape
4065 .radii
4066 .iter()
4067 .map(|radius| f64::from(*radius).clamp(0.0, max_radius))
4068 .sum();
4069 (2.0 * (width + height) - (2.0 - std::f64::consts::FRAC_PI_2) * radii_sum).max(0.0)
4070 }
4071 _ => 0.0,
4072 }
4073}
4074
4075#[cfg(not(target_arch = "wasm32"))]
4079fn analytic_lit_area(shape: &ShapeData) -> f64 {
4080 analytic_covered_area(shape) + aa_perimeter_allowance(shape)
4081}
4082
4083#[cfg(not(target_arch = "wasm32"))]
4085fn quad_aabb(shape: &ShapeData) -> [f64; 4] {
4086 let xs = [
4087 f64::from(shape.quad01[0]),
4088 f64::from(shape.quad01[2]),
4089 f64::from(shape.quad23[0]),
4090 f64::from(shape.quad23[2]),
4091 ];
4092 let ys = [
4093 f64::from(shape.quad01[1]),
4094 f64::from(shape.quad01[3]),
4095 f64::from(shape.quad23[1]),
4096 f64::from(shape.quad23[3]),
4097 ];
4098 let fold = |values: [f64; 4], pick: fn(f64, f64) -> f64| {
4099 values.into_iter().reduce(pick).unwrap_or(0.0)
4100 };
4101 [
4102 fold(xs, f64::min),
4103 fold(ys, f64::min),
4104 fold(xs, f64::max),
4105 fold(ys, f64::max),
4106 ]
4107}
4108
4109#[cfg(not(target_arch = "wasm32"))]
4113const CORNER_FILL_STRIPS: usize = 32;
4114
4115#[cfg(not(target_arch = "wasm32"))]
4124fn area_outside_inscribed_circle(aabb: [f64; 4], viewport: (u32, u32)) -> f64 {
4125 let viewport_w = f64::from(viewport.0);
4126 let viewport_h = f64::from(viewport.1);
4127 if viewport_w <= 0.0 || viewport_h <= 0.0 {
4128 return 0.0;
4129 }
4130 let x0 = aabb[0].max(0.0);
4131 let y0 = aabb[1].max(0.0);
4132 let x1 = aabb[2].min(viewport_w);
4133 let y1 = aabb[3].min(viewport_h);
4134 if x1 <= x0 || y1 <= y0 {
4135 return 0.0;
4136 }
4137 let center_x = viewport_w * 0.5;
4138 let center_y = viewport_h * 0.5;
4139 let radius = viewport_w.min(viewport_h) * 0.5;
4140 let strip = (x1 - x0) / CORNER_FILL_STRIPS as f64;
4141 let mut outside = 0.0;
4142 for index in 0..CORNER_FILL_STRIPS {
4143 let x = x0 + (index as f64 + 0.5) * strip;
4144 let dx = x - center_x;
4145 let chord_sq = radius * radius - dx * dx;
4146 let inside = if chord_sq > 0.0 {
4147 let half_chord = chord_sq.sqrt();
4148 (y1.min(center_y + half_chord) - y0.max(center_y - half_chord)).max(0.0)
4149 } else {
4150 0.0
4151 };
4152 outside += ((y1 - y0) - inside) * strip;
4153 }
4154 outside
4155}
4156
4157#[cfg(not(target_arch = "wasm32"))]
4161#[derive(Clone, Copy, Debug)]
4162struct FillDiagShapeRecord {
4163 drawn_px2: f64,
4167 lit_px2: f64,
4169 bucket: usize,
4171 opacity: FillOpacityClass,
4172 aabb: [f64; 4],
4174}
4175
4176#[cfg(not(target_arch = "wasm32"))]
4180fn fill_diag_capture_records(
4181 shape_data: &[ShapeData],
4182 mesh: Option<(&[MeshVertex], &[u32], &[u32])>,
4183) -> Vec<FillDiagShapeRecord> {
4184 shape_data
4185 .iter()
4186 .enumerate()
4187 .map(|(index, shape)| {
4188 let drawn_px2 = match mesh {
4189 Some((vertices, indices, index_prefix))
4193 if index_prefix[index + 1] > index_prefix[index] =>
4194 {
4195 let start = index_prefix[index] as usize;
4196 let end = index_prefix[index + 1] as usize;
4197 triangles_shoelace_area(vertices, &indices[start..end])
4198 }
4199 _ => quad_shoelace_area(shape),
4200 };
4201 FillDiagShapeRecord {
4202 drawn_px2,
4203 lit_px2: analytic_lit_area(shape).clamp(0.0, drawn_px2),
4204 bucket: fill_diag_bucket(shape),
4205 opacity: fill_opacity_class(shape),
4206 aabb: quad_aabb(shape),
4207 }
4208 })
4209 .collect()
4210}
4211
4212#[cfg(not(target_arch = "wasm32"))]
4215#[derive(Clone, Copy, Debug)]
4216struct FillDiagSlackEntry {
4217 slot: u32,
4218 shape: u32,
4219 bucket: usize,
4220 drawn_px2: f64,
4221 lit_px2: f64,
4222}
4223
4224#[cfg(not(target_arch = "wasm32"))]
4225const FILL_DIAG_SLACK_TOP: usize = 10;
4226
4227#[cfg(not(target_arch = "wasm32"))]
4272#[derive(Default)]
4273struct FillAreaDiag {
4274 frame: [std::cell::Cell<f64>; FILL_DIAG_BUCKETS],
4278 frame_lit: [std::cell::Cell<f64>; FILL_DIAG_BUCKETS],
4280 frame_opacity: [std::cell::Cell<f64>; 3],
4282 frame_corner: std::cell::Cell<f64>,
4284 viewport: std::cell::Cell<(u32, u32)>,
4287 window: [f64; FILL_DIAG_BUCKETS],
4289 window_lit: [f64; FILL_DIAG_BUCKETS],
4290 window_opacity: [f64; 3],
4291 window_corner: f64,
4292 window_frames: u32,
4293 slack_top: Vec<FillDiagSlackEntry>,
4296 slack_dumped: bool,
4297}
4298
4299#[cfg(not(target_arch = "wasm32"))]
4300impl FillAreaDiag {
4301 const ARC: usize = 0;
4302 const RRECT_STROKE: usize = 1;
4303 const RRECT_FILL: usize = 2;
4304 const RECT: usize = 3;
4305 const MESH: usize = 4;
4306 const RETAINED: usize = 5;
4307 const IMAGE_GLYPH: usize = 6;
4308 const EFFECT_COMPOSITE: usize = 7;
4309 const OFFSCREEN_SOURCE: usize = 8;
4310
4311 fn add(&self, bucket: usize, area_px2: f64) {
4312 let cell = &self.frame[bucket];
4313 cell.set(cell.get() + area_px2);
4314 }
4315
4316 fn add_lit(&self, bucket: usize, lit_px2: f64) {
4317 let cell = &self.frame_lit[bucket];
4318 cell.set(cell.get() + lit_px2);
4319 }
4320
4321 fn add_corner(&self, px2: f64) {
4322 self.frame_corner.set(self.frame_corner.get() + px2);
4323 }
4324
4325 fn is_full_frame(&self, viewport: ViewportUniformParams) -> bool {
4329 let (width, height) = self.viewport.get();
4330 width > 0
4331 && height > 0
4332 && viewport.width == width
4333 && viewport.height == height
4334 && viewport.offset == [0.0, 0.0]
4335 }
4336
4337 fn add_shape_quads(&self, shapes: &[ShapeData], viewport: ViewportUniformParams) {
4342 let full_frame = self.is_full_frame(viewport);
4343 let frame_viewport = self.viewport.get();
4344 let mut buckets = [0.0_f64; FILL_DIAG_BUCKETS];
4345 let mut lit_buckets = [0.0_f64; FILL_DIAG_BUCKETS];
4346 let mut opacity = [0.0_f64; 3];
4347 let mut corner = 0.0_f64;
4348 for shape in shapes {
4349 let bucket = fill_diag_bucket(shape);
4350 let quad = quad_shoelace_area(shape);
4351 let lit = analytic_lit_area(shape).clamp(0.0, quad);
4352 buckets[bucket] += quad;
4353 lit_buckets[bucket] += lit;
4354 opacity[fill_opacity_class(shape) as usize] += lit;
4355 if full_frame {
4356 corner += area_outside_inscribed_circle(quad_aabb(shape), frame_viewport);
4357 }
4358 }
4359 for (bucket, area) in buckets.into_iter().enumerate() {
4360 if area > 0.0 {
4361 self.add(bucket, area);
4362 }
4363 }
4364 for (bucket, lit) in lit_buckets.into_iter().enumerate() {
4365 if lit > 0.0 {
4366 self.add_lit(bucket, lit);
4367 }
4368 }
4369 for (class, lit) in self.frame_opacity.iter().zip(opacity) {
4370 class.set(class.get() + lit);
4371 }
4372 if corner > 0.0 {
4373 self.add_corner(corner);
4374 }
4375 }
4376
4377 fn note_static_span_skip(&self, shapes: &[ShapeData]) {
4386 for shape in shapes {
4387 let bucket = fill_diag_bucket(shape);
4388 let quad = quad_shoelace_area(shape);
4389 let lit = analytic_lit_area(shape).clamp(0.0, quad);
4390 self.add(bucket, -quad);
4391 self.add_lit(bucket, -lit);
4392 let class = &self.frame_opacity[fill_opacity_class(shape) as usize];
4393 class.set(class.get() - lit);
4394 }
4395 }
4396
4397 fn note_rim_mesh(&self, shape: &ShapeData, mesh_px2: f64) {
4404 let quad = quad_shoelace_area(shape);
4405 let lit = analytic_lit_area(shape).clamp(0.0, quad);
4406 self.add(Self::RRECT_STROKE, -quad);
4407 self.add_lit(Self::RRECT_STROKE, -lit);
4408 self.add(Self::MESH, mesh_px2);
4409 self.add_lit(Self::MESH, lit.min(mesh_px2));
4410 }
4411
4412 fn add_retained_range(
4418 &self,
4419 records: &[FillDiagShapeRecord],
4420 first: u32,
4421 last: u32,
4422 transform: &SimilarityTransform,
4423 ) {
4424 let Some(range) = records.get(first as usize..last as usize) else {
4425 return;
4426 };
4427 let scale = f64::from(transform.scale);
4428 let factor = scale * scale;
4429 let identity = transform.rot == [1.0, 0.0] && transform.scale == 1.0;
4430 let frame_viewport = self.viewport.get();
4431 let mut drawn = 0.0_f64;
4432 let mut lit = 0.0_f64;
4433 let mut opacity = [0.0_f64; 3];
4434 let mut corner = 0.0_f64;
4435 for record in range {
4436 drawn += record.drawn_px2;
4437 lit += record.lit_px2;
4438 opacity[record.opacity as usize] += record.lit_px2;
4439 if identity {
4440 corner += area_outside_inscribed_circle(record.aabb, frame_viewport);
4441 }
4442 }
4443 self.add(Self::RETAINED, drawn * factor);
4444 self.add_lit(Self::RETAINED, lit * factor);
4445 for (class, value) in self.frame_opacity.iter().zip(opacity) {
4446 class.set(class.get() + value * factor);
4447 }
4448 if corner > 0.0 {
4449 self.add_corner(corner);
4450 }
4451 }
4452
4453 fn note_retained_capture(&mut self, slot: u32, records: &[FillDiagShapeRecord]) {
4457 if self.slack_dumped {
4458 return;
4459 }
4460 for (index, record) in records.iter().enumerate() {
4461 if record.drawn_px2 - record.lit_px2 <= 0.0 {
4462 continue;
4463 }
4464 self.slack_top.push(FillDiagSlackEntry {
4465 slot,
4466 shape: index as u32,
4467 bucket: record.bucket,
4468 drawn_px2: record.drawn_px2,
4469 lit_px2: record.lit_px2,
4470 });
4471 }
4472 self.slack_top
4473 .sort_by(|a, b| (b.drawn_px2 - b.lit_px2).total_cmp(&(a.drawn_px2 - a.lit_px2)));
4474 self.slack_top.truncate(FILL_DIAG_SLACK_TOP);
4475 }
4476
4477 fn add_image_quad(&self, quad: &[[f32; 2]; 4]) {
4481 let corner = |index: usize| [f64::from(quad[index][0]), f64::from(quad[index][1])];
4482 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
4483 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
4484 };
4485 let [a, b, c, d] = [corner(0), corner(1), corner(2), corner(3)];
4486 let area = tri(a, b, c) + tri(c, b, d);
4487 self.add(Self::IMAGE_GLYPH, area);
4488 self.add_lit(Self::IMAGE_GLYPH, area);
4489 }
4490
4491 fn add_glyph_quad(&self, quad: &CachedTextGlyphQuad) {
4493 let area = quad.width as f64 * quad.height as f64;
4494 self.add(Self::IMAGE_GLYPH, area);
4495 self.add_lit(Self::IMAGE_GLYPH, area);
4496 }
4497
4498 fn add_effect_fill(&self, composite_px2: f64, offscreen_px2: f64) {
4502 if composite_px2 > 0.0 {
4503 self.add(Self::EFFECT_COMPOSITE, composite_px2);
4504 self.add_lit(Self::EFFECT_COMPOSITE, composite_px2);
4505 }
4506 if offscreen_px2 > 0.0 {
4507 self.add(Self::OFFSCREEN_SOURCE, offscreen_px2);
4508 self.add_lit(Self::OFFSCREEN_SOURCE, offscreen_px2);
4509 }
4510 }
4511
4512 fn add_offscreen_target_fill(&self, px2: f64) {
4518 if px2 > 0.0 {
4519 self.add(Self::OFFSCREEN_SOURCE, px2);
4520 self.add_lit(Self::OFFSCREEN_SOURCE, px2);
4521 }
4522 }
4523
4524 fn reset_frame(&self, width: u32, height: u32) {
4528 for cell in &self.frame {
4529 cell.set(0.0);
4530 }
4531 for cell in &self.frame_lit {
4532 cell.set(0.0);
4533 }
4534 for cell in &self.frame_opacity {
4535 cell.set(0.0);
4536 }
4537 self.frame_corner.set(0.0);
4538 self.viewport.set((width, height));
4539 }
4540
4541 fn finish_frame(&mut self, width: u32, height: u32) {
4546 for (total, cell) in self.window.iter_mut().zip(&self.frame) {
4547 *total += cell.get();
4548 }
4549 for (total, cell) in self.window_lit.iter_mut().zip(&self.frame_lit) {
4550 *total += cell.get();
4551 }
4552 for (total, cell) in self.window_opacity.iter_mut().zip(&self.frame_opacity) {
4553 *total += cell.get();
4554 }
4555 self.window_corner += self.frame_corner.get();
4556 self.window_frames += 1;
4557 if self.window_frames < FILL_DIAG_WINDOW_FRAMES {
4558 return;
4559 }
4560 let frames = f64::from(self.window_frames);
4561 let mega = |bucket: usize| self.window[bucket] / frames / 1e6;
4562 let total_mega = self.window.iter().sum::<f64>() / frames / 1e6;
4563 let screen_mega = f64::from(width) * f64::from(height) / 1e6;
4564 let overdraw = if screen_mega > 0.0 {
4565 total_mega / screen_mega
4566 } else {
4567 0.0
4568 };
4569 log::warn!(
4570 "[fill-diag] Mpx/frame: arc {:.1}, rrect-stroke {:.1}, rrect-fill {:.1}, \
4571 rect {:.1}, mesh {:.1}, retained {:.1}, img+glyph {:.1}, \
4572 effect-comp {:.1}, offscr-src {:.1}, total {:.1} \
4573 ({:.1}x overdraw of {:.3} Mpx)",
4574 mega(Self::ARC),
4575 mega(Self::RRECT_STROKE),
4576 mega(Self::RRECT_FILL),
4577 mega(Self::RECT),
4578 mega(Self::MESH),
4579 mega(Self::RETAINED),
4580 mega(Self::IMAGE_GLYPH),
4581 mega(Self::EFFECT_COMPOSITE),
4582 mega(Self::OFFSCREEN_SOURCE),
4583 total_mega,
4584 overdraw,
4585 screen_mega,
4586 );
4587 let lit = |bucket: usize| self.window_lit[bucket] / frames / 1e6;
4591 let slack = |bucket: usize| (mega(bucket) - lit(bucket)).max(0.0);
4592 let truth = |bucket: usize| format!("{:.2}|{:.2}", lit(bucket), slack(bucket));
4593 log::warn!(
4594 "[fill-truth] Mpx/frame lit|slack: arc {}, rrect-stroke {}, rrect-fill {}, \
4595 rect {}, mesh {}, retained {}, img+glyph {}, effect-comp {}, offscr-src {}; \
4596 lit alpha Mpx: opaque {:.2}, translucent {:.2}, nonsolid {:.2}; \
4597 corner-outside {:.2}",
4598 truth(Self::ARC),
4599 truth(Self::RRECT_STROKE),
4600 truth(Self::RRECT_FILL),
4601 truth(Self::RECT),
4602 truth(Self::MESH),
4603 truth(Self::RETAINED),
4604 truth(Self::IMAGE_GLYPH),
4605 truth(Self::EFFECT_COMPOSITE),
4606 truth(Self::OFFSCREEN_SOURCE),
4607 self.window_opacity[FillOpacityClass::Opaque as usize] / frames / 1e6,
4608 self.window_opacity[FillOpacityClass::Translucent as usize] / frames / 1e6,
4609 self.window_opacity[FillOpacityClass::NonSolid as usize] / frames / 1e6,
4610 self.window_corner / frames / 1e6,
4611 );
4612 if !self.slack_dumped && !self.slack_top.is_empty() {
4613 log::warn!("[fill-truth] top retained slack (once per process, capture-space px):");
4614 for (rank, entry) in self.slack_top.iter().enumerate() {
4615 log::warn!(
4616 "[fill-truth] #{} slot {} shape {} {}: quad {:.0}, lit {:.0}, \
4617 slack {:.0}",
4618 rank + 1,
4619 entry.slot,
4620 entry.shape,
4621 fill_diag_bucket_name(entry.bucket),
4622 entry.drawn_px2,
4623 entry.lit_px2,
4624 entry.drawn_px2 - entry.lit_px2,
4625 );
4626 }
4627 self.slack_dumped = true;
4628 self.slack_top = Vec::new();
4629 }
4630 self.window = [0.0; FILL_DIAG_BUCKETS];
4631 self.window_lit = [0.0; FILL_DIAG_BUCKETS];
4632 self.window_opacity = [0.0; 3];
4633 self.window_corner = 0.0;
4634 self.window_frames = 0;
4635 }
4636}
4637
4638#[cfg(not(target_arch = "wasm32"))]
4639struct ArcMeshBuild {
4640 vertices: Vec<MeshVertex>,
4641 indices: Vec<u32>,
4643 index_prefix: Vec<u32>,
4649 meshed_arcs: usize,
4650 meshed_rims: usize,
4651 meshed_segments: usize,
4652 passthrough: usize,
4653 meshed_stretches: usize,
4657 quad_area: f64,
4658 mesh_area: f64,
4661}
4662
4663#[cfg(not(target_arch = "wasm32"))]
4677fn build_arc_mesh_vertices(shape_data: &[ShapeData], min_mesh_px2: f64) -> Option<ArcMeshBuild> {
4678 let budget_bytes =
4679 (shape_data.len() * ARC_MESH_BUDGET_BYTES_PER_SHAPE).max(ARC_MESH_BUDGET_FLOOR_BYTES);
4680 let mut build = ArcMeshBuild {
4681 vertices: Vec::new(),
4682 indices: Vec::new(),
4683 index_prefix: Vec::with_capacity(shape_data.len() + 1),
4684 meshed_arcs: 0,
4685 meshed_rims: 0,
4686 meshed_segments: 0,
4687 passthrough: 0,
4688 meshed_stretches: 0,
4689 quad_area: 0.0,
4690 mesh_area: 0.0,
4691 };
4692 build.index_prefix.push(0);
4693 let mut previous_meshed = false;
4694 for (index, shape) in shape_data.iter().enumerate() {
4695 let start = build.indices.len();
4696 let quad_px2 = quad_shoelace_area(shape);
4697 let band = if quad_px2 >= min_mesh_px2 {
4706 arc_mesh_band(shape)
4707 .map(|band| (band, false))
4708 .or_else(|| rim_band_geometry(shape).map(|band| (band, true)))
4709 } else {
4710 None
4711 };
4712 let meshed = band.and_then(|(band, is_rim)| {
4713 emit_arc_band_mesh(
4714 shape,
4715 index as u32,
4716 &band,
4717 &mut build.vertices,
4718 &mut build.indices,
4719 )
4720 .map(|segments| (segments, is_rim))
4721 });
4722 match meshed {
4723 Some((segments, is_rim)) => {
4724 if is_rim {
4725 build.meshed_rims += 1;
4726 } else {
4727 build.meshed_arcs += 1;
4728 }
4729 build.meshed_segments += segments;
4730 if !previous_meshed {
4731 build.meshed_stretches += 1;
4732 }
4733 previous_meshed = true;
4734 build.mesh_area +=
4735 triangles_shoelace_area(&build.vertices, &build.indices[start..]);
4736 }
4737 None => {
4738 build.passthrough += 1;
4739 previous_meshed = false;
4740 build.mesh_area += quad_px2;
4741 }
4742 }
4743 if arc_mesh_bytes(build.vertices.len(), build.indices.len()) > budget_bytes {
4744 return None;
4745 }
4746 build.index_prefix.push(build.indices.len() as u32);
4747 build.quad_area += quad_px2;
4748 }
4749 Some(build)
4750}
4751
4752#[cfg(not(target_arch = "wasm32"))]
4755struct ReplaySlotStore {
4756 slots: std::collections::HashMap<u32, ReplaySlot, cranpose_ui_graphics::FxBuildHasher>,
4757 transform_buffer: wgpu::Buffer,
4758 free_ids: Vec<u32>,
4759 next_capture_epoch: u64,
4763}
4764
4765#[cfg(not(target_arch = "wasm32"))]
4766impl ReplaySlotStore {
4767 fn new(device: &wgpu::Device) -> Self {
4768 let transform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4769 label: Some("Replay Transform Buffer"),
4770 size: (MAX_REPLAY_SLOTS + SEGMENT_CAPTURE_SLOTS) as u64 * REPLAY_TRANSFORM_STRIDE,
4777 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4778 mapped_at_creation: false,
4779 });
4780 Self {
4781 slots: std::collections::HashMap::default(),
4782 transform_buffer,
4783 free_ids: (0..MAX_REPLAY_SLOTS).rev().collect(),
4784 next_capture_epoch: 1,
4785 }
4786 }
4787}
4788
4789#[cfg(not(target_arch = "wasm32"))]
4795fn retained_bundles_enabled() -> bool {
4796 std::env::var("CRANPOSE_RETAINED_BUNDLES").as_deref() != Ok("0")
4797}
4798
4799#[cfg(not(target_arch = "wasm32"))]
4807fn instanced_quads_enabled() -> bool {
4808 std::env::var("CRANPOSE_INSTANCED_QUADS").as_deref() != Ok("0")
4809}
4810
4811fn solid_trim_varyings_enabled() -> bool {
4824 std::env::var("CRANPOSE_SOLID_TRIM_VARYINGS").as_deref() == Ok("1")
4825}
4826
4827fn survive_gpu_errors_enabled() -> bool {
4836 std::env::var("CRANPOSE_SURVIVE_GPU_ERRORS").as_deref() != Ok("0")
4837}
4838
4839#[cfg(not(target_arch = "wasm32"))]
4859fn display_clip_cull_enabled() -> bool {
4860 std::env::var("CRANPOSE_ROUND_CULL").as_deref() == Ok("1")
4861}
4862
4863#[cfg(not(target_arch = "wasm32"))]
4867const INSTANCED_QUAD_INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
4868
4869#[cfg(not(target_arch = "wasm32"))]
4875struct InstancedQuadPipelines {
4876 pipeline: PassPipeline,
4877 pipeline_dst_out: PassPipeline,
4878 pipeline_solid: PassPipeline,
4882 index_buffer: wgpu::Buffer,
4885}
4886
4887#[cfg(not(target_arch = "wasm32"))]
4899enum RetainedCmd<'r> {
4900 Pipeline(&'r wgpu::RenderPipeline),
4901 Uniforms(&'r wgpu::BindGroup),
4903 SlotBindings(&'r wgpu::BindGroup, u32),
4905 MeshVertices(&'r wgpu::Buffer),
4907 Index(&'r wgpu::Buffer, wgpu::IndexFormat),
4908 Draw(Range<u32>),
4910 DrawIndexed(Range<u32>, Range<u32>),
4912}
4913
4914#[cfg(not(target_arch = "wasm32"))]
4925#[derive(Clone, Debug, PartialEq, Eq, Hash)]
4926struct RetainedBundleOpKey {
4927 slot: u32,
4928 capture_epoch: Option<u64>,
4933 first: u32,
4934 last: u32,
4935 retained_index: u32,
4936 has_mesh: bool,
4937}
4938
4939#[cfg(not(target_arch = "wasm32"))]
4943#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
4944struct RetainedBundleKey {
4945 depth: bool,
4950 ops: Vec<RetainedBundleOpKey>,
4951}
4952
4953#[cfg(not(target_arch = "wasm32"))]
4954struct RetainedBundleCacheEntry<B> {
4955 bundle: B,
4956 last_used_frame: u64,
4957}
4958
4959#[cfg(not(target_arch = "wasm32"))]
4968struct RetainedBundleCacheImpl<B> {
4969 entries: HashMap<RetainedBundleKey, RetainedBundleCacheEntry<B>>,
4970 frame: u64,
4971 rebuilds: u64,
4972 cached_executes: u64,
4973 window_rebuilds: u64,
4974 window_executes: u64,
4975}
4976
4977#[cfg(not(target_arch = "wasm32"))]
4978type RetainedBundleCache = RetainedBundleCacheImpl<wgpu::RenderBundle>;
4979
4980#[cfg(not(target_arch = "wasm32"))]
4981impl<B> RetainedBundleCacheImpl<B> {
4982 fn new() -> Self {
4983 Self {
4984 entries: HashMap::default(),
4985 frame: 0,
4986 rebuilds: 0,
4987 cached_executes: 0,
4988 window_rebuilds: 0,
4989 window_executes: 0,
4990 }
4991 }
4992
4993 fn hit(&mut self, key: &RetainedBundleKey) -> bool {
4996 let frame = self.frame;
4997 match self.entries.get_mut(key) {
4998 Some(entry) => {
4999 entry.last_used_frame = frame;
5000 self.cached_executes += 1;
5001 self.window_executes += 1;
5002 true
5003 }
5004 None => false,
5005 }
5006 }
5007
5008 fn insert(&mut self, key: RetainedBundleKey, bundle: B) {
5010 self.rebuilds += 1;
5011 self.window_rebuilds += 1;
5012 self.entries.insert(
5013 key,
5014 RetainedBundleCacheEntry {
5015 bundle,
5016 last_used_frame: self.frame,
5017 },
5018 );
5019 }
5020
5021 fn get(&self, key: &RetainedBundleKey) -> Option<&B> {
5022 self.entries.get(key).map(|entry| &entry.bundle)
5023 }
5024
5025 fn clear(&mut self) {
5030 self.entries.clear();
5031 }
5032
5033 fn end_frame(&mut self) {
5037 let frame = self.frame;
5038 self.entries
5039 .retain(|_, entry| entry.last_used_frame >= frame);
5040 self.frame = self.frame.wrapping_add(1);
5041 let due = self.frame.is_multiple_of(1024)
5046 || (cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG")
5047 && self.frame.is_multiple_of(120));
5048 if due && self.window_rebuilds + self.window_executes > 0 {
5049 log::warn!(
5050 "[retained-bundles] {} stretches, {} rebuilds, {} cached executes ({} live bundles)",
5051 self.window_rebuilds + self.window_executes,
5052 self.window_rebuilds,
5053 self.window_executes,
5054 self.entries.len(),
5055 );
5056 self.window_rebuilds = 0;
5057 self.window_executes = 0;
5058 }
5059 }
5060
5061 fn stats(&self) -> (u64, u64) {
5063 (self.rebuilds, self.cached_executes)
5064 }
5065}
5066
5067struct CachedImageTexture {
5068 _texture: wgpu::Texture,
5069 _view: wgpu::TextureView,
5070 nearest_bind_group: wgpu::BindGroup,
5071 linear_bind_group: wgpu::BindGroup,
5072 bytes: usize,
5079}
5080
5081impl CachedImageTexture {
5082 fn bind_group(&self, sampling: ImageSampling) -> &wgpu::BindGroup {
5083 match sampling {
5084 ImageSampling::Nearest => &self.nearest_bind_group,
5085 ImageSampling::Linear => &self.linear_bind_group,
5086 }
5087 }
5088}
5089
5090#[derive(Clone, Copy)]
5091struct GlyphAtlasEntry {
5092 x: u32,
5093 y: u32,
5094 width: u32,
5095 height: u32,
5096}
5097
5098fn next_glyph_atlas_size(current: u32, max: u32) -> u32 {
5105 current.saturating_mul(2).clamp(1, max.max(1))
5106}
5107
5108struct TextGlyphAtlas {
5109 texture: wgpu::Texture,
5110 _view: wgpu::TextureView,
5111 bind_group: wgpu::BindGroup,
5112 entries: BoundedLruCache<SoftwareGlyphAtlasKey, GlyphAtlasEntry>,
5113 generation: u64,
5114 size: u32,
5118 max_size: u32,
5124 cursor_x: u32,
5125 cursor_y: u32,
5126 row_height: u32,
5127 upload_scratch: Vec<u8>,
5128}
5129
5130impl TextGlyphAtlas {
5131 fn new(
5132 device: &wgpu::Device,
5133 image_layout: &wgpu::BindGroupLayout,
5134 sampler: &wgpu::Sampler,
5135 size: u32,
5136 ) -> Self {
5137 let max_size = TEXT_GLYPH_ATLAS_MAX_SIZE.min(device.limits().max_texture_dimension_2d);
5138 let size = size.clamp(TEXT_GLYPH_ATLAS_MIN_SIZE.min(max_size), max_size);
5139 let texture = Self::create_texture(device, size);
5140 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
5141 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5142 label: Some("Text Glyph Atlas Bind Group"),
5143 layout: image_layout,
5144 entries: &[
5145 wgpu::BindGroupEntry {
5146 binding: 0,
5147 resource: wgpu::BindingResource::TextureView(&view),
5148 },
5149 wgpu::BindGroupEntry {
5150 binding: 1,
5151 resource: wgpu::BindingResource::Sampler(sampler),
5152 },
5153 ],
5154 });
5155 Self {
5156 texture,
5157 _view: view,
5158 bind_group,
5159 entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
5160 generation: 0,
5161 size,
5162 max_size,
5163 cursor_x: TEXT_GLYPH_ATLAS_PADDING,
5164 cursor_y: TEXT_GLYPH_ATLAS_PADDING,
5165 row_height: 0,
5166 upload_scratch: Vec::new(),
5167 }
5168 }
5169
5170 fn create_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
5171 device.create_texture(&wgpu::TextureDescriptor {
5172 label: Some("Text Glyph Atlas Texture"),
5173 size: wgpu::Extent3d {
5174 width: size,
5175 height: size,
5176 depth_or_array_layers: 1,
5177 },
5178 mip_level_count: 1,
5179 sample_count: 1,
5180 dimension: wgpu::TextureDimension::D2,
5181 format: wgpu::TextureFormat::R8Unorm,
5182 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
5183 view_formats: &[],
5184 })
5185 }
5186
5187 fn reset(
5203 &mut self,
5204 device: &wgpu::Device,
5205 image_layout: &wgpu::BindGroupLayout,
5206 sampler: &wgpu::Sampler,
5207 ) {
5208 let generation = self.generation.wrapping_add(1);
5209 let grown = next_glyph_atlas_size(self.size, self.max_size);
5210 let mut next = Self::new(device, image_layout, sampler, grown);
5211 next.generation = generation;
5212 *self = next;
5213 }
5214
5215 fn generation(&self) -> u64 {
5216 self.generation
5217 }
5218
5219 fn size(&self) -> u32 {
5220 self.size
5221 }
5222
5223 fn entry(&mut self, key: &SoftwareGlyphAtlasKey) -> Option<GlyphAtlasEntry> {
5224 self.entries.get(key).copied()
5225 }
5226
5227 fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
5228 if width == 0
5229 || height == 0
5230 || width + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
5231 || height + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
5232 {
5233 return None;
5234 }
5235
5236 if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > self.size {
5237 self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
5238 self.cursor_y = self
5239 .cursor_y
5240 .saturating_add(self.row_height)
5241 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
5242 self.row_height = 0;
5243 }
5244 if self.cursor_y + height + TEXT_GLYPH_ATLAS_PADDING > self.size {
5245 return None;
5246 }
5247
5248 let entry = GlyphAtlasEntry {
5249 x: self.cursor_x,
5250 y: self.cursor_y,
5251 width,
5252 height,
5253 };
5254 self.cursor_x = self
5255 .cursor_x
5256 .saturating_add(width)
5257 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
5258 self.row_height = self.row_height.max(height);
5259 Some(entry)
5260 }
5261
5262 fn upload_glyph(
5263 &mut self,
5264 key: SoftwareGlyphAtlasKey,
5265 glyph: &SoftwareGlyphAtlasGlyph,
5266 queue: &wgpu::Queue,
5267 executor: &mut WgpuFrameGraphExecutor,
5268 frame_stats: &mut gpu_stats::FrameStats,
5269 ) -> Option<GlyphAtlasEntry> {
5270 if let Some(entry) = self.entry(&key) {
5271 frame_stats.record_text_glyph_atlas_hit();
5272 return Some(entry);
5273 }
5274
5275 let width = u32::try_from(glyph.mask.width).ok()?;
5276 let height = u32::try_from(glyph.mask.height).ok()?;
5277 let entry = self.allocate(width, height)?;
5278 self.upload_scratch.clear();
5279 self.upload_scratch.reserve(
5280 glyph
5281 .mask
5282 .alpha
5283 .len()
5284 .saturating_sub(self.upload_scratch.capacity()),
5285 );
5286 self.upload_scratch.extend(
5287 glyph
5288 .mask
5289 .alpha
5290 .iter()
5291 .map(|alpha| (alpha.clamp(0.0, 1.0) * 255.0).round() as u8),
5292 );
5293
5294 let upload_stats = executor.upload_texture(
5295 queue,
5296 wgpu::TexelCopyTextureInfo {
5297 texture: &self.texture,
5298 mip_level: 0,
5299 origin: wgpu::Origin3d {
5300 x: entry.x,
5301 y: entry.y,
5302 z: 0,
5303 },
5304 aspect: wgpu::TextureAspect::All,
5305 },
5306 &self.upload_scratch,
5307 wgpu::TexelCopyBufferLayout {
5308 offset: 0,
5309 bytes_per_row: Some(entry.width),
5310 rows_per_image: Some(entry.height),
5311 },
5312 wgpu::Extent3d {
5313 width: entry.width,
5314 height: entry.height,
5315 depth_or_array_layers: 1,
5316 },
5317 );
5318 frame_stats.record_command_stats(upload_stats);
5319 frame_stats.record_text_glyph_atlas_miss(entry.width, entry.height);
5320 self.entries.put(key, entry);
5321 Some(entry)
5322 }
5323}
5324
5325struct ImageDrawCmd {
5326 index_start: u32,
5327 scissor: (u32, u32, u32, u32),
5328 image_id: u64,
5329 sampling: ImageSampling,
5330}
5331
5332#[derive(Clone, Copy)]
5333enum GlyphDrawSource {
5334 Shared {
5335 index_start: u32,
5336 index_count: u32,
5337 },
5338 #[cfg(not(target_arch = "wasm32"))]
5339 Retained {
5340 cache_key: TextGlyphRunCacheKey,
5341 uniform_slot: usize,
5342 },
5343}
5344
5345#[derive(Clone, Copy)]
5346struct GlyphDrawCmd {
5347 source: GlyphDrawSource,
5348 scissor: (u32, u32, u32, u32),
5349}
5350
5351impl GlyphDrawCmd {
5352 fn shared(index_start: u32, index_count: u32, scissor: (u32, u32, u32, u32)) -> Self {
5353 Self {
5354 source: GlyphDrawSource::Shared {
5355 index_start,
5356 index_count,
5357 },
5358 scissor,
5359 }
5360 }
5361
5362 #[cfg(not(target_arch = "wasm32"))]
5363 fn retained(
5364 cache_key: TextGlyphRunCacheKey,
5365 uniform_slot: usize,
5366 scissor: (u32, u32, u32, u32),
5367 ) -> Self {
5368 Self {
5369 source: GlyphDrawSource::Retained {
5370 cache_key,
5371 uniform_slot,
5372 },
5373 scissor,
5374 }
5375 }
5376}
5377
5378#[derive(Clone, Copy, Debug, PartialEq)]
5379struct ImageUvRect {
5380 min: [f32; 2],
5381 max: [f32; 2],
5382 sample_bounds: [f32; 4],
5383}
5384
5385struct ShapeBatchBuffers {
5392 shape_buffer: wgpu::Buffer,
5393 gradient_buffer: wgpu::Buffer,
5394 bind_group: wgpu::BindGroup,
5395 shape_capacity: usize,
5396 gradient_capacity: usize,
5397 batch_limits: ShapeBatchLimits,
5398}
5399
5400#[cfg(target_arch = "wasm32")]
5401struct UniformBatchBuffer {
5402 buffer: wgpu::Buffer,
5403 bind_group: wgpu::BindGroup,
5404}
5405
5406#[cfg(target_arch = "wasm32")]
5407struct ImageBatchBuffers {
5408 vertex_buffer: wgpu::Buffer,
5409 index_buffer: wgpu::Buffer,
5410 vertex_capacity: usize,
5411 index_capacity: usize,
5412}
5413
5414#[derive(Clone, Copy, Debug, PartialEq)]
5415struct ViewportUniformParams {
5416 width: u32,
5417 height: u32,
5418 offset: [f32; 2],
5419}
5420
5421#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5422#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
5423enum UploadTarget {
5424 Uniform,
5425 ShapeData,
5426 ShapeGradient,
5427 ImageVertex,
5428 ImageIndex,
5429 #[cfg(not(target_arch = "wasm32"))]
5430 RetainedGlyphUniform,
5431 #[cfg(not(target_arch = "wasm32"))]
5434 ReplayTransform,
5435 #[cfg(not(target_arch = "wasm32"))]
5437 ReplayPaintData(u32),
5438}
5439
5440#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5441#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
5442struct PendingBufferCopy {
5443 source_offset: u64,
5444 target_offset: u64,
5445 size: u64,
5446 target: UploadTarget,
5447}
5448
5449#[derive(Default)]
5450struct StagedBufferUploads {
5451 bytes: Vec<u8>,
5452 copies: Vec<PendingBufferCopy>,
5453}
5454
5455impl StagedBufferUploads {
5456 fn clear(&mut self) {
5457 self.bytes.clear();
5458 self.copies.clear();
5459 }
5460
5461 fn shrink_retained_capacity(&mut self, max_bytes: usize, max_copies: usize) -> bool {
5462 let mut shrunk = false;
5463 if self.bytes.len() <= max_bytes && self.bytes.capacity() > max_bytes {
5464 self.bytes.shrink_to(max_bytes);
5465 shrunk = true;
5466 }
5467 if self.copies.len() <= max_copies && self.copies.capacity() > max_copies {
5468 self.copies.shrink_to(max_copies);
5469 shrunk = true;
5470 }
5471 shrunk
5472 }
5473
5474 fn is_empty(&self) -> bool {
5475 self.copies.is_empty()
5476 }
5477
5478 #[cfg(test)]
5479 fn payload_for_copy(&self, copy: PendingBufferCopy) -> &[u8] {
5480 let start = copy.source_offset as usize;
5481 let end = start + copy.size as usize;
5482 &self.bytes[start..end]
5483 }
5484
5485 #[cfg(not(target_arch = "wasm32"))]
5486 fn stage(&mut self, target: UploadTarget, bytes: &[u8]) {
5487 self.stage_at(target, 0, bytes);
5488 }
5489
5490 #[cfg(not(target_arch = "wasm32"))]
5495 fn record_upload_copy(
5496 &mut self,
5497 target: UploadTarget,
5498 source_offset: u64,
5499 target_offset: u64,
5500 size: u64,
5501 ) {
5502 if size == 0 {
5503 return;
5504 }
5505 self.copies.push(PendingBufferCopy {
5506 source_offset,
5507 target_offset,
5508 size,
5509 target,
5510 });
5511 }
5512
5513 #[cfg(not(target_arch = "wasm32"))]
5514 fn stage_at(&mut self, target: UploadTarget, target_offset: u64, bytes: &[u8]) {
5515 if bytes.is_empty() {
5516 return;
5517 }
5518
5519 debug_assert_eq!(
5520 bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize,
5521 0,
5522 "buffer uploads must be aligned to copy requirements"
5523 );
5524
5525 let aligned_offset = align_usize_to(self.bytes.len(), wgpu::COPY_BUFFER_ALIGNMENT as usize);
5526 if aligned_offset > self.bytes.len() {
5527 self.bytes.resize(aligned_offset, 0);
5528 }
5529
5530 let source_offset = self.bytes.len() as u64;
5531 self.bytes.extend_from_slice(bytes);
5532 self.copies.push(PendingBufferCopy {
5533 source_offset,
5534 target_offset,
5535 size: bytes.len() as u64,
5536 target,
5537 });
5538 }
5539
5540 fn truncate(&mut self, bytes_len: usize, copies_len: usize) {
5541 self.bytes.truncate(bytes_len);
5542 self.copies.truncate(copies_len);
5543 }
5544}
5545
5546fn shape_batch_bind_group_entries<'a>(
5551 shape_buffer: &'a wgpu::Buffer,
5552 gradient_buffer: &'a wgpu::Buffer,
5553 similarity_buffer: &'a wgpu::Buffer,
5554 paint_buffer: Option<&'a wgpu::Buffer>,
5555) -> Vec<wgpu::BindGroupEntry<'a>> {
5556 let mut entries = vec![
5557 wgpu::BindGroupEntry {
5558 binding: 0,
5559 resource: shape_buffer.as_entire_binding(),
5560 },
5561 wgpu::BindGroupEntry {
5562 binding: 1,
5563 resource: gradient_buffer.as_entire_binding(),
5564 },
5565 wgpu::BindGroupEntry {
5566 binding: 2,
5567 resource: similarity_buffer.as_entire_binding(),
5568 },
5569 ];
5570 if let Some(paint_buffer) = paint_buffer {
5571 entries.push(wgpu::BindGroupEntry {
5572 binding: 3,
5573 resource: paint_buffer.as_entire_binding(),
5574 });
5575 }
5576 entries
5577}
5578
5579impl ShapeBatchBuffers {
5580 fn new(
5581 device: &wgpu::Device,
5582 bind_group_layout: &wgpu::BindGroupLayout,
5583 similarity_buffer: &wgpu::Buffer,
5584 paint_buffer: Option<&wgpu::Buffer>,
5585 batch_limits: ShapeBatchLimits,
5586 ) -> Self {
5587 debug_assert_eq!(
5588 paint_buffer.is_some(),
5589 batch_limits.storage,
5590 "the paint binding exists exactly when the layout is in storage mode"
5591 );
5592 let initial_shape_cap = batch_limits.initial_shape_capacity();
5593 let initial_gradient_cap = batch_limits.initial_gradient_capacity();
5594
5595 let shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5596 label: Some("Shape Data Buffer"),
5597 size: (std::mem::size_of::<ShapeData>() * initial_shape_cap) as u64,
5598 usage: batch_limits.data_buffer_usage(),
5599 mapped_at_creation: false,
5600 });
5601
5602 let gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5603 label: Some("Gradient Buffer"),
5604 size: (std::mem::size_of::<GradientStop>() * initial_gradient_cap) as u64,
5605 usage: batch_limits.data_buffer_usage(),
5606 mapped_at_creation: false,
5607 });
5608
5609 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5610 label: Some("Shape Bind Group"),
5611 layout: bind_group_layout,
5612 entries: &shape_batch_bind_group_entries(
5613 &shape_buffer,
5614 &gradient_buffer,
5615 similarity_buffer,
5616 paint_buffer,
5617 ),
5618 });
5619
5620 Self {
5621 shape_buffer,
5622 gradient_buffer,
5623 bind_group,
5624 shape_capacity: initial_shape_cap,
5625 gradient_capacity: initial_gradient_cap,
5626 batch_limits,
5627 }
5628 }
5629
5630 fn ensure_capacity(
5633 &mut self,
5634 device: &wgpu::Device,
5635 bind_group_layout: &wgpu::BindGroupLayout,
5636 similarity_buffer: &wgpu::Buffer,
5637 paint_buffer: Option<&wgpu::Buffer>,
5638 shapes_needed: usize,
5639 gradients_needed: usize,
5640 ) {
5641 let mut need_bind_group_update = false;
5642
5643 if shapes_needed > self.shape_capacity
5647 && self.shape_capacity < self.batch_limits.max_shapes_per_batch
5648 {
5649 let new_cap = shapes_needed
5650 .next_power_of_two()
5651 .min(self.batch_limits.max_shapes_per_batch);
5652 self.shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5653 label: Some("Shape Data Buffer"),
5654 size: (std::mem::size_of::<ShapeData>() * new_cap) as u64,
5655 usage: self.batch_limits.data_buffer_usage(),
5656 mapped_at_creation: false,
5657 });
5658 self.shape_capacity = new_cap;
5659 need_bind_group_update = true;
5660 }
5661
5662 if gradients_needed > self.gradient_capacity
5663 && self.gradient_capacity < self.batch_limits.max_gradient_stops
5664 {
5665 let new_cap = gradients_needed
5666 .max(1)
5667 .next_power_of_two()
5668 .min(self.batch_limits.max_gradient_stops);
5669 self.gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5670 label: Some("Gradient Buffer"),
5671 size: (std::mem::size_of::<GradientStop>() * new_cap) as u64,
5672 usage: self.batch_limits.data_buffer_usage(),
5673 mapped_at_creation: false,
5674 });
5675 self.gradient_capacity = new_cap;
5676 need_bind_group_update = true;
5677 }
5678
5679 if need_bind_group_update {
5680 self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5681 label: Some("Shape Bind Group"),
5682 layout: bind_group_layout,
5683 entries: &shape_batch_bind_group_entries(
5684 &self.shape_buffer,
5685 &self.gradient_buffer,
5686 similarity_buffer,
5687 paint_buffer,
5688 ),
5689 });
5690 }
5691 }
5692}
5693
5694#[cfg(target_arch = "wasm32")]
5695impl UniformBatchBuffer {
5696 fn new(device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout) -> Self {
5697 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
5698 label: Some("Viewport Uniform Batch Buffer"),
5699 size: std::mem::size_of::<Uniforms>() as u64,
5700 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
5701 mapped_at_creation: false,
5702 });
5703 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
5704 label: Some("Viewport Uniform Batch Bind Group"),
5705 layout: bind_group_layout,
5706 entries: &[wgpu::BindGroupEntry {
5707 binding: 0,
5708 resource: buffer.as_entire_binding(),
5709 }],
5710 });
5711 Self { buffer, bind_group }
5712 }
5713}
5714
5715#[cfg(target_arch = "wasm32")]
5716impl ImageBatchBuffers {
5717 fn new(device: &wgpu::Device) -> Self {
5718 let vertex_capacity = 4;
5719 let index_capacity = 6;
5720 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5721 label: Some("Image Vertex Batch Buffer"),
5722 size: (std::mem::size_of::<Vertex>() * vertex_capacity) as u64,
5723 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
5724 mapped_at_creation: false,
5725 });
5726 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5727 label: Some("Image Index Batch Buffer"),
5728 size: (std::mem::size_of::<u32>() * index_capacity) as u64,
5729 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
5730 mapped_at_creation: false,
5731 });
5732 Self {
5733 vertex_buffer,
5734 index_buffer,
5735 vertex_capacity,
5736 index_capacity,
5737 }
5738 }
5739
5740 fn ensure_capacity(
5741 &mut self,
5742 device: &wgpu::Device,
5743 vertices_needed: usize,
5744 indices_needed: usize,
5745 ) {
5746 let hard_max_bytes = HARD_MAX_BUFFER_MB * 1024 * 1024;
5747 if vertices_needed > self.vertex_capacity {
5748 let desired = vertices_needed.next_power_of_two();
5749 let max_count = hard_max_bytes / std::mem::size_of::<Vertex>();
5750 let new_cap = desired.min(max_count);
5751 self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5752 label: Some("Image Vertex Batch Buffer"),
5753 size: (std::mem::size_of::<Vertex>() * new_cap) as u64,
5754 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
5755 mapped_at_creation: false,
5756 });
5757 self.vertex_capacity = new_cap;
5758 }
5759 if indices_needed > self.index_capacity {
5760 let desired = indices_needed.next_power_of_two();
5761 let max_count = hard_max_bytes / std::mem::size_of::<u32>();
5762 let new_cap = desired.min(max_count);
5763 self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
5764 label: Some("Image Index Batch Buffer"),
5765 size: (std::mem::size_of::<u32>() * new_cap) as u64,
5766 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
5767 mapped_at_creation: false,
5768 });
5769 self.index_capacity = new_cap;
5770 }
5771 }
5772}
5773
5774pub struct GpuRenderer {
5777 pub(crate) device: Arc<wgpu::Device>,
5778 pub(crate) queue: Arc<wgpu::Queue>,
5779 device_errors: Arc<DeviceErrorSentry>,
5785 renderer_epoch: u64,
5790 #[cfg(not(target_arch = "wasm32"))]
5796 store_feed_generation: u64,
5797 surface_format: wgpu::TextureFormat,
5798 adapter_backend: wgpu::Backend,
5799 shape_batch_limits: ShapeBatchLimits,
5800 pipeline_cache: Option<wgpu::PipelineCache>,
5806 pipeline: PassPipeline,
5807 pipeline_dst_out: PassPipeline,
5808 pipeline_solid: PassPipeline,
5810 #[cfg(not(target_arch = "wasm32"))]
5814 mesh_pipeline: PassPipeline,
5815 #[cfg(not(target_arch = "wasm32"))]
5821 instanced_quads: Option<InstancedQuadPipelines>,
5822 uniform_bind_group_layout: wgpu::BindGroupLayout,
5823 shape_bind_group_layout: wgpu::BindGroupLayout,
5824 dummy_paint_buffer: Option<wgpu::Buffer>,
5827 identity_similarity_buffer: wgpu::Buffer,
5830 #[cfg(not(target_arch = "wasm32"))]
5831 replay_slots: ReplaySlotStore,
5832 image_pipeline: PassPipeline,
5833 image_pipeline_dst_out: PassPipeline,
5834 glyph_atlas_pipeline: PassPipeline,
5835 #[cfg(not(target_arch = "wasm32"))]
5836 retained_glyph_atlas_pipeline: PassPipeline,
5837 image_bind_group_layout: wgpu::BindGroupLayout,
5838 #[cfg(not(target_arch = "wasm32"))]
5839 retained_glyph_uniform_bind_group_layout: wgpu::BindGroupLayout,
5840 image_nearest_sampler: wgpu::Sampler,
5841 image_linear_sampler: wgpu::Sampler,
5842 text_fonts: SoftwareTextFontSet,
5843 #[cfg(not(target_arch = "wasm32"))]
5845 upload_buffer: wgpu::Buffer,
5846 #[cfg(not(target_arch = "wasm32"))]
5847 uniform_buffer: wgpu::Buffer,
5848 #[cfg(not(target_arch = "wasm32"))]
5849 uniform_bind_group: wgpu::BindGroup,
5850 #[cfg(not(target_arch = "wasm32"))]
5851 shape_buffers: ShapeBatchBuffers,
5852 #[cfg(not(target_arch = "wasm32"))]
5853 image_vertex_buffer: wgpu::Buffer,
5854 #[cfg(not(target_arch = "wasm32"))]
5855 image_index_buffer: wgpu::Buffer,
5856 #[cfg(not(target_arch = "wasm32"))]
5857 retained_glyph_uniform_buffer: wgpu::Buffer,
5858 #[cfg(not(target_arch = "wasm32"))]
5859 retained_glyph_uniform_bind_group: wgpu::BindGroup,
5860 #[cfg(not(target_arch = "wasm32"))]
5861 retained_glyph_uniform_stride: u64,
5862 #[cfg(not(target_arch = "wasm32"))]
5863 retained_glyph_uniform_capacity: usize,
5864 #[cfg(not(target_arch = "wasm32"))]
5865 retained_glyph_uniform_cursor: usize,
5866 #[cfg(target_arch = "wasm32")]
5867 wasm_uniform_batches: Vec<UniformBatchBuffer>,
5868 #[cfg(target_arch = "wasm32")]
5869 wasm_uniform_batch_cursor: usize,
5870 #[cfg(target_arch = "wasm32")]
5871 wasm_shape_batches: Vec<ShapeBatchBuffers>,
5872 #[cfg(target_arch = "wasm32")]
5873 wasm_shape_batch_cursor: usize,
5874 #[cfg(target_arch = "wasm32")]
5875 wasm_image_batches: Vec<ImageBatchBuffers>,
5876 #[cfg(target_arch = "wasm32")]
5877 wasm_image_batch_cursor: usize,
5878 image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
5879 image_texture_cache_bytes: usize,
5881 text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
5882 text_glyph_atlas: TextGlyphAtlas,
5883 text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
5884 #[cfg(not(target_arch = "wasm32"))]
5885 text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedGpuTextGlyphRun>,
5886 text_glyph_mask_cache: SoftwareGlyphRasterCache,
5887 text_line_index_cache: TextLineIndexCache,
5888 scratch_shape_data: Vec<ShapeData>,
5889 scratch_gradients: Vec<GradientStop>,
5890 scratch_image_vertices: Vec<Vertex>,
5891 scratch_image_indices: Vec<u32>,
5892 scratch_image_cmds: Vec<ImageDrawCmd>,
5893 scratch_glyph_cmds: Vec<GlyphDrawCmd>,
5894 scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
5895 scratch_text_glyph_placements: Vec<SoftwareGlyphAtlasPlacement>,
5896 scratch_text_glyph_quads: Vec<CachedTextGlyphQuad>,
5897 scratch_segment_items: Vec<(usize, SegmentDrawItem)>,
5898 scratch_effect_ranges: Vec<Range<usize>>,
5899 scratch_layer_events: Vec<LayerEvent>,
5900 staged_uploads: StagedBufferUploads,
5901 frame_graph_executor: WgpuFrameGraphExecutor,
5902 deferred_offscreen_releases: Vec<OffscreenTarget>,
5903 effect_renderer: EffectRenderer,
5904 layer_surface_cache: LayerSurfaceCache,
5905 observed_scene_range_cache_misses: BoundedLruCache<LayerRasterCacheKey, ()>,
5906 shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
5907 shadow_surface_cache_bytes: u64,
5908 frame_stats: gpu_stats::FrameStats,
5909 last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
5910 pending_frame_warmup_frames: u8,
5911 frame_count: u64,
5912 gpu_stats_enabled: bool,
5913 warning_state: RendererWarningState,
5914 #[cfg(not(target_arch = "wasm32"))]
5915 replay_upload_stats: ReplayUploadStats,
5916 #[cfg(not(target_arch = "wasm32"))]
5917 segment_encode_stats: SegmentEncodeStats,
5918 #[cfg(not(target_arch = "wasm32"))]
5925 replay_color_patches: Vec<crate::scene::ColorPatch>,
5926 #[cfg(not(target_arch = "wasm32"))]
5930 color_patch_scratch: Vec<crate::scene::ColorPatch>,
5931 #[cfg(not(target_arch = "wasm32"))]
5937 replay_capture_shape_scratch: Vec<ShapeData>,
5938 #[cfg(not(target_arch = "wasm32"))]
5940 replay_capture_gradient_scratch: Vec<GradientStop>,
5941 #[cfg(not(target_arch = "wasm32"))]
5946 replay_ack_confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
5947 #[cfg(not(target_arch = "wasm32"))]
5952 replay_generation_drops: u64,
5953 #[cfg(not(target_arch = "wasm32"))]
5956 retained_bundle_cache: RetainedBundleCache,
5957 #[cfg(not(target_arch = "wasm32"))]
5962 rim_mesh_vertices: Vec<MeshVertex>,
5963 #[cfg(not(target_arch = "wasm32"))]
5964 rim_mesh_indices: Vec<u32>,
5965 #[cfg(not(target_arch = "wasm32"))]
5971 rim_mesh_vertex_buffer: Option<wgpu::Buffer>,
5972 #[cfg(not(target_arch = "wasm32"))]
5973 rim_mesh_index_buffer: Option<wgpu::Buffer>,
5974 #[cfg(not(target_arch = "wasm32"))]
5977 rim_mesh_uploaded_vertices: usize,
5978 #[cfg(not(target_arch = "wasm32"))]
5979 rim_mesh_uploaded_indices: usize,
5980 #[cfg(not(target_arch = "wasm32"))]
5983 rim_meshes_emitted: u64,
5984 #[cfg(not(target_arch = "wasm32"))]
5987 fill_area_diag: FillAreaDiag,
5988 #[cfg(not(target_arch = "wasm32"))]
5992 static_span: StaticSpanCache,
5993 #[cfg(not(target_arch = "wasm32"))]
5998 segment_surfaces: SegmentSurfaceCache,
5999 #[cfg(not(target_arch = "wasm32"))]
6004 display_clip: DisplayClipState,
6005}
6006
6007#[cfg(not(target_arch = "wasm32"))]
6010type DisplayClipResourceKey = ((u32, u32), DisplayVisibleRegion);
6011
6012#[cfg(not(target_arch = "wasm32"))]
6014struct DisplayClipState {
6015 visible_region: DisplayVisibleRegion,
6021 frame_root_view: Option<wgpu::TextureView>,
6027 pass_depth: Cell<bool>,
6032 resources: Option<(DisplayClipResourceKey, Option<DisplayClipResources>)>,
6037 occluder_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
6038}
6039
6040#[cfg(not(target_arch = "wasm32"))]
6041impl DisplayClipState {
6042 fn new() -> Self {
6043 Self {
6044 visible_region: DisplayVisibleRegion::Full,
6045 frame_root_view: None,
6046 pass_depth: Cell::new(false),
6047 resources: None,
6048 occluder_pipeline: LazyGpuResource::new("display-clip/occluder"),
6049 }
6050 }
6051}
6052
6053#[cfg(not(target_arch = "wasm32"))]
6055struct DisplayClipResources {
6056 depth_view: wgpu::TextureView,
6059 occluder_vertex_buffer: wgpu::Buffer,
6062 occluder_vertex_count: u32,
6063}
6064
6065#[cfg(not(target_arch = "wasm32"))]
6071#[derive(Default)]
6072struct ReplayUploadStats {
6073 calls: u64,
6074 patched_calls: u64,
6075 patches: u64,
6076 slots: u64,
6077 records: u64,
6078 bytes: u64,
6079 ideal_bytes: u64,
6080 max_frame_bytes: u64,
6081}
6082
6083#[cfg(not(target_arch = "wasm32"))]
6084impl ReplayUploadStats {
6085 const REPORT_CALLS: u64 = 1024;
6095
6096 fn note_frame(&mut self, patches: u64, slots: u64, records: u64, bytes: u64, ideal: u64) {
6097 self.calls += 1;
6098 if patches > 0 {
6099 self.patched_calls += 1;
6100 self.patches += patches;
6101 self.slots += slots;
6102 self.records += records;
6103 self.bytes += bytes;
6104 self.ideal_bytes += ideal;
6105 self.max_frame_bytes = self.max_frame_bytes.max(bytes);
6106 }
6107 if self.calls >= Self::REPORT_CALLS {
6108 let patched = self.patched_calls.max(1);
6109 log::warn!(
6110 "[replay-upload] {} patched of {} drains: avg {:.1} KB/frame (max {:.1} KB), \
6111 color-only would be {:.1} KB/frame; avg {} patches over {} records in {} slots",
6112 self.patched_calls,
6113 self.calls,
6114 self.bytes as f64 / patched as f64 / 1024.0,
6115 self.max_frame_bytes as f64 / 1024.0,
6116 self.ideal_bytes as f64 / patched as f64 / 1024.0,
6117 self.patches / patched,
6118 self.records / patched,
6119 self.slots / patched,
6120 );
6121 *self = Self::default();
6122 }
6123 }
6124}
6125
6126#[cfg(not(target_arch = "wasm32"))]
6133#[derive(Default)]
6134struct SegmentEncodeStats {
6135 calls: u64,
6136 partitions: u64,
6137 max_partitions: u64,
6138 encode_micros: u64,
6139 max_call_micros: u64,
6140}
6141
6142#[cfg(not(target_arch = "wasm32"))]
6143impl SegmentEncodeStats {
6144 const REPORT_CALLS: u64 = 1024;
6145
6146 fn note_call(&mut self, partitions: u64, micros: u64) {
6147 self.calls += 1;
6148 self.partitions += partitions;
6149 self.max_partitions = self.max_partitions.max(partitions);
6150 self.encode_micros += micros;
6151 self.max_call_micros = self.max_call_micros.max(micros);
6152 if self.calls >= Self::REPORT_CALLS {
6153 log::warn!(
6154 "[segment-encode] {} chunks: avg {:.1} partitions (max {}), \
6155 avg {:.2} ms encode (max {:.2})",
6156 self.calls,
6157 self.partitions as f64 / self.calls as f64,
6158 self.max_partitions,
6159 self.encode_micros as f64 / self.calls as f64 / 1000.0,
6160 self.max_call_micros as f64 / 1000.0,
6161 );
6162 *self = Self::default();
6163 }
6164 }
6165}
6166
6167fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
6168 let filter = match sampling {
6169 ImageSampling::Nearest => wgpu::FilterMode::Nearest,
6170 ImageSampling::Linear => wgpu::FilterMode::Linear,
6171 };
6172 wgpu::SamplerDescriptor {
6173 label: Some(match sampling {
6174 ImageSampling::Nearest => "Nearest Image Sampler",
6175 ImageSampling::Linear => "Linear Image Sampler",
6176 }),
6177 address_mode_u: wgpu::AddressMode::ClampToEdge,
6178 address_mode_v: wgpu::AddressMode::ClampToEdge,
6179 address_mode_w: wgpu::AddressMode::ClampToEdge,
6180 mag_filter: filter,
6181 min_filter: filter,
6182 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
6183 ..Default::default()
6184 }
6185}
6186
6187#[cfg(test)]
6188fn layer_raster_cache_candidate(
6189 layer: &LayerNode,
6190 root_scale: f32,
6191 has_backdrop_underlay: bool,
6192 allow_runtime_cache: bool,
6193) -> Option<(LayerRasterCacheKey, Rect)> {
6194 let mut layer_surface_requirements_cache = HashMap::new();
6195 let surface_requirements =
6196 layer_surface_requirements_cached(layer, &mut layer_surface_requirements_cache);
6197 let runtime_cache_is_safe = allow_runtime_cache
6198 && surface_requirements
6199 .surface_requirements
6200 .has_isolating_requirement()
6201 && !surface_requirements.contains_runtime_shader;
6202 let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
6203 || (allow_runtime_cache && surface_requirements.has_renderer_forced_surface())
6204 || runtime_cache_is_safe;
6205 if !cache_is_allowed {
6206 return None;
6207 }
6208 if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
6209 return None;
6210 }
6211 if surface_requirements.contains_runtime_shader {
6214 return None;
6215 }
6216
6217 let logical_rect = estimate_layer_surface_rect(layer);
6218 let pixel_size = surface_target_size(logical_rect, root_scale, u32::MAX);
6219 Some((
6220 LayerRasterCacheKey::new(
6221 layer.node_id,
6222 layer.target_content_hash(),
6223 layer.effect_hash(),
6224 logical_rect,
6225 pixel_size,
6226 ScaleBucket::from_scale(root_scale),
6227 ),
6228 logical_rect,
6229 ))
6230}
6231
6232impl GpuRenderer {
6233 #[allow(clippy::too_many_arguments)]
6234 pub fn new(
6235 device: Arc<wgpu::Device>,
6236 queue: Arc<wgpu::Queue>,
6237 surface_format: wgpu::TextureFormat,
6238 adapter_backend: wgpu::Backend,
6239 adapter_downlevel: wgpu::DownlevelFlags,
6245 text_fonts: SoftwareTextFontSet,
6246 renderer_epoch: u64,
6247 store_feed_generation: u64,
6248 ) -> Self {
6249 #[cfg(target_arch = "wasm32")]
6250 let _ = store_feed_generation;
6251 let construction_started = Instant::now();
6259 let device_errors = Arc::new(DeviceErrorSentry::default());
6265 if survive_gpu_errors_enabled() {
6266 let sentry = Arc::clone(&device_errors);
6267 device.on_uncaptured_error(Arc::new(move |error| sentry.record(&error)));
6268 }
6269 let shape_batch_limits = ShapeBatchLimits::for_device(&device, adapter_downlevel);
6270 let uniform_bind_group_layout =
6271 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6272 label: Some("Uniform Bind Group Layout"),
6273 entries: &[wgpu::BindGroupLayoutEntry {
6274 binding: 0,
6275 visibility: wgpu::ShaderStages::VERTEX,
6276 ty: wgpu::BindingType::Buffer {
6277 ty: wgpu::BufferBindingType::Uniform,
6278 has_dynamic_offset: false,
6279 min_binding_size: None,
6280 },
6281 count: None,
6282 }],
6283 });
6284 #[cfg(not(target_arch = "wasm32"))]
6285 let retained_glyph_uniform_bind_group_layout =
6286 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6287 label: Some("Retained Glyph Dynamic Uniform Bind Group Layout"),
6288 entries: &[wgpu::BindGroupLayoutEntry {
6289 binding: 0,
6290 visibility: wgpu::ShaderStages::VERTEX,
6291 ty: wgpu::BindingType::Buffer {
6292 ty: wgpu::BufferBindingType::Uniform,
6293 has_dynamic_offset: true,
6294 min_binding_size: wgpu::BufferSize::new(
6295 std::mem::size_of::<Uniforms>() as u64
6296 ),
6297 },
6298 count: None,
6299 }],
6300 });
6301
6302 let mut shape_bind_group_layout_entries = vec![
6312 wgpu::BindGroupLayoutEntry {
6313 binding: 0,
6314 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
6315 ty: wgpu::BindingType::Buffer {
6316 ty: shape_batch_limits.data_binding_type(),
6317 has_dynamic_offset: false,
6318 min_binding_size: None,
6319 },
6320 count: None,
6321 },
6322 wgpu::BindGroupLayoutEntry {
6323 binding: 1,
6324 visibility: wgpu::ShaderStages::FRAGMENT,
6325 ty: wgpu::BindingType::Buffer {
6326 ty: shape_batch_limits.data_binding_type(),
6327 has_dynamic_offset: false,
6328 min_binding_size: None,
6329 },
6330 count: None,
6331 },
6332 wgpu::BindGroupLayoutEntry {
6337 binding: 2,
6338 visibility: wgpu::ShaderStages::VERTEX,
6339 ty: wgpu::BindingType::Buffer {
6340 ty: wgpu::BufferBindingType::Uniform,
6341 has_dynamic_offset: true,
6342 min_binding_size: wgpu::BufferSize::new(
6343 std::mem::size_of::<SimilarityTransform>() as u64,
6344 ),
6345 },
6346 count: None,
6347 },
6348 ];
6349 if shape_batch_limits.storage {
6355 shape_bind_group_layout_entries.push(wgpu::BindGroupLayoutEntry {
6356 binding: 3,
6357 visibility: wgpu::ShaderStages::VERTEX,
6358 ty: wgpu::BindingType::Buffer {
6359 ty: wgpu::BufferBindingType::Storage { read_only: true },
6360 has_dynamic_offset: false,
6361 min_binding_size: None,
6362 },
6363 count: None,
6364 });
6365 }
6366 let shape_bind_group_layout =
6367 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6368 label: Some("Shape Bind Group Layout"),
6369 entries: &shape_bind_group_layout_entries,
6370 });
6371
6372 let identity_similarity_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6373 label: Some("Identity Similarity Buffer"),
6374 size: std::mem::size_of::<SimilarityTransform>() as u64,
6375 usage: wgpu::BufferUsages::UNIFORM,
6376 mapped_at_creation: true,
6377 });
6378 identity_similarity_buffer
6379 .slice(..)
6380 .get_mapped_range_mut()
6381 .copy_from_slice(bytemuck::bytes_of(&SimilarityTransform::IDENTITY));
6382 identity_similarity_buffer.unmap();
6383
6384 let dummy_paint_buffer = shape_batch_limits.storage.then(|| {
6389 device.create_buffer(&wgpu::BufferDescriptor {
6390 label: Some("Dummy Paint Buffer"),
6391 size: std::mem::size_of::<[f32; 4]>() as u64,
6392 usage: wgpu::BufferUsages::STORAGE,
6393 mapped_at_creation: false,
6394 })
6395 });
6396 #[cfg(not(target_arch = "wasm32"))]
6397 let replay_slot_store = ReplaySlotStore::new(&device);
6398
6399 let pipeline = PassPipeline::new("shape/src-over", "shape/src-over-depth");
6400 let pipeline_dst_out = PassPipeline::new("shape/dst-out", "shape/dst-out-depth");
6401 let pipeline_solid =
6402 PassPipeline::new("shape/solid-src-over", "shape/solid-src-over-depth");
6403 #[cfg(not(target_arch = "wasm32"))]
6404 let mesh_pipeline = PassPipeline::new("shape/mesh", "shape/mesh-depth");
6405 #[cfg(not(target_arch = "wasm32"))]
6411 let instanced_quads =
6412 (shape_batch_limits.storage && instanced_quads_enabled()).then(|| {
6413 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6414 label: Some("Instanced Quad Index Buffer"),
6415 size: std::mem::size_of_val(&INSTANCED_QUAD_INDICES) as u64,
6416 usage: wgpu::BufferUsages::INDEX,
6417 mapped_at_creation: true,
6418 });
6419 index_buffer
6420 .slice(..)
6421 .get_mapped_range_mut()
6422 .copy_from_slice(bytemuck::cast_slice(&INSTANCED_QUAD_INDICES));
6423 index_buffer.unmap();
6424 InstancedQuadPipelines {
6425 pipeline: PassPipeline::new(
6426 "shape/instanced-src-over",
6427 "shape/instanced-src-over-depth",
6428 ),
6429 pipeline_dst_out: PassPipeline::new(
6430 "shape/instanced-dst-out",
6431 "shape/instanced-dst-out-depth",
6432 ),
6433 pipeline_solid: PassPipeline::new(
6434 "shape/instanced-solid",
6435 "shape/instanced-solid-depth",
6436 ),
6437 index_buffer,
6438 }
6439 });
6440
6441 let image_bind_group_layout =
6442 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
6443 label: Some("Image Texture Bind Group Layout"),
6444 entries: &[
6445 wgpu::BindGroupLayoutEntry {
6446 binding: 0,
6447 visibility: wgpu::ShaderStages::FRAGMENT,
6448 ty: wgpu::BindingType::Texture {
6449 multisampled: false,
6450 view_dimension: wgpu::TextureViewDimension::D2,
6451 sample_type: wgpu::TextureSampleType::Float { filterable: true },
6452 },
6453 count: None,
6454 },
6455 wgpu::BindGroupLayoutEntry {
6456 binding: 1,
6457 visibility: wgpu::ShaderStages::FRAGMENT,
6458 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
6459 count: None,
6460 },
6461 ],
6462 });
6463
6464 let image_pipeline = PassPipeline::new("image/src-over", "image/src-over-depth");
6465 let image_pipeline_dst_out = PassPipeline::new("image/dst-out", "image/dst-out-depth");
6466 let glyph_atlas_pipeline = PassPipeline::new("glyph/shared", "glyph/shared-depth");
6467 #[cfg(not(target_arch = "wasm32"))]
6468 let retained_glyph_atlas_pipeline =
6469 PassPipeline::new("glyph/retained", "glyph/retained-depth");
6470
6471 #[cfg(not(target_arch = "wasm32"))]
6472 let upload_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6473 label: Some("Frame Upload Buffer"),
6474 size: INITIAL_UPLOAD_BUFFER_BYTES,
6475 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
6476 mapped_at_creation: false,
6477 });
6478
6479 #[cfg(not(target_arch = "wasm32"))]
6480 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6481 label: Some("Uniform Buffer"),
6482 size: std::mem::size_of::<Uniforms>() as u64,
6483 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
6484 mapped_at_creation: false,
6485 });
6486
6487 #[cfg(not(target_arch = "wasm32"))]
6488 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
6489 label: Some("Uniform Bind Group"),
6490 layout: &uniform_bind_group_layout,
6491 entries: &[wgpu::BindGroupEntry {
6492 binding: 0,
6493 resource: uniform_buffer.as_entire_binding(),
6494 }],
6495 });
6496
6497 #[cfg(not(target_arch = "wasm32"))]
6498 let shape_buffers = ShapeBatchBuffers::new(
6499 &device,
6500 &shape_bind_group_layout,
6501 &identity_similarity_buffer,
6502 dummy_paint_buffer.as_ref(),
6503 shape_batch_limits,
6504 );
6505
6506 let image_nearest_sampler =
6507 device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
6508 let image_linear_sampler =
6509 device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
6510 let text_glyph_atlas = TextGlyphAtlas::new(
6511 &device,
6512 &image_bind_group_layout,
6513 &image_nearest_sampler,
6514 TEXT_GLYPH_ATLAS_MIN_SIZE,
6515 );
6516
6517 #[cfg(not(target_arch = "wasm32"))]
6518 let image_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6519 label: Some("Image Vertex Buffer"),
6520 size: (std::mem::size_of::<Vertex>() * 4) as u64,
6521 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
6522 mapped_at_creation: false,
6523 });
6524
6525 #[cfg(not(target_arch = "wasm32"))]
6526 let image_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6527 label: Some("Image Index Buffer"),
6528 size: (std::mem::size_of::<u32>() * 6) as u64,
6529 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
6530 mapped_at_creation: false,
6531 });
6532 #[cfg(not(target_arch = "wasm32"))]
6533 let retained_glyph_uniform_stride = align_usize_to(
6534 std::mem::size_of::<Uniforms>(),
6535 (device.limits().min_uniform_buffer_offset_alignment as usize)
6536 .max(wgpu::COPY_BUFFER_ALIGNMENT as usize),
6537 ) as u64;
6538 #[cfg(not(target_arch = "wasm32"))]
6539 let retained_glyph_uniform_capacity = INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS;
6540 #[cfg(not(target_arch = "wasm32"))]
6541 let retained_glyph_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
6542 label: Some("Retained Glyph Uniform Buffer"),
6543 size: retained_glyph_uniform_stride * retained_glyph_uniform_capacity as u64,
6544 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
6545 mapped_at_creation: false,
6546 });
6547 #[cfg(not(target_arch = "wasm32"))]
6548 let retained_glyph_uniform_bind_group =
6549 device.create_bind_group(&wgpu::BindGroupDescriptor {
6550 label: Some("Retained Glyph Uniform Bind Group"),
6551 layout: &retained_glyph_uniform_bind_group_layout,
6552 entries: &[wgpu::BindGroupEntry {
6553 binding: 0,
6554 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
6555 buffer: &retained_glyph_uniform_buffer,
6556 offset: 0,
6557 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
6558 }),
6559 }],
6560 });
6561
6562 #[cfg(not(target_arch = "wasm32"))]
6569 let pipeline_cache = crate::pipeline_disk_cache::load(&device);
6570 #[cfg(target_arch = "wasm32")]
6571 let pipeline_cache: Option<wgpu::PipelineCache> = None;
6572 #[cfg(not(target_arch = "wasm32"))]
6573 if let Some(cache) = pipeline_cache.clone() {
6574 crate::pipeline_disk_cache::spawn_persist_schedule(cache);
6575 spawn_pipeline_prewarm(PipelinePrewarmInputs {
6576 device: Arc::clone(&device),
6577 cache: pipeline_cache.clone(),
6578 surface_format,
6579 uniform_layout: uniform_bind_group_layout.clone(),
6580 shape_layout: shape_bind_group_layout.clone(),
6581 image_layout: image_bind_group_layout.clone(),
6582 batch_limits: shape_batch_limits,
6583 instanced: instanced_quads.is_some(),
6584 });
6585 }
6586
6587 let effects_started = Instant::now();
6588 let effect_renderer = EffectRenderer::new(
6589 &device,
6590 pipeline_cache.clone(),
6591 surface_format,
6592 adapter_backend,
6593 );
6594 let effects_ms = instant_ms(effects_started, Instant::now());
6595
6596 let renderer = Self {
6597 device,
6598 queue,
6599 device_errors,
6600 renderer_epoch,
6601 #[cfg(not(target_arch = "wasm32"))]
6602 store_feed_generation,
6603 surface_format,
6604 adapter_backend,
6605 shape_batch_limits,
6606 pipeline_cache,
6607 pipeline,
6608 pipeline_dst_out,
6609 pipeline_solid,
6610 #[cfg(not(target_arch = "wasm32"))]
6611 mesh_pipeline,
6612 #[cfg(not(target_arch = "wasm32"))]
6613 instanced_quads,
6614 uniform_bind_group_layout,
6615 shape_bind_group_layout,
6616 dummy_paint_buffer,
6617 identity_similarity_buffer,
6618 #[cfg(not(target_arch = "wasm32"))]
6619 replay_slots: replay_slot_store,
6620 image_pipeline,
6621 image_pipeline_dst_out,
6622 glyph_atlas_pipeline,
6623 #[cfg(not(target_arch = "wasm32"))]
6624 retained_glyph_atlas_pipeline,
6625 image_bind_group_layout,
6626 #[cfg(not(target_arch = "wasm32"))]
6627 retained_glyph_uniform_bind_group_layout,
6628 image_nearest_sampler,
6629 image_linear_sampler,
6630 text_fonts,
6631 #[cfg(not(target_arch = "wasm32"))]
6632 upload_buffer,
6633 #[cfg(not(target_arch = "wasm32"))]
6634 uniform_buffer,
6635 #[cfg(not(target_arch = "wasm32"))]
6636 uniform_bind_group,
6637 #[cfg(not(target_arch = "wasm32"))]
6638 shape_buffers,
6639 #[cfg(not(target_arch = "wasm32"))]
6640 image_vertex_buffer,
6641 #[cfg(not(target_arch = "wasm32"))]
6642 image_index_buffer,
6643 #[cfg(not(target_arch = "wasm32"))]
6644 retained_glyph_uniform_buffer,
6645 #[cfg(not(target_arch = "wasm32"))]
6646 retained_glyph_uniform_bind_group,
6647 #[cfg(not(target_arch = "wasm32"))]
6648 retained_glyph_uniform_stride,
6649 #[cfg(not(target_arch = "wasm32"))]
6650 retained_glyph_uniform_capacity,
6651 #[cfg(not(target_arch = "wasm32"))]
6652 retained_glyph_uniform_cursor: 0,
6653 #[cfg(target_arch = "wasm32")]
6654 wasm_uniform_batches: Vec::new(),
6655 #[cfg(target_arch = "wasm32")]
6656 wasm_uniform_batch_cursor: 0,
6657 #[cfg(target_arch = "wasm32")]
6658 wasm_shape_batches: Vec::new(),
6659 #[cfg(target_arch = "wasm32")]
6660 wasm_shape_batch_cursor: 0,
6661 #[cfg(target_arch = "wasm32")]
6662 wasm_image_batches: Vec::new(),
6663 #[cfg(target_arch = "wasm32")]
6664 wasm_image_batch_cursor: 0,
6665 image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
6666 MAX_TEXTURE_CACHE_ITEMS,
6667 ),
6668 image_texture_cache_bytes: 0,
6669 text_image_cache: BoundedLruCache::with_capacity_at_least_one(
6670 MAX_TEXT_IMAGE_CACHE_ITEMS,
6671 ),
6672 text_glyph_atlas,
6673 text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
6674 MAX_TEXT_GLYPH_RUN_CACHE_ITEMS,
6675 ),
6676 #[cfg(not(target_arch = "wasm32"))]
6677 text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
6678 MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
6679 ),
6680 text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
6681 MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
6682 ),
6683 text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_CACHE_ITEMS),
6684 scratch_shape_data: Vec::new(),
6685 scratch_gradients: Vec::new(),
6686 scratch_image_vertices: Vec::new(),
6687 scratch_image_indices: Vec::new(),
6688 scratch_image_cmds: Vec::new(),
6689 scratch_glyph_cmds: Vec::new(),
6690 scratch_text_glyph_run: Vec::new(),
6691 scratch_text_glyph_placements: Vec::new(),
6692 scratch_text_glyph_quads: Vec::new(),
6693 scratch_segment_items: Vec::new(),
6694 scratch_effect_ranges: Vec::new(),
6695 scratch_layer_events: Vec::new(),
6696 staged_uploads: StagedBufferUploads::default(),
6697 frame_graph_executor: WgpuFrameGraphExecutor::new(),
6698 deferred_offscreen_releases: Vec::new(),
6699 effect_renderer,
6700 layer_surface_cache: LayerSurfaceCache::new(),
6701 observed_scene_range_cache_misses: BoundedLruCache::with_capacity_at_least_one(
6702 MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES,
6703 ),
6704 shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
6705 MAX_SHADOW_SURFACE_CACHE_ITEMS,
6706 ),
6707 shadow_surface_cache_bytes: 0,
6708 frame_stats: gpu_stats::FrameStats::default(),
6709 last_frame_stats: None,
6710 pending_frame_warmup_frames: 0,
6711 frame_count: 0,
6712 gpu_stats_enabled: gpu_stats_enabled(),
6713 warning_state: RendererWarningState::default(),
6714 #[cfg(not(target_arch = "wasm32"))]
6715 replay_upload_stats: ReplayUploadStats::default(),
6716 #[cfg(not(target_arch = "wasm32"))]
6717 segment_encode_stats: SegmentEncodeStats::default(),
6718 #[cfg(not(target_arch = "wasm32"))]
6719 replay_color_patches: Vec::new(),
6720 #[cfg(not(target_arch = "wasm32"))]
6721 color_patch_scratch: Vec::new(),
6722 #[cfg(not(target_arch = "wasm32"))]
6723 replay_capture_shape_scratch: Vec::new(),
6724 #[cfg(not(target_arch = "wasm32"))]
6725 replay_capture_gradient_scratch: Vec::new(),
6726 #[cfg(not(target_arch = "wasm32"))]
6727 replay_ack_confirmations: Vec::new(),
6728 #[cfg(not(target_arch = "wasm32"))]
6729 replay_generation_drops: 0,
6730 #[cfg(not(target_arch = "wasm32"))]
6731 retained_bundle_cache: RetainedBundleCache::new(),
6732 #[cfg(not(target_arch = "wasm32"))]
6733 rim_mesh_vertices: Vec::new(),
6734 #[cfg(not(target_arch = "wasm32"))]
6735 rim_mesh_indices: Vec::new(),
6736 #[cfg(not(target_arch = "wasm32"))]
6737 rim_mesh_vertex_buffer: None,
6738 #[cfg(not(target_arch = "wasm32"))]
6739 rim_mesh_index_buffer: None,
6740 #[cfg(not(target_arch = "wasm32"))]
6741 rim_mesh_uploaded_vertices: 0,
6742 #[cfg(not(target_arch = "wasm32"))]
6743 rim_mesh_uploaded_indices: 0,
6744 #[cfg(not(target_arch = "wasm32"))]
6745 rim_meshes_emitted: 0,
6746 #[cfg(not(target_arch = "wasm32"))]
6747 fill_area_diag: FillAreaDiag::default(),
6748 #[cfg(not(target_arch = "wasm32"))]
6749 static_span: StaticSpanCache::default(),
6750 #[cfg(not(target_arch = "wasm32"))]
6751 segment_surfaces: SegmentSurfaceCache::default(),
6752 #[cfg(not(target_arch = "wasm32"))]
6753 display_clip: DisplayClipState::new(),
6754 };
6755 log::info!(
6756 "[gpu-init] {:?} renderer ready in {:.1} ms (effects {:.1} ms); \
6757 pipelines build on first use",
6758 adapter_backend,
6759 instant_ms(construction_started, Instant::now()),
6760 effects_ms,
6761 );
6762 renderer
6763 }
6764
6765 #[cfg(not(target_arch = "wasm32"))]
6771 pub fn set_display_visible_region(&mut self, region: DisplayVisibleRegion) {
6772 self.display_clip.visible_region = region;
6773 }
6774
6775 #[cfg(not(target_arch = "wasm32"))]
6779 fn pass_depth(&self) -> bool {
6780 self.display_clip.pass_depth.get()
6781 }
6782
6783 #[cfg(target_arch = "wasm32")]
6784 fn pass_depth(&self) -> bool {
6785 false
6786 }
6787
6788 #[cfg(not(target_arch = "wasm32"))]
6796 fn display_clip_pass_depth_view(
6797 &mut self,
6798 target_view: &wgpu::TextureView,
6799 width: u32,
6800 height: u32,
6801 ) -> Option<wgpu::TextureView> {
6802 if !self.display_clip.visible_region.cullable() {
6803 return None;
6804 }
6805 if self.display_clip.frame_root_view.as_ref() != Some(target_view) {
6806 return None;
6807 }
6808 if !display_clip_cull_enabled() {
6809 return None;
6810 }
6811 self.ensure_display_clip_resources(width, height)
6812 }
6813
6814 #[cfg(not(target_arch = "wasm32"))]
6820 fn ensure_display_clip_resources(
6821 &mut self,
6822 width: u32,
6823 height: u32,
6824 ) -> Option<wgpu::TextureView> {
6825 let region = self.display_clip.visible_region;
6826 let key = ((width, height), region);
6827 if let Some((cached_key, resources)) = &self.display_clip.resources {
6828 if *cached_key == key {
6829 return resources
6830 .as_ref()
6831 .map(|resources| resources.depth_view.clone());
6832 }
6833 }
6834 let built = display_clip::tessellate_complement(region, width, height).map(|mesh| {
6835 let occluder_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
6836 label: Some("Display Clip Occluder Vertices"),
6837 size: std::mem::size_of_val(mesh.vertices.as_slice()) as u64,
6838 usage: wgpu::BufferUsages::VERTEX,
6839 mapped_at_creation: true,
6840 });
6841 occluder_vertex_buffer
6842 .slice(..)
6843 .get_mapped_range_mut()
6844 .copy_from_slice(bytemuck::cast_slice(&mesh.vertices));
6845 occluder_vertex_buffer.unmap();
6846 let depth_texture = self.device.create_texture(&wgpu::TextureDescriptor {
6847 label: Some("Display Clip Depth"),
6848 size: wgpu::Extent3d {
6849 width,
6850 height,
6851 depth_or_array_layers: 1,
6852 },
6853 mip_level_count: 1,
6854 sample_count: 1,
6855 dimension: wgpu::TextureDimension::D2,
6856 format: display_clip::DISPLAY_CLIP_DEPTH_FORMAT,
6857 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
6858 view_formats: &[],
6859 });
6860 match region {
6864 DisplayVisibleRegion::InscribedCircle => log::info!(
6865 "[display-clip] round display: corner cull active ({} px masked) at {width}x{height}",
6866 mesh.masked_px,
6867 ),
6868 _ => log::info!(
6869 "[display-clip] visible-region cull active for {region:?} ({} px masked) at {width}x{height}",
6870 mesh.masked_px,
6871 ),
6872 }
6873 DisplayClipResources {
6874 depth_view: depth_texture.create_view(&wgpu::TextureViewDescriptor::default()),
6875 occluder_vertex_buffer,
6876 occluder_vertex_count: mesh.vertices.len() as u32,
6877 }
6878 });
6879 let view = built.as_ref().map(|resources| resources.depth_view.clone());
6880 self.display_clip.resources = Some((key, built));
6881 view
6882 }
6883
6884 #[cfg(not(target_arch = "wasm32"))]
6888 fn draw_display_clip_occluder(
6889 &self,
6890 render_pass: &mut wgpu::RenderPass<'_>,
6891 width: u32,
6892 height: u32,
6893 ) {
6894 let Some((((size_w, size_h), _), Some(resources))) = &self.display_clip.resources else {
6895 return;
6896 };
6897 debug_assert_eq!((*size_w, *size_h), (width, height));
6898 let pipeline =
6899 self.display_clip
6900 .occluder_pipeline
6901 .get_or_init(self.adapter_backend, || {
6902 create_display_clip_occluder_pipeline(
6903 &self.device,
6904 self.pipeline_cache.as_ref(),
6905 self.surface_format,
6906 )
6907 });
6908 render_pass.set_scissor_rect(0, 0, width, height);
6909 render_pass.set_pipeline(pipeline);
6910 render_pass.set_vertex_buffer(0, resources.occluder_vertex_buffer.slice(..));
6911 render_pass.draw(0..resources.occluder_vertex_count, 0..1);
6912 self.frame_stats.add_draw_calls(1);
6913 }
6914
6915 fn shape_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
6916 let resource = match blend_mode {
6917 BlendMode::DstOut => &self.pipeline_dst_out,
6918 _ => &self.pipeline,
6919 };
6920 resource.get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
6921 create_shape_pipeline(
6922 &self.device,
6923 self.pipeline_cache.as_ref(),
6924 self.surface_format,
6925 &self.uniform_bind_group_layout,
6926 &self.shape_bind_group_layout,
6927 blend_mode,
6928 self.shape_batch_limits,
6929 false,
6930 "vs_main",
6931 "fs_main",
6932 depth,
6933 )
6934 })
6935 }
6936
6937 fn shape_pipeline_solid(&self) -> &wgpu::RenderPipeline {
6946 self.pipeline_solid
6947 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
6948 let solid_trim = solid_trim_varyings_enabled();
6949 let (vertex_entry, fragment_entry) = if solid_trim {
6950 ("vs_solid", "fs_solid_trim")
6951 } else {
6952 ("vs_main", "fs_solid")
6953 };
6954 create_shape_pipeline(
6955 &self.device,
6956 self.pipeline_cache.as_ref(),
6957 self.surface_format,
6958 &self.uniform_bind_group_layout,
6959 &self.shape_bind_group_layout,
6960 BlendMode::SrcOver,
6961 self.shape_batch_limits,
6962 solid_trim,
6963 vertex_entry,
6964 fragment_entry,
6965 depth,
6966 )
6967 })
6968 }
6969
6970 #[cfg(not(target_arch = "wasm32"))]
6971 fn mesh_pipeline(&self) -> &wgpu::RenderPipeline {
6972 self.mesh_pipeline
6973 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
6974 create_mesh_shape_pipeline(
6975 &self.device,
6976 self.pipeline_cache.as_ref(),
6977 self.surface_format,
6978 &self.uniform_bind_group_layout,
6979 &self.shape_bind_group_layout,
6980 self.shape_batch_limits,
6981 depth,
6982 )
6983 })
6984 }
6985
6986 #[cfg(not(target_arch = "wasm32"))]
6987 fn instanced_pipeline<'a>(
6988 &'a self,
6989 instanced: &'a InstancedQuadPipelines,
6990 blend_mode: BlendMode,
6991 ) -> &'a wgpu::RenderPipeline {
6992 let resource = match blend_mode {
6993 BlendMode::DstOut => &instanced.pipeline_dst_out,
6994 _ => &instanced.pipeline,
6995 };
6996 resource.get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
6997 create_instanced_shape_pipeline(
6998 &self.device,
6999 self.pipeline_cache.as_ref(),
7000 self.surface_format,
7001 &self.uniform_bind_group_layout,
7002 &self.shape_bind_group_layout,
7003 blend_mode,
7004 self.shape_batch_limits,
7005 false,
7006 "vs_shape_instanced",
7007 "fs_main",
7008 depth,
7009 )
7010 })
7011 }
7012
7013 #[cfg(not(target_arch = "wasm32"))]
7017 fn instanced_pipeline_solid<'a>(
7018 &'a self,
7019 instanced: &'a InstancedQuadPipelines,
7020 ) -> &'a wgpu::RenderPipeline {
7021 instanced
7022 .pipeline_solid
7023 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7024 let solid_trim = solid_trim_varyings_enabled();
7025 let (vertex_entry, fragment_entry) = if solid_trim {
7026 ("vs_solid_instanced", "fs_solid_trim")
7027 } else {
7028 ("vs_shape_instanced", "fs_solid")
7029 };
7030 create_instanced_shape_pipeline(
7031 &self.device,
7032 self.pipeline_cache.as_ref(),
7033 self.surface_format,
7034 &self.uniform_bind_group_layout,
7035 &self.shape_bind_group_layout,
7036 BlendMode::SrcOver,
7037 self.shape_batch_limits,
7038 solid_trim,
7039 vertex_entry,
7040 fragment_entry,
7041 depth,
7042 )
7043 })
7044 }
7045
7046 fn image_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
7047 let resource = match blend_mode {
7048 BlendMode::DstOut => &self.image_pipeline_dst_out,
7049 _ => &self.image_pipeline,
7050 };
7051 resource.get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7052 create_image_pipeline(
7053 &self.device,
7054 self.pipeline_cache.as_ref(),
7055 self.surface_format,
7056 &self.uniform_bind_group_layout,
7057 &self.image_bind_group_layout,
7058 blend_mode,
7059 depth,
7060 )
7061 })
7062 }
7063
7064 fn glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
7065 self.glyph_atlas_pipeline
7066 .get_or_init(self.adapter_backend, self.pass_depth(), |depth| {
7067 create_glyph_atlas_pipeline(
7068 &self.device,
7069 self.pipeline_cache.as_ref(),
7070 self.surface_format,
7071 &self.uniform_bind_group_layout,
7072 &self.image_bind_group_layout,
7073 depth,
7074 )
7075 })
7076 }
7077
7078 #[cfg(not(target_arch = "wasm32"))]
7079 fn retained_glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
7080 self.retained_glyph_atlas_pipeline.get_or_init(
7081 self.adapter_backend,
7082 self.pass_depth(),
7083 |depth| {
7084 create_glyph_atlas_pipeline(
7085 &self.device,
7086 self.pipeline_cache.as_ref(),
7087 self.surface_format,
7088 &self.retained_glyph_uniform_bind_group_layout,
7089 &self.image_bind_group_layout,
7090 depth,
7091 )
7092 },
7093 )
7094 }
7095
7096 fn ensure_image_cached(&mut self, image: &ImageBitmap) -> Result<(), String> {
7097 if self.image_texture_cache.get(&image.id()).is_some() {
7098 return Ok(());
7099 }
7100
7101 let size = wgpu::Extent3d {
7102 width: image.width(),
7103 height: image.height(),
7104 depth_or_array_layers: 1,
7105 };
7106
7107 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
7108 label: Some("Image Texture"),
7109 size,
7110 mip_level_count: 1,
7111 sample_count: 1,
7112 dimension: wgpu::TextureDimension::D2,
7113 format: wgpu::TextureFormat::Rgba8Unorm,
7114 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
7115 view_formats: &[],
7116 });
7117
7118 let upload_stats = self.frame_graph_executor.upload_texture(
7119 &self.queue,
7120 wgpu::TexelCopyTextureInfo {
7121 texture: &texture,
7122 mip_level: 0,
7123 origin: wgpu::Origin3d::ZERO,
7124 aspect: wgpu::TextureAspect::All,
7125 },
7126 image.pixels(),
7127 wgpu::TexelCopyBufferLayout {
7128 offset: 0,
7129 bytes_per_row: Some(4 * image.width()),
7130 rows_per_image: Some(image.height()),
7131 },
7132 size,
7133 );
7134 self.frame_stats.record_command_stats(upload_stats);
7135
7136 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
7137 let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
7138 let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
7139
7140 let bytes = image.width() as usize * image.height() as usize * 4;
7141 if let Some(replaced) = self.image_texture_cache.put(
7142 image.id(),
7143 CachedImageTexture {
7144 _texture: texture,
7145 _view: view,
7146 nearest_bind_group,
7147 linear_bind_group,
7148 bytes,
7149 },
7150 ) {
7151 self.image_texture_cache_bytes = self
7152 .image_texture_cache_bytes
7153 .saturating_sub(replaced.bytes);
7154 }
7155 self.image_texture_cache_bytes += bytes;
7156 while self.image_texture_cache_bytes > MAX_IMAGE_TEXTURE_CACHE_BYTES
7159 && self.image_texture_cache.len() > 1
7160 {
7161 let Some((_, evicted)) = self.image_texture_cache.pop_lru() else {
7162 break;
7163 };
7164 self.image_texture_cache_bytes =
7165 self.image_texture_cache_bytes.saturating_sub(evicted.bytes);
7166 }
7167 Ok(())
7168 }
7169
7170 fn image_bind_group(
7171 &self,
7172 view: &wgpu::TextureView,
7173 sampler: &wgpu::Sampler,
7174 ) -> wgpu::BindGroup {
7175 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
7176 label: Some("Image Texture Bind Group"),
7177 layout: &self.image_bind_group_layout,
7178 entries: &[
7179 wgpu::BindGroupEntry {
7180 binding: 0,
7181 resource: wgpu::BindingResource::TextureView(view),
7182 },
7183 wgpu::BindGroupEntry {
7184 binding: 1,
7185 resource: wgpu::BindingResource::Sampler(sampler),
7186 },
7187 ],
7188 })
7189 }
7190
7191 fn max_texture_dim(&self) -> u32 {
7194 self.effect_renderer.max_texture_dim()
7195 }
7196
7197 fn acquire_offscreen(&mut self, width: u32, height: u32) -> OffscreenTarget {
7198 self.effect_renderer
7199 .acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
7200 }
7201
7202 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
7203 self.acquire_offscreen(width, height)
7204 }
7205
7206 fn transient_offscreen_descriptor(
7207 &self,
7208 label: &'static str,
7209 width: u32,
7210 height: u32,
7211 ) -> FrameTextureDescriptor {
7212 let max_texture_dim = self.max_texture_dim();
7213 FrameTextureDescriptor::render_attachment(
7214 label,
7215 width.min(max_texture_dim),
7216 height.min(max_texture_dim),
7217 self.surface_format,
7218 )
7219 }
7220
7221 fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
7222 self.deferred_offscreen_releases.push(target);
7223 }
7224
7225 fn flush_deferred_offscreen_releases(&mut self) {
7226 for target in self.deferred_offscreen_releases.drain(..) {
7227 self.effect_renderer.release_offscreen(target);
7228 }
7229 }
7230
7231 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
7232 if let LayerSurfaceTexture::Owned(target) = target {
7233 self.defer_offscreen_release(target);
7234 }
7235 }
7236
7237 fn cached_layer_surface(
7238 &mut self,
7239 key: &LayerRasterCacheKey,
7240 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
7241 self.layer_surface_cache.get(key, &self.frame_stats)
7242 }
7243
7244 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
7245 admit_layer_surface_cache_miss_impl(key, &mut self.observed_scene_range_cache_misses)
7246 }
7247
7248 fn insert_cached_layer_surface(
7249 &mut self,
7250 key: LayerRasterCacheKey,
7251 target: OffscreenTarget,
7252 logical_rect: Rect,
7253 ) -> Rc<OffscreenTarget> {
7254 self.layer_surface_cache
7255 .insert(key, target, logical_rect, &self.frame_stats)
7256 }
7257
7258 fn cached_shadow_surface(
7259 &mut self,
7260 key: &ShadowSurfaceCacheKey,
7261 ) -> Option<Rc<OffscreenTarget>> {
7262 self.shadow_surface_cache
7263 .get(key)
7264 .map(|cached| cached.target.clone())
7265 }
7266
7267 fn cached_shape_shadow_composite(
7268 &mut self,
7269 shadow: &ShadowDraw,
7270 width: u32,
7271 height: u32,
7272 root_scale: f32,
7273 ) -> Option<CachedShadowComposite> {
7274 if shadow.blur_radius <= 0.0 || shadow.shapes.is_empty() || !shadow.texts.is_empty() {
7275 return None;
7276 }
7277
7278 let plan = shape_shadow_surface_plan(
7279 &shadow.shapes,
7280 shadow.clip,
7281 shadow.blur_radius,
7282 width,
7283 height,
7284 root_scale,
7285 self.max_texture_dim(),
7286 )?;
7287 let key = shape_shadow_surface_cache_key(
7288 &shadow.shapes,
7289 &shadow.brushes,
7290 plan.source_device_bounds,
7291 plan.pixel_radius,
7292 root_scale,
7293 )?;
7294 let cached = self.cached_shadow_surface(&key)?;
7295 let viewport_offset = [plan.source_device_bounds.x, plan.source_device_bounds.y];
7296 self.frame_stats.record_shadow_shape_cache_hit(
7297 plan.source_device_bounds.width,
7298 plan.source_device_bounds.height,
7299 );
7300
7301 let clip_scissor = shadow
7302 .clip
7303 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
7304 let scissor = clip_scissor.or(plan.processing_scissor);
7305 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
7306 mask.rect[0] -= viewport_offset[0];
7307 mask.rect[1] -= viewport_offset[1];
7308 mask
7309 });
7310 let dest_viewport = Some((
7311 viewport_offset[0],
7312 viewport_offset[1],
7313 plan.source_device_bounds.width as f32,
7314 plan.source_device_bounds.height as f32,
7315 ));
7316
7317 Some(CachedShadowComposite {
7318 source: cached,
7319 scissor,
7320 rounded_mask,
7321 dest_viewport,
7322 })
7323 }
7324
7325 fn insert_cached_shadow_surface(
7326 &mut self,
7327 key: ShadowSurfaceCacheKey,
7328 target: OffscreenTarget,
7329 ) {
7330 let byte_size = offscreen_byte_size(target.width, target.height);
7331 while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
7332 let Some((_evicted_key, evicted_entry)) = self.shadow_surface_cache.pop_lru() else {
7333 break;
7334 };
7335 self.shadow_surface_cache_bytes = self
7336 .shadow_surface_cache_bytes
7337 .saturating_sub(evicted_entry.byte_size);
7338 }
7339
7340 let cached = CachedShadowSurface {
7341 target: Rc::new(target),
7342 byte_size,
7343 };
7344 if let Some((_replaced_key, replaced_entry)) = self.shadow_surface_cache.push(key, cached) {
7345 self.shadow_surface_cache_bytes = self
7346 .shadow_surface_cache_bytes
7347 .saturating_sub(replaced_entry.byte_size);
7348 }
7349 self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
7350 }
7351
7352 fn supports_render_effect(&self, effect: &RenderEffect) -> bool {
7353 is_render_effect_supported(effect)
7354 }
7355}
7356
7357struct RecordingSurfaceBackend<'renderer, 'recorder, C: FrameCommandRecorder> {
7358 renderer: &'renderer mut GpuRenderer,
7359 recorder: &'recorder mut C,
7360}
7361
7362impl<C: FrameCommandRecorder> RecordingSurfaceBackend<'_, '_, C> {
7363 #[allow(clippy::too_many_arguments)]
7364 fn render_range_with_layer_events_to_target_recorded(
7365 &mut self,
7366 target: &OffscreenTarget,
7367 shapes: &[DrawShape],
7368 brushes: &[Brush],
7369 images: &[ImageDraw],
7370 texts: &[TextDraw],
7371 shadow_draws: &[ShadowDraw],
7372 retained_draws: &[RetainedDraw],
7373 draw_ops: &[DrawOp],
7374 effect_layers: &[EffectLayer],
7375 backdrop_layers: &[BackdropLayer],
7376 backdrop_input_hashes: &[u64],
7377 z_start: usize,
7378 z_end: usize,
7379 excluded_effect_layer: Option<usize>,
7380 width: u32,
7381 height: u32,
7382 root_scale: f32,
7383 backdrop_underlay: Option<&OffscreenTarget>,
7384 initial_load_op: wgpu::LoadOp<wgpu::Color>,
7385 ) -> Result<(), String> {
7386 if z_start >= z_end {
7387 if matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
7388 self.clear_target_view_with_load_op(&target.view, initial_load_op);
7389 }
7390 return Ok(());
7391 }
7392
7393 let mut effect_z_ranges = std::mem::take(&mut self.renderer.scratch_effect_ranges);
7394 collect_effect_ranges(
7395 effect_layers,
7396 z_start,
7397 z_end,
7398 excluded_effect_layer,
7399 &mut effect_z_ranges,
7400 );
7401 let mut events = std::mem::take(&mut self.renderer.scratch_layer_events);
7402 collect_layer_events(
7403 effect_layers,
7404 backdrop_layers,
7405 z_start,
7406 z_end,
7407 excluded_effect_layer,
7408 &mut events,
7409 );
7410
7411 let result = (|| -> Result<(), String> {
7412 let mut next_load_op = initial_load_op;
7413 let mut cursor_z = z_start;
7414 for event in &events {
7415 if event.z_index > cursor_z {
7416 self.render_non_effect_segment(
7417 &target.view,
7418 shapes,
7419 brushes,
7420 images,
7421 texts,
7422 shadow_draws,
7423 retained_draws,
7424 draw_ops,
7425 cursor_z,
7426 event.z_index,
7427 &effect_z_ranges,
7428 width,
7429 height,
7430 root_scale,
7431 next_load_op,
7432 )?;
7433 next_load_op = wgpu::LoadOp::Load;
7434 cursor_z = event.z_index;
7435 } else if event.z_index < cursor_z {
7436 continue;
7437 }
7438
7439 if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
7440 self.clear_target_view_with_load_op(&target.view, next_load_op);
7441 next_load_op = wgpu::LoadOp::Load;
7442 }
7443
7444 match event.kind {
7445 LayerEventKind::Backdrop(index) => {
7446 let layer = &backdrop_layers[index];
7447 let effective_backdrop_underlay = if backdrop_underlay.is_some()
7448 && backdrop_underlay_is_covered_by_local_content(
7449 shapes,
7450 brushes,
7451 images,
7452 shadow_draws,
7453 draw_ops,
7454 effect_layers,
7455 backdrop_layers,
7456 layer,
7457 ) {
7458 None
7459 } else {
7460 backdrop_underlay
7461 };
7462 execute_apply_backdrop_layer_to_target(
7463 self,
7464 target,
7465 layer,
7466 effective_backdrop_underlay,
7467 width,
7468 height,
7469 root_scale,
7470 backdrop_input_hashes.get(index).copied(),
7471 )?;
7472 }
7473 LayerEventKind::Effect(index) => {
7474 let layer = &effect_layers[index];
7475 if layer.z_start < cursor_z {
7476 continue;
7477 }
7478 execute_render_effect_layer_to_target(
7479 self,
7480 target,
7481 shapes,
7482 brushes,
7483 images,
7484 texts,
7485 shadow_draws,
7486 draw_ops,
7487 effect_layers,
7488 backdrop_layers,
7489 index,
7490 backdrop_underlay,
7491 width,
7492 height,
7493 root_scale,
7494 )?;
7495 cursor_z = cursor_z.max(layer.z_end);
7496 }
7497 }
7498 }
7499
7500 if cursor_z < z_end {
7501 self.render_non_effect_segment(
7502 &target.view,
7503 shapes,
7504 brushes,
7505 images,
7506 texts,
7507 shadow_draws,
7508 retained_draws,
7509 draw_ops,
7510 cursor_z,
7511 z_end,
7512 &effect_z_ranges,
7513 width,
7514 height,
7515 root_scale,
7516 next_load_op,
7517 )?;
7518 } else if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
7519 self.clear_target_view_with_load_op(&target.view, next_load_op);
7520 }
7521
7522 Ok(())
7523 })();
7524
7525 self.renderer.scratch_effect_ranges = effect_z_ranges;
7526 self.renderer.scratch_layer_events = events;
7527 result
7528 }
7529
7530 #[allow(clippy::too_many_arguments)]
7531 fn record_shader_composite(
7532 &mut self,
7533 source: &OffscreenTarget,
7534 shader: &RuntimeShader,
7535 effect_rect: [f32; 4],
7536 dest_view: &wgpu::TextureView,
7537 alpha: f32,
7538 load_op: wgpu::LoadOp<wgpu::Color>,
7539 scissor: Option<(u32, u32, u32, u32)>,
7540 blend_mode: BlendMode,
7541 dest_viewport: Option<(f32, f32, f32, f32)>,
7542 sample_mode: CompositeSampleMode,
7543 ) {
7544 let device = self.renderer.device.clone();
7545 if let Some(viewport) = direct_shader_composite_viewport(
7546 alpha,
7547 blend_mode,
7548 dest_viewport,
7549 sample_mode,
7550 (source.width, source.height),
7551 ) {
7552 let shader_applied = self
7553 .renderer
7554 .effect_renderer
7555 .encode_shader_src_over_to_view(
7556 self.recorder,
7557 &device,
7558 source,
7559 dest_view,
7560 shader,
7561 effect_rect,
7562 load_op,
7563 scissor,
7564 viewport,
7565 );
7566 if shader_applied {
7567 self.renderer
7568 .effect_renderer
7569 .debug_effects
7570 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7571 self.recorder.record_pass();
7572 self.renderer.effect_renderer.record_composite_pass();
7573 return;
7574 }
7575 }
7576 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7577 "Shader Effect Composite Scratch",
7578 source.width,
7579 source.height,
7580 );
7581 let scratch = self
7582 .recorder
7583 .acquire_transient_offscreen(&device, scratch_descriptor);
7584 let shader_applied = {
7585 self.renderer.effect_renderer.encode_shader(
7586 self.recorder,
7587 &device,
7588 source,
7589 &scratch.view,
7590 shader,
7591 effect_rect,
7592 )
7593 };
7594 let composite_source = if shader_applied {
7595 self.renderer
7596 .effect_renderer
7597 .debug_effects
7598 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7599 self.recorder.record_pass();
7600 &scratch
7601 } else {
7602 source
7603 };
7604 {
7605 self.renderer
7606 .effect_renderer
7607 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
7608 self.recorder,
7609 &device,
7610 composite_source,
7611 dest_view,
7612 alpha,
7613 load_op,
7614 scissor,
7615 None,
7616 supported_blend_mode(blend_mode),
7617 dest_viewport,
7618 sample_mode,
7619 );
7620 }
7621 self.recorder.record_pass();
7622 self.renderer.effect_renderer.record_composite_pass();
7623 self.recorder
7624 .release_transient_offscreen(scratch_descriptor, scratch);
7625 }
7626
7627 #[allow(clippy::too_many_arguments)]
7628 fn record_shader_projective_composite(
7629 &mut self,
7630 source: &OffscreenTarget,
7631 shader: &RuntimeShader,
7632 effect_rect: [f32; 4],
7633 dest_view: &wgpu::TextureView,
7634 viewport: (u32, u32),
7635 source_size: (f32, f32),
7636 inverse_matrix: [[f32; 3]; 3],
7637 dest_bounds: [[f32; 2]; 4],
7638 alpha: f32,
7639 load_op: wgpu::LoadOp<wgpu::Color>,
7640 scissor: Option<(u32, u32, u32, u32)>,
7641 blend_mode: BlendMode,
7642 sample_mode: CompositeSampleMode,
7643 ) {
7644 if projective_dest_bounds_rect(dest_bounds).is_none() {
7645 return;
7646 }
7647 let device = self.renderer.device.clone();
7648 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7649 "Shader Projective Composite Scratch",
7650 source.width,
7651 source.height,
7652 );
7653 let scratch = self
7654 .recorder
7655 .acquire_transient_offscreen(&device, scratch_descriptor);
7656 let shader_applied = {
7657 self.renderer.effect_renderer.encode_shader(
7658 self.recorder,
7659 &device,
7660 source,
7661 &scratch.view,
7662 shader,
7663 effect_rect,
7664 )
7665 };
7666 let composite_source = if shader_applied {
7667 self.renderer
7668 .effect_renderer
7669 .debug_effects
7670 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7671 self.recorder.record_pass();
7672 &scratch
7673 } else {
7674 source
7675 };
7676 let composited = {
7677 self.renderer
7678 .effect_renderer
7679 .encode_composite_to_view_projective(
7680 self.recorder,
7681 &device,
7682 composite_source,
7683 dest_view,
7684 viewport,
7685 source_size,
7686 inverse_matrix,
7687 dest_bounds,
7688 alpha,
7689 load_op,
7690 scissor,
7691 supported_blend_mode(blend_mode),
7692 sample_mode,
7693 )
7694 };
7695 if composited {
7696 self.recorder.record_pass();
7697 self.renderer.effect_renderer.record_composite_pass();
7698 }
7699 self.recorder
7700 .release_transient_offscreen(scratch_descriptor, scratch);
7701 }
7702
7703 #[allow(clippy::too_many_arguments)]
7704 fn record_effect_with_direct_shader_tail_composite(
7705 &mut self,
7706 source: &OffscreenTarget,
7707 first_effect: &RenderEffect,
7708 shader: &RuntimeShader,
7709 effect_rect: [f32; 4],
7710 dest_view: &wgpu::TextureView,
7711 load_op: wgpu::LoadOp<wgpu::Color>,
7712 scissor: Option<(u32, u32, u32, u32)>,
7713 dest_viewport: (f32, f32, f32, f32),
7714 ) -> Result<bool, String> {
7715 let device = self.renderer.device.clone();
7716 let intermediate_descriptor = self.renderer.transient_offscreen_descriptor(
7717 "Render Effect Direct Shader Tail Intermediate",
7718 source.width,
7719 source.height,
7720 );
7721 let intermediate = self
7722 .recorder
7723 .acquire_transient_offscreen(&device, intermediate_descriptor);
7724 let effect_scratch_targets = self
7725 .renderer
7726 .effect_renderer
7727 .acquire_recorded_effect_scratch_targets(
7728 self.recorder,
7729 &device,
7730 first_effect,
7731 source.width,
7732 source.height,
7733 self.renderer.surface_format,
7734 );
7735 let first_passes = {
7736 let mut effect_scratch_refs = effect_scratch_targets.refs();
7737 let pass_count = self.renderer.effect_renderer.encode_effect(
7738 self.recorder,
7739 &device,
7740 source,
7741 &intermediate.view,
7742 first_effect,
7743 effect_rect,
7744 &mut effect_scratch_refs,
7745 );
7746 match pass_count {
7747 Ok(pass_count) => effect_scratch_refs.assert_consumed().map(|()| pass_count),
7748 Err(error) => Err(error),
7749 }
7750 };
7751 let first_passes = match first_passes {
7752 Ok(pass_count) => pass_count,
7753 Err(error) => {
7754 effect_scratch_targets.release_into(self.recorder);
7755 self.recorder
7756 .release_transient_offscreen(intermediate_descriptor, intermediate);
7757 return Err(error);
7758 }
7759 };
7760 let shader_applied = self
7761 .renderer
7762 .effect_renderer
7763 .encode_shader_src_over_to_view(
7764 self.recorder,
7765 &device,
7766 &intermediate,
7767 dest_view,
7768 shader,
7769 effect_rect,
7770 load_op,
7771 scissor,
7772 dest_viewport,
7773 );
7774 self.recorder
7775 .record_passes(first_passes.saturating_add(u32::from(shader_applied)));
7776 effect_scratch_targets.release_into(self.recorder);
7777 self.recorder
7778 .release_transient_offscreen(intermediate_descriptor, intermediate);
7779 if !shader_applied {
7780 return Ok(false);
7781 }
7782 self.renderer
7783 .effect_renderer
7784 .debug_effects
7785 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7786 self.renderer.effect_renderer.record_composite_pass();
7787 Ok(true)
7788 }
7789
7790 #[allow(clippy::too_many_arguments)]
7791 fn record_effect_composite(
7792 &mut self,
7793 source: &OffscreenTarget,
7794 effect: &RenderEffect,
7795 effect_rect: [f32; 4],
7796 dest_view: &wgpu::TextureView,
7797 alpha: f32,
7798 load_op: wgpu::LoadOp<wgpu::Color>,
7799 scissor: Option<(u32, u32, u32, u32)>,
7800 blend_mode: BlendMode,
7801 dest_viewport: Option<(f32, f32, f32, f32)>,
7802 sample_mode: CompositeSampleMode,
7803 ) -> Result<(), String> {
7804 if let (
7805 RenderEffect::Chain { first, second },
7806 Some(viewport),
7807 BlendMode::SrcOver,
7808 CompositeSampleMode::Linear,
7809 ) = (
7810 effect,
7811 dest_viewport,
7812 supported_blend_mode(blend_mode),
7813 sample_mode,
7814 ) {
7815 if let (
7816 RenderEffect::Blur {
7817 radius_x,
7818 radius_y,
7819 edge_treatment,
7820 },
7821 RenderEffect::Shader { shader },
7822 ) = (first.as_ref(), second.as_ref())
7823 {
7824 if *radius_x > 0.0 || *radius_y > 0.0 {
7825 let device = self.renderer.device.clone();
7826 let (scratch_width, scratch_height) = crate::effect_renderer::blur_scratch_size(
7827 *radius_x,
7828 *radius_y,
7829 source.width,
7830 source.height,
7831 );
7832 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7833 "Blur Rounded Mask Scratch",
7834 scratch_width,
7835 scratch_height,
7836 );
7837 let scratch = self
7838 .recorder
7839 .acquire_transient_offscreen(&device, scratch_descriptor);
7840 let fused = self
7841 .renderer
7842 .effect_renderer
7843 .encode_blur_then_rounded_mask_src_over_to_view(
7844 self.recorder,
7845 &device,
7846 source,
7847 &scratch,
7848 dest_view,
7849 *radius_x,
7850 *radius_y,
7851 *edge_treatment,
7852 shader,
7853 effect_rect,
7854 load_op,
7855 scissor,
7856 viewport,
7857 );
7858 if fused {
7859 self.recorder.record_passes(2);
7860 self.renderer.effect_renderer.record_blur_pass();
7861 self.renderer
7862 .effect_renderer
7863 .debug_effects
7864 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
7865 self.renderer.effect_renderer.record_composite_pass();
7866 self.recorder
7867 .release_transient_offscreen(scratch_descriptor, scratch);
7868 return Ok(());
7869 }
7870 self.recorder
7871 .release_transient_offscreen(scratch_descriptor, scratch);
7872 }
7873 }
7874 }
7875 if let Some((first_effect, shader, viewport)) = direct_shader_tail_composite(
7876 effect,
7877 alpha,
7878 blend_mode,
7879 dest_viewport,
7880 sample_mode,
7881 (source.width, source.height),
7882 ) {
7883 if self.record_effect_with_direct_shader_tail_composite(
7884 source,
7885 first_effect,
7886 shader,
7887 effect_rect,
7888 dest_view,
7889 load_op,
7890 scissor,
7891 viewport,
7892 )? {
7893 return Ok(());
7894 }
7895 }
7896 let device = self.renderer.device.clone();
7897 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7898 "Render Effect Composite Scratch",
7899 source.width,
7900 source.height,
7901 );
7902 let scratch = self
7903 .recorder
7904 .acquire_transient_offscreen(&device, scratch_descriptor);
7905 let effect_scratch_targets = self
7906 .renderer
7907 .effect_renderer
7908 .acquire_recorded_effect_scratch_targets(
7909 self.recorder,
7910 &device,
7911 effect,
7912 source.width,
7913 source.height,
7914 self.renderer.surface_format,
7915 );
7916 let effect_passes = {
7917 let mut effect_scratch_refs = effect_scratch_targets.refs();
7918 let pass_count = self.renderer.effect_renderer.encode_effect(
7919 self.recorder,
7920 &device,
7921 source,
7922 &scratch.view,
7923 effect,
7924 effect_rect,
7925 &mut effect_scratch_refs,
7926 )?;
7927 effect_scratch_refs.assert_consumed()?;
7928 Ok(pass_count)
7929 };
7930 let effect_passes = match effect_passes {
7931 Ok(pass_count) => pass_count,
7932 Err(error) => {
7933 effect_scratch_targets.release_into(self.recorder);
7934 self.recorder
7935 .release_transient_offscreen(scratch_descriptor, scratch);
7936 return Err(error);
7937 }
7938 };
7939 {
7940 self.renderer
7941 .effect_renderer
7942 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
7943 self.recorder,
7944 &device,
7945 &scratch,
7946 dest_view,
7947 alpha,
7948 load_op,
7949 scissor,
7950 None,
7951 supported_blend_mode(blend_mode),
7952 dest_viewport,
7953 sample_mode,
7954 );
7955 }
7956 self.recorder.record_passes(effect_passes.saturating_add(1));
7957 self.renderer.effect_renderer.record_composite_pass();
7958 effect_scratch_targets.release_into(self.recorder);
7959 self.recorder
7960 .release_transient_offscreen(scratch_descriptor, scratch);
7961 Ok(())
7962 }
7963
7964 #[allow(clippy::too_many_arguments)]
7965 fn record_effect_projective_composite(
7966 &mut self,
7967 source: &OffscreenTarget,
7968 effect: &RenderEffect,
7969 effect_rect: [f32; 4],
7970 dest_view: &wgpu::TextureView,
7971 viewport: (u32, u32),
7972 source_size: (f32, f32),
7973 inverse_matrix: [[f32; 3]; 3],
7974 dest_bounds: [[f32; 2]; 4],
7975 alpha: f32,
7976 load_op: wgpu::LoadOp<wgpu::Color>,
7977 scissor: Option<(u32, u32, u32, u32)>,
7978 blend_mode: BlendMode,
7979 sample_mode: CompositeSampleMode,
7980 ) -> Result<(), String> {
7981 if projective_dest_bounds_rect(dest_bounds).is_none() {
7982 return Ok(());
7983 }
7984 let device = self.renderer.device.clone();
7985 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
7986 "Render Effect Projective Composite Scratch",
7987 source.width,
7988 source.height,
7989 );
7990 let scratch = self
7991 .recorder
7992 .acquire_transient_offscreen(&device, scratch_descriptor);
7993 let effect_scratch_targets = self
7994 .renderer
7995 .effect_renderer
7996 .acquire_recorded_effect_scratch_targets(
7997 self.recorder,
7998 &device,
7999 effect,
8000 source.width,
8001 source.height,
8002 self.renderer.surface_format,
8003 );
8004 let effect_passes = {
8005 let mut effect_scratch_refs = effect_scratch_targets.refs();
8006 let pass_count = self.renderer.effect_renderer.encode_effect(
8007 self.recorder,
8008 &device,
8009 source,
8010 &scratch.view,
8011 effect,
8012 effect_rect,
8013 &mut effect_scratch_refs,
8014 )?;
8015 effect_scratch_refs.assert_consumed()?;
8016 Ok(pass_count)
8017 };
8018 let effect_passes = match effect_passes {
8019 Ok(pass_count) => pass_count,
8020 Err(error) => {
8021 effect_scratch_targets.release_into(self.recorder);
8022 self.recorder
8023 .release_transient_offscreen(scratch_descriptor, scratch);
8024 return Err(error);
8025 }
8026 };
8027 let composited = {
8028 self.renderer
8029 .effect_renderer
8030 .encode_composite_to_view_projective(
8031 self.recorder,
8032 &device,
8033 &scratch,
8034 dest_view,
8035 viewport,
8036 source_size,
8037 inverse_matrix,
8038 dest_bounds,
8039 alpha,
8040 load_op,
8041 scissor,
8042 supported_blend_mode(blend_mode),
8043 sample_mode,
8044 )
8045 };
8046 if composited {
8047 self.recorder.record_passes(effect_passes.saturating_add(1));
8048 self.renderer.effect_renderer.record_composite_pass();
8049 } else {
8050 self.recorder.record_passes(effect_passes);
8051 }
8052 effect_scratch_targets.release_into(self.recorder);
8053 self.recorder
8054 .release_transient_offscreen(scratch_descriptor, scratch);
8055 Ok(())
8056 }
8057}
8058
8059impl<C: FrameCommandRecorder> SurfaceExecutionBackend for RecordingSurfaceBackend<'_, '_, C> {
8060 fn max_texture_dim(&self) -> u32 {
8061 self.renderer.max_texture_dim()
8062 }
8063
8064 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
8065 self.renderer.acquire_retained_surface(width, height)
8066 }
8067
8068 fn acquire_frame_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
8069 let descriptor =
8070 self.renderer
8071 .transient_offscreen_descriptor("Frame Surface", width, height);
8072 self.recorder
8073 .acquire_transient_offscreen(&self.renderer.device, descriptor)
8074 }
8075
8076 fn release_frame_surface(&mut self, target: OffscreenTarget) {
8077 let descriptor = self.renderer.transient_offscreen_descriptor(
8078 "Frame Surface",
8079 target.width,
8080 target.height,
8081 );
8082 self.recorder
8083 .release_transient_offscreen(descriptor, target);
8084 }
8085
8086 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
8087 self.renderer.release_layer_surface_target(target);
8088 }
8089
8090 fn cached_layer_surface(
8091 &mut self,
8092 key: &LayerRasterCacheKey,
8093 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
8094 self.renderer.cached_layer_surface(key)
8095 }
8096
8097 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
8098 self.renderer.admit_layer_surface_cache_miss(key)
8099 }
8100
8101 fn insert_cached_layer_surface(
8102 &mut self,
8103 key: LayerRasterCacheKey,
8104 target: OffscreenTarget,
8105 logical_rect: Rect,
8106 ) -> Rc<OffscreenTarget> {
8107 self.renderer
8108 .insert_cached_layer_surface(key, target, logical_rect)
8109 }
8110
8111 fn clear_target_view_with_load_op(
8112 &mut self,
8113 target_view: &wgpu::TextureView,
8114 load_op: wgpu::LoadOp<wgpu::Color>,
8115 ) {
8116 {
8117 let _clear = self
8118 .recorder
8119 .encoder()
8120 .begin_render_pass(&wgpu::RenderPassDescriptor {
8121 label: Some("Layer Event Clear Pass"),
8122 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8123 view: target_view,
8124 resolve_target: None,
8125 depth_slice: None,
8126 ops: wgpu::Operations {
8127 load: load_op,
8128 store: wgpu::StoreOp::Store,
8129 },
8130 })],
8131 depth_stencil_attachment: None,
8132 timestamp_writes: None,
8133 occlusion_query_set: None,
8134 multiview_mask: None,
8135 });
8136 }
8137 self.recorder.record_pass();
8138 }
8139
8140 #[allow(clippy::too_many_arguments)]
8141 fn render_non_effect_segment(
8142 &mut self,
8143 target_view: &wgpu::TextureView,
8144 shapes: &[DrawShape],
8145 brushes: &[Brush],
8146 images: &[ImageDraw],
8147 texts: &[TextDraw],
8148 shadow_draws: &[ShadowDraw],
8149 retained_draws: &[RetainedDraw],
8150 draw_ops: &[DrawOp],
8151 z_start: usize,
8152 z_end: usize,
8153 effect_z_ranges: &[Range<usize>],
8154 width: u32,
8155 height: u32,
8156 root_scale: f32,
8157 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8158 ) -> Result<(), String> {
8159 self.render_non_effect_segment_with_composites(
8160 target_view,
8161 shapes,
8162 brushes,
8163 images,
8164 texts,
8165 shadow_draws,
8166 retained_draws,
8167 draw_ops,
8168 z_start,
8169 z_end,
8170 effect_z_ranges,
8171 &[],
8172 &[],
8173 width,
8174 height,
8175 root_scale,
8176 initial_load_op,
8177 )
8178 }
8179
8180 #[allow(clippy::too_many_arguments)]
8181 fn render_non_effect_segment_with_composites(
8182 &mut self,
8183 target_view: &wgpu::TextureView,
8184 shapes: &[DrawShape],
8185 brushes: &[Brush],
8186 images: &[ImageDraw],
8187 texts: &[TextDraw],
8188 shadow_draws: &[ShadowDraw],
8189 retained_draws: &[RetainedDraw],
8190 draw_ops: &[DrawOp],
8191 z_start: usize,
8192 z_end: usize,
8193 effect_z_ranges: &[Range<usize>],
8194 composites: &[(usize, CompositeBatchItem<'_>)],
8195 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
8196 width: u32,
8197 height: u32,
8198 root_scale: f32,
8199 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8200 ) -> Result<(), String> {
8201 let mut ordered_items = std::mem::take(&mut self.renderer.scratch_segment_items);
8202 collect_non_effect_segment_items(
8203 shapes,
8204 images,
8205 texts,
8206 shadow_draws,
8207 draw_ops,
8208 z_start,
8209 z_end,
8210 effect_z_ranges,
8211 width,
8212 height,
8213 root_scale,
8214 &mut ordered_items,
8215 );
8216 #[cfg(not(target_arch = "wasm32"))]
8217 let raw_shadow_items = ordered_items
8218 .iter()
8219 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
8220 .count();
8221 let culled_shadow_items = retain_renderable_shadow_items(
8222 &mut ordered_items,
8223 shadow_draws,
8224 width,
8225 height,
8226 root_scale,
8227 self.renderer.max_texture_dim(),
8228 );
8229 #[cfg(target_arch = "wasm32")]
8230 let _ = culled_shadow_items;
8231 let mut cached_shadow_composites: Vec<(usize, CachedShadowComposite)> = Vec::new();
8232 ordered_items.extend(
8233 composites
8234 .iter()
8235 .enumerate()
8236 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::Composite(index))),
8237 );
8238 ordered_items.extend(
8239 shader_composites
8240 .iter()
8241 .enumerate()
8242 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::ShaderComposite(index))),
8243 );
8244 for (z_index, item) in &mut ordered_items {
8245 let SegmentDrawItem::Shadow(shadow_index) = *item else {
8246 continue;
8247 };
8248 let Some(composite) = self.renderer.cached_shape_shadow_composite(
8249 &shadow_draws[shadow_index],
8250 width,
8251 height,
8252 root_scale,
8253 ) else {
8254 continue;
8255 };
8256 let composite_index = composites.len() + cached_shadow_composites.len();
8257 cached_shadow_composites.push((*z_index, composite));
8258 *item = SegmentDrawItem::Composite(composite_index);
8259 }
8260 let mut merged_composites = Vec::with_capacity(
8261 composites
8262 .len()
8263 .saturating_add(cached_shadow_composites.len()),
8264 );
8265 merged_composites.extend(composites.iter().copied());
8266 merged_composites.extend(
8267 cached_shadow_composites
8268 .iter()
8269 .map(|(z_index, composite)| (*z_index, composite.batch_item())),
8270 );
8271 ordered_items.sort_unstable_by_key(|(z_index, _)| *z_index);
8275 #[cfg(not(target_arch = "wasm32"))]
8276 maybe_print_segment_diag(
8277 z_start..z_end,
8278 &ordered_items,
8279 shapes,
8280 brushes,
8281 images,
8282 SegmentDiagCounts {
8283 raw_shadow_items,
8284 culled_shadow_items,
8285 cached_shadow_composites: cached_shadow_composites.len(),
8286 composite_items: merged_composites.len(),
8287 shader_composite_items: shader_composites.len(),
8288 },
8289 self.renderer.shape_batch_limits,
8290 );
8291 let result = if ordered_items.is_empty() {
8292 Ok(SegmentCommandEncodeOutcome { first_batch: true })
8293 } else {
8294 self.renderer.encode_non_effect_segment_commands(
8295 self.recorder,
8296 target_view,
8297 &ordered_items,
8298 &merged_composites,
8299 shader_composites,
8300 shapes,
8301 brushes,
8302 images,
8303 texts,
8304 shadow_draws,
8305 retained_draws,
8306 initial_load_op,
8307 width,
8308 height,
8309 root_scale,
8310 )
8311 };
8312 self.renderer.scratch_segment_items = ordered_items;
8313 let outcome = result?;
8314 if outcome.first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
8315 self.clear_target_view_with_load_op(target_view, initial_load_op);
8316 }
8317 Ok(())
8318 }
8319
8320 fn render_range_with_layer_events_to_target(
8321 &mut self,
8322 target: &OffscreenTarget,
8323 shapes: &[DrawShape],
8324 brushes: &[Brush],
8325 images: &[ImageDraw],
8326 texts: &[TextDraw],
8327 shadow_draws: &[ShadowDraw],
8328 retained_draws: &[RetainedDraw],
8329 draw_ops: &[DrawOp],
8330 effect_layers: &[EffectLayer],
8331 backdrop_layers: &[BackdropLayer],
8332 backdrop_input_hashes: &[u64],
8333 z_start: usize,
8334 z_end: usize,
8335 excluded_effect_layer: Option<usize>,
8336 width: u32,
8337 height: u32,
8338 root_scale: f32,
8339 backdrop_underlay: Option<&OffscreenTarget>,
8340 initial_load_op: wgpu::LoadOp<wgpu::Color>,
8341 ) -> Result<(), String> {
8342 self.render_range_with_layer_events_to_target_recorded(
8343 target,
8344 shapes,
8345 brushes,
8346 images,
8347 texts,
8348 shadow_draws,
8349 retained_draws,
8350 draw_ops,
8351 effect_layers,
8352 backdrop_layers,
8353 backdrop_input_hashes,
8354 z_start,
8355 z_end,
8356 excluded_effect_layer,
8357 width,
8358 height,
8359 root_scale,
8360 backdrop_underlay,
8361 initial_load_op,
8362 )
8363 }
8364
8365 fn render_shadow_draw(
8366 &mut self,
8367 target_view: &wgpu::TextureView,
8368 shadow: &ShadowDraw,
8369 width: u32,
8370 height: u32,
8371 root_scale: f32,
8372 ) {
8373 self.renderer.encode_shadow_draw(
8374 self.recorder,
8375 target_view,
8376 shadow,
8377 width,
8378 height,
8379 root_scale,
8380 );
8381 }
8382
8383 fn composite_to_view_projective(
8384 &mut self,
8385 source: &OffscreenTarget,
8386 dest_view: &wgpu::TextureView,
8387 viewport: (u32, u32),
8388 source_size: (f32, f32),
8389 inverse_matrix: [[f32; 3]; 3],
8390 dest_bounds: [[f32; 2]; 4],
8391 alpha: f32,
8392 load_op: wgpu::LoadOp<wgpu::Color>,
8393 scissor: Option<(u32, u32, u32, u32)>,
8394 blend_mode: BlendMode,
8395 sample_mode: CompositeSampleMode,
8396 ) {
8397 let device = self.renderer.device.clone();
8398 let composited = {
8399 self.renderer
8400 .effect_renderer
8401 .encode_composite_to_view_projective(
8402 self.recorder,
8403 &device,
8404 source,
8405 dest_view,
8406 viewport,
8407 source_size,
8408 inverse_matrix,
8409 dest_bounds,
8410 alpha,
8411 load_op,
8412 scissor,
8413 supported_blend_mode(blend_mode),
8414 sample_mode,
8415 )
8416 };
8417 if composited {
8418 self.recorder.record_pass();
8419 self.renderer.effect_renderer.record_composite_pass();
8420 }
8421 }
8422
8423 fn composite_projective_surfaces_to_view(
8424 &mut self,
8425 dest_view: &wgpu::TextureView,
8426 viewport: (u32, u32),
8427 composites: &[ProjectiveSurfaceComposite<'_>],
8428 ) {
8429 let device = self.renderer.device.clone();
8430 let mut composite_count = 0_u32;
8431 for composite in composites
8432 .iter()
8433 .copied()
8434 .filter(|composite| projective_dest_bounds_rect(composite.dest_bounds).is_some())
8435 {
8436 let composited = {
8437 self.renderer
8438 .effect_renderer
8439 .encode_composite_to_view_projective(
8440 self.recorder,
8441 &device,
8442 composite.source,
8443 dest_view,
8444 viewport,
8445 composite.source_size,
8446 composite.inverse_matrix,
8447 composite.dest_bounds,
8448 composite.alpha,
8449 composite.load_op,
8450 composite.scissor,
8451 supported_blend_mode(composite.blend_mode),
8452 composite.sample_mode,
8453 )
8454 };
8455 if composited {
8456 composite_count = composite_count.saturating_add(1);
8457 }
8458 }
8459 if composite_count > 0 {
8460 self.recorder.record_passes(composite_count);
8461 self.renderer
8462 .effect_renderer
8463 .debug_composites
8464 .set(self.renderer.effect_renderer.debug_composites.get() + composite_count);
8465 }
8466 }
8467
8468 fn composite_surface_batch_to_view(
8469 &mut self,
8470 dest_view: &wgpu::TextureView,
8471 viewport: (u32, u32),
8472 load_op: wgpu::LoadOp<wgpu::Color>,
8473 composites: &[CompositeBatchItem<'_>],
8474 ) {
8475 if composites.is_empty() {
8476 return;
8477 }
8478 let device = self.renderer.device.clone();
8479 self.renderer
8480 .effect_renderer
8481 .encode_composite_batch_to_view_pass(
8482 self.recorder,
8483 &device,
8484 dest_view,
8485 viewport,
8486 load_op,
8487 composites,
8488 );
8489 self.recorder.record_pass();
8490 self.renderer.effect_renderer.record_composite_pass();
8491 }
8492
8493 fn copy_texture_region_to_target(
8494 &mut self,
8495 source: &OffscreenTarget,
8496 source_origin: (u32, u32),
8497 target: &OffscreenTarget,
8498 size: (u32, u32),
8499 ) -> bool {
8500 let (width, height) = size;
8501 if width == 0 || height == 0 || width > target.width || height > target.height {
8502 return false;
8503 }
8504 let Some(source_right) = source_origin.0.checked_add(width) else {
8505 return false;
8506 };
8507 let Some(source_bottom) = source_origin.1.checked_add(height) else {
8508 return false;
8509 };
8510 if source_right > source.width || source_bottom > source.height {
8511 return false;
8512 }
8513
8514 self.recorder.encoder().copy_texture_to_texture(
8515 wgpu::TexelCopyTextureInfo {
8516 texture: source.texture(),
8517 mip_level: 0,
8518 origin: wgpu::Origin3d {
8519 x: source_origin.0,
8520 y: source_origin.1,
8521 z: 0,
8522 },
8523 aspect: wgpu::TextureAspect::All,
8524 },
8525 wgpu::TexelCopyTextureInfo {
8526 texture: target.texture(),
8527 mip_level: 0,
8528 origin: wgpu::Origin3d::ZERO,
8529 aspect: wgpu::TextureAspect::All,
8530 },
8531 wgpu::Extent3d {
8532 width,
8533 height,
8534 depth_or_array_layers: 1,
8535 },
8536 );
8537 true
8538 }
8539
8540 fn shader_composite_batch_to_view(
8541 &mut self,
8542 dest_view: &wgpu::TextureView,
8543 viewport: (u32, u32),
8544 load_op: wgpu::LoadOp<wgpu::Color>,
8545 composites: &[ShaderCompositeBatchItem<'_>],
8546 ) -> bool {
8547 if composites.is_empty() {
8548 return true;
8549 }
8550 let device = self.renderer.device.clone();
8551 let encoded = self
8552 .renderer
8553 .effect_renderer
8554 .encode_shader_batch_src_over_to_view(
8555 self.recorder,
8556 &device,
8557 dest_view,
8558 viewport,
8559 load_op,
8560 composites,
8561 );
8562 if encoded {
8563 self.recorder.record_pass();
8564 self.renderer.effect_renderer.record_composite_pass();
8565 self.renderer
8566 .effect_renderer
8567 .debug_effects
8568 .set(self.renderer.effect_renderer.debug_effects.get() + composites.len() as u32);
8569 }
8570 encoded
8571 }
8572
8573 fn composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8574 &mut self,
8575 source: &OffscreenTarget,
8576 dest_view: &wgpu::TextureView,
8577 alpha: f32,
8578 load_op: wgpu::LoadOp<wgpu::Color>,
8579 scissor: Option<(u32, u32, u32, u32)>,
8580 rounded_mask: Option<RoundedCompositeMask>,
8581 blend_mode: BlendMode,
8582 dest_viewport: Option<(f32, f32, f32, f32)>,
8583 sample_mode: CompositeSampleMode,
8584 ) {
8585 let device = self.renderer.device.clone();
8586 {
8587 self.renderer
8588 .effect_renderer
8589 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8590 self.recorder,
8591 &device,
8592 source,
8593 dest_view,
8594 alpha,
8595 load_op,
8596 scissor,
8597 rounded_mask,
8598 supported_blend_mode(blend_mode),
8599 dest_viewport,
8600 sample_mode,
8601 );
8602 }
8603 self.recorder.record_pass();
8604 self.renderer.effect_renderer.record_composite_pass();
8605 }
8606
8607 fn apply_effect_and_composite_to_view(
8608 &mut self,
8609 source: &OffscreenTarget,
8610 effect: &RenderEffect,
8611 effect_rect: [f32; 4],
8612 dest_view: &wgpu::TextureView,
8613 alpha: f32,
8614 load_op: wgpu::LoadOp<wgpu::Color>,
8615 scissor: Option<(u32, u32, u32, u32)>,
8616 blend_mode: BlendMode,
8617 dest_viewport: Option<(f32, f32, f32, f32)>,
8618 sample_mode: CompositeSampleMode,
8619 ) -> Result<(), String> {
8620 self.record_effect_composite(
8621 source,
8622 effect,
8623 effect_rect,
8624 dest_view,
8625 alpha,
8626 load_op,
8627 scissor,
8628 blend_mode,
8629 dest_viewport,
8630 sample_mode,
8631 )
8632 }
8633
8634 fn apply_shader_and_composite_to_view(
8635 &mut self,
8636 source: &OffscreenTarget,
8637 shader: &RuntimeShader,
8638 effect_rect: [f32; 4],
8639 dest_view: &wgpu::TextureView,
8640 alpha: f32,
8641 load_op: wgpu::LoadOp<wgpu::Color>,
8642 scissor: Option<(u32, u32, u32, u32)>,
8643 blend_mode: BlendMode,
8644 dest_viewport: Option<(f32, f32, f32, f32)>,
8645 sample_mode: CompositeSampleMode,
8646 ) {
8647 self.record_shader_composite(
8648 source,
8649 shader,
8650 effect_rect,
8651 dest_view,
8652 alpha,
8653 load_op,
8654 scissor,
8655 blend_mode,
8656 dest_viewport,
8657 sample_mode,
8658 );
8659 }
8660
8661 fn apply_shader_and_composite_to_view_projective(
8662 &mut self,
8663 source: &OffscreenTarget,
8664 shader: &RuntimeShader,
8665 effect_rect: [f32; 4],
8666 dest_view: &wgpu::TextureView,
8667 viewport: (u32, u32),
8668 source_size: (f32, f32),
8669 inverse_matrix: [[f32; 3]; 3],
8670 dest_bounds: [[f32; 2]; 4],
8671 alpha: f32,
8672 load_op: wgpu::LoadOp<wgpu::Color>,
8673 scissor: Option<(u32, u32, u32, u32)>,
8674 blend_mode: BlendMode,
8675 sample_mode: CompositeSampleMode,
8676 ) {
8677 self.record_shader_projective_composite(
8678 source,
8679 shader,
8680 effect_rect,
8681 dest_view,
8682 viewport,
8683 source_size,
8684 inverse_matrix,
8685 dest_bounds,
8686 alpha,
8687 load_op,
8688 scissor,
8689 blend_mode,
8690 sample_mode,
8691 );
8692 }
8693
8694 fn apply_effect_and_composite_to_view_projective(
8695 &mut self,
8696 source: &OffscreenTarget,
8697 effect: &RenderEffect,
8698 effect_rect: [f32; 4],
8699 dest_view: &wgpu::TextureView,
8700 viewport: (u32, u32),
8701 source_size: (f32, f32),
8702 inverse_matrix: [[f32; 3]; 3],
8703 dest_bounds: [[f32; 2]; 4],
8704 alpha: f32,
8705 load_op: wgpu::LoadOp<wgpu::Color>,
8706 scissor: Option<(u32, u32, u32, u32)>,
8707 blend_mode: BlendMode,
8708 sample_mode: CompositeSampleMode,
8709 ) -> Result<(), String> {
8710 self.record_effect_projective_composite(
8711 source,
8712 effect,
8713 effect_rect,
8714 dest_view,
8715 viewport,
8716 source_size,
8717 inverse_matrix,
8718 dest_bounds,
8719 alpha,
8720 load_op,
8721 scissor,
8722 blend_mode,
8723 sample_mode,
8724 )
8725 }
8726
8727 fn is_render_effect_supported(&self, effect: &RenderEffect) -> bool {
8728 self.renderer.supports_render_effect(effect)
8729 }
8730
8731 fn warn_unsupported_effect_once(&self) {
8732 self.renderer.warning_state.warn_unsupported_effect_once();
8733 }
8734
8735 fn record_layer_cache_miss(&self, width: u32, height: u32) {
8736 self.renderer
8737 .frame_stats
8738 .record_layer_cache_miss(width, height);
8739 }
8740
8741 fn record_isolated_layer_render(
8742 &self,
8743 width: u32,
8744 height: u32,
8745 node_id: Option<NodeId>,
8746 logical_rect: Rect,
8747 requirements: SurfaceRequirementSet,
8748 ) {
8749 self.renderer.frame_stats.record_isolated_layer_render(
8750 width,
8751 height,
8752 node_id,
8753 logical_rect,
8754 requirements.into(),
8755 );
8756 }
8757}
8758
8759impl GpuRenderer {
8760 #[allow(clippy::too_many_arguments)]
8761 pub fn render(
8762 &mut self,
8763 view: &wgpu::TextureView,
8764 root_target: Option<&OffscreenTarget>,
8765 width: u32,
8766 height: u32,
8767 mut packet: FramePacket,
8768 surface_epoch: u64,
8769 returns: &mut RenderReturns,
8770 ) -> Result<(), String> {
8771 if let Some(confirmations) = packet.recycled_confirmations.take() {
8776 self.restore_replay_ack_confirmations(confirmations);
8777 }
8778 let cancel_reason = if packet.renderer_epoch != self.renderer_epoch {
8784 Some(CancelReason::RendererEpoch)
8785 } else if packet.surface_epoch != surface_epoch {
8786 Some(CancelReason::SurfaceEpoch)
8787 } else if packet.viewport != (width, height) {
8788 Some(CancelReason::Viewport)
8789 } else {
8790 None
8791 };
8792 if let Some(reason) = cancel_reason {
8793 return Self::cancel_packet(packet, reason, returns);
8794 }
8795 if self.device_errors.take_poison() {
8802 return Self::cancel_packet(packet, CancelReason::DeviceError, returns);
8803 }
8804 returns.frame_id = packet.frame_id;
8805 log::trace!("🎨 Rendering graph to {}x{}", width, height);
8806 let render_start = Instant::now();
8807
8808 #[cfg(target_arch = "wasm32")]
8809 {
8810 self.wasm_uniform_batch_cursor = 0;
8811 self.wasm_shape_batch_cursor = 0;
8812 self.wasm_image_batch_cursor = 0;
8813 }
8814 #[cfg(not(target_arch = "wasm32"))]
8815 {
8816 self.retained_glyph_uniform_cursor = 0;
8817 self.rim_mesh_vertices.clear();
8822 self.rim_mesh_indices.clear();
8823 self.rim_mesh_uploaded_vertices = 0;
8824 self.rim_mesh_uploaded_indices = 0;
8825 if fill_area_diag_enabled() {
8826 self.fill_area_diag.reset_frame(width, height);
8827 }
8828 self.static_span.armed = true;
8831 self.segment_surfaces.begin_frame();
8834 self.display_clip.frame_root_view = Some(view.clone());
8838 }
8839
8840 let text_cache_len = packet.text_cache_len;
8844 let result = self.render_graph(view, root_target, packet, returns);
8845 #[cfg(not(target_arch = "wasm32"))]
8846 {
8847 self.display_clip.frame_root_view = None;
8848 }
8849 let after_graph = Instant::now();
8850 self.flush_deferred_offscreen_releases();
8851 #[cfg(not(target_arch = "wasm32"))]
8852 {
8853 if fill_area_diag_enabled() {
8854 let (composite_px2, offscreen_px2) = self.effect_renderer.take_fill_diag_fill_px2();
8858 self.fill_area_diag
8859 .add_effect_fill(composite_px2, offscreen_px2);
8860 self.fill_area_diag.finish_frame(width, height);
8861 }
8862 }
8863
8864 #[cfg(target_arch = "wasm32")]
8865 {
8866 const WASM_BATCH_POOL_MARGIN: usize = 4;
8867 self.wasm_uniform_batches.truncate(
8868 self.wasm_uniform_batch_cursor
8869 .saturating_add(WASM_BATCH_POOL_MARGIN),
8870 );
8871 self.wasm_shape_batches.truncate(
8872 self.wasm_shape_batch_cursor
8873 .saturating_add(WASM_BATCH_POOL_MARGIN),
8874 );
8875 self.wasm_image_batches.truncate(
8876 self.wasm_image_batch_cursor
8877 .saturating_add(WASM_BATCH_POOL_MARGIN),
8878 );
8879 }
8880 self.staged_uploads
8881 .shrink_retained_capacity(RETAINED_STAGED_UPLOAD_BYTES, RETAINED_STAGED_UPLOAD_COPIES);
8882
8883 self.layer_surface_cache.finish_frame(&self.frame_stats);
8884 for target in self.layer_surface_cache.take_recycled() {
8885 self.defer_offscreen_release(target);
8886 }
8887 #[cfg(not(target_arch = "wasm32"))]
8888 self.retained_bundle_cache.end_frame();
8889
8890 self.frame_stats.offscreen_pool_size.set(
8891 self.effect_renderer
8892 .retained_offscreen_count()
8893 .saturating_add(self.frame_graph_executor.retained_texture_count())
8894 as u32,
8895 );
8896 self.frame_stats.offscreen_pool_bytes.set(
8897 (self.effect_renderer.retained_offscreen_bytes() as u64)
8898 .saturating_add(self.frame_graph_executor.retained_texture_bytes()),
8899 );
8900 self.frame_stats
8901 .text_pool_size
8902 .set(self.text_image_cache.len() as u32);
8903 self.frame_stats
8904 .image_cache_size
8905 .set(self.image_texture_cache.len() as u32);
8906 self.frame_stats.text_cache_size.set(text_cache_len as u32);
8907 self.effect_renderer
8908 .merge_and_reset_debug_counters(&self.frame_stats);
8909 self.frame_graph_executor.reset_upload_allocators();
8910 let snapshot = self.frame_stats.snapshot();
8911 self.last_frame_stats = Some(snapshot);
8912 PRESENTED_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
8913 update_frame_warmup_budget(&mut self.pending_frame_warmup_frames, &snapshot);
8914 self.frame_stats.maybe_print_snapshot(
8915 snapshot,
8916 &mut self.frame_count,
8917 self.gpu_stats_enabled,
8918 );
8919 if self.gpu_stats_enabled && self.frame_count.is_multiple_of(60) {
8920 gpu_stats::print_gpu_memory_report(&self.device, self.frame_count);
8921 }
8922 self.frame_stats.reset();
8923 let after_stats = Instant::now();
8924 if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
8925 log::warn!(
8926 "[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
8927 instant_ms(render_start, after_graph),
8928 instant_ms(after_graph, after_stats),
8929 );
8930 }
8931 if result.is_ok() {
8932 returns.outcome = PresentOutcome::Presented;
8935 }
8936 result
8937 }
8938
8939 pub(crate) fn cancel_packet(
8952 packet: FramePacket,
8953 reason: CancelReason,
8954 returns: &mut RenderReturns,
8955 ) -> Result<(), String> {
8956 let FramePacket {
8957 frame_id,
8958 viewport: _,
8959 renderer_epoch: _,
8960 surface_epoch: _,
8961 root_scale: _,
8962 root,
8963 overlay: _,
8964 replay,
8965 text_cache_len: _,
8966 recycled_confirmations: _,
8967 replay_preconsumed,
8968 } = packet;
8969 match root {
8970 PacketRoot::Direct(root) => {
8971 returns.scene = Some(root.scene);
8981 #[cfg(not(target_arch = "wasm32"))]
8982 if !replay_preconsumed {
8983 returns.cancelled_replay = Some(replay);
8984 }
8985 }
8986 PacketRoot::Surface(_) => {}
8987 }
8988 #[cfg(target_arch = "wasm32")]
8989 let _ = (replay, replay_preconsumed);
8990 returns.ack = None;
8991 returns.frame_id = frame_id;
8992 returns.outcome = PresentOutcome::Cancelled(reason);
8993 Ok(())
8994 }
8995
8996 pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
8997 self.last_frame_stats
8998 }
8999
9000 pub fn needs_frame_warmup(&self) -> bool {
9001 self.pending_frame_warmup_frames > 0
9002 }
9003
9004 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
9005 let layer_surface_cache_stats = self.layer_surface_cache.debug_stats();
9006 DebugCpuAllocationStats {
9007 scene_graph_node_count: 0,
9008 scene_graph_heap_bytes: 0,
9009 scene_hits_len: 0,
9010 scene_hits_cap: 0,
9011 scene_node_index_len: 0,
9012 scene_node_index_cap: 0,
9013 text_renderer_pool_len: self.text_image_cache.len(),
9014 text_renderer_pool_cap: self.text_image_cache.cap().get(),
9015 swash_image_cache_len: 0,
9016 swash_image_cache_cap: 0,
9017 swash_outline_cache_len: 0,
9018 swash_outline_cache_cap: 0,
9019 image_texture_cache_len: self.image_texture_cache.len(),
9020 image_texture_cache_cap: self.image_texture_cache.cap().get(),
9021 scratch_shape_data_cap: self.scratch_shape_data.capacity(),
9022 scratch_gradients_cap: self.scratch_gradients.capacity(),
9023 scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
9024 scratch_image_indices_cap: self.scratch_image_indices.capacity(),
9025 scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
9026 scratch_segment_items_cap: self.scratch_segment_items.capacity(),
9027 scratch_effect_ranges_cap: self.scratch_effect_ranges.capacity(),
9028 scratch_layer_events_cap: self.scratch_layer_events.capacity(),
9029 staged_upload_bytes_cap: self.staged_uploads.bytes.capacity(),
9030 staged_upload_copies_cap: self.staged_uploads.copies.capacity(),
9031 layer_surface_cache_len: layer_surface_cache_stats.entries_len,
9032 layer_surface_cache_cap: layer_surface_cache_stats.entries_cap,
9033 layer_surface_cache_identity_len: layer_surface_cache_stats.identity_len,
9034 layer_surface_cache_identity_cap: layer_surface_cache_stats.identity_cap,
9035 layer_surface_rect_cache_len: 0,
9038 layer_surface_rect_cache_cap: 0,
9039 layer_surface_requirements_cache_len: 0,
9040 layer_surface_requirements_cache_cap: 0,
9041 layer_cache_seen_this_frame_len: layer_surface_cache_stats.seen_this_frame_len,
9042 layer_cache_seen_this_frame_cap: layer_surface_cache_stats.seen_this_frame_cap,
9043 }
9044 }
9045
9046 pub fn render_to_rgba_pixels(
9047 &mut self,
9048 width: u32,
9049 height: u32,
9050 packet: FramePacket,
9051 surface_epoch: u64,
9052 returns: &mut RenderReturns,
9053 ) -> Result<Vec<u8>, String> {
9054 if width == 0 || height == 0 {
9055 return Err("Screenshot size must be non-zero".to_string());
9056 }
9057
9058 let output_texture = self.device.create_texture(&wgpu::TextureDescriptor {
9059 label: Some("Screenshot Output Texture"),
9060 size: wgpu::Extent3d {
9061 width,
9062 height,
9063 depth_or_array_layers: 1,
9064 },
9065 mip_level_count: 1,
9066 sample_count: 1,
9067 dimension: wgpu::TextureDimension::D2,
9068 format: self.surface_format,
9069 usage: if capture_root_target_reads() {
9070 wgpu::TextureUsages::RENDER_ATTACHMENT
9071 | wgpu::TextureUsages::COPY_SRC
9072 | wgpu::TextureUsages::TEXTURE_BINDING
9073 } else {
9074 wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC
9075 },
9076 view_formats: &[],
9077 });
9078 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
9079 let root_target = OffscreenTarget::from_readable_texture(&output_texture, &output_view);
9080
9081 self.render(
9082 &output_view,
9083 root_target.as_ref(),
9084 width,
9085 height,
9086 packet,
9087 surface_epoch,
9088 returns,
9089 )?;
9090
9091 let bytes_per_pixel = 4u32;
9092 let unpadded_bytes_per_row = width
9093 .checked_mul(bytes_per_pixel)
9094 .ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
9095 let padded_bytes_per_row =
9096 align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
9097 let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
9098
9099 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9100 label: Some("Screenshot Readback Buffer"),
9101 size: output_buffer_size,
9102 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
9103 mapped_at_creation: false,
9104 });
9105
9106 let device = self.device.clone();
9107 let queue = self.queue.clone();
9108 let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
9109 let source = graph.import_surface("screenshot-copy-source");
9110 graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
9111 context.encoder.copy_texture_to_buffer(
9112 wgpu::TexelCopyTextureInfo {
9113 texture: &output_texture,
9114 mip_level: 0,
9115 origin: wgpu::Origin3d::ZERO,
9116 aspect: wgpu::TextureAspect::All,
9117 },
9118 wgpu::TexelCopyBufferInfo {
9119 buffer: &output_buffer,
9120 layout: wgpu::TexelCopyBufferLayout {
9121 offset: 0,
9122 bytes_per_row: Some(padded_bytes_per_row),
9123 rows_per_image: Some(height),
9124 },
9125 },
9126 wgpu::Extent3d {
9127 width,
9128 height,
9129 depth_or_array_layers: 1,
9130 },
9131 );
9132 Ok(())
9133 });
9134 let mut executor = std::mem::take(&mut self.frame_graph_executor);
9135 let execution = executor.execute_recorded_graph(&device, &queue, graph);
9136 self.frame_graph_executor = executor;
9137 let execution = execution.map_err(|error| error.to_string())?;
9138 let submission_index = execution.submission;
9139 let copy_stats = execution.stats;
9140 self.last_frame_stats = self
9141 .last_frame_stats
9142 .map(|snapshot| snapshot.with_command_stats_added(copy_stats));
9143
9144 let buffer_slice = output_buffer.slice(..);
9145 let (tx, rx) = mpsc::channel();
9146 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
9147 let _ = tx.send(result);
9148 });
9149 let _ = self.device.poll(wgpu::PollType::Wait {
9150 submission_index: Some(submission_index),
9151 timeout: None,
9152 });
9153
9154 match rx.recv_timeout(Duration::from_secs(3)) {
9155 Ok(Ok(())) => {}
9156 Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
9157 Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
9158 }
9159
9160 let mapped = buffer_slice.get_mapped_range();
9161 let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
9162
9163 let src_row_len = padded_bytes_per_row as usize;
9164 let dst_row_len = unpadded_bytes_per_row as usize;
9165 for row in 0..height as usize {
9166 let src_offset = row * src_row_len;
9167 let dst_offset = row * dst_row_len;
9168 pixels[dst_offset..dst_offset + dst_row_len]
9169 .copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
9170 }
9171 drop(mapped);
9172 output_buffer.unmap();
9173
9174 self.convert_surface_pixels_to_rgba(&mut pixels)?;
9175 Ok(pixels)
9176 }
9177
9178 fn render_graph(
9179 &mut self,
9180 surface_view: &wgpu::TextureView,
9181 root_target: Option<&OffscreenTarget>,
9182 packet: FramePacket,
9183 returns: &mut RenderReturns,
9184 ) -> Result<(), String> {
9185 let device = self.device.clone();
9186 let queue = self.queue.clone();
9187 let graph_start = Instant::now();
9188
9189 #[cfg(not(target_arch = "wasm32"))]
9190 {
9191 let mut executor = std::mem::take(&mut self.frame_graph_executor);
9192 let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
9193 let surface = frame_graph.import_surface("renderer-surface");
9194 frame_graph.add_fallible_recorded_command_pass(
9195 Some("Renderer Frame Pass"),
9196 &[],
9197 &[surface],
9198 |frame_encoder| {
9199 self.render_graph_recorded(
9200 surface_view,
9201 root_target,
9202 packet,
9203 returns,
9204 frame_encoder,
9205 )
9206 },
9207 );
9208 let after_build = Instant::now();
9209 let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
9210 let after_execute = Instant::now();
9211 self.frame_graph_executor = executor;
9212 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
9213 log::warn!(
9214 "[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
9215 instant_ms(graph_start, after_build),
9216 instant_ms(after_build, after_execute),
9217 );
9218 }
9219
9220 match execution {
9221 Ok(execution) => {
9222 if execution.stats.pass_count > 0 {
9223 self.frame_stats.record_command_stats(execution.stats);
9224 }
9225 Ok(())
9226 }
9227 Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => Ok(()),
9228 Err(error) => Err(error.to_string()),
9229 }
9230 }
9231
9232 #[cfg(target_arch = "wasm32")]
9233 {
9234 let mut executor = std::mem::take(&mut self.frame_graph_executor);
9235 let (result, execution) = {
9236 let mut frame_encoder =
9237 executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
9238 let initial_pass_count = frame_encoder.recorded_pass_count();
9239 let result = self.render_graph_recorded(
9240 surface_view,
9241 root_target,
9242 packet,
9243 returns,
9244 &mut frame_encoder,
9245 );
9246 let execution =
9247 if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
9248 Some(frame_encoder.finish())
9249 } else {
9250 None
9251 };
9252 (result, execution)
9253 };
9254 let after_execute = Instant::now();
9255 self.frame_graph_executor = executor;
9256 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
9257 log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
9258 }
9259 if let Some(execution) = execution {
9260 self.frame_stats.record_command_stats(execution.stats);
9261 }
9262 result
9263 }
9264 }
9265
9266 fn render_graph_recorded<C: FrameCommandRecorder>(
9267 &mut self,
9268 surface_view: &wgpu::TextureView,
9269 root_target: Option<&OffscreenTarget>,
9270 packet: FramePacket,
9271 returns: &mut RenderReturns,
9272 frame_encoder: &mut C,
9273 ) -> Result<(), String> {
9274 let recorded_start = Instant::now();
9275
9276 #[cfg(not(target_arch = "wasm32"))]
9291 let mut packet = packet;
9292 #[cfg(not(target_arch = "wasm32"))]
9293 if !packet.replay_preconsumed {
9294 if let PacketRoot::Direct(root) = &packet.root {
9295 let ops = std::mem::take(&mut packet.replay);
9296 let (ack, recycled) = self.consume_replay_ops(
9297 ops,
9298 &root.scene.shapes,
9299 &root.scene.brushes,
9300 packet.root_scale,
9301 );
9302 returns.ack = Some((ack, recycled));
9303 }
9304 }
9305
9306 let FramePacket {
9307 frame_id,
9308 viewport: (width, height),
9309 renderer_epoch: _,
9310 surface_epoch: _,
9311 root_scale,
9312 root,
9313 overlay,
9314 replay: _,
9315 text_cache_len: _,
9316 recycled_confirmations: _,
9317 replay_preconsumed: _,
9318 } = packet;
9319
9320 let mut backend = RecordingSurfaceBackend {
9321 renderer: self,
9322 recorder: frame_encoder,
9323 };
9324
9325 let surface_packet = match root {
9326 PacketRoot::Direct(root) => {
9327 let direct_render_start = Instant::now();
9328 let result = match execute_render_root_direct(
9329 &mut backend,
9330 surface_view,
9331 root_target,
9332 *root,
9333 width,
9334 height,
9335 root_scale,
9336 wgpu::LoadOp::Clear(CLEAR_COLOR),
9337 ) {
9338 Ok(scene) => {
9343 returns.scene = Some(scene);
9344 Ok(())
9345 }
9346 Err((error, scene)) => {
9347 returns.scene = Some(scene);
9348 Err(error)
9349 }
9350 };
9351 if result.is_ok() {
9352 if let Some(overlay) = overlay {
9353 Self::render_overlay_packet(
9354 &mut backend,
9355 surface_view,
9356 overlay,
9357 width,
9358 height,
9359 root_scale,
9360 )?;
9361 }
9362 }
9363 let after_direct_render = Instant::now();
9364 if let Some(total_ms) =
9365 should_log_wgpu_render_stage(recorded_start, after_direct_render)
9366 {
9367 log::warn!(
9368 "[wgpu-render-stage:recorded-direct-root] frame={frame_id} total_ms={total_ms:.2} render_ms={:.2}",
9369 instant_ms(direct_render_start, after_direct_render),
9370 );
9371 }
9372 return result;
9373 }
9374 PacketRoot::Surface(surface_packet) => surface_packet,
9375 };
9376 let after_root_collect = Instant::now();
9377
9378 let RootSurfacePacket {
9379 lowered,
9380 source,
9381 transform_to_parent,
9382 node_id,
9383 backdrop,
9384 graphics_layer,
9385 local_bounds,
9386 clip_rect,
9387 shadow_clip,
9388 } = *surface_packet;
9389 let mut lowered = lowered;
9390 lowered.source = source;
9391
9392 let viewport_rect = Rect {
9397 x: 0.0,
9398 y: 0.0,
9399 width: width as f32 / root_scale,
9400 height: height as f32 / root_scale,
9401 };
9402 let root_surface = execute_render_layer_surface(
9403 &mut backend,
9404 &mut lowered,
9405 LayerSurfaceRequest {
9406 root_scale,
9407 backdrop_underlay: None,
9408 backdrop_underlay_color: None,
9409 allow_runtime_cache: false,
9410 logical_rect_override: Some(viewport_rect),
9411 capture_clip_override: None,
9412 activates_nested_capture: false,
9413 translation_context: TranslationRenderContext::default(),
9414 },
9415 )?;
9416 let root_quad = transform_to_parent.map_rect(root_surface.logical_rect);
9417 let root_dest_quad = scaled_quad(root_quad, root_scale);
9418
9419 let needs_root_composite_target =
9420 backdrop.is_some() || graphics_layer.shadow_elevation > 0.0;
9421
9422 if needs_root_composite_target {
9423 let composite_target = backend.acquire_frame_surface(width, height);
9424 backend.clear_target_view_with_load_op(
9425 &composite_target.view,
9426 wgpu::LoadOp::Clear(CLEAR_COLOR),
9427 );
9428
9429 if let Some(backdrop) = &backdrop {
9430 execute_apply_backdrop_layer_to_target(
9431 &mut backend,
9432 &composite_target,
9433 &BackdropLayer {
9434 node_id,
9435 rect: quad_bounds(transform_to_parent.map_rect(local_bounds)),
9436 clip: clip_rect.map(|clip| quad_bounds(transform_to_parent.map_rect(clip))),
9437 snap_anchor: None,
9438 effect: backdrop.clone(),
9439 z_index: 0,
9440 },
9441 None,
9442 width,
9443 height,
9444 root_scale,
9445 None,
9446 )?;
9447 }
9448
9449 let mut root_shadow_scene = CompositorScene::new();
9450 let root_shadow_clip =
9451 shadow_clip.map(|clip| quad_bounds(transform_to_parent.map_rect(clip)));
9452 push_layer_shadow(
9453 &mut root_shadow_scene,
9454 &graphics_layer,
9455 local_bounds,
9456 quad_bounds(transform_to_parent.map_rect(local_bounds)),
9457 root_shadow_clip,
9458 );
9459 for shadow in &root_shadow_scene.shadow_draws {
9460 backend.render_shadow_draw(
9461 &composite_target.view,
9462 shadow,
9463 width,
9464 height,
9465 root_scale,
9466 );
9467 }
9468
9469 let composite_dest_quad =
9470 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
9471 execute_composite_surface_to_view(
9472 &mut backend,
9473 root_surface.target.target(),
9474 &composite_target.view,
9475 (width, height),
9476 composite_dest_quad,
9477 root_surface.composite_alpha,
9478 wgpu::LoadOp::Load,
9479 None,
9480 root_surface.blend_mode,
9481 root_surface.sample_mode,
9482 )?;
9483 backend.composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
9484 &composite_target,
9485 surface_view,
9486 1.0,
9487 wgpu::LoadOp::Clear(CLEAR_COLOR),
9488 None,
9489 None,
9490 BlendMode::SrcOver,
9491 None,
9492 CompositeSampleMode::Linear,
9493 );
9494 backend.release_frame_surface(composite_target);
9495 } else {
9496 let composite_dest_quad =
9497 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
9498 execute_composite_surface_to_view(
9499 &mut backend,
9500 root_surface.target.target(),
9501 surface_view,
9502 (width, height),
9503 composite_dest_quad,
9504 root_surface.composite_alpha,
9505 wgpu::LoadOp::Clear(CLEAR_COLOR),
9506 None,
9507 root_surface.blend_mode,
9508 root_surface.sample_mode,
9509 )?;
9510 }
9511 backend.release_layer_surface_target(root_surface.target);
9512 if let Some(overlay) = overlay {
9513 Self::render_overlay_packet(
9514 &mut backend,
9515 surface_view,
9516 overlay,
9517 width,
9518 height,
9519 root_scale,
9520 )?;
9521 }
9522 let after_layer_render = Instant::now();
9523 if let Some(total_ms) = should_log_wgpu_render_stage(recorded_start, after_layer_render) {
9524 log::warn!(
9525 "[wgpu-render-stage:recorded-layer-root] total_ms={total_ms:.2} collect_ms={:.2} render_ms={:.2}",
9526 instant_ms(recorded_start, after_root_collect),
9527 instant_ms(after_root_collect, after_layer_render),
9528 );
9529 }
9530 Ok(())
9531 }
9532
9533 fn render_overlay_packet<C: FrameCommandRecorder>(
9537 backend: &mut RecordingSurfaceBackend<'_, '_, C>,
9538 surface_view: &wgpu::TextureView,
9539 overlay: CollectedLayer,
9540 width: u32,
9541 height: u32,
9542 root_scale: f32,
9543 ) -> Result<(), String> {
9544 if !overlay.child_layers.is_empty()
9545 || !root_direct_scene_events_are_supported(&overlay.scene, false)
9546 || !direct_root_child_underlays_are_supported(&overlay, false)
9547 {
9548 return Err("dev overlay graph must stay directly renderable".to_string());
9549 }
9550 execute_render_root_direct(
9551 backend,
9552 surface_view,
9553 None,
9554 overlay,
9555 width,
9556 height,
9557 root_scale,
9558 wgpu::LoadOp::Load,
9559 )
9560 .map(|_overlay_scene| ())
9561 .map_err(|(error, _overlay_scene)| error)
9562 }
9563
9564 #[allow(clippy::too_many_arguments)]
9565 fn encode_non_effect_segment_commands<C: FrameCommandRecorder>(
9566 &mut self,
9567 frame_encoder: &mut C,
9568 target_view: &wgpu::TextureView,
9569 ordered_items: &[(usize, SegmentDrawItem)],
9570 composites: &[(usize, CompositeBatchItem<'_>)],
9571 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
9572 shapes: &[DrawShape],
9573 brushes: &[Brush],
9574 images: &[ImageDraw],
9575 texts: &[TextDraw],
9576 shadow_draws: &[ShadowDraw],
9577 retained_draws: &[RetainedDraw],
9578 initial_load_op: wgpu::LoadOp<wgpu::Color>,
9579 width: u32,
9580 height: u32,
9581 root_scale: f32,
9582 ) -> Result<SegmentCommandEncodeOutcome, String> {
9583 let mut first_batch = true;
9584 for command in
9585 SegmentCommandIter::new(ordered_items, shapes, images, self.shape_batch_limits)
9586 {
9587 match command {
9588 SegmentRenderCommand::DrawChunk(chunk) => {
9589 let load_op = if first_batch {
9590 initial_load_op
9591 } else {
9592 wgpu::LoadOp::Load
9593 };
9594 let outcome = self.render_segment_draw_chunk(
9595 frame_encoder,
9596 target_view,
9597 ordered_items,
9598 composites,
9599 shader_composites,
9600 shapes,
9601 brushes,
9602 images,
9603 texts,
9604 retained_draws,
9605 chunk,
9606 width,
9607 height,
9608 root_scale,
9609 load_op,
9610 )?;
9611 if outcome.rendered_any {
9612 frame_encoder.record_passes(outcome.pass_count);
9613 first_batch = false;
9614 }
9615 }
9616 SegmentRenderCommand::Shadow(index) => {
9617 if first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
9618 {
9619 let _clear = frame_encoder.encoder().begin_render_pass(
9620 &wgpu::RenderPassDescriptor {
9621 label: Some("Shadow Pre-Clear"),
9622 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
9623 view: target_view,
9624 resolve_target: None,
9625 depth_slice: None,
9626 ops: wgpu::Operations {
9627 load: initial_load_op,
9628 store: wgpu::StoreOp::Store,
9629 },
9630 })],
9631 depth_stencil_attachment: None,
9632 timestamp_writes: None,
9633 occlusion_query_set: None,
9634 multiview_mask: None,
9635 },
9636 );
9637 }
9638 frame_encoder.record_pass();
9639 first_batch = false;
9640 }
9641 let pass_count_before = frame_encoder.recorded_pass_count();
9642 self.encode_shadow_draw(
9643 frame_encoder,
9644 target_view,
9645 &shadow_draws[index],
9646 width,
9647 height,
9648 root_scale,
9649 );
9650 if frame_encoder.recorded_pass_count() > pass_count_before {
9651 first_batch = false;
9652 }
9653 }
9654 }
9655 }
9656 Ok(SegmentCommandEncodeOutcome { first_batch })
9657 }
9658
9659 #[cfg(not(target_arch = "wasm32"))]
9660 #[allow(clippy::too_many_arguments)]
9661 fn render_segment_draw_chunk_fused_native<C: FrameCommandRecorder>(
9662 &mut self,
9663 frame_encoder: &mut C,
9664 target_view: &wgpu::TextureView,
9665 ordered_items: &[(usize, SegmentDrawItem)],
9666 composites: &[(usize, CompositeBatchItem<'_>)],
9667 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
9668 shapes: &[DrawShape],
9669 brushes: &[Brush],
9670 images: &[ImageDraw],
9671 texts: &[TextDraw],
9672 retained_draws: &[RetainedDraw],
9673 chunk: &SegmentDrawChunkPlan,
9674 width: u32,
9675 height: u32,
9676 root_scale: f32,
9677 load_op: wgpu::LoadOp<wgpu::Color>,
9678 ) -> Result<Option<SegmentRenderOutcome>, String> {
9679 let Some(partitions) = native_segment_fusion_partitions(
9680 ordered_items,
9681 shapes,
9682 brushes,
9683 chunk,
9684 self.shape_batch_limits,
9685 )?
9686 else {
9687 return Ok(None);
9688 };
9689
9690 let mut rendered_any = false;
9691 let mut pass_count = 0_u32;
9692 let mut next_load_op = load_op;
9693 let encode_started = Instant::now();
9694 let mut partition_count = 0_u64;
9695 for partition in partitions {
9696 partition_count += 1;
9697 let outcome = self.render_segment_draw_chunk_fused_native_partition(
9698 frame_encoder,
9699 target_view,
9700 ordered_items,
9701 composites,
9702 shader_composites,
9703 shapes,
9704 brushes,
9705 images,
9706 texts,
9707 retained_draws,
9708 &partition.chunk,
9709 partition.budget,
9710 width,
9711 height,
9712 root_scale,
9713 next_load_op,
9714 )?;
9715 if outcome.rendered_any {
9716 rendered_any = true;
9717 pass_count = pass_count.saturating_add(outcome.pass_count);
9718 next_load_op = wgpu::LoadOp::Load;
9719 }
9720 }
9721
9722 self.segment_encode_stats
9723 .note_call(partition_count, encode_started.elapsed().as_micros() as u64);
9724
9725 Ok(Some(SegmentRenderOutcome {
9726 rendered_any,
9727 pass_count,
9728 }))
9729 }
9730
9731 #[cfg(not(target_arch = "wasm32"))]
9732 #[allow(clippy::too_many_arguments)]
9733 fn render_segment_draw_chunk_fused_native_partition<C: FrameCommandRecorder>(
9734 &mut self,
9735 frame_encoder: &mut C,
9736 target_view: &wgpu::TextureView,
9737 ordered_items: &[(usize, SegmentDrawItem)],
9738 composites: &[(usize, CompositeBatchItem<'_>)],
9739 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
9740 shapes: &[DrawShape],
9741 brushes: &[Brush],
9742 images: &[ImageDraw],
9743 texts: &[TextDraw],
9744 retained_draws: &[RetainedDraw],
9745 chunk: &SegmentDrawChunkPlan,
9746 budget: NativeSegmentFusionBudget,
9747 width: u32,
9748 height: u32,
9749 root_scale: f32,
9750 load_op: wgpu::LoadOp<wgpu::Color>,
9751 ) -> Result<SegmentRenderOutcome, String> {
9752 let partition_start = Instant::now();
9753 let mut staged_uploads = self.take_staged_uploads();
9754 staged_uploads.clear();
9755 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
9756 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
9757 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
9758 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
9759 let mut span_cache = std::mem::take(&mut self.static_span);
9762 let mut segment_surfaces = std::mem::take(&mut self.segment_surfaces);
9765
9766 image_vertices.clear();
9767 image_indices.clear();
9768 image_cmds.clear();
9769 glyph_cmds.clear();
9770
9771 let result = (|| {
9772 let viewport = ViewportUniformParams {
9773 width,
9774 height,
9775 offset: [0.0, 0.0],
9776 };
9777 self.prewarm_offscreen_text_glyph_draws_in_chunk(
9778 ordered_items,
9779 texts,
9780 chunk,
9781 viewport,
9782 root_scale,
9783 &mut staged_uploads,
9784 &mut image_vertices,
9785 &mut image_indices,
9786 &mut glyph_cmds,
9787 )?;
9788 let mut shape_refs = Vec::with_capacity(budget.shape_count);
9789 for batch in chunk.iter() {
9790 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
9791 continue;
9792 };
9793 for (_, item) in &ordered_items[start..end] {
9794 let SegmentDrawItem::Shape(shape_index) = item else {
9795 return Err(format!(
9796 "shape batch contains non-shape draw item: {item:?}"
9797 ));
9798 };
9799 shape_refs.push(&shapes[*shape_index]);
9800 }
9801 }
9802 let after_shape_refs = Instant::now();
9803
9804 let mut direct_shape_uploads = StagedBufferUploads::default();
9805 let mut shape_upload_base = 0u64;
9806 if !shape_refs.is_empty() {
9807 let Some((_, upload_base)) = self.prepare_shapes_batch_direct(
9808 frame_encoder,
9809 shape_refs.iter().copied(),
9810 brushes,
9811 root_scale,
9812 viewport,
9813 &mut direct_shape_uploads,
9814 ) else {
9815 return Err(
9816 "native fused segment shape preparation produced no draw batch".to_string(),
9817 );
9818 };
9819 shape_upload_base = upload_base;
9820 }
9821 let after_shape_prepare = Instant::now();
9822
9823 let mut segment_captures: Vec<SegmentCaptureJob> = Vec::new();
9831 let mut segment_composite_plans: Vec<(usize, SegmentCompositePlan)> = Vec::new();
9832 if segment_surfaces.enabled() {
9833 self.plan_segment_surfaces(
9834 &mut segment_surfaces,
9835 ordered_items,
9836 chunk,
9837 retained_draws,
9838 &mut staged_uploads,
9839 &mut segment_captures,
9840 &mut segment_composite_plans,
9841 );
9842 }
9843
9844 let first_batch_info = match chunk.batches.first() {
9850 Some(&SegmentBatchPlan::Shape {
9851 start,
9852 end,
9853 blend_mode,
9854 }) => {
9855 let mut has_gradient = false;
9856 for (_, item) in &ordered_items[start..end] {
9857 if let SegmentDrawItem::Shape(shape_index) = item {
9858 has_gradient |=
9859 shape_gradient_stop_count(&shapes[*shape_index], brushes) > 0;
9860 }
9861 }
9862 Some((end - start, blend_mode, has_gradient))
9863 }
9864 _ => None,
9865 };
9866 let span_decision = span_cache.engage(
9867 load_op,
9868 first_batch_info,
9869 width,
9870 height,
9871 &self.scratch_shape_data,
9872 &self.scratch_gradients,
9873 );
9874 let span_skip = match span_decision {
9875 StaticSpanDecision::Hit { skip } => {
9876 if fill_area_diag_enabled() {
9877 self.fill_area_diag
9882 .note_static_span_skip(&self.scratch_shape_data[..skip]);
9883 }
9884 skip
9885 }
9886 _ => 0,
9887 };
9888
9889 let rim_mesh_on = rim_mesh_enabled();
9897 let mut chunk_rims: Vec<RimDraw> = Vec::new();
9898
9899 let mut fused_batches = Vec::with_capacity(chunk.batches.len());
9900 let mut shape_cursor = 0_u32;
9901 let mut composite_cursor = 0usize;
9902 let mut shader_composite_cursor = 0usize;
9903 for (batch_index, batch) in chunk.iter().enumerate() {
9904 match batch {
9905 SegmentBatchPlan::Shape {
9906 start,
9907 end,
9908 blend_mode,
9909 } => {
9910 let mut has_gradient = false;
9911 for (_, item) in &ordered_items[start..end] {
9912 let SegmentDrawItem::Shape(shape_index) = item else {
9913 return Err(format!(
9914 "shape batch contains non-shape draw item: {item:?}"
9915 ));
9916 };
9917 has_gradient |=
9918 shape_gradient_stop_count(&shapes[*shape_index], brushes) > 0;
9919 }
9920 let skip = if batch_index == 0 { span_skip } else { 0 };
9925 let shape_count = end - start;
9926 if shape_count > 0 {
9927 if rim_mesh_on
9928 && self.instanced_quads.is_some()
9929 && blend_mode == BlendMode::SrcOver
9930 {
9931 for offset in skip..shape_count {
9932 let global_index = shape_cursor + offset as u32;
9937 let converted = &self.scratch_shape_data[global_index as usize];
9938 let Some(band) = rim_mesh_band(converted) else {
9939 continue;
9940 };
9941 let vertex_mark = self.rim_mesh_vertices.len();
9942 let index_mark = self.rim_mesh_indices.len();
9943 if emit_arc_band_mesh(
9944 converted,
9945 global_index,
9946 &band,
9947 &mut self.rim_mesh_vertices,
9948 &mut self.rim_mesh_indices,
9949 )
9950 .is_none()
9951 {
9952 self.rim_mesh_vertices.truncate(vertex_mark);
9955 self.rim_mesh_indices.truncate(index_mark);
9956 continue;
9957 }
9958 if self.rim_mesh_vertices.len() > RIM_MESH_VERTEX_CAPACITY
9959 || self.rim_mesh_indices.len() > RIM_MESH_INDEX_CAPACITY
9960 {
9961 self.rim_mesh_vertices.truncate(vertex_mark);
9965 self.rim_mesh_indices.truncate(index_mark);
9966 rim_mesh_capacity_warn();
9967 continue;
9968 }
9969 chunk_rims.push(RimDraw {
9970 shape_index: global_index,
9971 first_index: index_mark as u32,
9972 index_count: (self.rim_mesh_indices.len() - index_mark)
9973 as u32,
9974 });
9975 if fill_area_diag_enabled() {
9976 self.fill_area_diag.note_rim_mesh(
9977 converted,
9978 triangles_shoelace_area(
9979 &self.rim_mesh_vertices,
9980 &self.rim_mesh_indices[index_mark..],
9981 ),
9982 );
9983 }
9984 self.rim_meshes_emitted += 1;
9985 if self.rim_meshes_emitted % 600 == 1 {
9986 log::debug!(
9987 "[rim-mesh] {} rims meshed lifetime ({} verts live this frame)",
9988 self.rim_meshes_emitted,
9989 self.rim_mesh_vertices.len(),
9990 );
9991 }
9992 }
9993 }
9994 if shape_count > skip {
9995 fused_batches.push(FusedSegmentBatch::Shape {
9996 batch: PreparedShapeBatch {
9997 vertex_start: (shape_cursor + skip as u32) * 6,
9998 vertex_count: (shape_count - skip) as u32 * 6,
9999 has_gradient,
10000 },
10001 blend_mode,
10002 });
10003 }
10004 shape_cursor += shape_count as u32;
10005 }
10006 }
10007 SegmentBatchPlan::Image {
10008 start,
10009 end,
10010 blend_mode,
10011 } => {
10012 let cmd_start = image_cmds.len();
10013 for (_, item) in &ordered_items[start..end] {
10014 let SegmentDrawItem::Image(image_index) = item else {
10015 return Err(format!(
10016 "image batch contains non-image draw item: {item:?}"
10017 ));
10018 };
10019 self.append_image_draw_cmd(
10020 &images[*image_index],
10021 viewport,
10022 root_scale,
10023 &mut image_vertices,
10024 &mut image_indices,
10025 &mut image_cmds,
10026 )?;
10027 }
10028 let cmd_end = image_cmds.len();
10029 if cmd_start < cmd_end {
10030 fused_batches.push(FusedSegmentBatch::Image {
10031 cmd_range: cmd_start..cmd_end,
10032 blend_mode,
10033 });
10034 }
10035 }
10036 SegmentBatchPlan::Text { start, end } => {
10037 let glyph_cmd_start = glyph_cmds.len();
10038 let image_cmd_start = image_cmds.len();
10039 let text_draws =
10040 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
10041 if !self.append_text_glyph_draws(
10042 text_draws,
10043 viewport,
10044 root_scale,
10045 false,
10046 &mut staged_uploads,
10047 &mut image_vertices,
10048 &mut image_indices,
10049 &mut glyph_cmds,
10050 )? {
10051 let text_draws =
10052 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
10053 self.append_text_image_draw_cmds(
10054 text_draws,
10055 viewport,
10056 root_scale,
10057 &mut image_vertices,
10058 &mut image_indices,
10059 &mut image_cmds,
10060 )?;
10061 }
10062 let image_cmd_end = image_cmds.len();
10063 let glyph_cmd_end = glyph_cmds.len();
10064 if image_cmd_start < image_cmd_end || glyph_cmd_start < glyph_cmd_end {
10065 fused_batches.push(FusedSegmentBatch::Text {
10066 image_cmd_range: image_cmd_start..image_cmd_end,
10067 glyph_cmd_range: glyph_cmd_start..glyph_cmd_end,
10068 });
10069 }
10070 }
10071 SegmentBatchPlan::Composite { start, end } => {
10072 for (_, item) in &ordered_items[start..end] {
10073 if !matches!(item, SegmentDrawItem::Composite(_)) {
10074 return Err(format!(
10075 "composite batch contains non-composite draw item: {item:?}"
10076 ));
10077 }
10078 }
10079 let draw_count = end - start;
10080 if draw_count > 0 {
10081 let draw_start = composite_cursor;
10082 composite_cursor += draw_count;
10083 fused_batches.push(FusedSegmentBatch::Composite {
10084 draw_range: draw_start..composite_cursor,
10085 });
10086 }
10087 }
10088 SegmentBatchPlan::ShaderComposite { start, end } => {
10089 for (_, item) in &ordered_items[start..end] {
10090 if !matches!(item, SegmentDrawItem::ShaderComposite(_)) {
10091 return Err(format!(
10092 "shader composite batch contains non-shader-composite draw item: {item:?}"
10093 ));
10094 }
10095 }
10096 let draw_count = end - start;
10097 if draw_count > 0 {
10098 let draw_start = shader_composite_cursor;
10099 shader_composite_cursor += draw_count;
10100 fused_batches.push(FusedSegmentBatch::ShaderComposite {
10101 draw_range: draw_start..shader_composite_cursor,
10102 });
10103 }
10104 }
10105 SegmentBatchPlan::Retained { start, end } => {
10106 self.stage_replay_patches(&mut staged_uploads);
10107 for (_, item) in &ordered_items[start..end] {
10108 let SegmentDrawItem::Retained(index) = item else {
10109 return Err(format!(
10110 "retained batch contains non-retained draw item: {item:?}"
10111 ));
10112 };
10113 let retained = retained_draws.get(*index).ok_or_else(|| {
10114 format!("retained draw index {index} out of bounds")
10115 })?;
10116 if (*index as u32) < MAX_REPLAY_SLOTS
10117 && self.replay_slots.slots.contains_key(&retained.slot)
10118 {
10119 let transform = retained.transform.with_retained_paint();
10120 staged_uploads.stage_at(
10121 UploadTarget::ReplayTransform,
10122 *index as u64 * REPLAY_TRANSFORM_STRIDE,
10123 bytemuck::bytes_of(&transform),
10124 );
10125 }
10126 }
10127 if end > start {
10128 fused_batches.push(FusedSegmentBatch::Retained {
10129 item_range: start..end,
10130 });
10131 }
10132 }
10133 }
10134 }
10135 if !chunk_rims.is_empty() {
10136 self.upload_transient_rim_meshes();
10137 }
10138 let after_batch_prepare = Instant::now();
10139
10140 if !image_indices.is_empty() {
10141 self.stage_native_image_buffers(
10142 &mut staged_uploads,
10143 viewport,
10144 &image_vertices,
10145 &image_indices,
10146 );
10147 }
10148
10149 let display_clip_depth_view =
10158 self.display_clip_pass_depth_view(target_view, width, height);
10159 let pass_depth = display_clip_depth_view.is_some();
10160
10161 let device = self.device.clone();
10162 let composite_items: Vec<_> = chunk
10163 .iter()
10164 .filter_map(|batch| match batch {
10165 SegmentBatchPlan::Composite { start, end } => Some((start, end)),
10166 _ => None,
10167 })
10168 .flat_map(|(start, end)| {
10169 ordered_items[start..end].iter().filter_map(|(_, item)| {
10170 let SegmentDrawItem::Composite(composite_index) = item else {
10171 return None;
10172 };
10173 composites
10174 .get(*composite_index)
10175 .map(|(_, composite)| *composite)
10176 })
10177 })
10178 .collect();
10179 let prepared_composites = self.effect_renderer.prepare_composite_batch_draws(
10180 frame_encoder,
10181 &device,
10182 load_op,
10183 &composite_items,
10184 pass_depth,
10185 );
10186 let shader_items: Vec<_> = chunk
10187 .iter()
10188 .filter_map(|batch| match batch {
10189 SegmentBatchPlan::ShaderComposite { start, end } => Some((start, end)),
10190 _ => None,
10191 })
10192 .flat_map(|(start, end)| {
10193 ordered_items[start..end].iter().filter_map(|(_, item)| {
10194 let SegmentDrawItem::ShaderComposite(composite_index) = item else {
10195 return None;
10196 };
10197 shader_composites
10198 .get(*composite_index)
10199 .map(|(_, composite)| *composite)
10200 })
10201 })
10202 .collect();
10203 let prepared_shaders = self
10204 .effect_renderer
10205 .prepare_shader_batch_draws(frame_encoder, &device, &shader_items, pass_depth)
10206 .ok_or_else(|| "shader composite batch preparation failed".to_string())?;
10207 if !shader_items.is_empty() {
10208 self.effect_renderer.record_composite_pass();
10209 self.effect_renderer
10210 .debug_effects
10211 .set(self.effect_renderer.debug_effects.get() + shader_items.len() as u32);
10212 }
10213 let span_blit_items =
10220 span_cache
10221 .texture
10222 .as_ref()
10223 .filter(|_| span_skip > 0)
10224 .map(|texture| CompositeBatchItem {
10225 source: texture,
10226 alpha: 1.0,
10227 scissor: None,
10228 rounded_mask: None,
10229 blend_mode: BlendMode::SrcOver,
10230 dest_viewport: None,
10231 source_viewport: None,
10232 sample_mode: CompositeSampleMode::Nearest,
10233 });
10234 let span_blit = match &span_blit_items {
10235 Some(item) => self.effect_renderer.prepare_composite_batch_draws(
10236 frame_encoder,
10237 &device,
10238 load_op,
10239 std::slice::from_ref(item),
10240 pass_depth,
10241 ),
10242 None => Vec::new(),
10243 };
10244 let mut prepared_segment_composites: Vec<(usize, PreparedProjectiveComposite<'_>)> =
10249 Vec::with_capacity(segment_composite_plans.len());
10250 for (index, plan) in &segment_composite_plans {
10251 let Some(entry) = segment_surfaces.entry(&plan.key) else {
10252 continue;
10253 };
10254 let item = ProjectiveCompositeItem {
10255 source: &entry.texture,
10256 viewport: (width, height),
10257 dest_quad: plan.dest_quad,
10258 inverse: plan.inverse,
10259 alpha: 1.0,
10260 blend_mode: BlendMode::SrcOver,
10261 sample_mode: if plan.identity {
10264 CompositeSampleMode::Nearest
10265 } else {
10266 CompositeSampleMode::Linear
10267 },
10268 };
10269 let prepared = self.effect_renderer.prepare_projective_composite_draw(
10270 frame_encoder,
10271 &device,
10272 &item,
10273 pass_depth,
10274 );
10275 prepared_segment_composites.push((*index, prepared));
10276 }
10277 let after_composite_prepare = Instant::now();
10278
10279 if fused_batches.is_empty() && span_blit.is_empty() {
10280 return Ok(SegmentRenderOutcome {
10281 rendered_any: false,
10282 pass_count: 0,
10283 });
10284 }
10285
10286 self.flush_staged_uploads_at(
10291 frame_encoder.encoder(),
10292 &direct_shape_uploads,
10293 shape_upload_base,
10294 );
10295 let upload_offset =
10296 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10297 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
10298 let after_upload = Instant::now();
10299
10300 let mut segment_capture_passes = 0u32;
10309 for job in &segment_captures {
10310 let Some(entry) = segment_surfaces.entry(&job.key) else {
10311 continue;
10312 };
10313 let Some(slot) = self.replay_slots.slots.get(&job.key.slot) else {
10314 continue;
10315 };
10316 let Some(uniform_group) =
10317 segment_surfaces.capture_uniform_bind_group(job.capture_index)
10318 else {
10319 continue;
10320 };
10321 let mut capture_pass =
10322 frame_encoder
10323 .encoder()
10324 .begin_render_pass(&wgpu::RenderPassDescriptor {
10325 label: Some("Segment Surface Capture Pass"),
10326 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10327 view: &entry.texture.view,
10328 resolve_target: None,
10329 depth_slice: None,
10330 ops: wgpu::Operations {
10331 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
10335 store: wgpu::StoreOp::Store,
10336 },
10337 })],
10338 depth_stencil_attachment: None,
10339 timestamp_writes: None,
10340 occlusion_query_set: None,
10341 multiview_mask: None,
10342 });
10343 let draws = self.encode_retained_op(
10344 slot,
10345 job.first,
10346 job.last,
10347 MAX_REPLAY_SLOTS + job.capture_index,
10348 &mut |cmd| match cmd {
10349 RetainedCmd::Uniforms(_) => {
10354 capture_pass.set_bind_group(0, uniform_group, &[])
10355 }
10356 RetainedCmd::Pipeline(pipeline) => capture_pass.set_pipeline(pipeline),
10357 RetainedCmd::SlotBindings(group, offset) => {
10358 capture_pass.set_bind_group(1, group, &[offset])
10359 }
10360 RetainedCmd::MeshVertices(buffer) => {
10361 capture_pass.set_vertex_buffer(0, buffer.slice(..))
10362 }
10363 RetainedCmd::Index(buffer, format) => {
10364 capture_pass.set_index_buffer(buffer.slice(..), format)
10365 }
10366 RetainedCmd::Draw(vertices) => capture_pass.draw(vertices, 0..1),
10367 RetainedCmd::DrawIndexed(indices, instances) => {
10368 capture_pass.draw_indexed(indices, 0, instances)
10369 }
10370 },
10371 );
10372 self.frame_stats.add_draw_calls(draws);
10373 segment_capture_passes += 1;
10374 }
10375
10376 let use_retained_bundles = retained_bundles_enabled();
10377 let mut retained_encode_ms = 0.0_f64;
10378 {
10379 let mut render_pass =
10380 frame_encoder
10381 .encoder()
10382 .begin_render_pass(&wgpu::RenderPassDescriptor {
10383 label: Some("Fused Segment Draw Pass"),
10384 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10385 view: target_view,
10386 resolve_target: None,
10387 depth_slice: None,
10388 ops: wgpu::Operations {
10389 load: load_op,
10390 store: wgpu::StoreOp::Store,
10391 },
10392 })],
10393 depth_stencil_attachment: display_clip_depth_view.as_ref().map(
10397 |view| wgpu::RenderPassDepthStencilAttachment {
10398 view,
10399 depth_ops: Some(wgpu::Operations {
10400 load: wgpu::LoadOp::Clear(
10401 crate::display_clip::DISPLAY_CLIP_DEPTH_CLEAR,
10402 ),
10403 store: wgpu::StoreOp::Discard,
10404 }),
10405 stencil_ops: None,
10406 },
10407 ),
10408 timestamp_writes: None,
10409 occlusion_query_set: None,
10410 multiview_mask: None,
10411 });
10412
10413 for draw in &span_blit {
10417 self.effect_renderer.draw_prepared_composite(
10418 &mut render_pass,
10419 (width, height),
10420 draw,
10421 pass_depth,
10422 );
10423 }
10424 if pass_depth {
10425 self.draw_display_clip_occluder(&mut render_pass, width, height);
10428 self.display_clip.pass_depth.set(true);
10429 }
10430
10431 for batch in &fused_batches {
10432 match batch {
10433 FusedSegmentBatch::Shape { batch, blend_mode } => {
10434 self.draw_prepared_shapes(
10435 &mut render_pass,
10436 *blend_mode,
10437 *batch,
10438 width,
10439 height,
10440 &chunk_rims,
10441 );
10442 }
10443 FusedSegmentBatch::Image {
10444 cmd_range,
10445 blend_mode,
10446 } => {
10447 self.draw_native_prepared_image_cmd_range(
10448 &mut render_pass,
10449 &image_cmds,
10450 cmd_range.clone(),
10451 *blend_mode,
10452 )?;
10453 }
10454 FusedSegmentBatch::Text {
10455 image_cmd_range,
10456 glyph_cmd_range,
10457 } => {
10458 if !image_cmd_range.is_empty() {
10459 self.draw_native_prepared_image_cmd_range(
10460 &mut render_pass,
10461 &image_cmds,
10462 image_cmd_range.clone(),
10463 BlendMode::SrcOver,
10464 )?;
10465 self.frame_stats.bump_text();
10466 }
10467 if !glyph_cmd_range.is_empty() {
10468 self.draw_native_prepared_glyph_cmd_range(
10469 &mut render_pass,
10470 &glyph_cmds,
10471 glyph_cmd_range.clone(),
10472 )?;
10473 }
10474 }
10475 FusedSegmentBatch::Composite { draw_range } => {
10476 for draw in
10477 prepared_composites.get(draw_range.clone()).ok_or_else(|| {
10478 "composite draw range is outside the prepared command buffer"
10479 .to_string()
10480 })?
10481 {
10482 self.effect_renderer.draw_prepared_composite(
10483 &mut render_pass,
10484 (width, height),
10485 draw,
10486 pass_depth,
10487 );
10488 }
10489 }
10490 FusedSegmentBatch::ShaderComposite { draw_range } => {
10491 for draw in prepared_shaders.get(draw_range.clone()).ok_or_else(|| {
10492 "shader composite draw range is outside the prepared command buffer"
10493 .to_string()
10494 })? {
10495 self.effect_renderer.draw_prepared_shader_src_over(
10496 &device,
10497 &mut render_pass,
10498 (width, height),
10499 draw,
10500 pass_depth,
10501 );
10502 }
10503 }
10504 FusedSegmentBatch::Retained { item_range } => {
10505 let retained_start = Instant::now();
10511 let stretch_has_composites = !prepared_segment_composites.is_empty()
10517 && ordered_items[item_range.clone()].iter().any(|(_, item)| {
10518 matches!(
10519 item,
10520 SegmentDrawItem::Retained(index)
10521 if prepared_segment_composites
10522 .iter()
10523 .any(|(prepared_index, _)| prepared_index == index)
10524 )
10525 });
10526 if use_retained_bundles && !stretch_has_composites {
10527 self.draw_retained_stretch_bundled(
10528 &mut render_pass,
10529 ordered_items,
10530 retained_draws,
10531 item_range.clone(),
10532 width,
10533 height,
10534 );
10535 } else {
10536 for (_, item) in &ordered_items[item_range.clone()] {
10537 if let SegmentDrawItem::Retained(index) = item {
10538 if let Some((_, prepared)) = prepared_segment_composites
10539 .iter()
10540 .find(|(prepared_index, _)| prepared_index == index)
10541 {
10542 self.effect_renderer
10550 .draw_prepared_projective_composite(
10551 &mut render_pass,
10552 (width, height),
10553 prepared,
10554 pass_depth,
10555 );
10556 self.frame_stats.add_draw_calls(1);
10557 } else if let Some(retained) = retained_draws.get(*index) {
10558 self.draw_retained_batch(
10559 &mut render_pass,
10560 retained,
10561 *index,
10562 width,
10563 height,
10564 );
10565 }
10566 }
10567 }
10568 }
10569 retained_encode_ms += instant_ms(retained_start, Instant::now());
10570 }
10571 }
10572 }
10573 }
10574 self.display_clip.pass_depth.set(false);
10580 let mut capture_passes = 0_u32;
10589 if let StaticSpanDecision::Capture { len, clear } = span_decision {
10590 let texture = match span_cache.texture.take() {
10591 Some(existing) if existing.width == width && existing.height == height => {
10592 existing
10593 }
10594 other => {
10595 if let Some(stale) = other {
10596 self.defer_offscreen_release(stale);
10597 }
10598 self.acquire_offscreen(width, height)
10599 }
10600 };
10601 {
10602 let mut capture_pass =
10603 frame_encoder
10604 .encoder()
10605 .begin_render_pass(&wgpu::RenderPassDescriptor {
10606 label: Some("Static Span Capture Pass"),
10607 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10608 view: &texture.view,
10609 resolve_target: None,
10610 depth_slice: None,
10611 ops: wgpu::Operations {
10612 load: wgpu::LoadOp::Clear(clear),
10613 store: wgpu::StoreOp::Store,
10614 },
10615 })],
10616 depth_stencil_attachment: None,
10617 timestamp_writes: None,
10618 occlusion_query_set: None,
10619 multiview_mask: None,
10620 });
10621 let has_gradient = first_batch_info
10625 .map(|(_, _, has_gradient)| has_gradient)
10626 .unwrap_or(false);
10627 self.draw_prepared_shapes(
10628 &mut capture_pass,
10629 BlendMode::SrcOver,
10630 PreparedShapeBatch {
10631 vertex_start: 0,
10632 vertex_count: len as u32 * 6,
10633 has_gradient,
10634 },
10635 width,
10636 height,
10637 &[],
10638 );
10639 if fill_area_diag_enabled() {
10640 self.fill_area_diag
10644 .add_shape_quads(&self.scratch_shape_data[..len], viewport);
10645 }
10646 span_cache.store_key(
10647 &self.scratch_shape_data[..len],
10648 &self.scratch_gradients,
10649 width,
10650 height,
10651 clear,
10652 has_gradient,
10653 );
10654 }
10655 span_cache.texture = Some(texture);
10656 capture_passes = 1;
10657 }
10658 let after_pass = Instant::now();
10659 if let Some(total_ms) = should_log_wgpu_render_stage(partition_start, after_pass) {
10660 log::warn!(
10661 "[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={}",
10662 instant_ms(partition_start, after_shape_refs),
10663 instant_ms(after_shape_refs, after_shape_prepare),
10664 instant_ms(after_shape_prepare, after_batch_prepare),
10665 instant_ms(after_batch_prepare, after_composite_prepare),
10666 instant_ms(after_composite_prepare, after_upload),
10667 instant_ms(after_upload, after_pass),
10668 fused_batches.len(),
10669 budget.shape_count,
10670 image_cmds.len(),
10671 glyph_cmds.len(),
10672 staged_uploads.bytes.len(),
10673 );
10674 }
10675
10676 Ok(SegmentRenderOutcome {
10677 rendered_any: true,
10678 pass_count: 1 + capture_passes + segment_capture_passes,
10679 })
10680 })();
10681
10682 self.display_clip.pass_depth.set(false);
10686 self.scratch_image_vertices = image_vertices;
10687 self.scratch_image_indices = image_indices;
10688 self.scratch_image_cmds = image_cmds;
10689 self.scratch_glyph_cmds = glyph_cmds;
10690 self.restore_staged_uploads(staged_uploads);
10691 self.static_span = span_cache;
10692 if result.is_err() {
10693 segment_surfaces.clear();
10696 }
10697 self.segment_surfaces = segment_surfaces;
10698 result
10699 }
10700
10701 #[allow(clippy::too_many_arguments)]
10702 fn render_segment_draw_chunk<C: FrameCommandRecorder>(
10703 &mut self,
10704 frame_encoder: &mut C,
10705 target_view: &wgpu::TextureView,
10706 ordered_items: &[(usize, SegmentDrawItem)],
10707 composites: &[(usize, CompositeBatchItem<'_>)],
10708 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
10709 shapes: &[DrawShape],
10710 brushes: &[Brush],
10711 images: &[ImageDraw],
10712 texts: &[TextDraw],
10713 retained_draws: &[RetainedDraw],
10714 chunk: SegmentDrawChunkPlan,
10715 width: u32,
10716 height: u32,
10717 root_scale: f32,
10718 load_op: wgpu::LoadOp<wgpu::Color>,
10719 ) -> Result<SegmentRenderOutcome, String> {
10720 #[cfg(target_arch = "wasm32")]
10721 let _ = retained_draws;
10722 #[cfg(not(target_arch = "wasm32"))]
10723 if let Some(outcome) = self.render_segment_draw_chunk_fused_native(
10724 frame_encoder,
10725 target_view,
10726 ordered_items,
10727 composites,
10728 shader_composites,
10729 shapes,
10730 brushes,
10731 images,
10732 texts,
10733 retained_draws,
10734 &chunk,
10735 width,
10736 height,
10737 root_scale,
10738 load_op,
10739 )? {
10740 return Ok(outcome);
10741 }
10742
10743 let mut staged_uploads = self.take_staged_uploads();
10744 let result = (|| {
10745 let mut rendered_any = false;
10746 let mut pass_count = 0_u32;
10747 let mut next_load_op = load_op;
10748 for batch in chunk.iter() {
10749 staged_uploads.clear();
10750 match batch {
10751 SegmentBatchPlan::Shape {
10752 start,
10753 end,
10754 blend_mode,
10755 } => {
10756 let slice = &ordered_items[start..end];
10757 if slice.len() > self.shape_batch_limits.max_shapes_per_batch {
10758 return Err(format!(
10759 "shape batch contains {} shapes, exceeding the renderer limit of {}",
10760 slice.len(),
10761 self.shape_batch_limits.max_shapes_per_batch
10762 ));
10763 }
10764 let viewport = ViewportUniformParams {
10765 width,
10766 height,
10767 offset: [0.0, 0.0],
10768 };
10769 for (_, item) in slice {
10770 if !matches!(item, SegmentDrawItem::Shape(_)) {
10771 return Err(format!(
10772 "shape batch contains non-shape draw item: {item:?}"
10773 ));
10774 }
10775 }
10776 let Some(prepared) = self.prepare_shapes_batch(
10777 slice.iter().filter_map(|(_, item)| match item {
10778 SegmentDrawItem::Shape(shape_index) => Some(&shapes[*shape_index]),
10779 _ => None,
10780 }),
10781 brushes,
10782 root_scale,
10783 viewport,
10784 &mut staged_uploads,
10785 ) else {
10786 continue;
10787 };
10788 let upload_offset = frame_encoder
10789 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10790 self.flush_staged_uploads_at(
10791 frame_encoder.encoder(),
10792 &staged_uploads,
10793 upload_offset,
10794 );
10795 {
10796 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10797 &wgpu::RenderPassDescriptor {
10798 label: Some("Segment Shape Pass"),
10799 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10800 view: target_view,
10801 resolve_target: None,
10802 depth_slice: None,
10803 ops: wgpu::Operations {
10804 load: next_load_op,
10805 store: wgpu::StoreOp::Store,
10806 },
10807 })],
10808 depth_stencil_attachment: None,
10809 timestamp_writes: None,
10810 occlusion_query_set: None,
10811 multiview_mask: None,
10812 },
10813 );
10814 self.draw_prepared_shapes(
10815 &mut render_pass,
10816 blend_mode,
10817 prepared,
10818 width,
10819 height,
10820 &[],
10821 );
10822 }
10823 pass_count = pass_count.saturating_add(1);
10824 rendered_any = true;
10825 next_load_op = wgpu::LoadOp::Load;
10826 }
10827 SegmentBatchPlan::Image {
10828 start,
10829 end,
10830 blend_mode,
10831 } => {
10832 let viewport = ViewportUniformParams {
10833 width,
10834 height,
10835 offset: [0.0, 0.0],
10836 };
10837 for (_, item) in &ordered_items[start..end] {
10838 if !matches!(item, SegmentDrawItem::Image(_)) {
10839 return Err(format!(
10840 "image batch contains non-image draw item: {item:?}"
10841 ));
10842 }
10843 }
10844 let prepared_images = self.prepare_image_draw_cmds(
10845 ordered_items[start..end]
10846 .iter()
10847 .filter_map(|(_, item)| match item {
10848 SegmentDrawItem::Image(image_index) => {
10849 Some(&images[*image_index])
10850 }
10851 _ => None,
10852 }),
10853 viewport,
10854 root_scale,
10855 &mut staged_uploads,
10856 )?;
10857 if prepared_images.is_empty() {
10858 self.scratch_image_cmds = prepared_images.into_cmds();
10859 continue;
10860 }
10861 let upload_offset = frame_encoder
10862 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10863 self.flush_staged_uploads_at(
10864 frame_encoder.encoder(),
10865 &staged_uploads,
10866 upload_offset,
10867 );
10868 let draw_result = {
10869 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10870 &wgpu::RenderPassDescriptor {
10871 label: Some("Segment Image Pass"),
10872 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10873 view: target_view,
10874 resolve_target: None,
10875 depth_slice: None,
10876 ops: wgpu::Operations {
10877 load: next_load_op,
10878 store: wgpu::StoreOp::Store,
10879 },
10880 })],
10881 depth_stencil_attachment: None,
10882 timestamp_writes: None,
10883 occlusion_query_set: None,
10884 multiview_mask: None,
10885 },
10886 );
10887 self.draw_prepared_images(
10888 &mut render_pass,
10889 &prepared_images,
10890 blend_mode,
10891 )
10892 };
10893 pass_count = pass_count.saturating_add(1);
10894 self.scratch_image_cmds = prepared_images.into_cmds();
10895 draw_result?;
10896 rendered_any = true;
10897 next_load_op = wgpu::LoadOp::Load;
10898 }
10899 SegmentBatchPlan::Text { start, end } => {
10900 let viewport = ViewportUniformParams {
10901 width,
10902 height,
10903 offset: [0.0, 0.0],
10904 };
10905 let text_draws =
10906 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
10907 if let Some(prepared_glyphs) = self.prepare_text_glyph_draw_cmds(
10908 text_draws,
10909 viewport,
10910 root_scale,
10911 &mut staged_uploads,
10912 )? {
10913 if prepared_glyphs.is_empty() {
10914 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
10915 continue;
10916 }
10917 let upload_offset = frame_encoder
10918 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10919 self.flush_staged_uploads_at(
10920 frame_encoder.encoder(),
10921 &staged_uploads,
10922 upload_offset,
10923 );
10924 {
10925 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10926 &wgpu::RenderPassDescriptor {
10927 label: Some("Segment Text Glyph Atlas Pass"),
10928 color_attachments: &[Some(
10929 wgpu::RenderPassColorAttachment {
10930 view: target_view,
10931 resolve_target: None,
10932 depth_slice: None,
10933 ops: wgpu::Operations {
10934 load: next_load_op,
10935 store: wgpu::StoreOp::Store,
10936 },
10937 },
10938 )],
10939 depth_stencil_attachment: None,
10940 timestamp_writes: None,
10941 occlusion_query_set: None,
10942 multiview_mask: None,
10943 },
10944 );
10945 self.draw_prepared_glyphs(&mut render_pass, &prepared_glyphs)?;
10946 }
10947 pass_count = pass_count.saturating_add(1);
10948 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
10949 rendered_any = true;
10950 next_load_op = wgpu::LoadOp::Load;
10951 } else {
10952 let text_draws =
10953 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
10954 let prepared_images = self.prepare_text_image_draw_cmds(
10955 text_draws,
10956 viewport,
10957 root_scale,
10958 &mut staged_uploads,
10959 )?;
10960 if prepared_images.is_empty() {
10961 self.scratch_image_cmds = prepared_images.into_cmds();
10962 continue;
10963 }
10964 let upload_offset = frame_encoder
10965 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10966 self.flush_staged_uploads_at(
10967 frame_encoder.encoder(),
10968 &staged_uploads,
10969 upload_offset,
10970 );
10971 {
10972 let mut render_pass = frame_encoder.encoder().begin_render_pass(
10973 &wgpu::RenderPassDescriptor {
10974 label: Some("Segment Text Pass"),
10975 color_attachments: &[Some(
10976 wgpu::RenderPassColorAttachment {
10977 view: target_view,
10978 resolve_target: None,
10979 depth_slice: None,
10980 ops: wgpu::Operations {
10981 load: next_load_op,
10982 store: wgpu::StoreOp::Store,
10983 },
10984 },
10985 )],
10986 depth_stencil_attachment: None,
10987 timestamp_writes: None,
10988 occlusion_query_set: None,
10989 multiview_mask: None,
10990 },
10991 );
10992 self.draw_prepared_images(
10993 &mut render_pass,
10994 &prepared_images,
10995 BlendMode::SrcOver,
10996 )?;
10997 }
10998 self.frame_stats.bump_text();
10999 pass_count = pass_count.saturating_add(1);
11000 self.scratch_image_cmds = prepared_images.into_cmds();
11001 rendered_any = true;
11002 next_load_op = wgpu::LoadOp::Load;
11003 }
11004 }
11005 SegmentBatchPlan::Composite { start, end } => {
11006 let batch_items: Vec<_> = ordered_items[start..end]
11007 .iter()
11008 .map(|(_, item)| match item {
11009 SegmentDrawItem::Composite(composite_index) => composites
11010 .get(*composite_index)
11011 .map(|(_, composite)| *composite)
11012 .ok_or_else(|| {
11013 "composite item index is outside the composite buffer"
11014 .to_string()
11015 }),
11016 other => Err(format!(
11017 "composite batch contains non-composite draw item: {other:?}"
11018 )),
11019 })
11020 .collect::<Result<_, _>>()?;
11021 let device = self.device.clone();
11022 self.effect_renderer.encode_composite_batch_to_view_pass(
11023 frame_encoder,
11024 &device,
11025 target_view,
11026 (width, height),
11027 next_load_op,
11028 &batch_items,
11029 );
11030 self.effect_renderer.record_composite_pass();
11031 pass_count = pass_count.saturating_add(1);
11032 rendered_any = true;
11033 next_load_op = wgpu::LoadOp::Load;
11034 }
11035 SegmentBatchPlan::ShaderComposite { start, end } => {
11036 let batch_items: Vec<_> = ordered_items[start..end]
11037 .iter()
11038 .map(|(_, item)| match item {
11039 SegmentDrawItem::ShaderComposite(composite_index) => {
11040 shader_composites
11041 .get(*composite_index)
11042 .map(|(_, composite)| *composite)
11043 .ok_or_else(|| {
11044 "shader composite item index is outside the shader composite buffer"
11045 .to_string()
11046 })
11047 }
11048 other => Err(format!(
11049 "shader composite batch contains non-shader-composite draw item: {other:?}"
11050 )),
11051 })
11052 .collect::<Result<Vec<_>, _>>()?;
11053 let device = self.device.clone();
11054 let encoded = self.effect_renderer.encode_shader_batch_src_over_to_view(
11055 frame_encoder,
11056 &device,
11057 target_view,
11058 (width, height),
11059 next_load_op,
11060 &batch_items,
11061 );
11062 if !encoded {
11063 return Err("shader composite batch failed to encode".to_string());
11064 }
11065 self.effect_renderer.record_composite_pass();
11066 self.effect_renderer.debug_effects.set(
11067 self.effect_renderer.debug_effects.get() + batch_items.len() as u32,
11068 );
11069 pass_count = pass_count.saturating_add(1);
11070 rendered_any = true;
11071 next_load_op = wgpu::LoadOp::Load;
11072 }
11073 SegmentBatchPlan::Retained { start, end } => {
11074 #[cfg(target_arch = "wasm32")]
11083 {
11084 let _ = (start, end);
11085 return Err("retained shape batches are native-only".to_string());
11086 }
11087 #[cfg(not(target_arch = "wasm32"))]
11088 {
11089 self.stage_replay_patches(&mut staged_uploads);
11090 for (_, item) in &ordered_items[start..end] {
11091 let SegmentDrawItem::Retained(index) = item else {
11092 return Err(format!(
11093 "retained batch contains non-retained draw item: {item:?}"
11094 ));
11095 };
11096 let retained = retained_draws.get(*index).ok_or_else(|| {
11097 format!("retained draw index {index} out of bounds")
11098 })?;
11099 if (*index as u32) < MAX_REPLAY_SLOTS
11100 && self.replay_slots.slots.contains_key(&retained.slot)
11101 {
11102 let transform = retained.transform.with_retained_paint();
11103 staged_uploads.stage_at(
11104 UploadTarget::ReplayTransform,
11105 *index as u64 * REPLAY_TRANSFORM_STRIDE,
11106 bytemuck::bytes_of(&transform),
11107 );
11108 }
11109 }
11110 let upload_offset = frame_encoder
11111 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11112 self.flush_staged_uploads_at(
11113 frame_encoder.encoder(),
11114 &staged_uploads,
11115 upload_offset,
11116 );
11117 {
11118 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11119 &wgpu::RenderPassDescriptor {
11120 label: Some("Segment Retained Pass"),
11121 color_attachments: &[Some(
11122 wgpu::RenderPassColorAttachment {
11123 view: target_view,
11124 resolve_target: None,
11125 depth_slice: None,
11126 ops: wgpu::Operations {
11127 load: next_load_op,
11128 store: wgpu::StoreOp::Store,
11129 },
11130 },
11131 )],
11132 depth_stencil_attachment: None,
11133 timestamp_writes: None,
11134 occlusion_query_set: None,
11135 multiview_mask: None,
11136 },
11137 );
11138 for (_, item) in &ordered_items[start..end] {
11139 if let SegmentDrawItem::Retained(index) = item {
11140 if let Some(retained) = retained_draws.get(*index) {
11141 self.draw_retained_batch(
11142 &mut render_pass,
11143 retained,
11144 *index,
11145 width,
11146 height,
11147 );
11148 }
11149 }
11150 }
11151 }
11152 pass_count = pass_count.saturating_add(1);
11153 rendered_any = true;
11154 next_load_op = wgpu::LoadOp::Load;
11155 }
11156 }
11157 }
11158 }
11159 Ok(SegmentRenderOutcome {
11160 rendered_any,
11161 pass_count,
11162 })
11163 })();
11164 self.restore_staged_uploads(staged_uploads);
11165 result
11166 }
11167
11168 fn viewport_uniforms(params: ViewportUniformParams) -> Uniforms {
11169 Uniforms {
11170 viewport: [params.width as f32, params.height as f32],
11171 viewport_offset: params.offset,
11172 }
11173 }
11174
11175 #[cfg(not(target_arch = "wasm32"))]
11176 fn stage_viewport_uniforms(
11177 &self,
11178 staged_uploads: &mut StagedBufferUploads,
11179 params: ViewportUniformParams,
11180 ) {
11181 let uniforms = Self::viewport_uniforms(params);
11182 staged_uploads.stage(UploadTarget::Uniform, bytemuck::bytes_of(&uniforms));
11183 }
11184
11185 #[cfg(not(target_arch = "wasm32"))]
11186 fn stage_retained_glyph_viewport_uniforms(
11187 &mut self,
11188 staged_uploads: &mut StagedBufferUploads,
11189 params: ViewportUniformParams,
11190 ) -> usize {
11191 let slot = self.claim_retained_glyph_uniform_slot();
11192 let uniforms = Self::viewport_uniforms(params);
11193 staged_uploads.stage_at(
11194 UploadTarget::RetainedGlyphUniform,
11195 self.retained_glyph_uniform_offset(slot),
11196 bytemuck::bytes_of(&uniforms),
11197 );
11198 slot
11199 }
11200
11201 #[cfg(not(target_arch = "wasm32"))]
11202 fn claim_retained_glyph_uniform_slot(&mut self) -> usize {
11203 let slot = self.retained_glyph_uniform_cursor;
11204 self.retained_glyph_uniform_cursor = self.retained_glyph_uniform_cursor.saturating_add(1);
11205 self.ensure_retained_glyph_uniform_capacity(slot.saturating_add(1));
11206 slot
11207 }
11208
11209 #[cfg(not(target_arch = "wasm32"))]
11210 fn retained_glyph_uniform_offset(&self, slot: usize) -> u64 {
11211 self.retained_glyph_uniform_stride * slot as u64
11212 }
11213
11214 #[cfg(not(target_arch = "wasm32"))]
11215 fn retained_glyph_uniform_dynamic_offset(&self, slot: usize) -> Result<u32, String> {
11216 let offset = self.retained_glyph_uniform_offset(slot);
11217 u32::try_from(offset).map_err(|_| {
11218 "retained glyph uniform offset exceeded WGPU dynamic offset range".to_string()
11219 })
11220 }
11221
11222 #[cfg(not(target_arch = "wasm32"))]
11223 fn ensure_retained_glyph_uniform_capacity(&mut self, required_slots: usize) {
11224 if required_slots <= self.retained_glyph_uniform_capacity {
11225 return;
11226 }
11227 let new_capacity = required_slots
11228 .next_power_of_two()
11229 .max(INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS);
11230 self.retained_glyph_uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11231 label: Some("Retained Glyph Uniform Buffer"),
11232 size: self.retained_glyph_uniform_stride * new_capacity as u64,
11233 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
11234 mapped_at_creation: false,
11235 });
11236 self.retained_glyph_uniform_bind_group =
11237 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
11238 label: Some("Retained Glyph Uniform Bind Group"),
11239 layout: &self.retained_glyph_uniform_bind_group_layout,
11240 entries: &[wgpu::BindGroupEntry {
11241 binding: 0,
11242 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
11243 buffer: &self.retained_glyph_uniform_buffer,
11244 offset: 0,
11245 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
11246 }),
11247 }],
11248 });
11249 self.retained_glyph_uniform_capacity = new_capacity;
11250 }
11251
11252 #[cfg(target_arch = "wasm32")]
11253 fn prepare_wasm_viewport_uniforms(&mut self, params: ViewportUniformParams) -> usize {
11254 let slot = self.claim_wasm_uniform_batch();
11255 let uniforms = Self::viewport_uniforms(params);
11256 let bytes = bytemuck::bytes_of(&uniforms);
11257 let upload_stats = self.frame_graph_executor.upload_buffer(
11258 &self.queue,
11259 &self.wasm_uniform_batches[slot].buffer,
11260 0,
11261 bytes,
11262 );
11263 self.frame_stats.record_command_stats(upload_stats);
11264 slot
11265 }
11266
11267 #[cfg(target_arch = "wasm32")]
11268 fn claim_wasm_uniform_batch(&mut self) -> usize {
11269 let slot = self.wasm_uniform_batch_cursor;
11270 self.wasm_uniform_batch_cursor += 1;
11271 while self.wasm_uniform_batches.len() <= slot {
11272 self.wasm_uniform_batches.push(UniformBatchBuffer::new(
11273 &self.device,
11274 &self.uniform_bind_group_layout,
11275 ));
11276 }
11277 slot
11278 }
11279
11280 #[cfg(target_arch = "wasm32")]
11281 fn claim_wasm_shape_batch(&mut self) -> usize {
11282 let slot = self.wasm_shape_batch_cursor;
11283 self.wasm_shape_batch_cursor += 1;
11284 while self.wasm_shape_batches.len() <= slot {
11285 self.wasm_shape_batches.push(ShapeBatchBuffers::new(
11286 &self.device,
11287 &self.shape_bind_group_layout,
11288 &self.identity_similarity_buffer,
11289 self.dummy_paint_buffer.as_ref(),
11290 self.shape_batch_limits,
11291 ));
11292 }
11293 slot
11294 }
11295
11296 #[cfg(target_arch = "wasm32")]
11297 fn claim_wasm_image_batch(&mut self) -> usize {
11298 let slot = self.wasm_image_batch_cursor;
11299 self.wasm_image_batch_cursor += 1;
11300 while self.wasm_image_batches.len() <= slot {
11301 self.wasm_image_batches
11302 .push(ImageBatchBuffers::new(&self.device));
11303 }
11304 slot
11305 }
11306
11307 #[cfg(target_arch = "wasm32")]
11308 fn write_wasm_buffer(&self, buffer: &wgpu::Buffer, bytes: &[u8]) {
11309 let upload_stats = self
11310 .frame_graph_executor
11311 .upload_buffer(&self.queue, buffer, 0, bytes);
11312 self.frame_stats.record_command_stats(upload_stats);
11313 }
11314
11315 fn take_staged_uploads(&mut self) -> StagedBufferUploads {
11316 let mut staged_uploads = std::mem::take(&mut self.staged_uploads);
11317 debug_assert!(
11318 staged_uploads.is_empty(),
11319 "renderer-owned staged uploads should be restored as empty scratch storage"
11320 );
11321 staged_uploads.clear();
11322 staged_uploads
11323 }
11324
11325 fn restore_staged_uploads(&mut self, mut staged_uploads: StagedBufferUploads) {
11326 staged_uploads.clear();
11327 self.staged_uploads = staged_uploads;
11328 }
11329
11330 #[cfg(not(target_arch = "wasm32"))]
11331 fn ensure_upload_buffer_capacity(&mut self, required_bytes: u64) {
11332 if required_bytes <= self.upload_buffer.size() {
11333 return;
11334 }
11335
11336 let new_size = required_bytes
11337 .next_power_of_two()
11338 .max(INITIAL_UPLOAD_BUFFER_BYTES);
11339 self.upload_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
11340 label: Some("Frame Upload Buffer"),
11341 size: new_size,
11342 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
11343 mapped_at_creation: false,
11344 });
11345 }
11346
11347 fn flush_staged_uploads_at(
11348 &mut self,
11349 encoder: &mut wgpu::CommandEncoder,
11350 staged_uploads: &StagedBufferUploads,
11351 upload_buffer_offset: u64,
11352 ) {
11353 if staged_uploads.is_empty() {
11354 return;
11355 }
11356 debug_assert_eq!(
11357 upload_buffer_offset % wgpu::COPY_BUFFER_ALIGNMENT,
11358 0,
11359 "upload-buffer base offset must satisfy copy alignment"
11360 );
11361
11362 #[cfg(target_arch = "wasm32")]
11363 {
11364 let _ = upload_buffer_offset;
11365 let _ = encoder;
11366 debug_assert!(
11367 staged_uploads.is_empty(),
11368 "wasm draw uploads use retained per-batch resource slots"
11369 );
11370 return;
11371 }
11372
11373 #[cfg(not(target_arch = "wasm32"))]
11374 {
11375 self.ensure_upload_buffer_capacity(
11376 upload_buffer_offset + staged_uploads.bytes.len() as u64,
11377 );
11378 let upload_stats = self.frame_graph_executor.upload_buffer(
11379 &self.queue,
11380 &self.upload_buffer,
11381 upload_buffer_offset,
11382 &staged_uploads.bytes,
11383 );
11384 self.frame_stats.record_command_stats(upload_stats);
11385
11386 for copy in &staged_uploads.copies {
11387 let target_buffer = match copy.target {
11388 UploadTarget::Uniform => &self.uniform_buffer,
11389 UploadTarget::ShapeData => &self.shape_buffers.shape_buffer,
11390 UploadTarget::ShapeGradient => &self.shape_buffers.gradient_buffer,
11391 UploadTarget::ImageVertex => &self.image_vertex_buffer,
11392 UploadTarget::ImageIndex => &self.image_index_buffer,
11393 UploadTarget::RetainedGlyphUniform => &self.retained_glyph_uniform_buffer,
11394 UploadTarget::ReplayTransform => &self.replay_slots.transform_buffer,
11395 UploadTarget::ReplayPaintData(slot) => {
11396 let Some(entry) = self.replay_slots.slots.get(&slot) else {
11399 continue;
11400 };
11401 &entry.paint_buffer
11402 }
11403 };
11404 encoder.copy_buffer_to_buffer(
11405 &self.upload_buffer,
11406 upload_buffer_offset + copy.source_offset,
11407 target_buffer,
11408 copy.target_offset,
11409 copy.size,
11410 );
11411 }
11412 }
11413 }
11414
11415 #[allow(clippy::too_many_arguments)]
11416 fn encode_shadow_draw<C: FrameCommandRecorder>(
11417 &mut self,
11418 frame_encoder: &mut C,
11419 target_view: &wgpu::TextureView,
11420 shadow: &ShadowDraw,
11421 width: u32,
11422 height: u32,
11423 root_scale: f32,
11424 ) {
11425 if shadow.shapes.is_empty() && shadow.texts.is_empty() {
11426 return;
11427 }
11428
11429 let shape_bounds_opt = shadow
11430 .shapes
11431 .iter()
11432 .map(|(shape, _)| shape.rect)
11433 .reduce(|a, b| Rect {
11434 x: a.x.min(b.x),
11435 y: a.y.min(b.y),
11436 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
11437 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
11438 });
11439
11440 let text_bounds_opt = shadow
11441 .texts
11442 .iter()
11443 .map(|text| text.rect)
11444 .reduce(|a, b| Rect {
11445 x: a.x.min(b.x),
11446 y: a.y.min(b.y),
11447 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
11448 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
11449 });
11450
11451 let combined_bounds = match (shape_bounds_opt, text_bounds_opt) {
11452 (Some(s), Some(t)) => Some(Rect {
11453 x: s.x.min(t.x),
11454 y: s.y.min(t.y),
11455 width: (s.x + s.width).max(t.x + t.width) - s.x.min(t.x),
11456 height: (s.y + s.height).max(t.y + t.height) - s.y.min(t.y),
11457 }),
11458 (Some(s), None) => Some(s),
11459 (None, Some(t)) => Some(t),
11460 (None, None) => None,
11461 };
11462
11463 let Some(shape_bounds) = combined_bounds else {
11464 return;
11465 };
11466
11467 let blur_margin = blur_extent_margin(shadow.blur_radius);
11468 let source_blur_bounds = Rect {
11469 x: shape_bounds.x - blur_margin,
11470 y: shape_bounds.y - blur_margin,
11471 width: shape_bounds.width + blur_margin * 2.0,
11472 height: shape_bounds.height + blur_margin * 2.0,
11473 };
11474 let mut visible_blur_bounds = source_blur_bounds;
11475 if let Some(clip) = shadow.clip {
11476 let clip_expanded = Rect {
11477 x: clip.x - blur_margin,
11478 y: clip.y - blur_margin,
11479 width: clip.width + blur_margin * 2.0,
11480 height: clip.height + blur_margin * 2.0,
11481 };
11482 let Some(intersection) = visible_blur_bounds.intersect(clip_expanded) else {
11483 return;
11484 };
11485 visible_blur_bounds = intersection;
11486 }
11487 let processing_scissor =
11488 scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
11489 if processing_scissor.is_none() {
11490 return;
11491 }
11492
11493 if shadow.blur_radius <= 0.0 {
11495 for (shape, blend_mode) in &shadow.shapes {
11496 self.encode_shapes_pass(
11497 frame_encoder,
11498 target_view,
11499 std::iter::once(shape),
11500 &shadow.brushes,
11501 *blend_mode,
11502 width,
11503 height,
11504 root_scale,
11505 wgpu::LoadOp::Load,
11506 [0.0, 0.0],
11507 );
11508 frame_encoder.record_pass();
11509 }
11510 if !shadow.texts.is_empty() {
11511 let mut staged_uploads = self.take_staged_uploads();
11512 let viewport = ViewportUniformParams {
11513 width,
11514 height,
11515 offset: [0.0, 0.0],
11516 };
11517 match self.prepare_text_image_draw_cmds(
11518 shadow.texts.iter(),
11519 viewport,
11520 root_scale,
11521 &mut staged_uploads,
11522 ) {
11523 Ok(prepared_images) if !prepared_images.is_empty() => {
11524 let upload_offset = frame_encoder
11525 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11526 self.flush_staged_uploads_at(
11527 frame_encoder.encoder(),
11528 &staged_uploads,
11529 upload_offset,
11530 );
11531 let draw_result = {
11532 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11533 &wgpu::RenderPassDescriptor {
11534 label: Some("Zero Blur Shadow Text Image Pass"),
11535 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11536 view: target_view,
11537 resolve_target: None,
11538 depth_slice: None,
11539 ops: wgpu::Operations {
11540 load: wgpu::LoadOp::Load,
11541 store: wgpu::StoreOp::Store,
11542 },
11543 })],
11544 depth_stencil_attachment: None,
11545 timestamp_writes: None,
11546 occlusion_query_set: None,
11547 multiview_mask: None,
11548 },
11549 );
11550 self.draw_prepared_images(
11551 &mut render_pass,
11552 &prepared_images,
11553 BlendMode::SrcOver,
11554 )
11555 };
11556 self.scratch_image_cmds = prepared_images.into_cmds();
11557 if let Err(e) = draw_result {
11558 eprintln!("Failed to draw text for zero-blur shadow: {}", e);
11559 } else {
11560 self.frame_stats.bump_text();
11561 frame_encoder.record_pass();
11562 }
11563 }
11564 Ok(prepared_images) => {
11565 self.scratch_image_cmds = prepared_images.into_cmds();
11566 }
11567 Err(e) => {
11568 eprintln!("Failed to prepare text image for zero-blur shadow: {}", e);
11569 }
11570 }
11571 self.restore_staged_uploads(staged_uploads);
11572 }
11573 return;
11574 }
11575
11576 let Some(device_bounds) =
11578 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)
11579 else {
11580 return;
11581 };
11582 let bounds_x = device_bounds.x;
11583 let bounds_y = device_bounds.y;
11584 let bounds_w = device_bounds.width;
11585 let bounds_h = device_bounds.height;
11586 let pixel_radius = shadow.blur_radius * root_scale;
11587
11588 if shadow.texts.is_empty() && !shadow.shapes.is_empty() {
11589 if let Some(plan) = shape_shadow_surface_plan(
11590 &shadow.shapes,
11591 shadow.clip,
11592 shadow.blur_radius,
11593 width,
11594 height,
11595 root_scale,
11596 self.max_texture_dim(),
11597 ) {
11598 if self.encode_shape_only_blurred_shadow_draw(
11599 frame_encoder,
11600 target_view,
11601 shadow,
11602 plan.source_device_bounds,
11603 plan.pixel_radius,
11604 plan.processing_scissor,
11605 width,
11606 height,
11607 root_scale,
11608 ) {
11609 return;
11610 }
11611 }
11612 }
11613
11614 if !shadow.texts.is_empty() {
11615 self.frame_stats.record_shadow_text_blur_fallback();
11616 }
11617
11618 let device = self.device.clone();
11619 let source_descriptor =
11620 self.transient_offscreen_descriptor("Shadow Source", bounds_w, bounds_h);
11621 let source = frame_encoder.acquire_transient_offscreen(&device, source_descriptor);
11622 let viewport_offset = [bounds_x, bounds_y];
11623 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
11624 let source_outcome = self.encode_shadow_shape_source_passes(
11625 frame_encoder,
11626 &source.view,
11627 &shadow.shapes,
11628 &shadow.brushes,
11629 bounds_w,
11630 bounds_h,
11631 viewport_offset,
11632 root_scale,
11633 &mut next_load_op,
11634 );
11635 frame_encoder.record_passes(source_outcome.pass_count);
11636 let mut rendered_any = source_outcome.rendered_any;
11637
11638 if !shadow.texts.is_empty() {
11639 let mut shifted_texts = shadow.texts.clone();
11640 for text in &mut shifted_texts {
11641 text.rect.x -= viewport_offset[0] / root_scale;
11642 text.rect.y -= viewport_offset[1] / root_scale;
11643 if let Some(clip) = text.clip.as_mut() {
11644 clip.x -= viewport_offset[0] / root_scale;
11645 clip.y -= viewport_offset[1] / root_scale;
11646 }
11647 }
11648
11649 let mut staged_uploads = self.take_staged_uploads();
11650 let viewport = ViewportUniformParams {
11651 width: bounds_w,
11652 height: bounds_h,
11653 offset: [0.0, 0.0],
11654 };
11655 match self.prepare_text_image_draw_cmds(
11656 shifted_texts.iter(),
11657 viewport,
11658 root_scale,
11659 &mut staged_uploads,
11660 ) {
11661 Ok(prepared_images) if !prepared_images.is_empty() => {
11662 let upload_offset = frame_encoder
11663 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11664 self.flush_staged_uploads_at(
11665 frame_encoder.encoder(),
11666 &staged_uploads,
11667 upload_offset,
11668 );
11669 let draw_result = {
11670 let mut render_pass = frame_encoder.encoder().begin_render_pass(
11671 &wgpu::RenderPassDescriptor {
11672 label: Some("Shadow Source Text Image Pass"),
11673 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11674 view: &source.view,
11675 resolve_target: None,
11676 depth_slice: None,
11677 ops: wgpu::Operations {
11678 load: next_load_op,
11679 store: wgpu::StoreOp::Store,
11680 },
11681 })],
11682 depth_stencil_attachment: None,
11683 timestamp_writes: None,
11684 occlusion_query_set: None,
11685 multiview_mask: None,
11686 },
11687 );
11688 self.draw_prepared_images(
11689 &mut render_pass,
11690 &prepared_images,
11691 BlendMode::SrcOver,
11692 )
11693 };
11694 self.scratch_image_cmds = prepared_images.into_cmds();
11695 if let Err(e) = draw_result {
11696 eprintln!("Failed to draw text for shadow: {}", e);
11697 } else {
11698 self.frame_stats.bump_text();
11699 frame_encoder.record_pass();
11700 rendered_any = true;
11701 }
11702 }
11703 Ok(prepared_images) => {
11704 self.scratch_image_cmds = prepared_images.into_cmds();
11705 }
11706 Err(e) => {
11707 eprintln!("Failed to prepare text image for shadow: {}", e);
11708 }
11709 }
11710 self.restore_staged_uploads(staged_uploads);
11711 }
11712
11713 if !rendered_any {
11714 frame_encoder.release_transient_offscreen(source_descriptor, source);
11715 return;
11716 }
11717
11718 let (scratch_w, scratch_h) = crate::effect_renderer::blur_scratch_size(
11719 pixel_radius,
11720 pixel_radius,
11721 bounds_w,
11722 bounds_h,
11723 );
11724 let scratch_descriptor =
11725 self.transient_offscreen_descriptor("Shadow Blur Scratch", scratch_w, scratch_h);
11726 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
11727 {
11728 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
11729 frame_encoder,
11730 &device,
11731 &source,
11732 &scratch,
11733 &source.view,
11734 pixel_radius,
11735 pixel_radius,
11736 TileMode::Decal,
11737 None, );
11739 }
11740 frame_encoder.record_passes(2);
11741
11742 let clip_scissor = shadow
11743 .clip
11744 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
11745 let scissor = clip_scissor.or(processing_scissor);
11746 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
11747 mask.rect[0] -= viewport_offset[0];
11750 mask.rect[1] -= viewport_offset[1];
11751 mask
11752 });
11753 let dest_viewport = Some((
11754 viewport_offset[0],
11755 viewport_offset[1],
11756 bounds_w as f32,
11757 bounds_h as f32,
11758 ));
11759 {
11760 self.effect_renderer
11761 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
11762 frame_encoder,
11763 &device,
11764 &source,
11765 target_view,
11766 1.0,
11767 wgpu::LoadOp::Load,
11768 scissor,
11769 rounded_mask,
11770 BlendMode::SrcOver,
11771 dest_viewport,
11772 CompositeSampleMode::Linear,
11773 );
11774 }
11775 frame_encoder.record_pass();
11776 self.effect_renderer.record_blur_pass();
11777 self.effect_renderer.record_composite_pass();
11778 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
11779 frame_encoder.release_transient_offscreen(source_descriptor, source);
11780 }
11781
11782 #[allow(clippy::too_many_arguments)]
11783 fn encode_shadow_shape_source_passes<C: FrameCommandRecorder>(
11784 &mut self,
11785 frame_encoder: &mut C,
11786 source_view: &wgpu::TextureView,
11787 shapes: &[(DrawShape, BlendMode)],
11788 brushes: &[Brush],
11789 width: u32,
11790 height: u32,
11791 viewport_offset: [f32; 2],
11792 root_scale: f32,
11793 next_load_op: &mut wgpu::LoadOp<wgpu::Color>,
11794 ) -> ShadowSourceRenderOutcome {
11795 if shapes.is_empty() {
11796 return ShadowSourceRenderOutcome {
11797 rendered_any: false,
11798 pass_count: 0,
11799 };
11800 }
11801
11802 let mut staged_uploads = self.take_staged_uploads();
11803 let mut rendered_any = false;
11804 let mut pass_count = 0_u32;
11805 let mut start = 0usize;
11806 while start < shapes.len() {
11807 let blend_mode = supported_blend_mode(shapes[start].1);
11808 let mut end = start + 1;
11809 while end < shapes.len()
11810 && end - start < self.shape_batch_limits.max_shapes_per_batch
11811 && supported_blend_mode(shapes[end].1) == blend_mode
11812 {
11813 end += 1;
11814 }
11815
11816 staged_uploads.clear();
11817 let viewport = ViewportUniformParams {
11818 width,
11819 height,
11820 offset: viewport_offset,
11821 };
11822 let viewport_rect_logical = viewport_rect_in_logical(viewport, root_scale);
11823 let Some(prepared_shape) = self.prepare_shapes_batch(
11824 shapes[start..end]
11825 .iter()
11826 .map(|(shape, _blend_mode)| shape)
11827 .filter(|shape| match viewport_rect_logical {
11828 Some(rect) => shape_draw_is_visible_in_rect(shape, rect, root_scale),
11829 None => false,
11830 }),
11831 brushes,
11832 root_scale,
11833 viewport,
11834 &mut staged_uploads,
11835 ) else {
11836 start = end;
11837 continue;
11838 };
11839
11840 let upload_offset =
11841 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
11842 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
11843
11844 {
11845 let mut render_pass =
11846 frame_encoder
11847 .encoder()
11848 .begin_render_pass(&wgpu::RenderPassDescriptor {
11849 label: Some("Shadow Source Shape Pass"),
11850 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
11851 view: source_view,
11852 resolve_target: None,
11853 depth_slice: None,
11854 ops: wgpu::Operations {
11855 load: *next_load_op,
11856 store: wgpu::StoreOp::Store,
11857 },
11858 })],
11859 depth_stencil_attachment: None,
11860 timestamp_writes: None,
11861 occlusion_query_set: None,
11862 multiview_mask: None,
11863 });
11864 self.draw_prepared_shapes(
11865 &mut render_pass,
11866 blend_mode,
11867 prepared_shape,
11868 width,
11869 height,
11870 &[],
11871 );
11872 }
11873
11874 #[cfg(not(target_arch = "wasm32"))]
11875 {
11876 if fill_area_diag_enabled() {
11877 self.fill_area_diag
11883 .add_offscreen_target_fill(f64::from(width) * f64::from(height));
11884 }
11885 }
11886
11887 pass_count = pass_count.saturating_add(1);
11888 rendered_any = true;
11889 *next_load_op = wgpu::LoadOp::Load;
11890 start = end;
11891 }
11892
11893 self.restore_staged_uploads(staged_uploads);
11894 ShadowSourceRenderOutcome {
11895 rendered_any,
11896 pass_count,
11897 }
11898 }
11899
11900 #[allow(clippy::too_many_arguments)]
11901 fn encode_shape_only_blurred_shadow_draw<C: FrameCommandRecorder>(
11902 &mut self,
11903 frame_encoder: &mut C,
11904 target_view: &wgpu::TextureView,
11905 shadow: &ShadowDraw,
11906 device_bounds: DevicePixelBounds,
11907 pixel_radius: f32,
11908 processing_scissor: Option<(u32, u32, u32, u32)>,
11909 width: u32,
11910 height: u32,
11911 root_scale: f32,
11912 ) -> bool {
11913 let bounds_w = device_bounds.width;
11914 let bounds_h = device_bounds.height;
11915 let viewport_offset = [device_bounds.x, device_bounds.y];
11916 let cache_key = shape_shadow_surface_cache_key(
11917 &shadow.shapes,
11918 &shadow.brushes,
11919 device_bounds,
11920 pixel_radius,
11921 root_scale,
11922 );
11923
11924 if let Some(key) = cache_key {
11925 if let Some(cached) = self.cached_shadow_surface(&key) {
11926 self.frame_stats
11927 .record_shadow_shape_cache_hit(bounds_w, bounds_h);
11928 let clip_scissor = shadow
11929 .clip
11930 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
11931 let scissor = clip_scissor.or(processing_scissor);
11932 let rounded_mask =
11933 inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
11934 mask.rect[0] -= viewport_offset[0];
11935 mask.rect[1] -= viewport_offset[1];
11936 mask
11937 });
11938 let dest_viewport = Some((
11939 viewport_offset[0],
11940 viewport_offset[1],
11941 bounds_w as f32,
11942 bounds_h as f32,
11943 ));
11944 {
11945 self.effect_renderer
11946 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
11947 frame_encoder,
11948 &self.device,
11949 &cached,
11950 target_view,
11951 1.0,
11952 wgpu::LoadOp::Load,
11953 scissor,
11954 rounded_mask,
11955 BlendMode::SrcOver,
11956 dest_viewport,
11957 CompositeSampleMode::Nearest,
11958 );
11959 }
11960 frame_encoder.record_pass();
11961 self.effect_renderer.record_composite_pass();
11962 return true;
11963 }
11964 self.frame_stats
11965 .record_shadow_shape_cache_miss(bounds_w, bounds_h);
11966 self.frame_stats.maybe_print_shadow_shape_cache_miss(
11967 bounds_w,
11968 bounds_h,
11969 key.content_hash,
11970 pixel_radius,
11971 viewport_offset,
11972 shadow.shapes.len(),
11973 shadow.clip,
11974 );
11975 }
11976
11977 let device = self.device.clone();
11978 let source_descriptor =
11979 self.transient_offscreen_descriptor("Shape Shadow Source", bounds_w, bounds_h);
11980 let source_is_cacheable = cache_key.is_some();
11981 let source = if source_is_cacheable {
11982 self.acquire_retained_surface(bounds_w, bounds_h)
11983 } else {
11984 frame_encoder.acquire_transient_offscreen(&device, source_descriptor)
11985 };
11986 let (scratch_w, scratch_h) = crate::effect_renderer::blur_scratch_size(
11987 pixel_radius,
11988 pixel_radius,
11989 bounds_w,
11990 bounds_h,
11991 );
11992 let scratch_descriptor =
11993 self.transient_offscreen_descriptor("Shape Shadow Blur Scratch", scratch_w, scratch_h);
11994 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
11995 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
11996 let source_outcome = self.encode_shadow_shape_source_passes(
11997 frame_encoder,
11998 &source.view,
11999 &shadow.shapes,
12000 &shadow.brushes,
12001 bounds_w,
12002 bounds_h,
12003 viewport_offset,
12004 root_scale,
12005 &mut next_load_op,
12006 );
12007 frame_encoder.record_passes(source_outcome.pass_count);
12008
12009 if !source_outcome.rendered_any {
12010 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
12011 if source_is_cacheable {
12012 self.defer_offscreen_release(source);
12013 } else {
12014 frame_encoder.release_transient_offscreen(source_descriptor, source);
12015 }
12016 return true;
12017 }
12018
12019 {
12020 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
12021 frame_encoder,
12022 &device,
12023 &source,
12024 &scratch,
12025 &source.view,
12026 pixel_radius,
12027 pixel_radius,
12028 TileMode::Decal,
12029 None,
12030 );
12031 }
12032 frame_encoder.record_passes(2);
12033
12034 let clip_scissor = shadow
12035 .clip
12036 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
12037 let scissor = clip_scissor.or(processing_scissor);
12038 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
12039 mask.rect[0] -= viewport_offset[0];
12040 mask.rect[1] -= viewport_offset[1];
12041 mask
12042 });
12043 let dest_viewport = Some((
12044 viewport_offset[0],
12045 viewport_offset[1],
12046 bounds_w as f32,
12047 bounds_h as f32,
12048 ));
12049 {
12050 self.effect_renderer
12051 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
12052 frame_encoder,
12053 &device,
12054 &source,
12055 target_view,
12056 1.0,
12057 wgpu::LoadOp::Load,
12058 scissor,
12059 rounded_mask,
12060 BlendMode::SrcOver,
12061 dest_viewport,
12062 CompositeSampleMode::Nearest,
12063 );
12064 }
12065 frame_encoder.record_pass();
12066
12067 self.effect_renderer.record_blur_pass();
12068 self.effect_renderer.record_composite_pass();
12069 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
12070 if let Some(key) = cache_key {
12071 self.insert_cached_shadow_surface(key, source);
12072 } else {
12073 frame_encoder.release_transient_offscreen(source_descriptor, source);
12074 }
12075 true
12076 }
12077
12078 fn prepare_shapes_batch<'a, I>(
12079 &mut self,
12080 layer_shapes: I,
12081 brushes: &[Brush],
12082 root_scale: f32,
12083 viewport: ViewportUniformParams,
12084 staged_uploads: &mut StagedBufferUploads,
12085 ) -> Option<PreparedShapeBatch>
12086 where
12087 I: Iterator<Item = &'a DrawShape>,
12088 {
12089 #[cfg(target_arch = "wasm32")]
12090 let _ = staged_uploads;
12091
12092 let shape_refs: Vec<&DrawShape> = layer_shapes
12097 .take(self.shape_batch_limits.max_shapes_per_batch)
12098 .collect();
12099 let shape_count = shape_refs.len();
12100 if shape_count == 0 {
12101 return None;
12102 }
12103
12104 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
12108 let mut total_gradient_stops = 0u32;
12109 gradient_offsets.push(0);
12110 for shape in &shape_refs {
12111 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
12112 gradient_offsets.push(total_gradient_stops);
12113 }
12114
12115 self.scratch_shape_data.clear();
12116 self.scratch_shape_data
12117 .resize(shape_count, ShapeData::zeroed());
12118 self.scratch_gradients.clear();
12119 self.scratch_gradients
12120 .resize(total_gradient_stops as usize, GradientStop::zeroed());
12121
12122 convert_shapes_into_outputs(
12123 &shape_refs,
12124 brushes,
12125 &gradient_offsets,
12126 root_scale,
12127 &mut self.scratch_shape_data,
12128 &mut self.scratch_gradients,
12129 );
12130 #[cfg(not(target_arch = "wasm32"))]
12131 {
12132 if fill_area_diag_enabled() {
12133 self.fill_area_diag
12134 .add_shape_quads(&self.scratch_shape_data, viewport);
12135 }
12136 }
12137
12138 #[cfg(not(target_arch = "wasm32"))]
12139 {
12140 self.shape_buffers.ensure_capacity(
12141 &self.device,
12142 &self.shape_bind_group_layout,
12143 &self.identity_similarity_buffer,
12144 self.dummy_paint_buffer.as_ref(),
12145 shape_count,
12146 self.scratch_gradients.len().max(1),
12147 );
12148 self.stage_viewport_uniforms(staged_uploads, viewport);
12149 staged_uploads.stage(
12150 UploadTarget::ShapeData,
12151 bytemuck::cast_slice(&self.scratch_shape_data),
12152 );
12153 if !self.scratch_gradients.is_empty() {
12154 staged_uploads.stage(
12155 UploadTarget::ShapeGradient,
12156 bytemuck::cast_slice(&self.scratch_gradients),
12157 );
12158 }
12159 }
12160
12161 #[cfg(target_arch = "wasm32")]
12162 let shape_slot = {
12163 let slot = self.claim_wasm_shape_batch();
12164 {
12165 let buffers = &mut self.wasm_shape_batches[slot];
12166 buffers.ensure_capacity(
12167 &self.device,
12168 &self.shape_bind_group_layout,
12169 &self.identity_similarity_buffer,
12170 self.dummy_paint_buffer.as_ref(),
12171 shape_count,
12172 self.scratch_gradients.len().max(1),
12173 );
12174 }
12175 let buffers = &self.wasm_shape_batches[slot];
12176 self.write_wasm_buffer(
12177 &buffers.shape_buffer,
12178 bytemuck::cast_slice(&self.scratch_shape_data),
12179 );
12180 if !self.scratch_gradients.is_empty() {
12181 self.write_wasm_buffer(
12182 &buffers.gradient_buffer,
12183 bytemuck::cast_slice(&self.scratch_gradients),
12184 );
12185 }
12186 slot
12187 };
12188
12189 #[cfg(target_arch = "wasm32")]
12190 let uniform_slot = self.prepare_wasm_viewport_uniforms(viewport);
12191
12192 Some(PreparedShapeBatch {
12193 vertex_start: 0,
12194 vertex_count: shape_count as u32 * 6,
12195 has_gradient: total_gradient_stops > 0,
12196 #[cfg(target_arch = "wasm32")]
12197 shape_slot,
12198 #[cfg(target_arch = "wasm32")]
12199 uniform_slot,
12200 })
12201 }
12202
12203 #[cfg(not(target_arch = "wasm32"))]
12210 fn prepare_shapes_batch_direct<'a, I, C: FrameCommandRecorder>(
12211 &mut self,
12212 frame_encoder: &mut C,
12213 layer_shapes: I,
12214 brushes: &[Brush],
12215 root_scale: f32,
12216 viewport: ViewportUniformParams,
12217 staged_uploads: &mut StagedBufferUploads,
12218 ) -> Option<(PreparedShapeBatch, u64)>
12219 where
12220 I: Iterator<Item = &'a DrawShape>,
12221 {
12222 let shape_refs: Vec<&DrawShape> = layer_shapes
12223 .take(self.shape_batch_limits.max_shapes_per_batch)
12224 .collect();
12225 let shape_count = shape_refs.len();
12226 if shape_count == 0 {
12227 return None;
12228 }
12229
12230 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
12231 let mut total_gradient_stops = 0u32;
12232 gradient_offsets.push(0);
12233 for shape in &shape_refs {
12234 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
12235 gradient_offsets.push(total_gradient_stops);
12236 }
12237
12238 self.shape_buffers.ensure_capacity(
12239 &self.device,
12240 &self.shape_bind_group_layout,
12241 &self.identity_similarity_buffer,
12242 self.dummy_paint_buffer.as_ref(),
12243 shape_count,
12244 (total_gradient_stops as usize).max(1),
12245 );
12246
12247 self.scratch_shape_data.clear();
12248 self.scratch_shape_data
12249 .resize(shape_count, ShapeData::zeroed());
12250 self.scratch_gradients.clear();
12251 self.scratch_gradients
12252 .resize(total_gradient_stops as usize, GradientStop::zeroed());
12253 convert_shapes_into_outputs(
12254 &shape_refs,
12255 brushes,
12256 &gradient_offsets,
12257 root_scale,
12258 &mut self.scratch_shape_data,
12259 &mut self.scratch_gradients,
12260 );
12261 if fill_area_diag_enabled() {
12262 self.fill_area_diag
12263 .add_shape_quads(&self.scratch_shape_data, viewport);
12264 }
12265
12266 let uniform_len = std::mem::size_of::<Uniforms>() as u64;
12273 let shape_len = (shape_count * std::mem::size_of::<ShapeData>()) as u64;
12274 let gradient_len = total_gradient_stops as u64 * std::mem::size_of::<GradientStop>() as u64;
12275 let total_len = uniform_len + shape_len + gradient_len;
12276 let upload_base = frame_encoder.allocate_staged_upload_bytes(total_len);
12277 self.ensure_upload_buffer_capacity(upload_base + total_len);
12278
12279 let shape_off = uniform_len;
12280 let gradient_off = shape_off + shape_len;
12281
12282 let uniforms = Self::viewport_uniforms(viewport);
12283 let mut upload_stats = self.frame_graph_executor.upload_buffer(
12284 &self.queue,
12285 &self.upload_buffer,
12286 upload_base,
12287 bytemuck::bytes_of(&uniforms),
12288 );
12289 upload_stats.upload_bytes += self
12290 .frame_graph_executor
12291 .upload_buffer(
12292 &self.queue,
12293 &self.upload_buffer,
12294 upload_base + shape_off,
12295 bytemuck::cast_slice(&self.scratch_shape_data),
12296 )
12297 .upload_bytes;
12298 if !self.scratch_gradients.is_empty() {
12299 upload_stats.upload_bytes += self
12300 .frame_graph_executor
12301 .upload_buffer(
12302 &self.queue,
12303 &self.upload_buffer,
12304 upload_base + gradient_off,
12305 bytemuck::cast_slice(&self.scratch_gradients),
12306 )
12307 .upload_bytes;
12308 }
12309 self.frame_stats.record_command_stats(upload_stats);
12310
12311 staged_uploads.record_upload_copy(UploadTarget::Uniform, 0, 0, uniform_len);
12312 staged_uploads.record_upload_copy(UploadTarget::ShapeData, shape_off, 0, shape_len);
12313 staged_uploads.record_upload_copy(
12314 UploadTarget::ShapeGradient,
12315 gradient_off,
12316 0,
12317 gradient_len,
12318 );
12319
12320 Some((
12321 PreparedShapeBatch {
12322 vertex_start: 0,
12323 vertex_count: shape_count as u32 * 6,
12324 has_gradient: total_gradient_stops > 0,
12325 },
12326 upload_base,
12327 ))
12328 }
12329
12330 pub(crate) fn replay_supported(&self) -> bool {
12336 #[cfg(target_arch = "wasm32")]
12340 {
12341 false
12342 }
12343 #[cfg(not(target_arch = "wasm32"))]
12344 {
12345 self.shape_batch_limits.storage
12346 }
12347 }
12348
12349 pub(crate) fn restore_replay_ack_confirmations(
12355 &mut self,
12356 confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
12357 ) {
12358 #[cfg(not(target_arch = "wasm32"))]
12359 {
12360 self.replay_ack_confirmations = confirmations;
12361 }
12362 #[cfg(target_arch = "wasm32")]
12363 let _ = confirmations;
12364 }
12365
12366 #[cfg(not(target_arch = "wasm32"))]
12369 pub(crate) fn surface_format(&self) -> wgpu::TextureFormat {
12370 self.surface_format
12371 }
12372
12373 #[cfg(not(target_arch = "wasm32"))]
12376 pub(crate) fn replay_ack_confirmations_capacity(&self) -> usize {
12377 self.replay_ack_confirmations.capacity()
12378 }
12379
12380 #[cfg(not(target_arch = "wasm32"))]
12392 pub(crate) fn take_replay_ack_early(
12393 &mut self,
12394 packet: &mut FramePacket,
12395 ) -> Option<(
12396 crate::frame_packet::ReplayAck,
12397 crate::frame_packet::ReplayFrameOps,
12398 )> {
12399 if packet.replay_preconsumed {
12400 return None;
12401 }
12402 let PacketRoot::Direct(root) = &packet.root else {
12403 return None;
12404 };
12405 let ops = std::mem::take(&mut packet.replay);
12406 let root_scale = packet.root_scale;
12407 let (ack, recycled) =
12408 self.consume_replay_ops(ops, &root.scene.shapes, &root.scene.brushes, root_scale);
12409 packet.replay_preconsumed = true;
12410 Some((ack, recycled))
12411 }
12412
12413 #[cfg(not(target_arch = "wasm32"))]
12428 fn consume_replay_ops(
12429 &mut self,
12430 mut ops: crate::frame_packet::ReplayFrameOps,
12431 shapes: &[DrawShape],
12432 brushes: &[Brush],
12433 root_scale: f32,
12434 ) -> (
12435 crate::frame_packet::ReplayAck,
12436 crate::frame_packet::ReplayFrameOps,
12437 ) {
12438 let acked_frame = ops.frame;
12442 if ops.generation < self.store_feed_generation {
12443 self.replay_generation_drops += 1;
12449 log::warn!(
12450 "[command-feed] dropping replay ops of generation {} against store \
12451 generation {} ({} captures, {} patches, {} releases; lifetime drops {})",
12452 ops.generation,
12453 self.store_feed_generation,
12454 ops.captures.len(),
12455 ops.color_patches.len(),
12456 ops.releases.len(),
12457 self.replay_generation_drops,
12458 );
12459 ops.captures.clear();
12460 ops.color_patches.clear();
12461 ops.releases.clear();
12462 return (
12463 crate::frame_packet::ReplayAck {
12464 generation: self.store_feed_generation,
12465 frame: acked_frame,
12466 confirmations: Vec::new(),
12467 },
12468 ops,
12469 );
12470 }
12471 if ops.generation > self.store_feed_generation {
12472 self.store_feed_generation = ops.generation;
12479 }
12480 let generation = ops.generation;
12481 for slot in ops.releases.drain(..) {
12484 self.release_replay_slot(slot);
12485 }
12486 let mut confirmations = std::mem::take(&mut self.replay_ack_confirmations);
12489 debug_assert!(confirmations.is_empty());
12490 let mut refs: Vec<&DrawShape> = Vec::new();
12494 for capture in ops.captures.drain(..) {
12495 if capture.frame != ops.frame {
12496 log::warn!(
12503 "[command-feed] dropping stale capture for slot {} of {:?} \
12504 (queued frame {}, ops frame {})",
12505 capture.key.1,
12506 capture.key.0,
12507 capture.frame,
12508 ops.frame,
12509 );
12510 continue;
12511 }
12512 let end = capture.shape_start + capture.shape_count;
12513 let Some(slice) = shapes.get(capture.shape_start..end) else {
12514 continue;
12515 };
12516 refs.clear();
12517 refs.extend(slice.iter());
12518 let Some(gpu_slot) = self.capture_replay_slot(&refs, brushes, root_scale) else {
12519 continue;
12520 };
12521 confirmations.push((capture.key, gpu_slot));
12522 }
12523 self.replay_color_patches.clear();
12531 std::mem::swap(&mut self.replay_color_patches, &mut ops.color_patches);
12532 (
12533 crate::frame_packet::ReplayAck {
12534 generation,
12535 frame: acked_frame,
12536 confirmations,
12537 },
12538 ops,
12539 )
12540 }
12541
12542 #[cfg(not(target_arch = "wasm32"))]
12547 pub(crate) fn replay_generation_drops(&self) -> u64 {
12548 self.replay_generation_drops
12549 }
12550
12551 #[cfg(not(target_arch = "wasm32"))]
12559 pub(crate) fn replay_ops_roundtrip_for_tests(&mut self, generation_skew: u64) -> usize {
12560 let generation = self.store_feed_generation.wrapping_add(generation_skew);
12561 let ops = crate::shape_replay::SHAPE_REPLAY
12562 .with(|state| state.borrow_mut().take_frame_ops(generation));
12563 let (ack, recycled) = self.consume_replay_ops(ops, &[], &[], 1.0);
12564 let confirmed = ack.confirmations.len();
12565 self.replay_ack_confirmations = crate::shape_replay::SHAPE_REPLAY
12566 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
12567 confirmed
12568 }
12569
12570 #[cfg(not(target_arch = "wasm32"))]
12577 fn stage_replay_patches(&mut self, staged_uploads: &mut StagedBufferUploads) {
12578 std::mem::swap(
12586 &mut self.replay_color_patches,
12587 &mut self.color_patch_scratch,
12588 );
12589 let total_patches = self.color_patch_scratch.len();
12590 if total_patches == 0 {
12591 self.replay_upload_stats.note_frame(0, 0, 0, 0, 0);
12592 return;
12593 }
12594
12595 #[derive(Clone, Copy)]
12601 struct DirtySpan {
12602 paint_min: u32,
12603 paint_max: u32,
12604 }
12605 const CLEAN: DirtySpan = DirtySpan {
12606 paint_min: u32::MAX,
12607 paint_max: 0,
12608 };
12609 let mut dirty: std::collections::HashMap<
12610 u32,
12611 DirtySpan,
12612 cranpose_ui_graphics::FxBuildHasher,
12613 > = std::collections::HashMap::default();
12614
12615 for patch in &self.color_patch_scratch {
12617 let Some(slot) = self.replay_slots.slots.get_mut(&patch.slot) else {
12618 continue;
12619 };
12620 let Some(paint) = slot.paint_mirror.get_mut(patch.shape_index as usize) else {
12621 continue;
12622 };
12623 *paint = patch.color;
12624 let span = dirty.entry(patch.slot).or_insert(CLEAN);
12625 span.paint_min = span.paint_min.min(patch.shape_index);
12626 span.paint_max = span.paint_max.max(patch.shape_index);
12627 }
12628
12629 let mut uploaded_records = 0u64;
12630 let mut uploaded_bytes = 0u64;
12631 let slots_touched = dirty.len() as u64;
12632 for (slot_id, span) in dirty {
12633 let Some(slot) = self.replay_slots.slots.get(&slot_id) else {
12634 continue;
12635 };
12636 if span.paint_min <= span.paint_max {
12637 let range = span.paint_min as usize..span.paint_max as usize + 1;
12638 uploaded_records += range.len() as u64;
12639 uploaded_bytes += (range.len() * std::mem::size_of::<[f32; 4]>()) as u64;
12640 staged_uploads.stage_at(
12641 UploadTarget::ReplayPaintData(slot_id),
12642 range.start as u64 * std::mem::size_of::<[f32; 4]>() as u64,
12643 bytemuck::cast_slice(&slot.paint_mirror[range]),
12644 );
12645 }
12646 }
12647 let ideal_bytes = total_patches as u64 * 16;
12650 self.replay_upload_stats.note_frame(
12651 total_patches as u64,
12652 slots_touched,
12653 uploaded_records,
12654 uploaded_bytes,
12655 ideal_bytes,
12656 );
12657 if cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG") {
12658 log::warn!(
12659 "[replay-upload] frame: {} patches -> {} records / {:.1} KB staged \
12660 across {} slots (color-only {:.1} KB)",
12661 total_patches,
12662 uploaded_records,
12663 uploaded_bytes as f64 / 1024.0,
12664 slots_touched,
12665 ideal_bytes as f64 / 1024.0,
12666 );
12667 }
12668 self.color_patch_scratch.clear();
12669 }
12670
12671 #[cfg(not(target_arch = "wasm32"))]
12674 pub(crate) fn capture_replay_slot(
12675 &mut self,
12676 shape_refs: &[&DrawShape],
12677 brushes: &[Brush],
12678 root_scale: f32,
12679 ) -> Option<u32> {
12680 if !self.shape_batch_limits.storage || shape_refs.is_empty() {
12681 return None;
12682 }
12683 let id = self.replay_slots.free_ids.pop()?;
12684 let shape_count = shape_refs.len();
12685
12686 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
12687 let mut total_gradient_stops = 0u32;
12688 gradient_offsets.push(0);
12689 for shape in shape_refs {
12690 total_gradient_stops += shape_gradient_stop_count(shape, brushes) as u32;
12691 gradient_offsets.push(total_gradient_stops);
12692 }
12693
12694 let mut shape_data = std::mem::take(&mut self.replay_capture_shape_scratch);
12697 shape_data.clear();
12698 shape_data.resize(shape_count, ShapeData::zeroed());
12699 let mut gradients = std::mem::take(&mut self.replay_capture_gradient_scratch);
12700 gradients.clear();
12701 gradients.resize(
12702 (total_gradient_stops as usize).max(1),
12703 GradientStop::zeroed(),
12704 );
12705 convert_shapes_into_outputs(
12706 shape_refs,
12707 brushes,
12708 &gradient_offsets,
12709 root_scale,
12710 &mut shape_data,
12711 &mut gradients,
12712 );
12713
12714 let shape_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12715 label: Some("Replay Shape Buffer"),
12716 size: (std::mem::size_of::<ShapeData>() * shape_count) as u64,
12717 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
12718 mapped_at_creation: true,
12719 });
12720 shape_buffer
12721 .slice(..)
12722 .get_mapped_range_mut()
12723 .copy_from_slice(bytemuck::cast_slice(&shape_data));
12724 shape_buffer.unmap();
12725
12726 let gradient_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12727 label: Some("Replay Gradient Buffer"),
12728 size: (std::mem::size_of::<GradientStop>() * gradients.len()) as u64,
12729 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
12730 mapped_at_creation: true,
12731 });
12732 gradient_buffer
12733 .slice(..)
12734 .get_mapped_range_mut()
12735 .copy_from_slice(bytemuck::cast_slice(&gradients));
12736 gradient_buffer.unmap();
12737
12738 let mut mesh_fill_records: Option<Vec<FillDiagShapeRecord>> = None;
12742 let mut submitted_area_scale = 1.0f32;
12743 let mesh = if arc_mesh_enabled() {
12744 match build_arc_mesh_vertices(&shape_data, retained_mesh_min_px2()) {
12745 Some(build) => {
12746 let meshed_shapes = build.meshed_arcs + build.meshed_rims;
12747 let within_stretch_cap = build.meshed_stretches <= MESH_SLOT_MAX_STRETCHES;
12752 let cut = if build.quad_area > 0.0 {
12753 (1.0 - build.mesh_area / build.quad_area) * 100.0
12754 } else {
12755 0.0
12756 };
12757 log::warn!(
12764 "[arc-mesh] slot {id}: {} arcs + {} rims meshed ({} segs, \
12765 {} stretches), {} instanced; {} unique verts / {} indices; \
12766 quad_px {:.0} -> submit_px {:.0} (-{:.1}%)",
12767 build.meshed_arcs,
12768 build.meshed_rims,
12769 build.meshed_segments,
12770 build.meshed_stretches,
12771 build.passthrough,
12772 build.vertices.len(),
12773 build.indices.len(),
12774 build.quad_area,
12775 build.mesh_area,
12776 cut,
12777 );
12778 if !within_stretch_cap {
12779 log::warn!(
12780 "[arc-mesh] slot {id}: {} meshed stretches exceed the \
12781 {MESH_SLOT_MAX_STRETCHES}-stretch switch cap; slot stays instanced",
12782 build.meshed_stretches,
12783 );
12784 }
12785 let keep_mesh = meshed_shapes > 0 && within_stretch_cap;
12786 if keep_mesh && build.quad_area > 0.0 {
12787 submitted_area_scale =
12793 (build.mesh_area / build.quad_area).clamp(0.05, 1.0) as f32;
12794 }
12795 if keep_mesh && fill_area_diag_enabled() {
12796 mesh_fill_records = Some(fill_diag_capture_records(
12797 &shape_data,
12798 Some((&build.vertices, &build.indices, &build.index_prefix)),
12799 ));
12800 }
12801 keep_mesh.then(|| {
12804 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12805 label: Some("Replay Mesh Vertex Buffer"),
12806 size: (std::mem::size_of::<MeshVertex>() * build.vertices.len()) as u64,
12807 usage: wgpu::BufferUsages::VERTEX,
12808 mapped_at_creation: true,
12809 });
12810 vertex_buffer
12811 .slice(..)
12812 .get_mapped_range_mut()
12813 .copy_from_slice(bytemuck::cast_slice(&build.vertices));
12814 vertex_buffer.unmap();
12815 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12816 label: Some("Replay Mesh Index Buffer"),
12817 size: (std::mem::size_of::<u32>() * build.indices.len()) as u64,
12818 usage: wgpu::BufferUsages::INDEX,
12819 mapped_at_creation: true,
12820 });
12821 index_buffer
12822 .slice(..)
12823 .get_mapped_range_mut()
12824 .copy_from_slice(bytemuck::cast_slice(&build.indices));
12825 index_buffer.unmap();
12826 ReplaySlotMesh {
12827 vertex_buffer,
12828 index_buffer,
12829 index_prefix: build.index_prefix,
12830 meshed_arcs: build.meshed_arcs,
12831 meshed_rims: build.meshed_rims,
12832 passthrough: build.passthrough,
12833 }
12834 })
12835 }
12836 None => {
12837 log::warn!(
12838 "[arc-mesh] slot {id}: geometry byte budget overflowed for \
12839 {shape_count} shapes; whole slot stays instanced"
12840 );
12841 None
12842 }
12843 }
12844 } else {
12845 None
12846 };
12847
12848 let fill_diag_shapes = if fill_area_diag_enabled() {
12849 let records =
12850 mesh_fill_records.unwrap_or_else(|| fill_diag_capture_records(&shape_data, None));
12851 self.fill_area_diag.note_retained_capture(id, &records);
12854 records
12855 } else {
12856 Vec::new()
12857 };
12858
12859 let mut shape_aabbs = Vec::with_capacity(shape_count);
12864 let mut area_prefix = Vec::with_capacity(shape_count + 1);
12865 area_prefix.push(0.0f32);
12866 for shape in &shape_data {
12867 let corners = [
12868 [shape.quad01[0], shape.quad01[1]],
12869 [shape.quad01[2], shape.quad01[3]],
12870 [shape.quad23[0], shape.quad23[1]],
12871 [shape.quad23[2], shape.quad23[3]],
12872 ];
12873 let mut min_x = f32::INFINITY;
12874 let mut min_y = f32::INFINITY;
12875 let mut max_x = f32::NEG_INFINITY;
12876 let mut max_y = f32::NEG_INFINITY;
12877 for corner in corners {
12878 min_x = min_x.min(corner[0]);
12879 min_y = min_y.min(corner[1]);
12880 max_x = max_x.max(corner[0]);
12881 max_y = max_y.max(corner[1]);
12882 }
12883 shape_aabbs.push([min_x, min_y, max_x, max_y]);
12884 let ring = [corners[0], corners[1], corners[3], corners[2]];
12887 let mut doubled = 0.0f32;
12888 for i in 0..4 {
12889 let a = ring[i];
12890 let b = ring[(i + 1) % 4];
12891 doubled += a[0] * b[1] - b[0] * a[1];
12892 }
12893 let area = (doubled * 0.5).abs();
12894 let running = *area_prefix.last().expect("prefix seeded with 0.0");
12895 area_prefix.push(running + area);
12896 }
12897
12898 let paint: Vec<[f32; 4]> = shape_data.iter().map(|shape| shape.color).collect();
12901 let paint_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
12902 label: Some("Replay Paint Buffer"),
12903 size: (std::mem::size_of::<[f32; 4]>() * shape_count) as u64,
12904 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
12905 mapped_at_creation: true,
12906 });
12907 paint_buffer
12908 .slice(..)
12909 .get_mapped_range_mut()
12910 .copy_from_slice(bytemuck::cast_slice(&paint));
12911 paint_buffer.unmap();
12912
12913 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
12914 label: Some("Replay Shape Bind Group"),
12915 layout: &self.shape_bind_group_layout,
12916 entries: &[
12917 wgpu::BindGroupEntry {
12918 binding: 0,
12919 resource: shape_buffer.as_entire_binding(),
12920 },
12921 wgpu::BindGroupEntry {
12922 binding: 1,
12923 resource: gradient_buffer.as_entire_binding(),
12924 },
12925 wgpu::BindGroupEntry {
12929 binding: 2,
12930 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
12931 buffer: &self.replay_slots.transform_buffer,
12932 offset: 0,
12933 size: Some(
12934 std::num::NonZeroU64::new(
12935 std::mem::size_of::<SimilarityTransform>() as u64
12936 )
12937 .expect("similarity transform is non-empty"),
12938 ),
12939 }),
12940 },
12941 wgpu::BindGroupEntry {
12942 binding: 3,
12943 resource: paint_buffer.as_entire_binding(),
12944 },
12945 ],
12946 });
12947
12948 let capture_epoch = self.replay_slots.next_capture_epoch;
12949 self.replay_slots.next_capture_epoch += 1;
12950 self.replay_slots.slots.insert(
12951 id,
12952 ReplaySlot {
12953 paint_buffer,
12954 bind_group,
12955 shape_count: shape_count as u32,
12956 paint_mirror: paint,
12957 mesh,
12958 capture_epoch,
12959 has_gradient: total_gradient_stops > 0,
12960 fill_diag_shapes,
12961 shape_aabbs,
12962 area_prefix,
12963 submitted_area_scale,
12964 },
12965 );
12966 self.replay_capture_shape_scratch = shape_data;
12969 self.replay_capture_gradient_scratch = gradients;
12970 Some(id)
12971 }
12972
12973 #[cfg(not(target_arch = "wasm32"))]
12975 pub(crate) fn release_replay_slot(&mut self, id: u32) {
12976 if self.replay_slots.slots.remove(&id).is_some() {
12977 self.replay_slots.free_ids.push(id);
12978 self.retained_bundle_cache.clear();
12984 self.segment_surfaces.drop_slot(id);
12987 }
12988 }
12989
12990 #[cfg(not(target_arch = "wasm32"))]
12994 #[doc(hidden)]
12995 pub fn segment_surface_stats(&self) -> (u64, u64, u64, u64, u64) {
12996 let stats = &self.segment_surfaces.stats;
12997 (
12998 stats.captures,
12999 stats.composites,
13000 stats.dirty_recaptures,
13001 stats.rejected_churn,
13002 stats.rejected_economics,
13003 )
13004 }
13005
13006 #[cfg(not(target_arch = "wasm32"))]
13009 #[doc(hidden)]
13010 pub fn instanced_quads_active(&self) -> bool {
13011 self.instanced_quads.is_some()
13012 }
13013
13014 #[cfg(not(target_arch = "wasm32"))]
13017 #[doc(hidden)]
13018 pub fn replay_slot_mesh_stats(&self) -> (usize, usize) {
13019 let meshed = self
13020 .replay_slots
13021 .slots
13022 .values()
13023 .filter(|slot| slot.mesh.is_some())
13024 .count();
13025 (meshed, self.replay_slots.slots.len())
13026 }
13027
13028 #[cfg(not(target_arch = "wasm32"))]
13032 #[doc(hidden)]
13033 pub fn replay_slot_mesh_engagement(&self) -> (usize, usize, usize) {
13034 self.replay_slots
13035 .slots
13036 .values()
13037 .filter_map(|slot| slot.mesh.as_ref())
13038 .fold((0, 0, 0), |(arcs, rims, passthrough), mesh| {
13039 (
13040 arcs + mesh.meshed_arcs,
13041 rims + mesh.meshed_rims,
13042 passthrough + mesh.passthrough,
13043 )
13044 })
13045 }
13046
13047 #[cfg(not(target_arch = "wasm32"))]
13053 #[allow(clippy::too_many_arguments)]
13054 fn plan_segment_surfaces(
13055 &mut self,
13056 segment_surfaces: &mut SegmentSurfaceCache,
13057 ordered_items: &[(usize, SegmentDrawItem)],
13058 chunk: &SegmentDrawChunkPlan,
13059 retained_draws: &[RetainedDraw],
13060 staged_uploads: &mut StagedBufferUploads,
13061 captures: &mut Vec<SegmentCaptureJob>,
13062 composites: &mut Vec<(usize, SegmentCompositePlan)>,
13063 ) {
13064 segment_surfaces.ensure_dirty_map(
13065 self.replay_color_patches
13066 .iter()
13067 .map(|patch| (patch.slot, patch.shape_index)),
13068 );
13069 let max_texture_dim = self.effect_renderer.max_texture_dim();
13070 for batch in chunk.iter() {
13071 let SegmentBatchPlan::Retained { start, end } = batch else {
13072 continue;
13073 };
13074 for (_, item) in &ordered_items[start..end] {
13075 let SegmentDrawItem::Retained(index) = item else {
13076 continue;
13077 };
13078 if (*index as u32) >= MAX_REPLAY_SLOTS {
13081 continue;
13082 }
13083 let Some(retained) = retained_draws.get(*index) else {
13084 continue;
13085 };
13086 let transform = retained.transform;
13087 let (key, capture_epoch, dirty) = {
13088 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
13089 continue;
13090 };
13091 let first = retained.first_shape.min(slot.shape_count);
13092 let last = retained
13093 .first_shape
13094 .saturating_add(retained.shape_count)
13095 .min(slot.shape_count);
13096 if first >= last {
13097 continue;
13098 }
13099 (
13100 SegmentSurfaceKey {
13101 slot: retained.slot,
13102 first_shape: first,
13103 shape_count: last - first,
13104 },
13105 slot.capture_epoch,
13106 segment_surfaces.range_dirty(retained.slot, first, last),
13107 )
13108 };
13109 let first = key.first_shape;
13110 let last = key.first_shape + key.shape_count;
13111 let slots = &self.replay_slots.slots;
13112 let decision =
13113 segment_surfaces.decide(key, capture_epoch, dirty, transform.scale, || {
13114 let slot = slots.get(&key.slot)?;
13115 plan_segment_capture_geometry(slot, first, last, transform, max_texture_dim)
13116 });
13117 let SegmentSurfaceDecision::Composite { capture } = decision else {
13118 continue;
13119 };
13120 if let Some(plan) = capture {
13121 let texture = segment_surfaces
13122 .take_texture_for_recapture(&key, &plan.rect)
13123 .unwrap_or_else(|| {
13124 let device = self.device.clone();
13125 self.effect_renderer.acquire_offscreen(
13126 &device,
13127 plan.rect.width,
13128 plan.rect.height,
13129 Some(&self.frame_stats),
13130 )
13131 });
13132 segment_surfaces.install_entry(
13133 key,
13134 capture_epoch,
13135 transform.center,
13136 transform.rot,
13137 transform.scale,
13138 plan.rect,
13139 texture,
13140 );
13141 staged_uploads.stage_at(
13145 UploadTarget::ReplayTransform,
13146 (MAX_REPLAY_SLOTS + plan.index) as u64 * REPLAY_TRANSFORM_STRIDE,
13147 bytemuck::bytes_of(&transform.with_retained_paint()),
13148 );
13149 let uniforms = Self::viewport_uniforms(ViewportUniformParams {
13156 width: plan.rect.width,
13157 height: plan.rect.height,
13158 offset: plan.rect.origin,
13159 });
13160 let device = self.device.clone();
13161 let capture_uniforms =
13162 segment_surfaces.capture_uniforms(&device, &self.uniform_bind_group_layout);
13163 let upload_stats = self.frame_graph_executor.upload_buffer(
13164 &self.queue,
13165 &capture_uniforms.buffer,
13166 plan.index as u64 * SEGMENT_CAPTURE_UNIFORM_STRIDE,
13167 bytemuck::bytes_of(&uniforms),
13168 );
13169 self.frame_stats.record_command_stats(upload_stats);
13170 captures.push(SegmentCaptureJob {
13171 key,
13172 first,
13173 last,
13174 capture_index: plan.index,
13175 });
13176 composites.push((
13180 *index,
13181 SegmentCompositePlan {
13182 key,
13183 dest_quad: segment_identity_quad(&plan.rect),
13184 inverse: segment_identity_inverse(&plan.rect),
13185 identity: true,
13186 },
13187 ));
13188 } else {
13189 let Some(entry) = segment_surfaces.entry(&key) else {
13190 continue;
13191 };
13192 let t_now =
13193 Affine2::from_similarity(transform.center, transform.rot, transform.scale);
13194 let t_cap =
13195 Affine2::from_similarity(entry.cap_center, entry.cap_rot, entry.cap_scale);
13196 let Some(cap_inverse) = t_cap.invert() else {
13197 segment_surfaces.remove(&key);
13198 continue;
13199 };
13200 let effective = t_now.compose(&cap_inverse);
13201 let rect = entry.rect;
13202 let plan = if effective.is_identity_for_sampling() {
13203 SegmentCompositePlan {
13206 key,
13207 dest_quad: segment_identity_quad(&rect),
13208 inverse: segment_identity_inverse(&rect),
13209 identity: true,
13210 }
13211 } else {
13212 let Some(inverse) = effective.invert() else {
13213 segment_surfaces.remove(&key);
13214 continue;
13215 };
13216 SegmentCompositePlan {
13217 key,
13218 dest_quad: segment_identity_quad(&rect).map(|c| effective.apply(c)),
13219 inverse: [
13220 [
13221 inverse.l[0][0],
13222 inverse.l[0][1],
13223 inverse.t[0] - rect.origin[0],
13224 ],
13225 [
13226 inverse.l[1][0],
13227 inverse.l[1][1],
13228 inverse.t[1] - rect.origin[1],
13229 ],
13230 [0.0, 0.0, 1.0],
13231 ],
13232 identity: false,
13233 }
13234 };
13235 composites.push((*index, plan));
13236 }
13237 }
13238 }
13239 }
13240
13241 #[cfg(not(target_arch = "wasm32"))]
13245 fn draw_retained_batch(
13246 &self,
13247 render_pass: &mut wgpu::RenderPass<'_>,
13248 retained: &RetainedDraw,
13249 retained_index: usize,
13250 width: u32,
13251 height: u32,
13252 ) {
13253 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
13254 return;
13255 };
13256 if retained_index as u32 >= MAX_REPLAY_SLOTS {
13257 return;
13258 }
13259 let first = retained.first_shape.min(slot.shape_count);
13260 let last = retained
13261 .first_shape
13262 .saturating_add(retained.shape_count)
13263 .min(slot.shape_count);
13264 if first >= last {
13265 return;
13266 }
13267 if fill_area_diag_enabled() {
13268 self.fill_area_diag.add_retained_range(
13269 &slot.fill_diag_shapes,
13270 first,
13271 last,
13272 &retained.transform,
13273 );
13274 }
13275 self.frame_stats.bump_shapes();
13276 render_pass.set_scissor_rect(0, 0, width, height);
13277 let draws =
13278 self.encode_retained_op(
13279 slot,
13280 first,
13281 last,
13282 retained_index as u32,
13283 &mut |cmd| match cmd {
13284 RetainedCmd::Pipeline(pipeline) => render_pass.set_pipeline(pipeline),
13285 RetainedCmd::Uniforms(group) => render_pass.set_bind_group(0, group, &[]),
13286 RetainedCmd::SlotBindings(group, offset) => {
13287 render_pass.set_bind_group(1, group, &[offset])
13288 }
13289 RetainedCmd::MeshVertices(buffer) => {
13290 render_pass.set_vertex_buffer(0, buffer.slice(..))
13291 }
13292 RetainedCmd::Index(buffer, format) => {
13293 render_pass.set_index_buffer(buffer.slice(..), format)
13294 }
13295 RetainedCmd::Draw(vertices) => render_pass.draw(vertices, 0..1),
13296 RetainedCmd::DrawIndexed(indices, instances) => {
13297 render_pass.draw_indexed(indices, 0, instances)
13298 }
13299 },
13300 );
13301 self.frame_stats.add_draw_calls(draws);
13302 }
13303
13304 #[cfg(not(target_arch = "wasm32"))]
13330 fn encode_retained_op<'r>(
13331 &'r self,
13332 slot: &'r ReplaySlot,
13333 first: u32,
13334 last: u32,
13335 retained_index: u32,
13336 sink: &mut impl FnMut(RetainedCmd<'r>),
13337 ) -> u32 {
13338 sink(RetainedCmd::Uniforms(&self.uniform_bind_group));
13339 sink(RetainedCmd::SlotBindings(
13340 &slot.bind_group,
13341 retained_index * REPLAY_TRANSFORM_STRIDE as u32,
13342 ));
13343 let Some(mesh) = slot.mesh.as_ref() else {
13344 self.encode_retained_instanced(slot, first..last, sink);
13345 return 1;
13346 };
13347 sink(RetainedCmd::MeshVertices(&mesh.vertex_buffer));
13348 let prefix = &mesh.index_prefix;
13349 let meshed_at = |shape: u32| prefix[shape as usize + 1] > prefix[shape as usize];
13350 let mut draws = 0;
13351 let mut cursor = first;
13352 while cursor < last {
13353 let run_meshed = meshed_at(cursor);
13354 let mut end = cursor + 1;
13355 while end < last && meshed_at(end) == run_meshed {
13356 end += 1;
13357 }
13358 if run_meshed {
13359 sink(RetainedCmd::Pipeline(self.mesh_pipeline()));
13360 sink(RetainedCmd::Index(
13361 &mesh.index_buffer,
13362 wgpu::IndexFormat::Uint32,
13363 ));
13364 sink(RetainedCmd::DrawIndexed(
13365 prefix[cursor as usize]..prefix[end as usize],
13366 0..1,
13367 ));
13368 } else {
13369 self.encode_retained_instanced(slot, cursor..end, sink);
13370 }
13371 draws += 1;
13372 cursor = end;
13373 }
13374 draws
13375 }
13376
13377 #[cfg(not(target_arch = "wasm32"))]
13383 fn encode_retained_instanced<'r>(
13384 &'r self,
13385 slot: &ReplaySlot,
13386 range: Range<u32>,
13387 sink: &mut impl FnMut(RetainedCmd<'r>),
13388 ) {
13389 match &self.instanced_quads {
13390 Some(instanced) => {
13391 if slot.has_gradient {
13392 sink(RetainedCmd::Pipeline(
13393 self.instanced_pipeline(instanced, BlendMode::SrcOver),
13394 ));
13395 } else {
13396 sink(RetainedCmd::Pipeline(
13397 self.instanced_pipeline_solid(instanced),
13398 ));
13399 }
13400 sink(RetainedCmd::Index(
13401 &instanced.index_buffer,
13402 wgpu::IndexFormat::Uint16,
13403 ));
13404 sink(RetainedCmd::DrawIndexed(0..6, range));
13405 }
13406 None => {
13407 if slot.has_gradient {
13408 sink(RetainedCmd::Pipeline(
13409 self.shape_pipeline(BlendMode::SrcOver),
13410 ));
13411 } else {
13412 sink(RetainedCmd::Pipeline(self.shape_pipeline_solid()));
13413 }
13414 sink(RetainedCmd::Draw(range.start * 6..range.end * 6));
13415 }
13416 }
13417 }
13418
13419 #[cfg(not(target_arch = "wasm32"))]
13427 fn retained_bundle_key(
13428 &self,
13429 ordered_items: &[(usize, SegmentDrawItem)],
13430 retained_draws: &[RetainedDraw],
13431 item_range: Range<usize>,
13432 ) -> RetainedBundleKey {
13433 let mut ops = Vec::with_capacity(item_range.len());
13434 for (_, item) in &ordered_items[item_range] {
13435 let SegmentDrawItem::Retained(index) = item else {
13436 continue;
13437 };
13438 let Some(retained) = retained_draws.get(*index) else {
13439 continue;
13440 };
13441 let slot = self.replay_slots.slots.get(&retained.slot);
13442 let (first, last) = match slot {
13443 Some(slot) => (
13444 retained.first_shape.min(slot.shape_count),
13445 retained
13446 .first_shape
13447 .saturating_add(retained.shape_count)
13448 .min(slot.shape_count),
13449 ),
13450 None => (
13451 retained.first_shape,
13452 retained.first_shape.saturating_add(retained.shape_count),
13453 ),
13454 };
13455 ops.push(RetainedBundleOpKey {
13456 slot: retained.slot,
13457 capture_epoch: slot.map(|slot| slot.capture_epoch),
13458 first,
13459 last,
13460 retained_index: *index as u32,
13461 has_mesh: slot.is_some_and(|slot| slot.mesh.is_some())
13462 && self.shape_batch_limits.storage,
13463 });
13464 }
13465 RetainedBundleKey {
13466 depth: self.pass_depth(),
13467 ops,
13468 }
13469 }
13470
13471 #[cfg(not(target_arch = "wasm32"))]
13478 fn build_retained_bundle(&self, key: &RetainedBundleKey) -> wgpu::RenderBundle {
13479 let mut encoder =
13480 self.device
13481 .create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
13482 label: Some("Retained Stretch Bundle"),
13483 color_formats: &[Some(self.surface_format)],
13487 depth_stencil: key.depth.then_some(wgpu::RenderBundleDepthStencil {
13492 format: display_clip::DISPLAY_CLIP_DEPTH_FORMAT,
13493 depth_read_only: true,
13494 stencil_read_only: true,
13495 }),
13496 sample_count: 1,
13497 multiview: None,
13498 });
13499 for op in &key.ops {
13500 if op.capture_epoch.is_none()
13501 || op.retained_index >= MAX_REPLAY_SLOTS
13502 || op.first >= op.last
13503 {
13504 continue;
13505 }
13506 let Some(slot) = self.replay_slots.slots.get(&op.slot) else {
13507 continue;
13508 };
13509 self.encode_retained_op(
13514 slot,
13515 op.first,
13516 op.last,
13517 op.retained_index,
13518 &mut |cmd| match cmd {
13519 RetainedCmd::Pipeline(pipeline) => encoder.set_pipeline(pipeline),
13520 RetainedCmd::Uniforms(group) => encoder.set_bind_group(0, group, &[]),
13521 RetainedCmd::SlotBindings(group, offset) => {
13522 encoder.set_bind_group(1, group, &[offset])
13523 }
13524 RetainedCmd::MeshVertices(buffer) => {
13525 encoder.set_vertex_buffer(0, buffer.slice(..))
13526 }
13527 RetainedCmd::Index(buffer, format) => {
13528 encoder.set_index_buffer(buffer.slice(..), format)
13529 }
13530 RetainedCmd::Draw(vertices) => encoder.draw(vertices, 0..1),
13531 RetainedCmd::DrawIndexed(indices, instances) => {
13532 encoder.draw_indexed(indices, 0, instances)
13533 }
13534 },
13535 );
13536 }
13537 encoder.finish(&wgpu::RenderBundleDescriptor {
13538 label: Some("Retained Stretch Bundle"),
13539 })
13540 }
13541
13542 #[cfg(not(target_arch = "wasm32"))]
13550 fn draw_retained_stretch_bundled(
13551 &mut self,
13552 render_pass: &mut wgpu::RenderPass<'_>,
13553 ordered_items: &[(usize, SegmentDrawItem)],
13554 retained_draws: &[RetainedDraw],
13555 item_range: Range<usize>,
13556 width: u32,
13557 height: u32,
13558 ) {
13559 let key = self.retained_bundle_key(ordered_items, retained_draws, item_range);
13560 if !self.retained_bundle_cache.hit(&key) {
13561 let bundle = self.build_retained_bundle(&key);
13562 self.retained_bundle_cache.insert(key.clone(), bundle);
13563 }
13564 for op in &key.ops {
13567 if op.capture_epoch.is_some()
13568 && op.retained_index < MAX_REPLAY_SLOTS
13569 && op.first < op.last
13570 {
13571 self.frame_stats.bump_shapes();
13572 self.frame_stats.add_draw_calls(1);
13573 if fill_area_diag_enabled() {
13574 let slot = self.replay_slots.slots.get(&op.slot);
13576 let retained = retained_draws.get(op.retained_index as usize);
13577 if let (Some(slot), Some(retained)) = (slot, retained) {
13578 self.fill_area_diag.add_retained_range(
13579 &slot.fill_diag_shapes,
13580 op.first,
13581 op.last,
13582 &retained.transform,
13583 );
13584 }
13585 }
13586 }
13587 }
13588 render_pass.set_scissor_rect(0, 0, width, height);
13593 if let Some(bundle) = self.retained_bundle_cache.get(&key) {
13594 render_pass.execute_bundles(std::iter::once(bundle));
13595 }
13596 }
13597
13598 #[cfg(not(target_arch = "wasm32"))]
13601 #[doc(hidden)]
13602 pub fn retained_bundle_stats(&self) -> (u64, u64) {
13603 self.retained_bundle_cache.stats()
13604 }
13605
13606 #[cfg(not(target_arch = "wasm32"))]
13609 #[doc(hidden)]
13610 pub fn rim_meshes_emitted(&self) -> u64 {
13611 self.rim_meshes_emitted
13612 }
13613
13614 #[doc(hidden)]
13618 pub fn device_error_count(&self) -> u64 {
13619 self.device_errors.error_count()
13620 }
13621
13622 #[cfg(not(target_arch = "wasm32"))]
13629 #[doc(hidden)]
13630 pub fn static_span_stats(&self) -> (u64, u64) {
13631 (self.static_span.hits, self.static_span.recaptures)
13632 }
13633
13634 #[cfg(not(target_arch = "wasm32"))]
13641 fn upload_transient_rim_meshes(&mut self) {
13642 let device = self.device.clone();
13643 let mut upload_stats = crate::frame_graph::FrameCommandStats::default();
13644 if self.rim_mesh_vertices.len() > self.rim_mesh_uploaded_vertices {
13645 let vertex_buffer = self.rim_mesh_vertex_buffer.get_or_insert_with(|| {
13646 device.create_buffer(&wgpu::BufferDescriptor {
13647 label: Some("Rim Mesh Vertex Buffer"),
13648 size: (RIM_MESH_VERTEX_CAPACITY * std::mem::size_of::<MeshVertex>()) as u64,
13649 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
13650 mapped_at_creation: false,
13651 })
13652 });
13653 upload_stats.upload_bytes += self
13654 .frame_graph_executor
13655 .upload_buffer(
13656 &self.queue,
13657 vertex_buffer,
13658 (self.rim_mesh_uploaded_vertices * std::mem::size_of::<MeshVertex>()) as u64,
13659 bytemuck::cast_slice(
13660 &self.rim_mesh_vertices[self.rim_mesh_uploaded_vertices..],
13661 ),
13662 )
13663 .upload_bytes;
13664 self.rim_mesh_uploaded_vertices = self.rim_mesh_vertices.len();
13665 }
13666 if self.rim_mesh_indices.len() > self.rim_mesh_uploaded_indices {
13667 let index_buffer = self.rim_mesh_index_buffer.get_or_insert_with(|| {
13668 device.create_buffer(&wgpu::BufferDescriptor {
13669 label: Some("Rim Mesh Index Buffer"),
13670 size: (RIM_MESH_INDEX_CAPACITY * std::mem::size_of::<u32>()) as u64,
13671 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
13672 mapped_at_creation: false,
13673 })
13674 });
13675 upload_stats.upload_bytes += self
13676 .frame_graph_executor
13677 .upload_buffer(
13678 &self.queue,
13679 index_buffer,
13680 (self.rim_mesh_uploaded_indices * std::mem::size_of::<u32>()) as u64,
13681 bytemuck::cast_slice(&self.rim_mesh_indices[self.rim_mesh_uploaded_indices..]),
13682 )
13683 .upload_bytes;
13684 self.rim_mesh_uploaded_indices = self.rim_mesh_indices.len();
13685 }
13686 if upload_stats.upload_bytes > 0 {
13687 self.frame_stats.record_command_stats(upload_stats);
13688 }
13689 }
13690
13691 fn draw_prepared_shapes(
13692 &self,
13693 render_pass: &mut wgpu::RenderPass<'_>,
13694 blend_mode: BlendMode,
13695 batch: PreparedShapeBatch,
13696 width: u32,
13697 height: u32,
13698 rims: &[RimDraw],
13699 ) {
13700 #[cfg(target_arch = "wasm32")]
13701 let _ = rims;
13702 self.frame_stats.bump_shapes();
13703 self.frame_stats.add_draw_calls(1);
13704 render_pass.set_scissor_rect(0, 0, width, height);
13705 #[cfg(not(target_arch = "wasm32"))]
13706 let (uniform_bind_group, shape_buffers) = (&self.uniform_bind_group, &self.shape_buffers);
13707 #[cfg(target_arch = "wasm32")]
13708 let (uniform_bind_group, shape_buffers) = (
13709 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
13710 &self.wasm_shape_batches[batch.shape_slot],
13711 );
13712 #[cfg(not(target_arch = "wasm32"))]
13717 if let Some(instanced) = &self.instanced_quads {
13718 assert!(
13719 batch.vertex_start.is_multiple_of(6) && batch.vertex_count.is_multiple_of(6),
13720 "shape batches are whole shapes: vertex range {}..+{} must be \
13721 six-aligned to convert to an instance range",
13722 batch.vertex_start,
13723 batch.vertex_count,
13724 );
13725 let set_instanced_pipeline = |render_pass: &mut wgpu::RenderPass<'_>| {
13728 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
13729 render_pass.set_pipeline(self.instanced_pipeline_solid(instanced));
13730 } else {
13731 render_pass.set_pipeline(self.instanced_pipeline(instanced, blend_mode));
13732 }
13733 };
13734 set_instanced_pipeline(render_pass);
13735 render_pass.set_bind_group(0, uniform_bind_group, &[]);
13736 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
13739 let first_shape = batch.vertex_start / 6;
13740 let shape_count = batch.vertex_count / 6;
13741 render_pass
13742 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
13743 debug_assert!(
13747 rims.windows(2)
13748 .all(|pair| pair[0].shape_index < pair[1].shape_index),
13749 "rim draws must arrive in ascending shape order"
13750 );
13751 let rim_start = rims.partition_point(|rim| rim.shape_index < first_shape);
13752 let rim_end = rims.partition_point(|rim| rim.shape_index < first_shape + shape_count);
13753 let batch_rims = &rims[rim_start..rim_end];
13754 let rim_buffers = match (&self.rim_mesh_vertex_buffer, &self.rim_mesh_index_buffer) {
13755 (Some(vertex_buffer), Some(index_buffer)) if !batch_rims.is_empty() => {
13756 Some((vertex_buffer, index_buffer))
13757 }
13758 _ => None,
13759 };
13760 let Some((rim_vertex_buffer, rim_index_buffer)) = rim_buffers else {
13761 render_pass.draw_indexed(0..6, 0, first_shape..first_shape + shape_count);
13762 return;
13763 };
13764 let mut draw_calls = 0u32;
13772 let mut cursor = first_shape;
13773 for rim in batch_rims {
13774 if cursor < rim.shape_index {
13775 render_pass.draw_indexed(0..6, 0, cursor..rim.shape_index);
13776 draw_calls += 1;
13777 }
13778 render_pass.set_pipeline(self.mesh_pipeline());
13779 render_pass.set_vertex_buffer(0, rim_vertex_buffer.slice(..));
13780 render_pass.set_index_buffer(rim_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
13781 render_pass.draw_indexed(
13782 rim.first_index..rim.first_index + rim.index_count,
13783 0,
13784 0..1,
13785 );
13786 draw_calls += 1;
13787 set_instanced_pipeline(render_pass);
13788 render_pass
13789 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
13790 cursor = rim.shape_index + 1;
13791 }
13792 if cursor < first_shape + shape_count {
13793 render_pass.draw_indexed(0..6, 0, cursor..first_shape + shape_count);
13794 draw_calls += 1;
13795 }
13796 self.frame_stats
13798 .add_draw_calls(draw_calls.saturating_sub(1));
13799 return;
13800 }
13801 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
13802 render_pass.set_pipeline(self.shape_pipeline_solid());
13803 } else {
13804 render_pass.set_pipeline(self.shape_pipeline(blend_mode));
13805 }
13806 render_pass.set_bind_group(0, uniform_bind_group, &[]);
13807 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
13810 render_pass.draw(
13813 batch.vertex_start..batch.vertex_start + batch.vertex_count,
13814 0..1,
13815 );
13816 }
13817
13818 #[allow(clippy::too_many_arguments)]
13821 fn encode_shapes_pass<'a, I, C: FrameCommandRecorder>(
13822 &mut self,
13823 frame_encoder: &mut C,
13824 target_view: &wgpu::TextureView,
13825 layer_shapes: I,
13826 brushes: &[Brush],
13827 blend_mode: BlendMode,
13828 width: u32,
13829 height: u32,
13830 root_scale: f32,
13831 load_op: wgpu::LoadOp<wgpu::Color>,
13832 viewport_offset: [f32; 2],
13833 ) where
13834 I: Iterator<Item = &'a DrawShape>,
13835 {
13836 let mut staged_uploads = self.take_staged_uploads();
13837 let viewport = ViewportUniformParams {
13838 width,
13839 height,
13840 offset: viewport_offset,
13841 };
13842 let viewport_rect_logical = viewport_rect_in_logical(viewport, root_scale);
13843 let Some(batch) = self.prepare_shapes_batch(
13844 layer_shapes.filter(|shape| match viewport_rect_logical {
13845 Some(rect) => shape_draw_is_visible_in_rect(shape, rect, root_scale),
13846 None => false,
13847 }),
13848 brushes,
13849 root_scale,
13850 viewport,
13851 &mut staged_uploads,
13852 ) else {
13853 self.restore_staged_uploads(staged_uploads);
13854 return;
13855 };
13856 let upload_offset =
13857 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
13858 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
13859 self.restore_staged_uploads(staged_uploads);
13860 let mut render_pass =
13861 frame_encoder
13862 .encoder()
13863 .begin_render_pass(&wgpu::RenderPassDescriptor {
13864 label: Some("Shape Pass"),
13865 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
13866 view: target_view,
13867 resolve_target: None,
13868 depth_slice: None,
13869 ops: wgpu::Operations {
13870 load: load_op,
13871 store: wgpu::StoreOp::Store,
13872 },
13873 })],
13874 depth_stencil_attachment: None,
13875 timestamp_writes: None,
13876 occlusion_query_set: None,
13877 multiview_mask: None,
13878 });
13879 self.draw_prepared_shapes(&mut render_pass, blend_mode, batch, width, height, &[]);
13880 }
13881
13882 fn draw_prepared_images(
13883 &mut self,
13884 render_pass: &mut wgpu::RenderPass<'_>,
13885 batch: &PreparedImageBatch,
13886 blend_mode: BlendMode,
13887 ) -> Result<(), String> {
13888 if batch.cmds.is_empty() {
13889 return Ok(());
13890 }
13891 self.frame_stats.bump_images();
13892 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
13893 render_pass.set_pipeline(self.image_pipeline(blend_mode));
13894 #[cfg(not(target_arch = "wasm32"))]
13895 let (uniform_bind_group, vertex_buffer, index_buffer) = (
13896 &self.uniform_bind_group,
13897 &self.image_vertex_buffer,
13898 &self.image_index_buffer,
13899 );
13900 #[cfg(target_arch = "wasm32")]
13901 let (uniform_bind_group, vertex_buffer, index_buffer) = (
13902 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
13903 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
13904 &self.wasm_image_batches[batch.image_slot].index_buffer,
13905 );
13906 render_pass.set_bind_group(0, uniform_bind_group, &[]);
13907 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
13908 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
13909
13910 for cmd in &batch.cmds {
13911 let (sx, sy, sw, sh) = cmd.scissor;
13912 render_pass.set_scissor_rect(sx, sy, sw, sh);
13913
13914 let cached = self
13915 .image_texture_cache
13916 .get(&cmd.image_id)
13917 .ok_or_else(|| "image texture missing from cache".to_string())?;
13918 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
13919 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
13920 }
13921 Ok(())
13922 }
13923
13924 fn draw_prepared_glyphs(
13925 &mut self,
13926 render_pass: &mut wgpu::RenderPass<'_>,
13927 batch: &PreparedGlyphBatch,
13928 ) -> Result<(), String> {
13929 if batch.cmds.is_empty() {
13930 return Ok(());
13931 }
13932 #[cfg(not(target_arch = "wasm32"))]
13933 {
13934 self.draw_native_prepared_glyph_cmd_range(
13935 render_pass,
13936 &batch.cmds,
13937 0..batch.cmds.len(),
13938 )?;
13939 }
13940 #[cfg(target_arch = "wasm32")]
13941 {
13942 self.frame_stats.bump_text();
13943 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
13944 render_pass.set_pipeline(self.glyph_atlas_pipeline());
13945 let (uniform_bind_group, vertex_buffer, index_buffer) = (
13946 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
13947 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
13948 &self.wasm_image_batches[batch.image_slot].index_buffer,
13949 );
13950 render_pass.set_bind_group(0, uniform_bind_group, &[]);
13951 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
13952 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
13953 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
13954
13955 for cmd in &batch.cmds {
13956 let (sx, sy, sw, sh) = cmd.scissor;
13957 render_pass.set_scissor_rect(sx, sy, sw, sh);
13958 let GlyphDrawSource::Shared {
13959 index_start,
13960 index_count,
13961 } = cmd.source;
13962 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
13963 }
13964 }
13965 Ok(())
13966 }
13967
13968 #[cfg(not(target_arch = "wasm32"))]
13969 fn draw_native_prepared_image_cmd_range(
13970 &mut self,
13971 render_pass: &mut wgpu::RenderPass<'_>,
13972 cmds: &[ImageDrawCmd],
13973 cmd_range: Range<usize>,
13974 blend_mode: BlendMode,
13975 ) -> Result<(), String> {
13976 let Some(cmds) = cmds.get(cmd_range) else {
13977 return Err("image command range is outside the prepared command buffer".to_string());
13978 };
13979 if cmds.is_empty() {
13980 return Ok(());
13981 }
13982
13983 self.frame_stats.bump_images();
13984 self.frame_stats.add_draw_calls(cmds.len() as u32);
13985 render_pass.set_pipeline(self.image_pipeline(blend_mode));
13986 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
13987 render_pass.set_index_buffer(self.image_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
13988 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
13989
13990 for cmd in cmds {
13991 let (sx, sy, sw, sh) = cmd.scissor;
13992 render_pass.set_scissor_rect(sx, sy, sw, sh);
13993
13994 let cached = self
13995 .image_texture_cache
13996 .get(&cmd.image_id)
13997 .ok_or_else(|| "image texture missing from cache".to_string())?;
13998 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
13999 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
14000 }
14001 Ok(())
14002 }
14003
14004 #[cfg(not(target_arch = "wasm32"))]
14005 fn draw_native_prepared_glyph_cmd_range(
14006 &mut self,
14007 render_pass: &mut wgpu::RenderPass<'_>,
14008 cmds: &[GlyphDrawCmd],
14009 cmd_range: Range<usize>,
14010 ) -> Result<(), String> {
14011 let Some(cmds) = cmds.get(cmd_range) else {
14012 return Err("glyph command range is outside the prepared command buffer".to_string());
14013 };
14014 if cmds.is_empty() {
14015 return Ok(());
14016 }
14017
14018 self.frame_stats.bump_text();
14019 self.frame_stats.add_draw_calls(cmds.len() as u32);
14020
14021 let mut shared_buffers_bound = false;
14022 let mut retained_pipeline_bound = false;
14023 for cmd in cmds {
14024 let (sx, sy, sw, sh) = cmd.scissor;
14025 render_pass.set_scissor_rect(sx, sy, sw, sh);
14026 match cmd.source {
14027 GlyphDrawSource::Shared {
14028 index_start,
14029 index_count,
14030 } => {
14031 if retained_pipeline_bound || !shared_buffers_bound {
14032 render_pass.set_pipeline(self.glyph_atlas_pipeline());
14033 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
14034 retained_pipeline_bound = false;
14035 }
14036 if !shared_buffers_bound {
14037 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
14038 render_pass.set_index_buffer(
14039 self.image_index_buffer.slice(..),
14040 wgpu::IndexFormat::Uint32,
14041 );
14042 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
14043 shared_buffers_bound = true;
14044 }
14045 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
14046 }
14047 GlyphDrawSource::Retained {
14048 cache_key,
14049 uniform_slot,
14050 } => {
14051 shared_buffers_bound = false;
14052 if !retained_pipeline_bound {
14053 render_pass.set_pipeline(self.retained_glyph_atlas_pipeline());
14054 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
14055 retained_pipeline_bound = true;
14056 }
14057 let cached = self
14058 .text_glyph_gpu_run_cache
14059 .peek(&cache_key)
14060 .ok_or_else(|| "retained glyph buffer missing from cache".to_string())?;
14061 let dynamic_offset =
14062 self.retained_glyph_uniform_dynamic_offset(uniform_slot)?;
14063 render_pass.set_bind_group(
14064 0,
14065 &self.retained_glyph_uniform_bind_group,
14066 &[dynamic_offset],
14067 );
14068 render_pass
14069 .set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
14070 render_pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
14071 render_pass.draw_indexed(0..cached.index_count, 0, 0..1);
14072 }
14073 }
14074 }
14075 Ok(())
14076 }
14077
14078 fn append_image_draw_cmd(
14079 &mut self,
14080 image_draw: &ImageDraw,
14081 viewport: ViewportUniformParams,
14082 root_scale: f32,
14083 image_vertices: &mut Vec<Vertex>,
14084 image_indices: &mut Vec<u32>,
14085 image_cmds: &mut Vec<ImageDrawCmd>,
14086 ) -> Result<(), String> {
14087 let snap_delta = image_draw
14088 .snap_anchor
14089 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
14090 .unwrap_or_default();
14091 let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
14092 if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
14093 return Ok(());
14094 }
14095
14096 let (tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
14097 if tint[3] <= 0.0 {
14098 return Ok(());
14099 }
14100
14101 let prepared_image = if let Some(filter) = cpu_filter {
14102 apply_filter_to_bitmap(&image_draw.image, filter)?
14103 } else {
14104 image_draw.image.clone()
14105 };
14106 self.ensure_image_cached(&prepared_image)?;
14107
14108 let mut adjusted_image = ImageDraw {
14109 rect,
14110 local_rect: image_draw.local_rect.translate(snap_delta.x, snap_delta.y),
14111 quad: translate_quad(image_draw.quad, snap_delta),
14112 snap_anchor: image_draw.snap_anchor,
14113 image: image_draw.image.clone(),
14114 alpha: image_draw.alpha,
14115 color_filter: image_draw.color_filter,
14116 sampling: image_draw.sampling,
14117 z_index: image_draw.z_index,
14118 clip: image_draw.clip,
14119 blend_mode: image_draw.blend_mode,
14120 src_rect: image_draw.src_rect,
14121 motion_context_animated: image_draw.motion_context_animated,
14122 };
14123 snap_nearest_image_to_device_pixels(&mut adjusted_image, root_scale);
14124 let Some(scissor) =
14125 scissor_rect_for_image(&adjusted_image, root_scale, viewport.width, viewport.height)
14126 else {
14127 return Ok(());
14128 };
14129
14130 let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
14131 return Ok(());
14132 };
14133 let device_quad =
14134 nearest_image_device_quad(&adjusted_image, root_scale).unwrap_or_else(|| {
14135 if adjusted_image.snap_anchor.is_some() {
14136 canonicalized_scaled_quad(adjusted_image.quad, root_scale)
14137 } else {
14138 scaled_quad(adjusted_image.quad, root_scale)
14139 }
14140 });
14141 #[cfg(not(target_arch = "wasm32"))]
14142 {
14143 if fill_area_diag_enabled() {
14144 self.fill_area_diag.add_image_quad(&device_quad);
14145 }
14146 }
14147
14148 let base_vertex = image_vertices.len() as u32;
14149 let index_start = image_indices.len() as u32;
14150 image_indices.extend_from_slice(&[
14151 base_vertex,
14152 base_vertex + 1,
14153 base_vertex + 2,
14154 base_vertex + 2,
14155 base_vertex + 1,
14156 base_vertex + 3,
14157 ]);
14158 image_vertices.extend_from_slice(&[
14159 Vertex {
14160 position: device_quad[0],
14161 color: tint,
14162 uv: [uv_rect.min[0], uv_rect.min[1]],
14163 uv_bounds: uv_rect.sample_bounds,
14164 },
14165 Vertex {
14166 position: device_quad[1],
14167 color: tint,
14168 uv: [uv_rect.max[0], uv_rect.min[1]],
14169 uv_bounds: uv_rect.sample_bounds,
14170 },
14171 Vertex {
14172 position: device_quad[2],
14173 color: tint,
14174 uv: [uv_rect.min[0], uv_rect.max[1]],
14175 uv_bounds: uv_rect.sample_bounds,
14176 },
14177 Vertex {
14178 position: device_quad[3],
14179 color: tint,
14180 uv: [uv_rect.max[0], uv_rect.max[1]],
14181 uv_bounds: uv_rect.sample_bounds,
14182 },
14183 ]);
14184
14185 image_cmds.push(ImageDrawCmd {
14186 index_start,
14187 scissor,
14188 image_id: prepared_image.id(),
14189 sampling: image_draw.sampling,
14190 });
14191 Ok(())
14192 }
14193
14194 #[cfg(not(target_arch = "wasm32"))]
14195 fn stage_native_image_buffers(
14196 &mut self,
14197 staged_uploads: &mut StagedBufferUploads,
14198 viewport: ViewportUniformParams,
14199 image_vertices: &[Vertex],
14200 image_indices: &[u32],
14201 ) {
14202 if image_indices.is_empty() {
14203 return;
14204 }
14205
14206 self.stage_viewport_uniforms(staged_uploads, viewport);
14207 let needed_bytes = std::mem::size_of_val(image_vertices) as u64;
14212 if needed_bytes > self.image_vertex_buffer.size() {
14213 self.image_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14214 label: Some("Image Vertex Buffer"),
14215 size: needed_bytes.next_power_of_two(),
14216 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
14217 mapped_at_creation: false,
14218 });
14219 }
14220 let needed_index_bytes = std::mem::size_of_val(image_indices) as u64;
14221 if needed_index_bytes > self.image_index_buffer.size() {
14222 self.image_index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14223 label: Some("Image Index Buffer"),
14224 size: needed_index_bytes.next_power_of_two(),
14225 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
14226 mapped_at_creation: false,
14227 });
14228 }
14229
14230 staged_uploads.stage(
14231 UploadTarget::ImageVertex,
14232 bytemuck::cast_slice(image_vertices),
14233 );
14234 staged_uploads.stage(
14235 UploadTarget::ImageIndex,
14236 bytemuck::cast_slice(image_indices),
14237 );
14238 }
14239
14240 fn prepare_image_draw_cmds<'a, I>(
14243 &mut self,
14244 layer_images: I,
14245 viewport: ViewportUniformParams,
14246 root_scale: f32,
14247 staged_uploads: &mut StagedBufferUploads,
14248 ) -> Result<PreparedImageBatch, String>
14249 where
14250 I: Iterator<Item = &'a ImageDraw>,
14251 {
14252 #[cfg(target_arch = "wasm32")]
14253 let _ = staged_uploads;
14254
14255 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
14256 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
14257 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
14258 image_vertices.clear();
14259 image_indices.clear();
14260 image_cmds.clear();
14261
14262 for image_draw in layer_images {
14263 self.append_image_draw_cmd(
14264 image_draw,
14265 viewport,
14266 root_scale,
14267 &mut image_vertices,
14268 &mut image_indices,
14269 &mut image_cmds,
14270 )?;
14271 }
14272
14273 #[cfg(not(target_arch = "wasm32"))]
14274 if !image_cmds.is_empty() {
14275 self.stage_native_image_buffers(
14276 staged_uploads,
14277 viewport,
14278 &image_vertices,
14279 &image_indices,
14280 );
14281 }
14282
14283 #[cfg(target_arch = "wasm32")]
14284 let image_slot = if image_cmds.is_empty() {
14285 0
14286 } else {
14287 let slot = self.claim_wasm_image_batch();
14288 {
14289 let buffers = &mut self.wasm_image_batches[slot];
14290 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
14291 }
14292 let buffers = &self.wasm_image_batches[slot];
14293 self.write_wasm_buffer(
14294 &buffers.vertex_buffer,
14295 bytemuck::cast_slice(&image_vertices),
14296 );
14297 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
14298 slot
14299 };
14300
14301 #[cfg(target_arch = "wasm32")]
14302 let uniform_slot = if image_cmds.is_empty() {
14303 0
14304 } else {
14305 self.prepare_wasm_viewport_uniforms(viewport)
14306 };
14307
14308 self.scratch_image_vertices = image_vertices;
14309 self.scratch_image_indices = image_indices;
14310 Ok(PreparedImageBatch {
14311 cmds: image_cmds,
14312 #[cfg(target_arch = "wasm32")]
14313 image_slot,
14314 #[cfg(target_arch = "wasm32")]
14315 uniform_slot,
14316 })
14317 }
14318
14319 fn glyph_atlas_entry_for(
14320 &mut self,
14321 glyph: &SoftwareGlyphAtlasGlyph,
14322 ) -> Result<GlyphAtlasEntry, String> {
14323 if let Some(entry) = self.text_glyph_atlas.upload_glyph(
14324 glyph.key,
14325 glyph,
14326 &self.queue,
14327 &mut self.frame_graph_executor,
14328 &mut self.frame_stats,
14329 ) {
14330 return Ok(entry);
14331 }
14332
14333 self.text_glyph_atlas.reset(
14334 &self.device,
14335 &self.image_bind_group_layout,
14336 &self.image_nearest_sampler,
14337 );
14338 Err("text glyph atlas filled and was reset".to_string())
14339 }
14340
14341 fn glyph_atlas_entry_for_cached(
14342 &mut self,
14343 glyph: &SoftwareGlyphAtlasPlacement,
14344 ) -> Option<GlyphAtlasEntry> {
14345 let entry = self.text_glyph_atlas.entry(&glyph.key)?;
14346 self.frame_stats.record_text_glyph_atlas_hit();
14347 Some(entry)
14348 }
14349
14350 fn glyph_atlas_entry_for_placement(
14351 &mut self,
14352 glyph: &SoftwareGlyphAtlasPlacement,
14353 ) -> Result<GlyphAtlasEntry, String> {
14354 if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
14355 return Ok(entry);
14356 }
14357
14358 let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
14359 return Err("text glyph placement has no retained raster mask".to_string());
14360 };
14361 self.glyph_atlas_entry_for(&upload_glyph)
14362 }
14363
14364 fn prepare_text_glyph_quads(
14365 &mut self,
14366 run_key: TextGlyphRunCacheKey,
14367 atlas_generation: u64,
14368 cached_glyph_run: Option<&[SoftwareGlyphAtlasPlacement]>,
14369 collected_run: &[SoftwareGlyphAtlasRunGlyph],
14370 generated_quads: &mut Vec<CachedTextGlyphQuad>,
14371 ) -> Result<Rc<[CachedTextGlyphQuad]>, String> {
14372 generated_quads.clear();
14373 if let Some(glyph_run) = cached_glyph_run {
14374 for glyph in glyph_run {
14375 if glyph.width == 0 || glyph.height == 0 || glyph.color.3 <= 0.0 {
14376 continue;
14377 }
14378 let entry = self.glyph_atlas_entry_for_placement(glyph)?;
14379 generated_quads.push(cached_text_glyph_quad(
14384 glyph,
14385 entry,
14386 self.text_glyph_atlas.size(),
14387 ));
14388 }
14389 } else {
14390 for run_glyph in collected_run {
14391 let placement = run_glyph.placement();
14392 if placement.width == 0 || placement.height == 0 || placement.color.3 <= 0.0 {
14393 continue;
14394 }
14395 let entry = match run_glyph {
14396 SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
14397 self.glyph_atlas_entry_for_placement(placement)?
14398 }
14399 SoftwareGlyphAtlasRunGlyph::New(glyph) => self.glyph_atlas_entry_for(glyph)?,
14400 };
14401 generated_quads.push(cached_text_glyph_quad(
14402 &placement,
14403 entry,
14404 self.text_glyph_atlas.size(),
14405 ));
14406 }
14407 }
14408
14409 let quads: Rc<[CachedTextGlyphQuad]> = Rc::from(generated_quads.clone().into_boxed_slice());
14410 if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
14411 cached.quads = Some(Rc::clone(&quads));
14412 cached.atlas_generation = atlas_generation;
14413 }
14414 Ok(quads)
14415 }
14416
14417 #[allow(clippy::too_many_arguments)]
14418 fn append_text_glyph_quad_run(
14419 &mut self,
14420 source_raster_rect: Rect,
14421 quads: &[CachedTextGlyphQuad],
14422 clip: Option<Rect>,
14423 viewport: ViewportUniformParams,
14424 root_scale: f32,
14425 image_vertices: &mut Vec<Vertex>,
14426 image_indices: &mut Vec<u32>,
14427 record_cached_hits: bool,
14428 ) -> usize {
14429 let mut appended = 0usize;
14430 for quad in quads {
14431 if !cached_text_glyph_quad_is_visible_in_viewport(
14432 source_raster_rect,
14433 quad,
14434 clip,
14435 viewport,
14436 root_scale,
14437 ) {
14438 continue;
14439 }
14440 if append_cached_text_glyph_quad(
14441 source_raster_rect,
14442 quad,
14443 image_vertices,
14444 image_indices,
14445 ) {
14446 if record_cached_hits {
14447 self.frame_stats.record_text_glyph_atlas_hit();
14448 }
14449 #[cfg(not(target_arch = "wasm32"))]
14450 {
14451 if fill_area_diag_enabled() {
14452 self.fill_area_diag.add_glyph_quad(quad);
14453 }
14454 }
14455 appended = appended.saturating_add(1);
14456 }
14457 }
14458 appended
14459 }
14460
14461 #[cfg(not(target_arch = "wasm32"))]
14462 fn retained_glyph_viewport(
14463 viewport: ViewportUniformParams,
14464 source_raster_rect: Rect,
14465 ) -> ViewportUniformParams {
14466 ViewportUniformParams {
14467 width: viewport.width,
14468 height: viewport.height,
14469 offset: [
14470 viewport.offset[0] - source_raster_rect.x,
14471 viewport.offset[1] - source_raster_rect.y,
14472 ],
14473 }
14474 }
14475
14476 #[cfg(not(target_arch = "wasm32"))]
14477 fn retained_text_glyph_run_ready(&mut self, cache_key: TextGlyphRunCacheKey) -> bool {
14478 let atlas_generation = self.text_glyph_atlas.generation();
14479 self.text_glyph_gpu_run_cache
14480 .peek(&cache_key)
14481 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
14482 }
14483
14484 #[cfg(not(target_arch = "wasm32"))]
14485 #[allow(clippy::too_many_arguments)]
14486 fn emit_retained_text_glyph_run_if_ready(
14487 &mut self,
14488 cache_key: TextGlyphRunCacheKey,
14489 quads: &[CachedTextGlyphQuad],
14490 clip: Option<Rect>,
14491 viewport: ViewportUniformParams,
14492 source_raster_rect: Rect,
14493 scissor: (u32, u32, u32, u32),
14494 staged_uploads: &mut StagedBufferUploads,
14495 glyph_cmds: &mut Vec<GlyphDrawCmd>,
14496 ) -> bool {
14497 if !should_use_retained_text_glyph_run(quads.len(), clip) {
14498 return false;
14499 }
14500 if !self.retained_text_glyph_run_ready(cache_key)
14501 && !self.ensure_retained_text_glyph_run(cache_key, quads)
14502 {
14503 return false;
14504 }
14505
14506 let uniform_slot = self.stage_retained_glyph_viewport_uniforms(
14507 staged_uploads,
14508 Self::retained_glyph_viewport(viewport, source_raster_rect),
14509 );
14510 if fill_area_diag_enabled() {
14511 for quad in quads {
14514 self.fill_area_diag.add_glyph_quad(quad);
14515 }
14516 }
14517 glyph_cmds.push(GlyphDrawCmd::retained(cache_key, uniform_slot, scissor));
14518 true
14519 }
14520
14521 #[cfg(not(target_arch = "wasm32"))]
14522 fn ensure_retained_text_glyph_run(
14523 &mut self,
14524 cache_key: TextGlyphRunCacheKey,
14525 quads: &[CachedTextGlyphQuad],
14526 ) -> bool {
14527 let atlas_generation = self.text_glyph_atlas.generation();
14528 if self
14529 .text_glyph_gpu_run_cache
14530 .peek(&cache_key)
14531 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
14532 {
14533 return true;
14534 }
14535
14536 let mut vertices = Vec::with_capacity(quads.len().saturating_mul(4));
14537 let mut indices = Vec::with_capacity(quads.len().saturating_mul(6));
14538 let origin = Rect {
14539 x: 0.0,
14540 y: 0.0,
14541 width: 0.0,
14542 height: 0.0,
14543 };
14544 for quad in quads {
14545 append_cached_text_glyph_quad(origin, quad, &mut vertices, &mut indices);
14546 }
14547 if indices.is_empty() {
14548 return false;
14549 }
14550
14551 let vertex_bytes = bytemuck::cast_slice(&vertices);
14552 let index_bytes = bytemuck::cast_slice(&indices);
14553 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14554 label: Some("Retained Text Glyph Vertex Buffer"),
14555 size: vertex_bytes.len() as u64,
14556 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
14557 mapped_at_creation: false,
14558 });
14559 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
14560 label: Some("Retained Text Glyph Index Buffer"),
14561 size: index_bytes.len() as u64,
14562 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
14563 mapped_at_creation: false,
14564 });
14565 let vertex_upload =
14566 self.frame_graph_executor
14567 .upload_buffer(&self.queue, &vertex_buffer, 0, vertex_bytes);
14568 self.frame_stats.record_command_stats(vertex_upload);
14569 let index_upload =
14570 self.frame_graph_executor
14571 .upload_buffer(&self.queue, &index_buffer, 0, index_bytes);
14572 self.frame_stats.record_command_stats(index_upload);
14573
14574 self.text_glyph_gpu_run_cache.put(
14575 cache_key,
14576 CachedGpuTextGlyphRun {
14577 vertex_buffer,
14578 index_buffer,
14579 index_count: indices.len() as u32,
14580 atlas_generation,
14581 },
14582 );
14583 true
14584 }
14585
14586 #[allow(clippy::too_many_arguments)]
14587 fn append_text_glyph_draws<'a, I>(
14588 &mut self,
14589 layer_texts: I,
14590 viewport: ViewportUniformParams,
14591 root_scale: f32,
14592 allow_offscreen_prewarm: bool,
14593 staged_uploads: &mut StagedBufferUploads,
14594 image_vertices: &mut Vec<Vertex>,
14595 image_indices: &mut Vec<u32>,
14596 glyph_cmds: &mut Vec<GlyphDrawCmd>,
14597 ) -> Result<bool, String>
14598 where
14599 I: IntoIterator<Item = &'a TextDraw>,
14600 {
14601 let append_start = Instant::now();
14602 let initial_vertex_len = image_vertices.len();
14603 let initial_index_len = image_indices.len();
14604 let initial_cmd_len = glyph_cmds.len();
14605 let initial_staged_bytes_len = staged_uploads.bytes.len();
14606 let initial_staged_copies_len = staged_uploads.copies.len();
14607 let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
14608 let mut collected_placements = std::mem::take(&mut self.scratch_text_glyph_placements);
14609 let mut generated_quads = std::mem::take(&mut self.scratch_text_glyph_quads);
14610 generated_quads.clear();
14611 let mut visited = 0usize;
14612 let mut emitted_glyphs = 0usize;
14613 let mut prewarmed_glyphs = 0usize;
14614 let mut run_hits = 0usize;
14615 let mut run_misses = 0usize;
14616
14617 for text_draw in layer_texts {
14618 visited = visited.saturating_add(1);
14619 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
14620 self.text_raster_geometry(text_draw, root_scale)
14621 else {
14622 continue;
14623 };
14624 if !static_text_motion {
14625 image_vertices.truncate(initial_vertex_len);
14626 image_indices.truncate(initial_index_len);
14627 glyph_cmds.truncate(initial_cmd_len);
14628 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
14629 self.scratch_text_glyph_run = collected_run;
14630 self.scratch_text_glyph_placements = collected_placements;
14631 self.scratch_text_glyph_quads = generated_quads;
14632 return Ok(false);
14633 }
14634 let is_visible =
14635 text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale);
14636 let draw_action = text_glyph_draw_action(
14637 is_visible,
14638 text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale),
14639 allow_offscreen_prewarm,
14640 );
14641 if draw_action == TextGlyphDrawAction::Skip {
14642 continue;
14643 }
14644
14645 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
14646 let source_draw = raster_source.draw.as_ref();
14647 let source_raster_rect = raster_source.raster_rect;
14648
14649 let run_key = Self::text_glyph_run_cache_key(
14650 source_draw,
14651 source_raster_rect,
14652 text_scale,
14653 static_text_motion,
14654 );
14655 let atlas_generation = self.text_glyph_atlas.generation();
14656 let mut cached_quad_run = None;
14657 let mut miss_collect_ms = None;
14658 let mut miss_cached_glyphs = 0usize;
14659 let mut miss_new_glyphs = 0usize;
14660 let cached_glyph_run = if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
14661 run_hits = run_hits.saturating_add(1);
14662 if cached.atlas_generation == atlas_generation {
14663 cached_quad_run = cached.quads.as_ref().map(Rc::clone);
14664 }
14665 Some(Rc::clone(&cached.glyphs))
14666 } else {
14667 run_misses = run_misses.saturating_add(1);
14668 collected_run.clear();
14669 let collect_start = Instant::now();
14670 let collect_result = collect_solid_text_atlas_run(
14671 source_draw.text.as_ref(),
14672 source_raster_rect,
14673 &source_draw.text_style,
14674 source_draw.color,
14675 source_draw.font_size,
14676 text_scale,
14677 &self.text_fonts,
14678 &mut self.text_glyph_mask_cache,
14679 &mut collected_run,
14680 );
14681 miss_collect_ms = Some(instant_ms(collect_start, Instant::now()));
14682 if collect_result.is_none() {
14683 if text_atlas_fallback_diag_enabled() {
14684 let preview: String = source_draw.text.text.chars().take(96).collect();
14685 log::warn!(
14686 "[text-atlas-fallback] node={:?} visible={} prewarm={} spans={} links={} text_len={} preview={:?} span_style={:?} paragraph_style={:?}",
14687 source_draw.node_id,
14688 is_visible,
14689 draw_action == TextGlyphDrawAction::PrewarmOffscreen,
14690 source_draw.text.span_styles.len(),
14691 source_draw.text.links.len(),
14692 source_draw.text.text.len(),
14693 preview,
14694 source_draw.text_style.span_style,
14695 source_draw.text_style.paragraph_style,
14696 );
14697 }
14698 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
14699 continue;
14700 }
14701 image_vertices.truncate(initial_vertex_len);
14702 image_indices.truncate(initial_index_len);
14703 glyph_cmds.truncate(initial_cmd_len);
14704 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
14705 self.scratch_text_glyph_run = collected_run;
14706 self.scratch_text_glyph_placements = collected_placements;
14707 self.scratch_text_glyph_quads = generated_quads;
14708 return Ok(false);
14709 }
14710 if text_glyph_run_diag_enabled() {
14711 miss_cached_glyphs = collected_run
14712 .iter()
14713 .filter(|glyph| matches!(glyph, SoftwareGlyphAtlasRunGlyph::Cached(_)))
14714 .count();
14715 miss_new_glyphs = collected_run.len().saturating_sub(miss_cached_glyphs);
14716 }
14717 collected_placements.clear();
14718 collected_placements.extend(
14719 collected_run
14720 .iter()
14721 .map(SoftwareGlyphAtlasRunGlyph::placement),
14722 );
14723 let glyphs: Rc<[SoftwareGlyphAtlasPlacement]> =
14724 Rc::from(collected_placements.clone().into_boxed_slice());
14725 self.text_glyph_run_cache.put(
14726 run_key,
14727 CachedTextGlyphRun {
14728 glyphs,
14729 quads: None,
14730 atlas_generation: 0,
14731 },
14732 );
14733 None
14734 };
14735
14736 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
14737 let prewarm_quads = if let Some(quad_run) = cached_quad_run {
14738 quad_run
14739 } else {
14740 let prepare_start = Instant::now();
14741 match self.prepare_text_glyph_quads(
14742 run_key,
14743 atlas_generation,
14744 cached_glyph_run.as_deref(),
14745 &collected_run,
14746 &mut generated_quads,
14747 ) {
14748 Ok(quads) => {
14749 if let Some(collect_ms) = miss_collect_ms {
14750 if text_glyph_run_diag_enabled() {
14751 log::warn!(
14752 "[text-glyph-run-diag] visible=false glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
14753 quads.len(),
14754 miss_cached_glyphs,
14755 miss_new_glyphs,
14756 collect_ms,
14757 instant_ms(prepare_start, Instant::now()),
14758 );
14759 }
14760 }
14761 quads
14762 }
14763 Err(_) => continue,
14764 }
14765 };
14766 #[cfg(not(target_arch = "wasm32"))]
14767 if should_use_retained_text_glyph_run(prewarm_quads.len(), source_draw.clip) {
14768 self.ensure_retained_text_glyph_run(run_key, prewarm_quads.as_ref());
14769 }
14770 prewarmed_glyphs = prewarmed_glyphs.saturating_add(prewarm_quads.len());
14771 continue;
14772 }
14773
14774 let draw_rect = Rect {
14775 x: source_raster_rect.x / root_scale,
14776 y: source_raster_rect.y / root_scale,
14777 width: source_raster_rect.width / root_scale,
14778 height: source_raster_rect.height / root_scale,
14779 };
14780 let Some(scissor) = scissor_rect_for_layer(
14781 draw_rect,
14782 source_draw.clip,
14783 root_scale,
14784 viewport.width,
14785 viewport.height,
14786 ) else {
14787 continue;
14788 };
14789
14790 #[cfg(not(target_arch = "wasm32"))]
14791 if let Some(quad_run) = cached_quad_run.as_ref() {
14792 if should_use_retained_text_glyph_run(quad_run.len(), source_draw.clip)
14793 && self.emit_retained_text_glyph_run_if_ready(
14794 run_key,
14795 quad_run.as_ref(),
14796 source_draw.clip,
14797 viewport,
14798 source_raster_rect,
14799 scissor,
14800 staged_uploads,
14801 glyph_cmds,
14802 )
14803 {
14804 emitted_glyphs = emitted_glyphs.saturating_add(quad_run.len());
14805 continue;
14806 }
14807 }
14808
14809 let index_start = image_indices.len() as u32;
14810 if let Some(quad_run) = cached_quad_run {
14811 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
14812 source_raster_rect,
14813 quad_run.as_ref(),
14814 source_draw.clip,
14815 viewport,
14816 root_scale,
14817 image_vertices,
14818 image_indices,
14819 true,
14820 ));
14821 } else {
14822 let prepare_start = Instant::now();
14823 let Ok(quad_run) = self.prepare_text_glyph_quads(
14824 run_key,
14825 atlas_generation,
14826 cached_glyph_run.as_deref(),
14827 &collected_run,
14828 &mut generated_quads,
14829 ) else {
14830 image_vertices.truncate(initial_vertex_len);
14831 image_indices.truncate(initial_index_len);
14832 glyph_cmds.truncate(initial_cmd_len);
14833 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
14834 self.scratch_text_glyph_run = collected_run;
14835 self.scratch_text_glyph_placements = collected_placements;
14836 self.scratch_text_glyph_quads = generated_quads;
14837 return Ok(false);
14838 };
14839 if let Some(collect_ms) = miss_collect_ms {
14840 if text_glyph_run_diag_enabled() {
14841 log::warn!(
14842 "[text-glyph-run-diag] visible=true glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
14843 quad_run.len(),
14844 miss_cached_glyphs,
14845 miss_new_glyphs,
14846 collect_ms,
14847 instant_ms(prepare_start, Instant::now()),
14848 );
14849 }
14850 }
14851 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
14852 source_raster_rect,
14853 quad_run.as_ref(),
14854 source_draw.clip,
14855 viewport,
14856 root_scale,
14857 image_vertices,
14858 image_indices,
14859 false,
14860 ));
14861 }
14862 let index_count = image_indices.len() as u32 - index_start;
14863 if index_count > 0 {
14864 glyph_cmds.push(GlyphDrawCmd::shared(index_start, index_count, scissor));
14865 }
14866 }
14867
14868 self.scratch_text_glyph_run = collected_run;
14869 self.scratch_text_glyph_placements = collected_placements;
14870 self.scratch_text_glyph_quads = generated_quads;
14871 let append_end = Instant::now();
14872 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
14873 log::warn!(
14874 "[wgpu-render-stage:text-glyph-atlas] total_ms={total_ms:.2} visited={} cmds={} glyphs={} prewarmed={} run_hits={} run_misses={}",
14875 visited,
14876 glyph_cmds.len().saturating_sub(initial_cmd_len),
14877 emitted_glyphs,
14878 prewarmed_glyphs,
14879 run_hits,
14880 run_misses,
14881 );
14882 }
14883 Ok(true)
14884 }
14885
14886 #[cfg(not(target_arch = "wasm32"))]
14887 fn text_glyph_prewarm_decision(
14888 &self,
14889 text_draw: &TextDraw,
14890 viewport: ViewportUniformParams,
14891 root_scale: f32,
14892 ) -> TextGlyphPrewarmDecision {
14893 let Some((logical_rect, _, clip, _, static_text_motion)) =
14894 self.text_raster_geometry(text_draw, root_scale)
14895 else {
14896 return TextGlyphPrewarmDecision::MissingGeometry;
14897 };
14898 if !static_text_motion {
14899 return TextGlyphPrewarmDecision::DynamicMotion;
14900 }
14901 if text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
14902 return TextGlyphPrewarmDecision::Visible;
14903 }
14904 if text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale) {
14905 TextGlyphPrewarmDecision::Candidate
14906 } else {
14907 TextGlyphPrewarmDecision::OutsidePrewarmWindow
14908 }
14909 }
14910
14911 #[cfg(not(target_arch = "wasm32"))]
14912 #[allow(clippy::too_many_arguments)]
14913 fn prewarm_offscreen_text_glyph_draws_in_chunk(
14914 &mut self,
14915 ordered_items: &[(usize, SegmentDrawItem)],
14916 texts: &[TextDraw],
14917 chunk: &SegmentDrawChunkPlan,
14918 viewport: ViewportUniformParams,
14919 root_scale: f32,
14920 staged_uploads: &mut StagedBufferUploads,
14921 image_vertices: &mut Vec<Vertex>,
14922 image_indices: &mut Vec<u32>,
14923 glyph_cmds: &mut Vec<GlyphDrawCmd>,
14924 ) -> Result<(), String> {
14925 let prewarm_start = Instant::now();
14926 let diag_enabled = cranpose_core::env_flag!("CRANPOSE_TEXT_PREWARM_DIAG");
14927 let mut text_items = 0usize;
14928 let mut candidates = 0usize;
14929 let mut missing_geometry = 0usize;
14930 let mut dynamic_motion = 0usize;
14931 let mut visible = 0usize;
14932 let mut outside = 0usize;
14933 let mut already_prepared = 0usize;
14934 let mut admitted_candidates = 0usize;
14935 let mut skipped_unbounded = 0usize;
14936 let mut skipped_budget = 0usize;
14937 let initial_vertex_len = image_vertices.len();
14938 let initial_index_len = image_indices.len();
14939 let initial_cmd_len = glyph_cmds.len();
14940 let initial_staged_bytes_len = staged_uploads.bytes.len();
14941 let initial_staged_copies_len = staged_uploads.copies.len();
14942 'batches: for batch in chunk.iter() {
14943 let SegmentBatchPlan::Text { start, end } = batch else {
14944 continue;
14945 };
14946 for (_, item) in &ordered_items[start..end] {
14947 if offscreen_text_glyph_prewarm_budget_exhausted(prewarm_start, admitted_candidates)
14948 {
14949 skipped_budget = skipped_budget.saturating_add(1);
14950 break 'batches;
14951 }
14952 let SegmentDrawItem::Text(text_index) = item else {
14953 return Err(format!(
14954 "text prewarm batch contains non-text draw item: {item:?}"
14955 ));
14956 };
14957 let Some(text_draw) = texts.get(*text_index) else {
14958 continue;
14959 };
14960 text_items = text_items.saturating_add(1);
14961 match self.text_glyph_prewarm_decision(text_draw, viewport, root_scale) {
14962 TextGlyphPrewarmDecision::Candidate => {}
14963 TextGlyphPrewarmDecision::MissingGeometry => {
14964 missing_geometry = missing_geometry.saturating_add(1);
14965 continue;
14966 }
14967 TextGlyphPrewarmDecision::DynamicMotion => {
14968 dynamic_motion = dynamic_motion.saturating_add(1);
14969 continue;
14970 }
14971 TextGlyphPrewarmDecision::Visible => {
14972 visible = visible.saturating_add(1);
14973 continue;
14974 }
14975 TextGlyphPrewarmDecision::OutsidePrewarmWindow => {
14976 outside = outside.saturating_add(1);
14977 continue;
14978 }
14979 }
14980
14981 candidates = candidates.saturating_add(1);
14982 let Some((_, raster_rect, _, text_scale, static_text_motion)) =
14983 self.text_raster_geometry(text_draw, root_scale)
14984 else {
14985 missing_geometry = missing_geometry.saturating_add(1);
14986 continue;
14987 };
14988 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
14989 let source_draw = raster_source.draw.as_ref();
14990 let run_key = Self::text_glyph_run_cache_key(
14991 source_draw,
14992 raster_source.raster_rect,
14993 text_scale,
14994 static_text_motion,
14995 );
14996 let atlas_generation = self.text_glyph_atlas.generation();
14997 let cached_glyphs = if let Some(cached) = self.text_glyph_run_cache.peek(&run_key) {
14998 if cached.atlas_generation == atlas_generation && cached.quads.is_some() {
14999 already_prepared = already_prepared.saturating_add(1);
15000 continue;
15001 }
15002 Some(cached.glyphs.len())
15003 } else {
15004 None
15005 };
15006 if !offscreen_text_glyph_prewarm_work_is_bounded(
15007 cached_glyphs,
15008 source_draw.text.text.len(),
15009 ) {
15010 skipped_unbounded = skipped_unbounded.saturating_add(1);
15011 continue;
15012 }
15013 admitted_candidates = admitted_candidates.saturating_add(1);
15014 self.append_text_glyph_draws(
15015 std::iter::once(text_draw),
15016 viewport,
15017 root_scale,
15018 true,
15019 staged_uploads,
15020 image_vertices,
15021 image_indices,
15022 glyph_cmds,
15023 )?;
15024 image_vertices.truncate(initial_vertex_len);
15025 image_indices.truncate(initial_index_len);
15026 glyph_cmds.truncate(initial_cmd_len);
15027 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
15028 }
15029 }
15030
15031 if diag_enabled && text_items > 0 {
15032 log::warn!(
15033 "[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}"
15034 );
15035 }
15036 if admitted_candidates > 0 {
15037 if let Some(total_ms) = should_log_wgpu_render_stage(prewarm_start, Instant::now()) {
15038 log::warn!(
15039 "[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}"
15040 );
15041 }
15042 }
15043 Ok(())
15044 }
15045
15046 fn prepare_text_glyph_draw_cmds<'a, I>(
15047 &mut self,
15048 layer_texts: I,
15049 viewport: ViewportUniformParams,
15050 root_scale: f32,
15051 staged_uploads: &mut StagedBufferUploads,
15052 ) -> Result<Option<PreparedGlyphBatch>, String>
15053 where
15054 I: IntoIterator<Item = &'a TextDraw>,
15055 {
15056 #[cfg(target_arch = "wasm32")]
15057 let _ = staged_uploads;
15058
15059 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
15060 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
15061 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
15062 image_vertices.clear();
15063 image_indices.clear();
15064 glyph_cmds.clear();
15065
15066 if !self.append_text_glyph_draws(
15067 layer_texts,
15068 viewport,
15069 root_scale,
15070 false,
15071 staged_uploads,
15072 &mut image_vertices,
15073 &mut image_indices,
15074 &mut glyph_cmds,
15075 )? {
15076 self.scratch_image_vertices = image_vertices;
15077 self.scratch_image_indices = image_indices;
15078 self.scratch_glyph_cmds = glyph_cmds;
15079 return Ok(None);
15080 }
15081
15082 #[cfg(not(target_arch = "wasm32"))]
15083 if !image_indices.is_empty() {
15084 self.stage_native_image_buffers(
15085 staged_uploads,
15086 viewport,
15087 &image_vertices,
15088 &image_indices,
15089 );
15090 }
15091
15092 #[cfg(target_arch = "wasm32")]
15093 let image_slot = if glyph_cmds.is_empty() {
15094 0
15095 } else {
15096 let slot = self.claim_wasm_image_batch();
15097 {
15098 let buffers = &mut self.wasm_image_batches[slot];
15099 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
15100 }
15101 let buffers = &self.wasm_image_batches[slot];
15102 self.write_wasm_buffer(
15103 &buffers.vertex_buffer,
15104 bytemuck::cast_slice(&image_vertices),
15105 );
15106 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
15107 slot
15108 };
15109
15110 #[cfg(target_arch = "wasm32")]
15111 let uniform_slot = if glyph_cmds.is_empty() {
15112 0
15113 } else {
15114 self.prepare_wasm_viewport_uniforms(viewport)
15115 };
15116
15117 self.scratch_image_vertices = image_vertices;
15118 self.scratch_image_indices = image_indices;
15119 Ok(Some(PreparedGlyphBatch {
15120 cmds: glyph_cmds,
15121 #[cfg(target_arch = "wasm32")]
15122 image_slot,
15123 #[cfg(target_arch = "wasm32")]
15124 uniform_slot,
15125 }))
15126 }
15127
15128 #[allow(clippy::too_many_arguments)]
15129 fn append_image_bitmap_draw_cmd(
15130 &mut self,
15131 image: &ImageBitmap,
15132 rect: Rect,
15133 clip: Option<Rect>,
15134 sampling: ImageSampling,
15135 viewport: ViewportUniformParams,
15136 root_scale: f32,
15137 image_vertices: &mut Vec<Vertex>,
15138 image_indices: &mut Vec<u32>,
15139 image_cmds: &mut Vec<ImageDrawCmd>,
15140 ) -> Result<(), String> {
15141 if rect.width <= 0.0 || rect.height <= 0.0 {
15142 return Ok(());
15143 }
15144
15145 self.ensure_image_cached(image)?;
15146
15147 let (device_quad, scissor_rect) =
15148 if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
15149 let left_px = (rect.x * root_scale).round();
15150 let top_px = (rect.y * root_scale).round();
15151 let width_px = (rect.width * root_scale).round().max(1.0);
15152 let height_px = (rect.height * root_scale).round().max(1.0);
15153 let snapped_rect = Rect {
15154 x: left_px / root_scale,
15155 y: top_px / root_scale,
15156 width: width_px / root_scale,
15157 height: height_px / root_scale,
15158 };
15159 let right_px = left_px + width_px;
15160 let bottom_px = top_px + height_px;
15161 (
15162 [
15163 [left_px, top_px],
15164 [right_px, top_px],
15165 [left_px, bottom_px],
15166 [right_px, bottom_px],
15167 ],
15168 snapped_rect,
15169 )
15170 } else {
15171 (
15172 rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
15173 rect,
15174 )
15175 };
15176
15177 let Some(scissor) = scissor_rect_for_layer(
15178 scissor_rect,
15179 clip,
15180 root_scale,
15181 viewport.width,
15182 viewport.height,
15183 ) else {
15184 return Ok(());
15185 };
15186 let Some(uv_rect) = image_uv_rect(image, None) else {
15187 return Ok(());
15188 };
15189 #[cfg(not(target_arch = "wasm32"))]
15190 {
15191 if fill_area_diag_enabled() {
15192 self.fill_area_diag.add_image_quad(&device_quad);
15193 }
15194 }
15195
15196 let base_vertex = image_vertices.len() as u32;
15197 let index_start = image_indices.len() as u32;
15198 image_indices.extend_from_slice(&[
15199 base_vertex,
15200 base_vertex + 1,
15201 base_vertex + 2,
15202 base_vertex + 2,
15203 base_vertex + 1,
15204 base_vertex + 3,
15205 ]);
15206 let color = [1.0, 1.0, 1.0, 1.0];
15207 image_vertices.extend_from_slice(&[
15208 Vertex {
15209 position: device_quad[0],
15210 color,
15211 uv: [uv_rect.min[0], uv_rect.min[1]],
15212 uv_bounds: uv_rect.sample_bounds,
15213 },
15214 Vertex {
15215 position: device_quad[1],
15216 color,
15217 uv: [uv_rect.max[0], uv_rect.min[1]],
15218 uv_bounds: uv_rect.sample_bounds,
15219 },
15220 Vertex {
15221 position: device_quad[2],
15222 color,
15223 uv: [uv_rect.min[0], uv_rect.max[1]],
15224 uv_bounds: uv_rect.sample_bounds,
15225 },
15226 Vertex {
15227 position: device_quad[3],
15228 color,
15229 uv: [uv_rect.max[0], uv_rect.max[1]],
15230 uv_bounds: uv_rect.sample_bounds,
15231 },
15232 ]);
15233 image_cmds.push(ImageDrawCmd {
15234 index_start,
15235 scissor,
15236 image_id: image.id(),
15237 sampling,
15238 });
15239 Ok(())
15240 }
15241
15242 #[allow(clippy::too_many_arguments)]
15243 fn append_text_image_draw_cmds<'a, I>(
15244 &mut self,
15245 layer_texts: I,
15246 viewport: ViewportUniformParams,
15247 root_scale: f32,
15248 image_vertices: &mut Vec<Vertex>,
15249 image_indices: &mut Vec<u32>,
15250 image_cmds: &mut Vec<ImageDrawCmd>,
15251 ) -> Result<(), String>
15252 where
15253 I: Iterator<Item = &'a TextDraw>,
15254 {
15255 let append_start = Instant::now();
15256 let initial_len = image_cmds.len();
15257 let mut visited = 0usize;
15258 let mut hit_count = 0usize;
15259 let mut miss_count = 0usize;
15260 for text_draw in layer_texts {
15261 visited = visited.saturating_add(1);
15262 let _ = text_draw.node_id;
15263 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
15264 self.text_raster_geometry(text_draw, root_scale)
15265 else {
15266 continue;
15267 };
15268 if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
15269 continue;
15270 }
15271
15272 let raster_source = self.text_image_raster_source(
15273 text_draw,
15274 logical_rect,
15275 raster_rect,
15276 clip,
15277 root_scale,
15278 static_text_motion,
15279 );
15280 let source_draw = raster_source.draw.as_ref();
15281 let source_raster_rect = raster_source.raster_rect;
15282
15283 let cache_key = Self::text_image_cache_key(
15284 source_draw,
15285 source_raster_rect,
15286 text_scale,
15287 static_text_motion,
15288 );
15289 let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
15290 self.frame_stats
15291 .record_text_image_cache_hit(cached.image.width(), cached.image.height());
15292 hit_count = hit_count.saturating_add(1);
15293 cached.image.clone()
15294 } else {
15295 let Some(image) =
15296 self.rasterize_text_draw_to_image(source_draw, source_raster_rect, text_scale)
15297 else {
15298 continue;
15299 };
15300 self.frame_stats
15301 .record_text_image_cache_miss(image.width(), image.height());
15302 miss_count = miss_count.saturating_add(1);
15303 self.text_image_cache.put(
15304 cache_key,
15305 CachedTextImage {
15306 image: image.clone(),
15307 },
15308 );
15309 image
15310 };
15311
15312 let draw_origin = if static_text_motion {
15313 Point::new(
15314 source_raster_rect.x / root_scale,
15315 source_raster_rect.y / root_scale,
15316 )
15317 } else {
15318 Point::new(logical_rect.x, logical_rect.y)
15319 };
15320 let draw_rect = Rect {
15321 x: draw_origin.x,
15322 y: draw_origin.y,
15323 width: image.width() as f32 / root_scale,
15324 height: image.height() as f32 / root_scale,
15325 };
15326 self.append_image_bitmap_draw_cmd(
15327 &image,
15328 draw_rect,
15329 clip,
15330 ImageSampling::Nearest,
15331 viewport,
15332 root_scale,
15333 image_vertices,
15334 image_indices,
15335 image_cmds,
15336 )?;
15337 }
15338 let append_end = Instant::now();
15339 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
15340 log::warn!(
15341 "[wgpu-render-stage:text-images] total_ms={total_ms:.2} visited={} emitted={} hits={} misses={}",
15342 visited,
15343 image_cmds.len().saturating_sub(initial_len),
15344 hit_count,
15345 miss_count,
15346 );
15347 }
15348 Ok(())
15349 }
15350
15351 fn text_image_raster_source<'a>(
15352 &mut self,
15353 text_draw: &'a TextDraw,
15354 logical_rect: Rect,
15355 raster_rect: Rect,
15356 clip: Option<Rect>,
15357 root_scale: f32,
15358 static_text_motion: bool,
15359 ) -> TextRasterSource<'a> {
15360 let Some(clip) = clip else {
15361 return TextRasterSource {
15362 draw: Cow::Borrowed(text_draw),
15363 raster_rect,
15364 };
15365 };
15366 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
15367 return TextRasterSource {
15368 draw: Cow::Borrowed(text_draw),
15369 raster_rect,
15370 };
15371 }
15372
15373 let line_starts = self.text_line_index_cache.line_starts(&text_draw.text);
15374 clipped_text_raster_source_with_line_starts(
15375 text_draw,
15376 logical_rect,
15377 raster_rect,
15378 clip,
15379 root_scale,
15380 line_starts.as_ref(),
15381 )
15382 }
15383
15384 fn prepare_text_image_draw_cmds<'a, I>(
15385 &mut self,
15386 layer_texts: I,
15387 viewport: ViewportUniformParams,
15388 root_scale: f32,
15389 staged_uploads: &mut StagedBufferUploads,
15390 ) -> Result<PreparedImageBatch, String>
15391 where
15392 I: Iterator<Item = &'a TextDraw>,
15393 {
15394 #[cfg(target_arch = "wasm32")]
15395 let _ = staged_uploads;
15396
15397 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
15398 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
15399 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
15400 image_vertices.clear();
15401 image_indices.clear();
15402 image_cmds.clear();
15403
15404 self.append_text_image_draw_cmds(
15405 layer_texts,
15406 viewport,
15407 root_scale,
15408 &mut image_vertices,
15409 &mut image_indices,
15410 &mut image_cmds,
15411 )?;
15412
15413 #[cfg(not(target_arch = "wasm32"))]
15414 if !image_cmds.is_empty() {
15415 self.stage_native_image_buffers(
15416 staged_uploads,
15417 viewport,
15418 &image_vertices,
15419 &image_indices,
15420 );
15421 }
15422
15423 #[cfg(target_arch = "wasm32")]
15424 let image_slot = if image_cmds.is_empty() {
15425 0
15426 } else {
15427 let slot = self.claim_wasm_image_batch();
15428 {
15429 let buffers = &mut self.wasm_image_batches[slot];
15430 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
15431 }
15432 let buffers = &self.wasm_image_batches[slot];
15433 self.write_wasm_buffer(
15434 &buffers.vertex_buffer,
15435 bytemuck::cast_slice(&image_vertices),
15436 );
15437 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
15438 slot
15439 };
15440
15441 #[cfg(target_arch = "wasm32")]
15442 let uniform_slot = if image_cmds.is_empty() {
15443 0
15444 } else {
15445 self.prepare_wasm_viewport_uniforms(viewport)
15446 };
15447
15448 self.scratch_image_vertices = image_vertices;
15449 self.scratch_image_indices = image_indices;
15450 Ok(PreparedImageBatch {
15451 cmds: image_cmds,
15452 #[cfg(target_arch = "wasm32")]
15453 image_slot,
15454 #[cfg(target_arch = "wasm32")]
15455 uniform_slot,
15456 })
15457 }
15458
15459 fn text_raster_geometry(
15460 &self,
15461 text_draw: &TextDraw,
15462 root_scale: f32,
15463 ) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
15464 text_raster_geometry_for_draw(text_draw, root_scale)
15465 }
15466
15467 fn text_image_cache_key(
15468 text_draw: &TextDraw,
15469 raster_rect: Rect,
15470 text_scale: f32,
15471 static_text_motion: bool,
15472 ) -> TextImageCacheKey {
15473 let mut state = default_hash::new();
15474 text_draw.text.render_hash().hash(&mut state);
15475 text_draw.text_style.render_hash().hash(&mut state);
15476 text_draw.color.render_hash().hash(&mut state);
15477 hash_text_raster_geometry_for_cache(raster_rect, static_text_motion, &mut state);
15478 text_draw.font_size.to_bits().hash(&mut state);
15479 text_scale.to_bits().hash(&mut state);
15480 text_draw.layout_options.hash(&mut state);
15481 TextImageCacheKey(state.finish())
15482 }
15483
15484 fn text_glyph_run_cache_key(
15485 text_draw: &TextDraw,
15486 raster_rect: Rect,
15487 text_scale: f32,
15488 static_text_motion: bool,
15489 ) -> TextGlyphRunCacheKey {
15490 TextGlyphRunCacheKey(
15491 Self::text_image_cache_key(text_draw, raster_rect, text_scale, static_text_motion).0,
15492 )
15493 }
15494
15495 fn rasterize_text_draw_to_image(
15496 &mut self,
15497 text_draw: &TextDraw,
15498 raster_rect: Rect,
15499 text_scale: f32,
15500 ) -> Option<ImageBitmap> {
15501 if text_draw.text.span_styles.is_empty() {
15502 let font = self.text_fonts.resolve(&text_draw.text_style)?;
15503 return rasterize_text_to_image_with_glyph_cache(
15504 text_draw.text.text.as_str(),
15505 raster_rect,
15506 &text_draw.text_style,
15507 text_draw.color,
15508 text_draw.font_size,
15509 text_scale,
15510 font,
15511 &mut self.text_glyph_mask_cache,
15512 );
15513 }
15514
15515 if let Some(image) = rasterize_annotated_text_to_image_with_glyph_cache(
15516 text_draw.text.as_ref(),
15517 raster_rect,
15518 &text_draw.text_style,
15519 text_draw.color,
15520 text_draw.font_size,
15521 text_scale,
15522 &self.text_fonts,
15523 &mut self.text_glyph_mask_cache,
15524 ) {
15525 return Some(image);
15526 }
15527
15528 rasterize_spanned_text_to_image(
15529 text_draw,
15530 raster_rect,
15531 text_scale,
15532 &self.text_fonts,
15533 &mut self.text_glyph_mask_cache,
15534 )
15535 }
15536}
15537
15538fn rasterize_spanned_text_to_image(
15539 text_draw: &TextDraw,
15540 raster_rect: Rect,
15541 text_scale: f32,
15542 fonts: &SoftwareTextFontSet,
15543 glyph_cache: &mut SoftwareGlyphRasterCache,
15544) -> Option<ImageBitmap> {
15545 let width = raster_rect.width.ceil().max(1.0) as u32;
15546 let height = raster_rect.height.ceil().max(1.0) as u32;
15547 let mut canvas = vec![0_u8; (width as usize) * (height as usize) * 4];
15548 let boundaries = text_draw.text.span_boundaries();
15549 let base_line_height = text_draw
15550 .text_style
15551 .resolve_line_height(14.0, text_draw.font_size)
15552 .max(1.0);
15553 let mut current_line_height = base_line_height;
15554 let mut cursor_x = raster_rect.x;
15555 let mut cursor_y = raster_rect.y;
15556
15557 for window in boundaries.windows(2) {
15558 let start = window[0];
15559 let end = window[1];
15560 if start == end {
15561 continue;
15562 }
15563
15564 let chunk = &text_draw.text.text[start..end];
15565 let mut merged_span = text_draw.text_style.span_style.clone();
15566 for span in &text_draw.text.span_styles {
15567 if span.range.start <= start && span.range.end >= end {
15568 merged_span = merged_span.merge(&span.item);
15569 }
15570 }
15571
15572 let mut chunk_style = text_draw.text_style.clone();
15573 chunk_style.span_style = merged_span;
15574
15575 for part in chunk.split_inclusive('\n') {
15576 let has_newline = part.ends_with('\n');
15577 let content = if has_newline {
15578 &part[..part.len().saturating_sub(1)]
15579 } else {
15580 part
15581 };
15582
15583 if !content.is_empty() {
15584 let chunk_font_size = chunk_style.resolve_font_size(text_draw.font_size);
15585 let Some(font) = fonts.resolve(&chunk_style) else {
15586 continue;
15587 };
15588 let metrics = measure_text_with_font(content, &chunk_style, chunk_font_size, font);
15589 let segment_rect = Rect {
15590 x: cursor_x,
15591 y: cursor_y,
15592 width: (metrics.width * text_scale).ceil().max(1.0),
15593 height: (metrics.height * text_scale).ceil().max(1.0),
15594 };
15595 if let Some(segment_image) = rasterize_text_to_image_with_glyph_cache(
15596 content,
15597 segment_rect,
15598 &chunk_style,
15599 chunk_style.resolve_text_color(text_draw.color),
15600 chunk_font_size,
15601 text_scale,
15602 font,
15603 glyph_cache,
15604 ) {
15605 composite_text_segment(
15606 &mut canvas,
15607 width,
15608 height,
15609 raster_rect,
15610 segment_rect,
15611 &segment_image,
15612 );
15613 }
15614 cursor_x += metrics.width * text_scale;
15615 current_line_height = current_line_height.max(metrics.line_height.max(1.0));
15616 }
15617
15618 if has_newline {
15619 cursor_x = raster_rect.x;
15620 cursor_y += current_line_height * text_scale;
15621 current_line_height = base_line_height;
15622 }
15623 }
15624 }
15625
15626 ImageBitmap::from_rgba8(width, height, canvas).ok()
15627}
15628
15629struct TextRasterSource<'a> {
15630 draw: Cow<'a, TextDraw>,
15631 raster_rect: Rect,
15632}
15633
15634fn text_glyph_raster_source(text_draw: &TextDraw, raster_rect: Rect) -> TextRasterSource<'_> {
15635 TextRasterSource {
15636 draw: Cow::Borrowed(text_draw),
15637 raster_rect,
15638 }
15639}
15640
15641#[cfg(test)]
15642fn clipped_text_raster_source<'a>(
15643 text_draw: &'a TextDraw,
15644 logical_rect: Rect,
15645 raster_rect: Rect,
15646 clip: Option<Rect>,
15647 root_scale: f32,
15648 static_text_motion: bool,
15649) -> TextRasterSource<'a> {
15650 let Some(clip) = clip else {
15651 return TextRasterSource {
15652 draw: Cow::Borrowed(text_draw),
15653 raster_rect,
15654 };
15655 };
15656 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
15657 return TextRasterSource {
15658 draw: Cow::Borrowed(text_draw),
15659 raster_rect,
15660 };
15661 }
15662 let line_starts = line_start_offsets(text_draw.text.text.as_str());
15663 clipped_text_raster_source_with_line_starts(
15664 text_draw,
15665 logical_rect,
15666 raster_rect,
15667 clip,
15668 root_scale,
15669 &line_starts,
15670 )
15671}
15672
15673fn clipped_text_raster_source_with_line_starts<'a>(
15674 text_draw: &'a TextDraw,
15675 logical_rect: Rect,
15676 raster_rect: Rect,
15677 clip: Rect,
15678 root_scale: f32,
15679 line_starts: &[usize],
15680) -> TextRasterSource<'a> {
15681 if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
15682 return TextRasterSource {
15683 draw: Cow::Borrowed(text_draw),
15684 raster_rect,
15685 };
15686 }
15687
15688 let Some(visible_rect) = logical_rect.intersect(clip) else {
15689 return TextRasterSource {
15690 draw: Cow::Borrowed(text_draw),
15691 raster_rect,
15692 };
15693 };
15694
15695 let line_count = line_starts.len().max(1);
15696 let line_height = logical_rect.height / line_count as f32;
15697 if !line_height.is_finite() || line_height <= 0.0 {
15698 return TextRasterSource {
15699 draw: Cow::Borrowed(text_draw),
15700 raster_rect,
15701 };
15702 }
15703
15704 let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
15705 let visible_bottom =
15706 ((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
15707 let start_line = visible_top.saturating_sub(1).max(0) as usize;
15708 let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
15709 let end_line = end_line.min(line_count);
15710 if start_line == 0 && end_line >= line_count {
15711 return TextRasterSource {
15712 draw: Cow::Borrowed(text_draw),
15713 raster_rect,
15714 };
15715 }
15716
15717 let byte_start = line_starts[start_line];
15718 let byte_end = line_end_offset(text_draw.text.text.as_str(), line_starts, end_line - 1);
15719 if byte_start >= byte_end {
15720 return TextRasterSource {
15721 draw: Cow::Borrowed(text_draw),
15722 raster_rect,
15723 };
15724 }
15725
15726 let slice_y = logical_rect.y + start_line as f32 * line_height;
15727 let slice_height = (end_line - start_line) as f32 * line_height;
15728 let mut slice_raster_rect = Rect {
15729 x: logical_rect.x * root_scale,
15730 y: slice_y * root_scale,
15731 width: logical_rect.width * root_scale,
15732 height: slice_height * root_scale,
15733 };
15734 slice_raster_rect.x = slice_raster_rect.x.round();
15735 slice_raster_rect.y = slice_raster_rect.y.round();
15736 slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
15737 slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
15738
15739 let mut sliced_draw = text_draw.clone();
15740 sliced_draw.rect = Rect {
15741 x: logical_rect.x,
15742 y: slice_y,
15743 width: logical_rect.width,
15744 height: slice_height,
15745 };
15746 sliced_draw.text = Arc::new(text_draw.text.subsequence(byte_start..byte_end));
15747
15748 TextRasterSource {
15749 draw: Cow::Owned(sliced_draw),
15750 raster_rect: slice_raster_rect,
15751 }
15752}
15753
15754fn line_start_offsets(text: &str) -> Vec<usize> {
15755 let mut starts =
15756 Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
15757 starts.push(0);
15758 starts.extend(
15759 text.char_indices()
15760 .filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
15761 );
15762 starts
15763}
15764
15765fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
15766 line_starts.get(line + 1).copied().unwrap_or(text.len())
15767}
15768
15769fn composite_text_segment(
15770 canvas: &mut [u8],
15771 canvas_width: u32,
15772 canvas_height: u32,
15773 canvas_rect: Rect,
15774 segment_rect: Rect,
15775 segment_image: &ImageBitmap,
15776) {
15777 let offset_x = (segment_rect.x - canvas_rect.x).round() as i32;
15778 let offset_y = (segment_rect.y - canvas_rect.y).round() as i32;
15779 let src = segment_image.pixels();
15780 for sy in 0..segment_image.height() as i32 {
15781 let dy = offset_y + sy;
15782 if dy < 0 || dy >= canvas_height as i32 {
15783 continue;
15784 }
15785 for sx in 0..segment_image.width() as i32 {
15786 let dx = offset_x + sx;
15787 if dx < 0 || dx >= canvas_width as i32 {
15788 continue;
15789 }
15790 let src_index = ((sy as u32 * segment_image.width() + sx as u32) * 4) as usize;
15791 let dst_index = ((dy as u32 * canvas_width + dx as u32) * 4) as usize;
15792 blend_rgba_pixel(
15793 &mut canvas[dst_index..dst_index + 4],
15794 &src[src_index..src_index + 4],
15795 );
15796 }
15797 }
15798}
15799
15800fn blend_rgba_pixel(dst: &mut [u8], src: &[u8]) {
15801 let src_alpha = src[3] as f32 / 255.0;
15802 if src_alpha <= 0.0 {
15803 return;
15804 }
15805 let dst_alpha = dst[3] as f32 / 255.0;
15806 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
15807 if out_alpha <= f32::EPSILON {
15808 dst.copy_from_slice(&[0, 0, 0, 0]);
15809 return;
15810 }
15811
15812 for channel in 0..3 {
15813 let src_channel = src[channel] as f32 / 255.0;
15814 let dst_channel = dst[channel] as f32 / 255.0;
15815 let src_premult = src_channel * src_alpha;
15816 let dst_premult = dst_channel * dst_alpha;
15817 dst[channel] =
15818 (((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0) * 255.0)
15819 .round() as u8;
15820 }
15821 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
15822}
15823
15824fn align_to(value: u32, alignment: u32) -> u32 {
15825 debug_assert!(alignment > 0);
15826 value.div_ceil(alignment) * alignment
15827}
15828
15829#[cfg(not(target_arch = "wasm32"))]
15830fn align_usize_to(value: usize, alignment: usize) -> usize {
15831 debug_assert!(alignment > 0);
15832 value.div_ceil(alignment) * alignment
15833}
15834
15835impl GpuRenderer {
15836 fn convert_surface_pixels_to_rgba(&self, pixels: &mut [u8]) -> Result<(), String> {
15837 match self.surface_format {
15838 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => Ok(()),
15839 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
15840 for pixel in pixels.as_chunks_mut::<4>().0 {
15841 pixel.swap(0, 2);
15842 }
15843 Ok(())
15844 }
15845 format => Err(format!(
15846 "Screenshot readback unsupported for texture format: {format:?}"
15847 )),
15848 }
15849 }
15850}
15851
15852fn is_in_effect_range(z_index: usize, effect_z_ranges: &[Range<usize>]) -> bool {
15853 effect_z_ranges.iter().any(|range| range.contains(&z_index))
15854}
15855
15856#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15857enum SegmentDrawItem {
15858 Shape(usize),
15859 Image(usize),
15860 Text(usize),
15861 Shadow(usize),
15862 Composite(usize),
15863 ShaderComposite(usize),
15864 Retained(usize),
15865}
15866
15867#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15868enum SegmentBatchPlan {
15869 Shape {
15870 start: usize,
15871 end: usize,
15872 blend_mode: BlendMode,
15873 },
15874 Image {
15875 start: usize,
15876 end: usize,
15877 blend_mode: BlendMode,
15878 },
15879 Text {
15880 start: usize,
15881 end: usize,
15882 },
15883 Composite {
15884 start: usize,
15885 end: usize,
15886 },
15887 ShaderComposite {
15888 start: usize,
15889 end: usize,
15890 },
15891 Retained {
15894 start: usize,
15895 end: usize,
15896 },
15897}
15898
15899#[derive(Clone, Debug, Default, PartialEq, Eq)]
15900struct SegmentDrawChunkPlan {
15901 batches: Vec<SegmentBatchPlan>,
15902}
15903
15904struct SegmentRenderOutcome {
15905 rendered_any: bool,
15906 pass_count: u32,
15907}
15908
15909struct SegmentCommandEncodeOutcome {
15910 first_batch: bool,
15911}
15912
15913#[cfg(not(target_arch = "wasm32"))]
15914#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15915enum TextGlyphPrewarmDecision {
15916 Candidate,
15917 MissingGeometry,
15918 DynamicMotion,
15919 Visible,
15920 OutsidePrewarmWindow,
15921}
15922
15923#[cfg(not(target_arch = "wasm32"))]
15924#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15925struct NativeSegmentFusionBudget {
15926 shape_count: usize,
15927 gradient_stop_count: usize,
15928}
15929
15930#[cfg(not(target_arch = "wasm32"))]
15931#[derive(Clone, Debug, PartialEq, Eq)]
15932struct NativeSegmentFusionPartition {
15933 chunk: SegmentDrawChunkPlan,
15934 budget: NativeSegmentFusionBudget,
15935}
15936
15937#[cfg(not(target_arch = "wasm32"))]
15938#[derive(Clone, Debug, PartialEq, Eq)]
15939enum FusedSegmentBatch {
15940 Shape {
15941 batch: PreparedShapeBatch,
15942 blend_mode: BlendMode,
15943 },
15944 Image {
15945 cmd_range: Range<usize>,
15946 blend_mode: BlendMode,
15947 },
15948 Text {
15949 image_cmd_range: Range<usize>,
15950 glyph_cmd_range: Range<usize>,
15951 },
15952 Composite {
15953 draw_range: Range<usize>,
15954 },
15955 ShaderComposite {
15956 draw_range: Range<usize>,
15957 },
15958 Retained {
15959 item_range: Range<usize>,
15960 },
15961}
15962
15963struct ShadowSourceRenderOutcome {
15964 rendered_any: bool,
15965 pass_count: u32,
15966}
15967
15968#[cfg(not(target_arch = "wasm32"))]
15972struct SegmentCaptureJob {
15973 key: SegmentSurfaceKey,
15974 first: u32,
15975 last: u32,
15976 capture_index: u32,
15977}
15978
15979#[cfg(not(target_arch = "wasm32"))]
15983struct SegmentCompositePlan {
15984 key: SegmentSurfaceKey,
15985 dest_quad: [[f32; 2]; 4],
15986 inverse: [[f32; 3]; 3],
15987 identity: bool,
15988}
15989
15990#[cfg(not(target_arch = "wasm32"))]
15993fn segment_identity_quad(rect: &CaptureRect) -> [[f32; 2]; 4] {
15994 let [x, y] = rect.origin;
15995 let width = rect.width as f32;
15996 let height = rect.height as f32;
15997 [
15998 [x, y],
15999 [x + width, y],
16000 [x, y + height],
16001 [x + width, y + height],
16002 ]
16003}
16004
16005#[cfg(not(target_arch = "wasm32"))]
16008fn segment_identity_inverse(rect: &CaptureRect) -> [[f32; 3]; 3] {
16009 [
16010 [1.0, 0.0, -rect.origin[0]],
16011 [0.0, 1.0, -rect.origin[1]],
16012 [0.0, 0.0, 1.0],
16013 ]
16014}
16015
16016#[cfg(not(target_arch = "wasm32"))]
16021fn plan_segment_capture_geometry(
16022 slot: &ReplaySlot,
16023 first: u32,
16024 last: u32,
16025 transform: SimilarityTransform,
16026 max_texture_dim: u32,
16027) -> Option<(CaptureRect, f32)> {
16028 let range = first as usize..last as usize;
16029 let aabbs = slot.shape_aabbs.get(range)?;
16030 if aabbs.is_empty() {
16031 return None;
16032 }
16033 let affine = Affine2::from_similarity(transform.center, transform.rot, transform.scale);
16034 let mut min = [f32::INFINITY; 2];
16035 let mut max = [f32::NEG_INFINITY; 2];
16036 for aabb in aabbs {
16037 for corner in [
16038 [aabb[0], aabb[1]],
16039 [aabb[2], aabb[1]],
16040 [aabb[0], aabb[3]],
16041 [aabb[2], aabb[3]],
16042 ] {
16043 let p = affine.apply(corner);
16044 min[0] = min[0].min(p[0]);
16045 min[1] = min[1].min(p[1]);
16046 max[0] = max[0].max(p[0]);
16047 max[1] = max[1].max(p[1]);
16048 }
16049 }
16050 let rect = crate::segment_surface::snap_capture_rect(min, max, max_texture_dim)?;
16051 let base_area = slot.area_prefix.get(last as usize).copied()?
16052 - slot.area_prefix.get(first as usize).copied()?;
16053 let member_px = base_area * transform.scale * transform.scale * slot.submitted_area_scale;
16054 Some((rect, member_px))
16055}
16056
16057impl SegmentDrawChunkPlan {
16058 fn is_empty(&self) -> bool {
16059 self.batches.is_empty()
16060 }
16061
16062 fn push(&mut self, batch: SegmentBatchPlan) {
16063 self.batches.push(batch);
16064 }
16065
16066 fn iter(&self) -> impl Iterator<Item = SegmentBatchPlan> + '_ {
16067 self.batches.iter().copied()
16068 }
16069}
16070
16071#[derive(Clone, Debug, PartialEq, Eq)]
16072enum SegmentRenderCommand {
16073 DrawChunk(SegmentDrawChunkPlan),
16074 Shadow(usize),
16075}
16076
16077struct SegmentCommandIter<'a> {
16078 ordered_items: &'a [(usize, SegmentDrawItem)],
16079 shapes: &'a [DrawShape],
16080 images: &'a [ImageDraw],
16081 cursor: usize,
16082 batch_limits: ShapeBatchLimits,
16083}
16084
16085impl<'a> SegmentCommandIter<'a> {
16086 fn new(
16087 ordered_items: &'a [(usize, SegmentDrawItem)],
16088 shapes: &'a [DrawShape],
16089 images: &'a [ImageDraw],
16090 batch_limits: ShapeBatchLimits,
16091 ) -> Self {
16092 Self {
16093 ordered_items,
16094 shapes,
16095 images,
16096 cursor: 0,
16097 batch_limits,
16098 }
16099 }
16100}
16101
16102impl Iterator for SegmentCommandIter<'_> {
16103 type Item = SegmentRenderCommand;
16104
16105 fn next(&mut self) -> Option<Self::Item> {
16106 if self.cursor >= self.ordered_items.len() {
16107 return None;
16108 }
16109
16110 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
16111 self.cursor += 1;
16112 return Some(SegmentRenderCommand::Shadow(index));
16113 }
16114
16115 let mut chunk = SegmentDrawChunkPlan::default();
16116 while self.cursor < self.ordered_items.len() {
16117 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
16118 if chunk.is_empty() {
16119 self.cursor += 1;
16120 return Some(SegmentRenderCommand::Shadow(index));
16121 }
16122 break;
16123 }
16124
16125 let Some((batch, next_cursor)) = segment_batch_plan_at_cursor(
16126 self.ordered_items,
16127 self.shapes,
16128 self.images,
16129 self.cursor,
16130 self.batch_limits,
16131 ) else {
16132 break;
16133 };
16134 chunk.push(batch);
16135 self.cursor = next_cursor;
16136 }
16137
16138 Some(SegmentRenderCommand::DrawChunk(chunk))
16139 }
16140}
16141
16142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16143struct PreparedShapeBatch {
16144 vertex_start: u32,
16147 vertex_count: u32,
16148 has_gradient: bool,
16151 #[cfg(target_arch = "wasm32")]
16152 shape_slot: usize,
16153 #[cfg(target_arch = "wasm32")]
16154 uniform_slot: usize,
16155}
16156
16157struct PreparedImageBatch {
16158 cmds: Vec<ImageDrawCmd>,
16159 #[cfg(target_arch = "wasm32")]
16160 image_slot: usize,
16161 #[cfg(target_arch = "wasm32")]
16162 uniform_slot: usize,
16163}
16164
16165impl PreparedImageBatch {
16166 fn is_empty(&self) -> bool {
16167 self.cmds.is_empty()
16168 }
16169
16170 fn into_cmds(self) -> Vec<ImageDrawCmd> {
16171 self.cmds
16172 }
16173}
16174
16175struct PreparedGlyphBatch {
16176 cmds: Vec<GlyphDrawCmd>,
16177 #[cfg(target_arch = "wasm32")]
16178 image_slot: usize,
16179 #[cfg(target_arch = "wasm32")]
16180 uniform_slot: usize,
16181}
16182
16183impl PreparedGlyphBatch {
16184 fn is_empty(&self) -> bool {
16185 self.cmds.is_empty()
16186 }
16187
16188 fn into_cmds(self) -> Vec<GlyphDrawCmd> {
16189 self.cmds
16190 }
16191}
16192
16193#[cfg(not(target_arch = "wasm32"))]
16194fn gradient_stop_count_for_shape(shape: &DrawShape, brushes: &[Brush]) -> usize {
16195 match shape.brush {
16196 SceneBrush::Solid(_) => 0,
16197 SceneBrush::Gradient(index) => match &brushes[index as usize] {
16198 Brush::Solid(_) => 0,
16199 Brush::LinearGradient { colors, .. }
16200 | Brush::RadialGradient { colors, .. }
16201 | Brush::SweepGradient { colors, .. } => colors.len(),
16202 },
16203 }
16204}
16205
16206#[cfg(not(target_arch = "wasm32"))]
16207fn native_segment_fusion_budget(
16208 ordered_items: &[(usize, SegmentDrawItem)],
16209 shapes: &[DrawShape],
16210 brushes: &[Brush],
16211 chunk: &SegmentDrawChunkPlan,
16212 batch_limits: ShapeBatchLimits,
16213) -> Result<Option<NativeSegmentFusionBudget>, String> {
16214 let mut shape_count = 0usize;
16215 let mut gradient_stop_count = 0usize;
16216
16217 for batch in chunk.iter() {
16218 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
16219 continue;
16220 };
16221 for (_, item) in &ordered_items[start..end] {
16222 let SegmentDrawItem::Shape(shape_index) = item else {
16223 return Err(format!(
16224 "shape batch contains non-shape draw item: {item:?}"
16225 ));
16226 };
16227 let shape = &shapes[*shape_index];
16228 shape_count = shape_count.saturating_add(1);
16229 gradient_stop_count =
16230 gradient_stop_count.saturating_add(gradient_stop_count_for_shape(shape, brushes));
16231 }
16232 }
16233
16234 if shape_count > batch_limits.max_shapes_per_batch
16235 || gradient_stop_count > batch_limits.max_gradient_stops
16236 {
16237 return Ok(None);
16238 }
16239
16240 Ok(Some(NativeSegmentFusionBudget {
16241 shape_count,
16242 gradient_stop_count,
16243 }))
16244}
16245
16246#[cfg(not(target_arch = "wasm32"))]
16247fn push_native_segment_fusion_partition(
16248 partitions: &mut Vec<NativeSegmentFusionPartition>,
16249 current: &mut SegmentDrawChunkPlan,
16250 current_budget: &mut NativeSegmentFusionBudget,
16251) {
16252 if current.is_empty() {
16253 return;
16254 }
16255
16256 partitions.push(NativeSegmentFusionPartition {
16257 chunk: std::mem::take(current),
16258 budget: *current_budget,
16259 });
16260 *current_budget = NativeSegmentFusionBudget {
16261 shape_count: 0,
16262 gradient_stop_count: 0,
16263 };
16264}
16265
16266#[cfg(not(target_arch = "wasm32"))]
16267fn native_segment_fusion_partitions(
16268 ordered_items: &[(usize, SegmentDrawItem)],
16269 shapes: &[DrawShape],
16270 brushes: &[Brush],
16271 chunk: &SegmentDrawChunkPlan,
16272 batch_limits: ShapeBatchLimits,
16273) -> Result<Option<Vec<NativeSegmentFusionPartition>>, String> {
16274 if let Some(budget) =
16275 native_segment_fusion_budget(ordered_items, shapes, brushes, chunk, batch_limits)?
16276 {
16277 return Ok(Some(vec![NativeSegmentFusionPartition {
16278 chunk: chunk.clone(),
16279 budget,
16280 }]));
16281 }
16282
16283 let mut partitions = Vec::new();
16284 let mut current = SegmentDrawChunkPlan::default();
16285 let mut current_budget = NativeSegmentFusionBudget {
16286 shape_count: 0,
16287 gradient_stop_count: 0,
16288 };
16289
16290 for batch in chunk.iter() {
16291 let SegmentBatchPlan::Shape {
16292 start,
16293 end,
16294 blend_mode,
16295 } = batch
16296 else {
16297 current.push(batch);
16298 continue;
16299 };
16300
16301 let mut run_start = start;
16302 for (item_cursor, (_, item)) in ordered_items.iter().enumerate().take(end).skip(start) {
16303 let SegmentDrawItem::Shape(shape_index) = *item else {
16304 return Err(format!(
16305 "shape batch contains non-shape draw item: {:?}",
16306 item
16307 ));
16308 };
16309 let gradient_stop_count = gradient_stop_count_for_shape(&shapes[shape_index], brushes);
16310 if gradient_stop_count > batch_limits.max_gradient_stops {
16311 return Ok(None);
16312 }
16313
16314 let fits_shape_count =
16315 current_budget.shape_count.saturating_add(1) <= batch_limits.max_shapes_per_batch;
16316 let fits_gradient_count = current_budget
16317 .gradient_stop_count
16318 .saturating_add(gradient_stop_count)
16319 <= batch_limits.max_gradient_stops;
16320 if !fits_shape_count || !fits_gradient_count {
16321 if run_start < item_cursor {
16322 current.push(SegmentBatchPlan::Shape {
16323 start: run_start,
16324 end: item_cursor,
16325 blend_mode,
16326 });
16327 }
16328 push_native_segment_fusion_partition(
16329 &mut partitions,
16330 &mut current,
16331 &mut current_budget,
16332 );
16333 run_start = item_cursor;
16334 }
16335
16336 current_budget.shape_count = current_budget.shape_count.saturating_add(1);
16337 current_budget.gradient_stop_count = current_budget
16338 .gradient_stop_count
16339 .saturating_add(gradient_stop_count);
16340 }
16341
16342 if run_start < end {
16343 current.push(SegmentBatchPlan::Shape {
16344 start: run_start,
16345 end,
16346 blend_mode,
16347 });
16348 }
16349 }
16350
16351 push_native_segment_fusion_partition(&mut partitions, &mut current, &mut current_budget);
16352 Ok(Some(partitions))
16353}
16354
16355fn segment_batch_plan_at_cursor(
16356 ordered_items: &[(usize, SegmentDrawItem)],
16357 shapes: &[DrawShape],
16358 images: &[ImageDraw],
16359 start: usize,
16360 batch_limits: ShapeBatchLimits,
16361) -> Option<(SegmentBatchPlan, usize)> {
16362 match ordered_items[start].1 {
16363 SegmentDrawItem::Shape(index) => {
16364 let blend_mode = supported_blend_mode(shapes[index].blend_mode);
16365 let mut end = start + 1;
16366 let shape_limit = (start + batch_limits.max_shapes_per_batch).min(ordered_items.len());
16367 while end < shape_limit {
16368 match ordered_items[end].1 {
16369 SegmentDrawItem::Shape(next_index)
16370 if supported_blend_mode(shapes[next_index].blend_mode) == blend_mode =>
16371 {
16372 end += 1;
16373 }
16374 _ => break,
16375 }
16376 }
16377 Some((
16378 SegmentBatchPlan::Shape {
16379 start,
16380 end,
16381 blend_mode,
16382 },
16383 end,
16384 ))
16385 }
16386 SegmentDrawItem::Image(index) => {
16387 let blend_mode = supported_blend_mode(images[index].blend_mode);
16388 let mut end = start + 1;
16389 while end < ordered_items.len() {
16390 match ordered_items[end].1 {
16391 SegmentDrawItem::Image(next_index)
16392 if supported_blend_mode(images[next_index].blend_mode) == blend_mode =>
16393 {
16394 end += 1;
16395 }
16396 _ => break,
16397 }
16398 }
16399 Some((
16400 SegmentBatchPlan::Image {
16401 start,
16402 end,
16403 blend_mode,
16404 },
16405 end,
16406 ))
16407 }
16408 SegmentDrawItem::Text(_) => {
16409 let mut end = start + 1;
16410 while end < ordered_items.len() {
16411 if matches!(ordered_items[end].1, SegmentDrawItem::Text(_)) {
16412 end += 1;
16413 } else {
16414 break;
16415 }
16416 }
16417 Some((SegmentBatchPlan::Text { start, end }, end))
16418 }
16419 SegmentDrawItem::Composite(_) => {
16420 let mut end = start + 1;
16421 while end < ordered_items.len() {
16422 if matches!(ordered_items[end].1, SegmentDrawItem::Composite(_)) {
16423 end += 1;
16424 } else {
16425 break;
16426 }
16427 }
16428 Some((SegmentBatchPlan::Composite { start, end }, end))
16429 }
16430 SegmentDrawItem::ShaderComposite(_) => {
16431 let mut end = start + 1;
16432 while end < ordered_items.len() {
16433 if matches!(ordered_items[end].1, SegmentDrawItem::ShaderComposite(_)) {
16434 end += 1;
16435 } else {
16436 break;
16437 }
16438 }
16439 Some((SegmentBatchPlan::ShaderComposite { start, end }, end))
16440 }
16441 SegmentDrawItem::Retained(_) => {
16442 let mut end = start + 1;
16443 while end < ordered_items.len() {
16444 if matches!(ordered_items[end].1, SegmentDrawItem::Retained(_)) {
16445 end += 1;
16446 } else {
16447 break;
16448 }
16449 }
16450 Some((SegmentBatchPlan::Retained { start, end }, end))
16451 }
16452 SegmentDrawItem::Shadow(_) => None,
16453 }
16454}
16455
16456#[allow(clippy::too_many_arguments)]
16457fn collect_non_effect_segment_items(
16458 shapes: &[DrawShape],
16459 _images: &[ImageDraw],
16460 _texts: &[TextDraw],
16461 _shadow_draws: &[ShadowDraw],
16462 draw_ops: &[DrawOp],
16463 z_start: usize,
16464 z_end: usize,
16465 effect_z_ranges: &[Range<usize>],
16466 width: u32,
16467 height: u32,
16468 root_scale: f32,
16469 scratch: &mut Vec<(usize, SegmentDrawItem)>,
16470) {
16471 scratch.clear();
16472 let viewport = ViewportUniformParams {
16473 width,
16474 height,
16475 offset: [0.0, 0.0],
16476 };
16477
16478 scratch.extend(draw_ops.iter().filter_map(|op| {
16479 if op.z_index < z_start
16480 || op.z_index >= z_end
16481 || is_in_effect_range(op.z_index, effect_z_ranges)
16482 {
16483 return None;
16484 }
16485 let item = match op.kind {
16486 DrawOpKind::Shape(index) => {
16487 let shape = shapes.get(index)?;
16488 if !shape_draw_is_visible_in_viewport(shape, viewport, root_scale) {
16489 return None;
16490 }
16491 SegmentDrawItem::Shape(index)
16492 }
16493 DrawOpKind::Image(index) => SegmentDrawItem::Image(index),
16494 DrawOpKind::Text(index) => SegmentDrawItem::Text(index),
16495 DrawOpKind::Shadow(index) => SegmentDrawItem::Shadow(index),
16496 DrawOpKind::Retained(index) => SegmentDrawItem::Retained(index),
16497 };
16498 Some((op.z_index, item))
16499 }));
16500}
16501
16502fn retain_renderable_shadow_items(
16503 ordered_items: &mut Vec<(usize, SegmentDrawItem)>,
16504 shadow_draws: &[ShadowDraw],
16505 width: u32,
16506 height: u32,
16507 root_scale: f32,
16508 max_texture_dim: u32,
16509) -> usize {
16510 let original_len = ordered_items.len();
16511 ordered_items.retain(|(_, item)| match item {
16512 SegmentDrawItem::Shadow(index) => shadow_draws.get(*index).is_some_and(|shadow| {
16513 shadow_draw_may_render(shadow, width, height, root_scale, max_texture_dim)
16514 }),
16515 _ => true,
16516 });
16517 original_len.saturating_sub(ordered_items.len())
16518}
16519
16520#[cfg(not(target_arch = "wasm32"))]
16521#[derive(Clone, Copy)]
16522struct SegmentDiagCounts {
16523 raw_shadow_items: usize,
16524 culled_shadow_items: usize,
16525 cached_shadow_composites: usize,
16526 composite_items: usize,
16527 shader_composite_items: usize,
16528}
16529
16530#[cfg(not(target_arch = "wasm32"))]
16531fn maybe_print_segment_diag(
16532 z_range: Range<usize>,
16533 ordered_items: &[(usize, SegmentDrawItem)],
16534 shapes: &[DrawShape],
16535 brushes: &[Brush],
16536 images: &[ImageDraw],
16537 counts: SegmentDiagCounts,
16538 batch_limits: ShapeBatchLimits,
16539) {
16540 if !cranpose_core::env_flag!("CRANPOSE_SEGMENT_DIAG") {
16541 return;
16542 }
16543 let line = SEGMENT_DIAG_LINES.fetch_add(1, Ordering::Relaxed);
16544 if line >= 64 {
16545 return;
16546 }
16547
16548 let remaining_shadow_items = ordered_items
16549 .iter()
16550 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
16551 .count();
16552 let commands: Vec<_> =
16553 SegmentCommandIter::new(ordered_items, shapes, images, batch_limits).collect();
16554 let draw_chunks = commands
16555 .iter()
16556 .filter(|command| matches!(command, SegmentRenderCommand::DrawChunk(_)))
16557 .count();
16558 let shadow_commands = commands
16559 .iter()
16560 .filter(|command| matches!(command, SegmentRenderCommand::Shadow(_)))
16561 .count();
16562 let mut native_partitions = 0usize;
16563 let mut native_unfused_chunks = 0usize;
16564 for command in &commands {
16565 let SegmentRenderCommand::DrawChunk(chunk) = command else {
16566 continue;
16567 };
16568 match native_segment_fusion_partitions(ordered_items, shapes, brushes, chunk, batch_limits)
16569 {
16570 Ok(Some(partitions)) => native_partitions += partitions.len(),
16571 Ok(None) | Err(_) => native_unfused_chunks += 1,
16572 }
16573 }
16574
16575 eprintln!(
16576 "[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={}",
16577 z_range.start,
16578 z_range.end,
16579 ordered_items.len(),
16580 counts.raw_shadow_items,
16581 counts.culled_shadow_items,
16582 counts.cached_shadow_composites,
16583 remaining_shadow_items,
16584 counts.composite_items,
16585 counts.shader_composite_items,
16586 draw_chunks,
16587 shadow_commands,
16588 native_partitions,
16589 native_unfused_chunks,
16590 );
16591}
16592
16593pub(crate) fn has_backdrop_layer_in_range(
16594 backdrop_layers: &[BackdropLayer],
16595 z_start: usize,
16596 z_end: usize,
16597) -> bool {
16598 backdrop_layers
16599 .iter()
16600 .any(|layer| layer.z_index >= z_start && layer.z_index < z_end)
16601}
16602
16603pub(crate) fn scissor_rect_for_rect(
16604 rect: Rect,
16605 root_scale: f32,
16606 width: u32,
16607 height: u32,
16608) -> Option<(u32, u32, u32, u32)> {
16609 let mut left = canonicalize_device_coordinate(rect.x * root_scale);
16610 let mut top = canonicalize_device_coordinate(rect.y * root_scale);
16611 let mut right = canonicalize_device_coordinate((rect.x + rect.width) * root_scale);
16612 let mut bottom = canonicalize_device_coordinate((rect.y + rect.height) * root_scale);
16613
16614 left = left.max(0.0).min(width as f32).floor();
16615 top = top.max(0.0).min(height as f32).floor();
16616 right = right.max(0.0).min(width as f32).ceil();
16617 bottom = bottom.max(0.0).min(height as f32).ceil();
16618
16619 if right <= left || bottom <= top {
16620 return None;
16621 }
16622
16623 Some((
16624 left as u32,
16625 top as u32,
16626 (right - left) as u32,
16627 (bottom - top) as u32,
16628 ))
16629}
16630
16631fn scissor_rect_for_layer(
16632 rect: Rect,
16633 clip: Option<Rect>,
16634 root_scale: f32,
16635 width: u32,
16636 height: u32,
16637) -> Option<(u32, u32, u32, u32)> {
16638 let clipped_rect = match clip {
16639 Some(clip_rect) => rect.intersect(clip_rect)?,
16640 None => rect,
16641 };
16642
16643 scissor_rect_for_rect(clipped_rect, root_scale, width, height)
16644}
16645
16646fn tint_for_image(
16647 color_filter: Option<ColorFilter>,
16648 alpha: f32,
16649) -> ([f32; 4], Option<ColorFilter>) {
16650 let alpha = alpha.clamp(0.0, 1.0);
16651 match color_filter {
16652 Some(filter) if filter.supports_gpu_vertex_modulation() => {
16653 let Some(tint) = filter.gpu_vertex_tint() else {
16654 return ([1.0, 1.0, 1.0, alpha], Some(filter));
16655 };
16656 (
16657 [
16658 tint[0].clamp(0.0, 1.0),
16659 tint[1].clamp(0.0, 1.0),
16660 tint[2].clamp(0.0, 1.0),
16661 (tint[3] * alpha).clamp(0.0, 1.0),
16662 ],
16663 None,
16664 )
16665 }
16666 Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
16667 None => ([1.0, 1.0, 1.0, alpha], None),
16668 }
16669}
16670
16671fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
16672 let Some(src) = src_rect else {
16673 return Some(ImageUvRect {
16674 min: [0.0, 0.0],
16675 max: [1.0, 1.0],
16676 sample_bounds: [0.0, 0.0, 1.0, 1.0],
16677 });
16678 };
16679
16680 let (u_min, u_max, u_bound_min, u_bound_max) =
16681 source_axis_uv(src.x, src.width, image.width() as f32)?;
16682 let (v_min, v_max, v_bound_min, v_bound_max) =
16683 source_axis_uv(src.y, src.height, image.height() as f32)?;
16684
16685 Some(ImageUvRect {
16686 min: [u_min, v_min],
16687 max: [u_max, v_max],
16688 sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
16689 })
16690}
16691
16692fn glyph_atlas_uv_rect(entry: GlyphAtlasEntry, atlas_size: u32) -> ImageUvRect {
16697 let atlas_width = atlas_size as f32;
16698 let atlas_height = atlas_size as f32;
16699 let min = [entry.x as f32 / atlas_width, entry.y as f32 / atlas_height];
16700 let max = [
16701 (entry.x + entry.width) as f32 / atlas_width,
16702 (entry.y + entry.height) as f32 / atlas_height,
16703 ];
16704 let center_min = [
16705 (entry.x as f32 + 0.5) / atlas_width,
16706 (entry.y as f32 + 0.5) / atlas_height,
16707 ];
16708 let center_max = [
16709 (entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) / atlas_width,
16710 (entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) / atlas_height,
16711 ];
16712 ImageUvRect {
16713 min,
16714 max,
16715 sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
16716 }
16717}
16718
16719fn snap_nearest_image_to_device_pixels(image: &mut ImageDraw, root_scale: f32) {
16720 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
16721 return;
16722 }
16723
16724 let Some(rect) = axis_aligned_quad_rect(image.quad) else {
16725 return;
16726 };
16727
16728 let left_px = (rect.x * root_scale).round();
16729 let top_px = (rect.y * root_scale).round();
16730 let width_px = (rect.width * root_scale).round().max(1.0);
16731 let height_px = (rect.height * root_scale).round().max(1.0);
16732 let snapped = Rect {
16733 x: left_px / root_scale,
16734 y: top_px / root_scale,
16735 width: width_px / root_scale,
16736 height: height_px / root_scale,
16737 };
16738
16739 image.rect = snapped;
16740 image.local_rect = Rect {
16741 x: image.local_rect.x + snapped.x - rect.x,
16742 y: image.local_rect.y + snapped.y - rect.y,
16743 width: snapped.width,
16744 height: snapped.height,
16745 };
16746 image.quad = crate::rect_to_quad(snapped);
16747}
16748
16749fn nearest_image_device_quad(image: &ImageDraw, root_scale: f32) -> Option<[[f32; 2]; 4]> {
16750 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
16751 return None;
16752 }
16753
16754 let rect = axis_aligned_quad_rect(image.quad)?;
16755 let left_px = (rect.x * root_scale).round();
16756 let top_px = (rect.y * root_scale).round();
16757 let width_px = (rect.width * root_scale).round().max(1.0);
16758 let height_px = (rect.height * root_scale).round().max(1.0);
16759 let right_px = left_px + width_px;
16760 let bottom_px = top_px + height_px;
16761 Some([
16762 [left_px, top_px],
16763 [right_px, top_px],
16764 [left_px, bottom_px],
16765 [right_px, bottom_px],
16766 ])
16767}
16768
16769fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
16770 if !start.is_finite()
16771 || !extent.is_finite()
16772 || !image_extent.is_finite()
16773 || extent == 0.0
16774 || image_extent <= 0.0
16775 {
16776 return None;
16777 }
16778
16779 let end = start + extent;
16780 let edge_min = start.min(end).clamp(0.0, image_extent);
16781 let edge_max = start.max(end).clamp(0.0, image_extent);
16782 if edge_max <= edge_min {
16783 return None;
16784 }
16785
16786 let center_min = edge_min + 0.5;
16787 let center_max = edge_max - 0.5;
16788 let (bound_min, bound_max) = if center_min <= center_max {
16789 (center_min, center_max)
16790 } else {
16791 let center = (edge_min + edge_max) * 0.5;
16792 (center, center)
16793 };
16794
16795 Some((
16796 edge_min / image_extent,
16797 edge_max / image_extent,
16798 bound_min / image_extent,
16799 bound_max / image_extent,
16800 ))
16801}
16802
16803fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
16804 let mut filtered = Vec::with_capacity(image.pixels().len());
16805 for pixel in image.pixels().as_chunks::<4>().0 {
16806 let rgba = [
16807 pixel[0] as f32 / 255.0,
16808 pixel[1] as f32 / 255.0,
16809 pixel[2] as f32 / 255.0,
16810 pixel[3] as f32 / 255.0,
16811 ];
16812 let out = filter.apply_rgba(rgba);
16813 filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
16814 filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
16815 filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
16816 filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
16817 }
16818 ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
16819 .map_err(|error| format!("failed to build filtered bitmap: {error}"))
16820}
16821
16822fn scissor_rect_for_image(
16823 image: &ImageDraw,
16824 root_scale: f32,
16825 width: u32,
16826 height: u32,
16827) -> Option<(u32, u32, u32, u32)> {
16828 scissor_rect_for_layer(image.rect, image.clip, root_scale, width, height)
16829}
16830
16831fn inner_shadow_composite_mask(
16832 shadow: &ShadowDraw,
16833 root_scale: f32,
16834) -> Option<RoundedCompositeMask> {
16835 if !shadow
16836 .shapes
16837 .iter()
16838 .any(|(_, mode)| *mode == BlendMode::DstOut)
16839 {
16840 return None;
16841 }
16842 let (fill, _) = shadow.shapes.first()?;
16843 let rect = fill.local_rect;
16844 if rect.width <= 0.0 || rect.height <= 0.0 {
16845 return None;
16846 }
16847
16848 let radii = fill.shape.map_or([0.0; 4], |rounded| {
16849 let resolved = rounded.resolve(rect.width, rect.height);
16850 [
16851 resolved.top_left * root_scale,
16852 resolved.top_right * root_scale,
16853 resolved.bottom_left * root_scale,
16854 resolved.bottom_right * root_scale,
16855 ]
16856 });
16857
16858 Some(RoundedCompositeMask {
16859 rect: [
16860 rect.x * root_scale,
16861 rect.y * root_scale,
16862 rect.width * root_scale,
16863 rect.height * root_scale,
16864 ],
16865 radii,
16866 })
16867}
16868
16869#[cfg(test)]
16870mod shape_batch_limits_tests {
16871 use super::*;
16872
16873 fn generous_limits() -> wgpu::Limits {
16876 wgpu::Limits {
16877 max_storage_buffers_per_shader_stage: 8,
16878 max_storage_buffer_binding_size: 128 << 20,
16879 max_uniform_buffer_binding_size: 16 << 10,
16880 ..wgpu::Limits::default()
16881 }
16882 }
16883
16884 #[test]
16885 fn a_device_without_vertex_storage_takes_the_uniform_path() {
16886 let limits = ShapeBatchLimits::select(&generous_limits(), wgpu::DownlevelFlags::empty());
16892 assert!(
16893 !limits.storage,
16894 "no VERTEX_STORAGE must mean uniform mode, whatever the limit says"
16895 );
16896 }
16897
16898 #[test]
16899 fn a_device_with_vertex_storage_still_takes_the_storage_path() {
16900 let limits = ShapeBatchLimits::select(&generous_limits(), wgpu::DownlevelFlags::all());
16901 assert!(
16902 limits.storage,
16903 "the flag must not cost storage mode on a device that has it"
16904 );
16905 }
16906
16907 #[test]
16908 fn the_limit_still_gates_storage_when_the_flag_is_present() {
16909 let mut limits = generous_limits();
16910 limits.max_storage_buffers_per_shader_stage = 1;
16911 let limits = ShapeBatchLimits::select(&limits, wgpu::DownlevelFlags::all());
16912 assert!(!limits.storage, "two bindings are needed, not one");
16913 }
16914}
16915
16916#[cfg(test)]
16917mod tests {
16918 use super::*;
16919 use crate::normalized_scene::visible_draw_rect;
16920 use cranpose_foundation::lazy::{remember_lazy_list_state, LazyListScope, LazyListState};
16921 use cranpose_render_common::graph::{DrawPrimitiveNode, IsolationReasons, TextPrimitiveNode};
16922 use cranpose_render_common::raster_cache::LayerRasterCacheHashes;
16923 use cranpose_render_common::scene_builder::build_graph_from_applier;
16924 use cranpose_ui::text::{
16925 AnnotatedString, BaselineShift, RangeStyle, Shadow, SpanStyle, TextDecoration,
16926 TextDrawStyle, TextGeometricTransform, TextMotion, TextUnit,
16927 };
16928 use cranpose_ui::{
16929 LayoutEngine, LazyColumn, LazyColumnSpec, Modifier, Size, Text, TextLayoutOptions,
16930 TextStyle,
16931 };
16932 use cranpose_ui_graphics::{
16933 Brush, Color, CornerRadii, DrawPrimitive, Rect, RenderEffect, RoundedCornerShape,
16934 RuntimeShader,
16935 };
16936
16937 fn chunk(batches: &[SegmentBatchPlan]) -> SegmentDrawChunkPlan {
16938 let mut chunk = SegmentDrawChunkPlan::default();
16939 for batch in batches {
16940 chunk.push(*batch);
16941 }
16942 chunk
16943 }
16944
16945 fn with_test_app_context<R>(block: impl FnOnce() -> R) -> R {
16946 let app_context = cranpose_ui::AppContext::new();
16947 app_context.enter(block)
16948 }
16949
16950 fn assert_snap_anchor_close(actual: Option<SnapAnchor>, expected_origin: Point, message: &str) {
16951 let Some(actual) = actual else {
16952 panic!("{message}: missing snap anchor");
16953 };
16954 let expected = SnapAnchor::rigid(expected_origin);
16955 assert_eq!(
16956 actual.device_pixel_step, expected.device_pixel_step,
16957 "{message}: device pixel step changed"
16958 );
16959 assert!(
16960 (actual.origin.x - expected.origin.x).abs() <= 1e-4
16961 && (actual.origin.y - expected.origin.y).abs() <= 1e-4,
16962 "{message}: expected origin {:?}, got {:?}",
16963 expected.origin,
16964 actual.origin
16965 );
16966 }
16967
16968 fn effect_layer(z_start: usize, z_end: usize) -> EffectLayer {
16969 EffectLayer {
16970 rect: Rect {
16971 x: 0.0,
16972 y: 0.0,
16973 width: 10.0,
16974 height: 10.0,
16975 },
16976 clip: None,
16977 snap_anchor: None,
16978 effect: Some(RenderEffect::blur(4.0)),
16979 blend_mode: BlendMode::SrcOver,
16980 composite_alpha: 1.0,
16981 z_start,
16982 z_end,
16983 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
16984 }
16985 }
16986
16987 #[test]
16988 fn direct_shader_composite_accepts_box4_when_viewport_preserves_source_pixels() {
16989 assert_eq!(
16990 direct_shader_composite_viewport(
16991 1.0,
16992 BlendMode::SrcOver,
16993 Some((12.0, 18.0, 64.0, 32.0)),
16994 CompositeSampleMode::Box4,
16995 (64, 32),
16996 ),
16997 Some((12.0, 18.0, 64.0, 32.0))
16998 );
16999 }
17000
17001 #[test]
17002 fn direct_shader_composite_rejects_box4_when_viewport_resamples_source() {
17003 assert_eq!(
17004 direct_shader_composite_viewport(
17005 1.0,
17006 BlendMode::SrcOver,
17007 Some((12.0, 18.0, 64.5, 32.0)),
17008 CompositeSampleMode::Box4,
17009 (64, 32),
17010 ),
17011 None
17012 );
17013 assert_eq!(
17014 direct_shader_composite_viewport(
17015 1.0,
17016 BlendMode::SrcOver,
17017 Some((12.25, 18.0, 64.0, 32.0)),
17018 CompositeSampleMode::Box4,
17019 (64, 32),
17020 ),
17021 None
17022 );
17023 }
17024
17025 fn test_text_draw(rect: Rect, text_motion: TextMotion) -> TextDraw {
17026 let mut text_style = TextStyle::default();
17027 text_style.paragraph_style.text_motion = Some(text_motion);
17028 TextDraw {
17029 node_id: 42,
17030 rect,
17031 snap_anchor: None,
17032 translated_content_context: false,
17033 text: Arc::new(AnnotatedString::new("stable markdown row".to_string()).render_string()),
17034 color: Color::WHITE,
17035 text_style,
17036 font_size: 14.0,
17037 scale: 1.0,
17038 layout_options: TextLayoutOptions::default(),
17039 z_index: 0,
17040 clip: None,
17041 }
17042 }
17043
17044 #[test]
17045 fn static_text_image_cache_key_ignores_absolute_scroll_position() {
17046 let base = test_text_draw(
17047 Rect {
17048 x: 12.25,
17049 y: 40.75,
17050 width: 220.0,
17051 height: 24.0,
17052 },
17053 TextMotion::Static,
17054 );
17055 let scrolled = test_text_draw(
17056 Rect {
17057 x: 12.75,
17058 y: -318.5,
17059 width: 220.0,
17060 height: 24.0,
17061 },
17062 TextMotion::Static,
17063 );
17064
17065 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, true);
17066 let scrolled_key = GpuRenderer::text_image_cache_key(&scrolled, scrolled.rect, 1.0, true);
17067
17068 assert_eq!(
17069 base_key, scrolled_key,
17070 "scrolling static text must reuse the same raster cache entry"
17071 );
17072 }
17073
17074 #[test]
17075 fn static_text_glyph_run_cache_key_ignores_absolute_scroll_position() {
17076 let base = test_text_draw(
17077 Rect {
17078 x: 12.25,
17079 y: 40.75,
17080 width: 220.0,
17081 height: 24.0,
17082 },
17083 TextMotion::Static,
17084 );
17085 let scrolled = test_text_draw(
17086 Rect {
17087 x: 12.75,
17088 y: -318.5,
17089 width: 220.0,
17090 height: 24.0,
17091 },
17092 TextMotion::Static,
17093 );
17094
17095 let base_key = GpuRenderer::text_glyph_run_cache_key(&base, base.rect, 1.0, true);
17096 let scrolled_key =
17097 GpuRenderer::text_glyph_run_cache_key(&scrolled, scrolled.rect, 1.0, true);
17098
17099 assert_eq!(
17100 base_key, scrolled_key,
17101 "scrolling static text must reuse the same retained glyph run"
17102 );
17103 }
17104
17105 #[test]
17106 fn static_multiline_text_glyph_source_keeps_full_text_when_image_source_slices() {
17107 let rect = Rect {
17108 x: 8.0,
17109 y: 100.0,
17110 width: 240.0,
17111 height: 1_000.0,
17112 };
17113 let mut draw = test_text_draw(rect, TextMotion::Static);
17114 let lines = (0..100)
17115 .map(|line| format!("line-{line:03}"))
17116 .collect::<Vec<_>>()
17117 .join("\n");
17118 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
17119
17120 let raster_rect = Rect {
17121 x: 16.0,
17122 y: 200.0,
17123 width: 480.0,
17124 height: 2_000.0,
17125 };
17126 let clipped = clipped_text_raster_source(
17127 &draw,
17128 rect,
17129 raster_rect,
17130 Some(Rect {
17131 x: 0.0,
17132 y: 610.0,
17133 width: 800.0,
17134 height: 40.0,
17135 }),
17136 2.0,
17137 true,
17138 );
17139 let glyph = text_glyph_raster_source(&draw, raster_rect);
17140
17141 assert!(
17142 matches!(clipped.draw, Cow::Owned(_)),
17143 "the image source should still slice large clipped multiline text"
17144 );
17145 assert!(
17146 matches!(glyph.draw, Cow::Borrowed(_)),
17147 "the glyph source must keep a stable full-text run key while scrolling"
17148 );
17149
17150 let clipped_key = GpuRenderer::text_glyph_run_cache_key(
17151 clipped.draw.as_ref(),
17152 clipped.raster_rect,
17153 2.0,
17154 true,
17155 );
17156 let glyph_key = GpuRenderer::text_glyph_run_cache_key(
17157 glyph.draw.as_ref(),
17158 glyph.raster_rect,
17159 2.0,
17160 true,
17161 );
17162
17163 assert_ne!(
17164 clipped_key, glyph_key,
17165 "image slicing must not force glyph rendering onto per-scroll line-window cache keys"
17166 );
17167 }
17168
17169 #[cfg(not(target_arch = "wasm32"))]
17170 #[test]
17171 fn retained_glyph_viewport_offsets_relative_vertices_by_source_origin() {
17172 let viewport = ViewportUniformParams {
17173 width: 800,
17174 height: 600,
17175 offset: [10.0, 20.0],
17176 };
17177 let source = Rect {
17178 x: 40.0,
17179 y: 90.0,
17180 width: 120.0,
17181 height: 48.0,
17182 };
17183
17184 let retained = GpuRenderer::retained_glyph_viewport(viewport, source);
17185
17186 assert_eq!(retained.width, viewport.width);
17187 assert_eq!(retained.height, viewport.height);
17188 assert_eq!(retained.offset, [-30.0, -70.0]);
17189 }
17190
17191 #[cfg(not(target_arch = "wasm32"))]
17192 #[test]
17193 fn tiny_text_glyph_runs_stay_in_shared_uploads() {
17194 assert!(
17195 !should_use_retained_text_glyph_run(8, None),
17196 "tiny labels must stay in the shared fused batch"
17197 );
17198 }
17199
17200 #[cfg(not(target_arch = "wasm32"))]
17201 #[test]
17202 fn line_sized_text_glyph_runs_stay_in_shared_uploads() {
17203 assert!(
17204 !should_use_retained_text_glyph_run(64, None),
17205 "Markdown scroll frames contain many line-sized text runs; retaining each one creates per-run buffer binds instead of one shared glyph batch"
17206 );
17207 }
17208
17209 #[cfg(not(target_arch = "wasm32"))]
17210 #[test]
17211 fn large_clipped_text_glyph_runs_stay_in_shared_uploads() {
17212 assert!(
17213 !should_use_retained_text_glyph_run(
17214 MIN_RETAINED_TEXT_GLYPH_QUADS.saturating_mul(2),
17215 Some(Rect {
17216 x: 0.0,
17217 y: 0.0,
17218 width: 200.0,
17219 height: 100.0,
17220 }),
17221 ),
17222 "clipped lazy-list text must not draw a full retained run outside the viewport"
17223 );
17224 }
17225
17226 #[test]
17227 fn normal_text_glyph_draw_skips_offscreen_prewarm_candidates() {
17228 assert_eq!(
17229 text_glyph_draw_action(false, true, false),
17230 TextGlyphDrawAction::Skip,
17231 "normal draw traversal must not prepare offscreen text"
17232 );
17233 }
17234
17235 #[test]
17236 fn bounded_text_glyph_prewarm_admits_offscreen_candidates() {
17237 assert_eq!(
17238 text_glyph_draw_action(false, true, true),
17239 TextGlyphDrawAction::PrewarmOffscreen,
17240 "only the bounded prewarm path may prepare offscreen text"
17241 );
17242 }
17243
17244 #[test]
17245 fn visible_text_glyph_draws_are_always_admitted() {
17246 assert_eq!(
17247 text_glyph_draw_action(true, false, false),
17248 TextGlyphDrawAction::DrawVisible
17249 );
17250 assert_eq!(
17251 text_glyph_draw_action(true, true, true),
17252 TextGlyphDrawAction::DrawVisible
17253 );
17254 }
17255
17256 #[cfg(not(target_arch = "wasm32"))]
17257 #[test]
17258 fn offscreen_text_prewarm_skips_large_uncached_text_runs() {
17259 assert!(
17260 !offscreen_text_glyph_prewarm_work_is_bounded(
17261 None,
17262 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS + 1,
17263 ),
17264 "offscreen prewarm must not collect large uncached text runs in an input frame"
17265 );
17266 }
17267
17268 #[cfg(not(target_arch = "wasm32"))]
17269 #[test]
17270 fn offscreen_text_prewarm_admits_small_uncached_text_runs() {
17271 assert!(
17272 offscreen_text_glyph_prewarm_work_is_bounded(
17273 None,
17274 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
17275 ),
17276 "small labels can be warmed without risking a frame-budget spike"
17277 );
17278 }
17279
17280 #[cfg(not(target_arch = "wasm32"))]
17281 #[test]
17282 fn offscreen_text_prewarm_skips_large_cached_runs_without_quads() {
17283 assert!(
17284 !offscreen_text_glyph_prewarm_work_is_bounded(
17285 Some(MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS + 1),
17286 0,
17287 ),
17288 "cached glyph placements can still be too large to prepare during input frames"
17289 );
17290 }
17291
17292 #[cfg(not(target_arch = "wasm32"))]
17293 #[test]
17294 fn offscreen_text_prewarm_stops_after_candidate_budget() {
17295 assert!(
17296 offscreen_text_glyph_prewarm_budget_exhausted(
17297 Instant::now(),
17298 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES,
17299 ),
17300 "prewarm must be bounded by candidate count even when each candidate is cheap"
17301 );
17302 }
17303
17304 #[test]
17305 fn clipped_cached_glyph_quads_are_filtered_to_viewport() {
17306 fn quad(y: i32) -> CachedTextGlyphQuad {
17307 CachedTextGlyphQuad {
17308 x: 8,
17309 y,
17310 width: 20,
17311 height: 10,
17312 color: (1.0, 1.0, 1.0, 1.0),
17313 uv: ImageUvRect {
17314 min: [0.0, 0.0],
17315 max: [1.0, 1.0],
17316 sample_bounds: [0.0, 0.0, 1.0, 1.0],
17317 },
17318 }
17319 }
17320
17321 let source = Rect {
17322 x: 0.0,
17323 y: 0.0,
17324 width: 320.0,
17325 height: 400.0,
17326 };
17327 let clip = Some(Rect {
17328 x: 0.0,
17329 y: 0.0,
17330 width: 320.0,
17331 height: 80.0,
17332 });
17333 let viewport = ViewportUniformParams {
17334 width: 320,
17335 height: 80,
17336 offset: [0.0, 0.0],
17337 };
17338
17339 assert!(cached_text_glyph_quad_is_visible_in_viewport(
17340 source,
17341 &quad(40),
17342 clip,
17343 viewport,
17344 1.0,
17345 ));
17346 assert!(
17347 !cached_text_glyph_quad_is_visible_in_viewport(source, &quad(140), clip, viewport, 1.0,),
17348 "glyphs outside the effective clip should not enter the frame command stream"
17349 );
17350 }
17351
17352 #[test]
17353 fn small_scene_range_cache_miss_observes_first_render() {
17354 let key = LayerRasterCacheKey::scene_range(
17355 0xCACE,
17356 Rect {
17357 x: 0.0,
17358 y: 0.0,
17359 width: 120.0,
17360 height: 80.0,
17361 },
17362 (120, 80),
17363 ScaleBucket::from_scale(1.0),
17364 );
17365
17366 assert!(
17367 !first_cache_miss_admission(&key),
17368 "a small scene-range miss should render directly first instead of materializing a tiny one-frame retained target"
17369 );
17370 assert!(
17371 repeated_cache_miss_admission(&key),
17372 "a repeated small scene-range miss is stable enough to materialize into the retained cache"
17373 );
17374 }
17375
17376 #[test]
17377 fn large_scene_range_cache_miss_requires_repeated_stable_key() {
17378 let key = LayerRasterCacheKey::scene_range(
17379 0xCACE,
17380 Rect {
17381 x: 0.0,
17382 y: 0.0,
17383 width: 1200.0,
17384 height: 900.0,
17385 },
17386 (1200, 900),
17387 ScaleBucket::from_scale(1.0),
17388 );
17389
17390 assert!(
17391 !first_cache_miss_admission(&key),
17392 "a large first scene-range miss should render directly instead of materializing a multi-MB one-frame cache entry"
17393 );
17394 assert!(
17395 repeated_cache_miss_admission(&key),
17396 "a repeated scene-range miss is stable enough to materialize into the retained cache"
17397 );
17398 }
17399
17400 #[test]
17401 fn renderer_warmup_frame_is_requested_for_cache_miss_stats_only() {
17402 let stats = gpu_stats::FrameStats::default();
17403 let mut snapshot = stats.snapshot();
17404 assert!(
17405 !frame_stats_need_warmup_frame(&snapshot),
17406 "a clean frame must not keep a static scene redrawing"
17407 );
17408
17409 snapshot.layer_cache_misses = 1;
17410 assert!(frame_stats_need_warmup_frame(&snapshot));
17411 snapshot.layer_cache_misses = 0;
17412
17413 snapshot.shadow_shape_cache_misses = 1;
17414 assert!(frame_stats_need_warmup_frame(&snapshot));
17415 snapshot.shadow_shape_cache_misses = 0;
17416
17417 snapshot.text_image_cache_misses = 1;
17418 assert!(frame_stats_need_warmup_frame(&snapshot));
17419 snapshot.text_image_cache_misses = 0;
17420
17421 snapshot.text_glyph_atlas_misses = 1;
17422 assert!(frame_stats_need_warmup_frame(&snapshot));
17423 }
17424
17425 #[test]
17426 fn renderer_warmup_budget_is_consumed_by_a_repeated_cache_miss() {
17427 let stats = gpu_stats::FrameStats::default();
17428 let mut snapshot = stats.snapshot();
17429 snapshot.layer_cache_misses = 1;
17430 let mut pending_frames = 0;
17431
17432 update_frame_warmup_budget(&mut pending_frames, &snapshot);
17433 assert_eq!(pending_frames, CACHE_MISS_WARMUP_FRAMES);
17434
17435 update_frame_warmup_budget(&mut pending_frames, &snapshot);
17436 assert_eq!(
17437 pending_frames, 0,
17438 "a cache miss during the warmup frame must not replenish its budget"
17439 );
17440 }
17441
17442 #[test]
17443 fn non_scene_layer_surface_cache_miss_admits_first_render() {
17444 let key = LayerRasterCacheKey::new(
17445 Some(77),
17446 0xC0FFEE,
17447 0,
17448 Rect {
17449 x: 0.0,
17450 y: 0.0,
17451 width: 120.0,
17452 height: 80.0,
17453 },
17454 (120, 80),
17455 ScaleBucket::from_scale(1.0),
17456 );
17457
17458 assert!(
17459 first_cache_miss_admission(&key),
17460 "ordinary retained layer surfaces should still cache on first miss"
17461 );
17462 }
17463
17464 #[test]
17465 fn text_image_cache_key_is_content_addressed_not_node_addressed() {
17466 let first = test_text_draw(
17467 Rect {
17468 x: 12.25,
17469 y: 40.75,
17470 width: 220.0,
17471 height: 24.0,
17472 },
17473 TextMotion::Static,
17474 );
17475 let mut second = first.clone();
17476 second.node_id = first.node_id + 1;
17477
17478 let first_key = GpuRenderer::text_image_cache_key(&first, first.rect, 1.0, true);
17479 let second_key = GpuRenderer::text_image_cache_key(&second, second.rect, 1.0, true);
17480
17481 assert_eq!(
17482 first_key, second_key,
17483 "text raster cache keys must be based on rendered pixels, not node identity"
17484 );
17485 }
17486
17487 #[test]
17488 fn animated_text_image_cache_key_keeps_fractional_phase_only() {
17489 let base = test_text_draw(
17490 Rect {
17491 x: 12.25,
17492 y: 40.75,
17493 width: 220.0,
17494 height: 24.0,
17495 },
17496 TextMotion::Animated,
17497 );
17498 let integer_translated = test_text_draw(
17499 Rect {
17500 x: 44.25,
17501 y: 88.75,
17502 width: 220.0,
17503 height: 24.0,
17504 },
17505 TextMotion::Animated,
17506 );
17507 let phase_shifted = test_text_draw(
17508 Rect {
17509 x: 44.5,
17510 y: 88.75,
17511 width: 220.0,
17512 height: 24.0,
17513 },
17514 TextMotion::Animated,
17515 );
17516
17517 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, false);
17518 let translated_key = GpuRenderer::text_image_cache_key(
17519 &integer_translated,
17520 integer_translated.rect,
17521 1.0,
17522 false,
17523 );
17524 let phase_shifted_key =
17525 GpuRenderer::text_image_cache_key(&phase_shifted, phase_shifted.rect, 1.0, false);
17526
17527 assert_eq!(
17528 base_key, translated_key,
17529 "integer translation should not invalidate animated text raster cache entries"
17530 );
17531 assert_ne!(
17532 base_key, phase_shifted_key,
17533 "fractional phase affects animated text rasterization and must stay in the key"
17534 );
17535 }
17536
17537 #[test]
17538 fn animated_translated_text_raster_geometry_applies_snap_anchor() {
17539 let mut base = test_text_draw(
17540 Rect {
17541 x: 14.25,
17542 y: 16.50,
17543 width: 220.0,
17544 height: 24.0,
17545 },
17546 TextMotion::Animated,
17547 );
17548 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.50)));
17549
17550 let mut scrolled = test_text_draw(
17551 Rect {
17552 x: 14.25,
17553 y: 15.80,
17554 width: 220.0,
17555 height: 24.0,
17556 },
17557 TextMotion::Animated,
17558 );
17559 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 15.80)));
17560
17561 let (base_logical, base_raster, _, _, base_static) =
17562 text_raster_geometry_for_draw(&base, 1.0).expect("base text geometry");
17563 let (scrolled_logical, scrolled_raster, _, _, scrolled_static) =
17564 text_raster_geometry_for_draw(&scrolled, 1.0).expect("scrolled text geometry");
17565
17566 assert!(!base_static);
17567 assert!(!scrolled_static);
17568 assert!((base_logical.x - 14.0).abs() < f32::EPSILON);
17569 assert!((base_logical.y - 17.0).abs() < f32::EPSILON);
17570 assert!((scrolled_logical.x - 14.0).abs() < f32::EPSILON);
17571 assert!((scrolled_logical.y - 16.0).abs() < f32::EPSILON);
17572 assert_eq!(base_raster.x.fract(), 0.0);
17573 assert_eq!(base_raster.y.fract(), 0.0);
17574 assert_eq!(scrolled_raster.x.fract(), 0.0);
17575 assert_eq!(scrolled_raster.y.fract(), 0.0);
17576
17577 let base_key = GpuRenderer::text_image_cache_key(&base, base_raster, 1.0, false);
17578 let scrolled_key =
17579 GpuRenderer::text_image_cache_key(&scrolled, scrolled_raster, 1.0, false);
17580 assert_eq!(
17581 base_key, scrolled_key,
17582 "translated animated text should keep a stable raster phase while scrolling"
17583 );
17584 }
17585
17586 #[test]
17587 fn translated_static_text_moves_one_device_pixel_at_half_pixel_phase() {
17588 let root_scale = 1.25;
17589 let mut base = test_text_draw(
17590 Rect {
17591 x: 14.0,
17592 y: 276.0,
17593 width: 220.0,
17594 height: 24.0,
17595 },
17596 TextMotion::Static,
17597 );
17598 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.600_006)));
17599
17600 let mut scrolled = test_text_draw(
17601 Rect {
17602 x: 14.0,
17603 y: 275.2,
17604 width: 220.0,
17605 height: 24.0,
17606 },
17607 TextMotion::Static,
17608 );
17609 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 126.799_99)));
17610
17611 let (_, base_raster, _, _, _) =
17612 text_raster_geometry_for_draw(&base, root_scale).expect("base text geometry");
17613 let (_, scrolled_raster, _, _, _) =
17614 text_raster_geometry_for_draw(&scrolled, root_scale).expect("scrolled text geometry");
17615
17616 assert_eq!(
17617 base_raster.y - scrolled_raster.y,
17618 1.0,
17619 "one physical pixel of rigid scrolling must move static text by one raster pixel"
17620 );
17621 }
17622
17623 #[test]
17624 fn translated_text_snap_does_not_move_its_fixed_ancestor_clip() {
17625 let root_scale = 1.25;
17626 let fixed_clip = Rect {
17627 x: 8.0,
17628 y: 20.0,
17629 width: 300.0,
17630 height: 680.0,
17631 };
17632 let mut draw = test_text_draw(
17633 Rect {
17634 x: 14.0,
17635 y: 276.0,
17636 width: 220.0,
17637 height: 24.0,
17638 },
17639 TextMotion::Static,
17640 );
17641 draw.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.4)));
17642 draw.clip = Some(fixed_clip);
17643
17644 let (_, _, clip, _, _) =
17645 text_raster_geometry_for_draw(&draw, root_scale).expect("clipped text geometry");
17646
17647 assert_eq!(
17648 clip,
17649 Some(fixed_clip),
17650 "content pixel snapping must not translate a fixed ancestor clip"
17651 );
17652 }
17653
17654 #[test]
17655 fn clipped_static_multiline_text_raster_source_limits_visible_line_window() {
17656 let rect = Rect {
17657 x: 8.0,
17658 y: 100.0,
17659 width: 240.0,
17660 height: 1_000.0,
17661 };
17662 let mut draw = test_text_draw(rect, TextMotion::Static);
17663 let lines = (0..100)
17664 .map(|line| format!("line-{line:03}"))
17665 .collect::<Vec<_>>()
17666 .join("\n");
17667 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
17668
17669 let raster_rect = Rect {
17670 x: 16.0,
17671 y: 200.0,
17672 width: 480.0,
17673 height: 2_000.0,
17674 };
17675 let source = clipped_text_raster_source(
17676 &draw,
17677 rect,
17678 raster_rect,
17679 Some(Rect {
17680 x: 0.0,
17681 y: 610.0,
17682 width: 800.0,
17683 height: 40.0,
17684 }),
17685 2.0,
17686 true,
17687 );
17688
17689 let Cow::Owned(sliced_draw) = source.draw else {
17690 panic!("clipped static multiline text should rasterize only the visible line window");
17691 };
17692 let sliced_text = sliced_draw.text.text.as_str();
17693 assert!(sliced_text.contains("line-050"));
17694 assert!(sliced_text.contains("line-055"));
17695 assert!(!sliced_text.contains("line-000"));
17696 assert!(!sliced_text.contains("line-099"));
17697 assert_eq!(source.raster_rect.x, raster_rect.x);
17698 assert!(source.raster_rect.y > raster_rect.y);
17699 assert!(source.raster_rect.height < raster_rect.height);
17700 }
17701
17702 #[test]
17703 fn clipped_static_multiline_text_raster_source_slices_short_multiline_text() {
17704 let rect = Rect {
17705 x: 8.0,
17706 y: 100.0,
17707 width: 240.0,
17708 height: 320.0,
17709 };
17710 let mut draw = test_text_draw(rect, TextMotion::Static);
17711 let lines = (0..24)
17712 .map(|line| format!("code-line-{line:02}"))
17713 .collect::<Vec<_>>()
17714 .join("\n");
17715 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
17716
17717 let raster_rect = Rect {
17718 x: 16.0,
17719 y: 200.0,
17720 width: 480.0,
17721 height: 640.0,
17722 };
17723 let source = clipped_text_raster_source(
17724 &draw,
17725 rect,
17726 raster_rect,
17727 Some(Rect {
17728 x: 0.0,
17729 y: 190.0,
17730 width: 800.0,
17731 height: 120.0,
17732 }),
17733 2.0,
17734 true,
17735 );
17736
17737 let Cow::Owned(sliced_draw) = source.draw else {
17738 panic!("clipped multiline text should rasterize only the visible line window");
17739 };
17740 assert!(sliced_draw.text.text.as_str().contains("code-line-06"));
17741 assert!(!sliced_draw.text.text.as_str().contains("code-line-00"));
17742 assert!(!sliced_draw.text.text.as_str().contains("code-line-23"));
17743 assert_eq!(source.raster_rect.x, raster_rect.x);
17744 assert!(source.raster_rect.y > raster_rect.y);
17745 assert!(source.raster_rect.height < raster_rect.height);
17746 }
17747
17748 #[test]
17749 fn text_line_index_cache_reuses_retained_index_for_same_text_instance() {
17750 let mut cache = TextLineIndexCache::new(4);
17751 let text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
17752
17753 let first = cache.line_starts(&text);
17754 let second = cache.line_starts(&text);
17755
17756 assert_eq!(first.as_ref(), &[0, 2, 4]);
17757 assert!(
17758 Rc::ptr_eq(&first, &second),
17759 "retained text should not rebuild its line index on every clipped frame"
17760 );
17761 }
17762
17763 #[test]
17764 fn text_line_index_cache_is_retained_text_instance_local() {
17765 let mut cache = TextLineIndexCache::new(4);
17766 let first_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
17767 let second_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
17768
17769 let first = cache.line_starts(&first_text);
17770 let second = cache.line_starts(&second_text);
17771
17772 assert_eq!(first.as_ref(), second.as_ref());
17773 assert!(
17774 !Rc::ptr_eq(&first, &second),
17775 "line index lookup should not hash large text contents to find unrelated retained nodes"
17776 );
17777 }
17778
17779 #[test]
17780 fn device_pixel_bounds_for_rect_snaps_origin_and_extents() {
17781 let bounds = device_pixel_bounds_for_rect(
17782 Rect {
17783 x: 10.25,
17784 y: 14.6,
17785 width: 20.1,
17786 height: 9.2,
17787 },
17788 200,
17789 120,
17790 2.0,
17791 )
17792 .expect("rect should intersect the viewport");
17793
17794 assert_eq!(
17795 bounds,
17796 DevicePixelBounds {
17797 x: 20.0,
17798 y: 29.0,
17799 width: 41,
17800 height: 19,
17801 }
17802 );
17803 }
17804
17805 #[test]
17806 fn visible_layer_rect_intersects_clip_and_viewport() {
17807 let visible = visible_layer_rect(
17808 Rect {
17809 x: -10.0,
17810 y: 5.0,
17811 width: 80.0,
17812 height: 40.0,
17813 },
17814 Some(Rect {
17815 x: 4.0,
17816 y: 8.0,
17817 width: 20.0,
17818 height: 50.0,
17819 }),
17820 2.0,
17821 60,
17822 40,
17823 )
17824 .expect("visible rect");
17825
17826 assert_eq!(
17827 visible,
17828 Rect {
17829 x: 4.0,
17830 y: 8.0,
17831 width: 20.0,
17832 height: 12.0,
17833 }
17834 );
17835 }
17836
17837 #[test]
17838 fn clamp_effect_surface_scale_caps_large_surfaces_but_keeps_base_scale() {
17839 let clamped = clamp_effect_surface_scale(
17840 Rect {
17841 x: 0.0,
17842 y: 0.0,
17843 width: 1200.0,
17844 height: 900.0,
17845 },
17846 1.0,
17847 8.0,
17848 16_384,
17849 );
17850
17851 assert!(
17852 clamped < 8.0,
17853 "large translated effect layers must be capped to avoid OOM, got {clamped}"
17854 );
17855 assert!(
17856 clamped >= 1.0,
17857 "effect surfaces must not fall below destination resolution, got {clamped}"
17858 );
17859 }
17860
17861 #[test]
17862 fn clamp_effect_surface_scale_keeps_decorated_text_capture_scale() {
17863 let clamped = clamp_effect_surface_scale(
17864 Rect {
17865 x: 0.0,
17866 y: 0.0,
17867 width: 446.0,
17868 height: 44.0,
17869 },
17870 1.0,
17871 9.0,
17872 16_384,
17873 );
17874
17875 assert_eq!(
17876 clamped, 9.0,
17877 "decorated text motion-stable captures must keep full scale"
17878 );
17879 }
17880
17881 fn backdrop_layer(z_index: usize) -> BackdropLayer {
17882 BackdropLayer {
17883 node_id: Some(700 + z_index),
17884 rect: Rect {
17885 x: 0.0,
17886 y: 0.0,
17887 width: 10.0,
17888 height: 10.0,
17889 },
17890 clip: None,
17891 snap_anchor: None,
17892 effect: RenderEffect::blur(2.0),
17893 z_index,
17894 }
17895 }
17896
17897 fn test_shape(z_index: usize, blend_mode: BlendMode) -> DrawShape {
17898 DrawShape {
17899 rect: Rect {
17900 x: 0.0,
17901 y: 0.0,
17902 width: 8.0,
17903 height: 8.0,
17904 },
17905 local_rect: Rect {
17906 x: 0.0,
17907 y: 0.0,
17908 width: 8.0,
17909 height: 8.0,
17910 },
17911 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
17912 snap_anchor: None,
17913 brush: SceneBrush::Solid(Color::BLACK),
17914 shape: None,
17915 stroke: None,
17916 arc: None,
17917 z_index,
17918 clip: None,
17919 blend_mode,
17920 motion_context_animated: false,
17921 }
17922 }
17923
17924 #[test]
17925 fn shape_shadow_content_hash_ignores_viewport_translation() {
17926 fn translate_shape(shape: &DrawShape, dx: f32, dy: f32) -> DrawShape {
17927 let mut translated = *shape;
17928 translated.rect.x += dx;
17929 translated.rect.y += dy;
17930 translated.local_rect.x += dx;
17931 translated.local_rect.y += dy;
17932 for point in &mut translated.quad {
17933 point[0] += dx;
17934 point[1] += dy;
17935 }
17936 translated.snap_anchor = translated.snap_anchor.map(|anchor| {
17937 SnapAnchor::rigid(Point::new(anchor.origin.x + dx, anchor.origin.y + dy))
17938 });
17939 translated.clip = translated.clip.map(|mut clip| {
17940 clip.x += dx;
17941 clip.y += dy;
17942 clip
17943 });
17944 translated
17945 }
17946
17947 let mut first = test_shape(1, BlendMode::SrcOver);
17948 first.rect = Rect {
17949 x: 10.0,
17950 y: 20.0,
17951 width: 80.0,
17952 height: 40.0,
17953 };
17954 first.local_rect = first.rect;
17955 first.quad = [[10.0, 20.0], [90.0, 20.0], [10.0, 60.0], [90.0, 60.0]];
17956 first.snap_anchor = Some(SnapAnchor::rigid(Point::new(7.0, 11.0)));
17957 first.shape = Some(RoundedCornerShape::uniform(8.0));
17958 first.clip = Some(Rect {
17959 x: 8.0,
17960 y: 18.0,
17961 width: 86.0,
17962 height: 44.0,
17963 });
17964 let mut cutout = test_shape(2, BlendMode::DstOut);
17965 cutout.rect = Rect {
17966 x: 18.0,
17967 y: 26.0,
17968 width: 62.0,
17969 height: 22.0,
17970 };
17971 cutout.local_rect = cutout.rect;
17972 cutout.quad = [[18.0, 26.0], [80.0, 26.0], [18.0, 48.0], [80.0, 48.0]];
17973 cutout.shape = Some(RoundedCornerShape::uniform(4.0));
17974
17975 let dx = 37.0;
17976 let dy = -11.5;
17977 let translated = translate_shape(&first, dx, dy);
17978 let translated_cutout = translate_shape(&cutout, dx, dy);
17979
17980 let root_scale = 1.25;
17981 let first_shapes = vec![(first, BlendMode::SrcOver), (cutout, BlendMode::DstOut)];
17982 let translated_shapes = vec![
17983 (translated, BlendMode::SrcOver),
17984 (translated_cutout, BlendMode::DstOut),
17985 ];
17986
17987 let first_hash = shape_shadow_content_hash(&first_shapes, &[], root_scale);
17988 let translated_hash = shape_shadow_content_hash(&translated_shapes, &[], root_scale);
17989
17990 assert_eq!(first_hash, translated_hash);
17991
17992 let mut changed_shapes = translated_shapes;
17993 changed_shapes[0].0.rect.width += 1.0;
17994 let changed_hash = shape_shadow_content_hash(&changed_shapes, &[], root_scale);
17995
17996 assert_ne!(first_hash, changed_hash);
17997 }
17998
17999 #[test]
18000 fn shape_shadow_content_hash_is_stable_under_fractional_scale_scroll() {
18001 fn shadow_shapes_at(y: f32) -> Vec<(DrawShape, BlendMode)> {
18007 let mut shape = test_shape(1, BlendMode::SrcOver);
18008 shape.rect = Rect {
18009 x: 24.0,
18010 y,
18011 width: 180.0,
18012 height: 90.0,
18013 };
18014 shape.local_rect = shape.rect;
18015 shape.quad = crate::rect_to_quad(shape.rect);
18016 shape.shape = Some(RoundedCornerShape::uniform(14.0));
18017 vec![(shape, BlendMode::SrcOver)]
18018 }
18019
18020 let root_scale = 130.0f32 / 96.0;
18021 let blur_radius = 18.0f32;
18022 let pixel_radius = blur_radius * root_scale;
18023
18024 let key_at = |y: f32| {
18025 let shapes = shadow_shapes_at(y);
18026 let plan =
18027 shape_shadow_surface_plan(&shapes, None, blur_radius, 1600, 1600, root_scale, 8192)
18028 .expect("surface plan");
18029 shape_shadow_surface_cache_key(
18030 &shapes,
18031 &[],
18032 plan.source_device_bounds,
18033 pixel_radius,
18034 root_scale,
18035 )
18036 .expect("cache key")
18037 };
18038
18039 let base = key_at(640.0);
18044 for step in 1..=12 {
18045 let scrolled = key_at(640.0 - step as f32 * 4.0);
18046 assert_eq!(
18047 base, scrolled,
18048 "scrolled shadow cache key must stay stable at fractional scale (step {step})"
18049 );
18050 }
18051 }
18052
18053 #[test]
18054 fn shape_shadow_cache_key_uses_unclipped_source_bounds_for_scrolled_clip() {
18055 fn translated_card_shadow(y: f32) -> Vec<(DrawShape, BlendMode)> {
18056 let mut shape = test_shape(1, BlendMode::SrcOver);
18057 shape.rect = Rect {
18058 x: 24.0,
18059 y,
18060 width: 280.0,
18061 height: 120.0,
18062 };
18063 shape.local_rect = shape.rect;
18064 shape.quad = [[24.0, y], [304.0, y], [24.0, y + 120.0], [304.0, y + 120.0]];
18065 shape.shape = Some(RoundedCornerShape::uniform(18.0));
18066 vec![(shape, BlendMode::SrcOver)]
18067 }
18068
18069 let root_scale = 1.0;
18070 let blur_radius = 18.0;
18071 let viewport_clip = Rect {
18072 x: 0.0,
18073 y: 96.0,
18074 width: 360.0,
18075 height: 720.0,
18076 };
18077 let key_for = |y: f32| {
18078 let shapes = translated_card_shadow(y);
18079 let plan = shape_shadow_surface_plan(
18080 &shapes,
18081 Some(viewport_clip),
18082 blur_radius,
18083 360,
18084 900,
18085 root_scale,
18086 4096,
18087 )
18088 .expect("surface plan");
18089 shape_shadow_surface_cache_key(
18090 &shapes,
18091 &[],
18092 plan.source_device_bounds,
18093 plan.pixel_radius,
18094 root_scale,
18095 )
18096 .expect("cache key")
18097 };
18098
18099 assert_eq!(key_for(740.0), key_for(756.0));
18102 }
18103
18104 #[test]
18105 fn shape_visibility_uses_nonzero_viewport_offset_for_cropped_offscreen() {
18106 let mut shape = test_shape(1, BlendMode::SrcOver);
18107 shape.rect = Rect {
18108 x: 24.0,
18109 y: 740.0,
18110 width: 280.0,
18111 height: 120.0,
18112 };
18113 shape.local_rect = shape.rect;
18114 shape.quad = [[24.0, 740.0], [304.0, 740.0], [24.0, 860.0], [304.0, 860.0]];
18115 let viewport = ViewportUniformParams {
18116 width: 316,
18117 height: 228,
18118 offset: [6.0, 686.0],
18119 };
18120
18121 assert!(shape_draw_is_visible_in_viewport(&shape, viewport, 1.0));
18122 }
18123
18124 #[test]
18125 fn text_prewarm_uses_nonzero_viewport_offset_for_cropped_offscreen() {
18126 let viewport = ViewportUniformParams {
18127 width: 316,
18128 height: 228,
18129 offset: [6.0, 686.0],
18130 };
18131 let text_rect = Rect {
18132 x: 24.0,
18133 y: 740.0,
18134 width: 280.0,
18135 height: 40.0,
18136 };
18137
18138 assert!(text_draw_is_visible_in_viewport(
18139 text_rect, None, viewport, 1.0
18140 ));
18141 assert!(text_draw_should_prewarm_in_viewport(
18142 text_rect, None, viewport, 1.0
18143 ));
18144 }
18145
18146 fn test_shadow_draw(shapes: Vec<(DrawShape, BlendMode)>) -> ShadowDraw {
18147 ShadowDraw {
18148 shapes,
18149 brushes: vec![],
18150 texts: vec![],
18151 blur_radius: 8.0,
18152 clip: None,
18153 z_index: 0,
18154 }
18155 }
18156
18157 fn test_image(z_index: usize, blend_mode: BlendMode) -> ImageDraw {
18158 ImageDraw {
18159 rect: Rect {
18160 x: 0.0,
18161 y: 0.0,
18162 width: 8.0,
18163 height: 8.0,
18164 },
18165 local_rect: Rect {
18166 x: 0.0,
18167 y: 0.0,
18168 width: 8.0,
18169 height: 8.0,
18170 },
18171 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
18172 snap_anchor: None,
18173 image: ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image"),
18174 alpha: 1.0,
18175 color_filter: None,
18176 sampling: ImageSampling::Nearest,
18177 z_index,
18178 clip: None,
18179 blend_mode,
18180 src_rect: None,
18181 motion_context_animated: false,
18182 }
18183 }
18184
18185 #[test]
18186 fn image_sampler_descriptors_match_requested_sampling() {
18187 let nearest = image_sampler_descriptor(ImageSampling::Nearest);
18188 assert_eq!(nearest.mag_filter, wgpu::FilterMode::Nearest);
18189 assert_eq!(nearest.min_filter, wgpu::FilterMode::Nearest);
18190
18191 let linear = image_sampler_descriptor(ImageSampling::Linear);
18192 assert_eq!(linear.mag_filter, wgpu::FilterMode::Linear);
18193 assert_eq!(linear.min_filter, wgpu::FilterMode::Linear);
18194 }
18195
18196 #[test]
18197 fn image_uv_rect_clamps_source_rect_to_texel_centers() {
18198 let image = ImageBitmap::from_rgba8(24, 16, vec![0; 24 * 16 * 4]).expect("image");
18199 let uv = image_uv_rect(
18200 &image,
18201 Some(Rect {
18202 x: 0.0,
18203 y: 0.0,
18204 width: 16.0,
18205 height: 16.0,
18206 }),
18207 )
18208 .expect("uv rect");
18209
18210 assert_eq!(uv.min, [0.0, 0.0]);
18211 assert_eq!(uv.max, [16.0 / 24.0, 1.0]);
18212 assert_eq!(
18213 uv.sample_bounds,
18214 [0.5 / 24.0, 0.5 / 16.0, 15.5 / 24.0, 15.5 / 16.0]
18215 );
18216 }
18217
18218 #[test]
18219 fn image_uv_rect_keeps_full_image_unclamped() {
18220 let image = ImageBitmap::from_rgba8(2, 2, vec![0; 16]).expect("image");
18221 let uv = image_uv_rect(&image, None).expect("uv rect");
18222
18223 assert_eq!(uv.min, [0.0, 0.0]);
18224 assert_eq!(uv.max, [1.0, 1.0]);
18225 assert_eq!(uv.sample_bounds, [0.0, 0.0, 1.0, 1.0]);
18226 }
18227
18228 fn test_text(z_index: usize) -> TextDraw {
18229 TextDraw {
18230 node_id: 0,
18231 rect: Rect {
18232 x: 0.0,
18233 y: 0.0,
18234 width: 8.0,
18235 height: 8.0,
18236 },
18237 snap_anchor: None,
18238 translated_content_context: false,
18239 text: Arc::new(cranpose_ui::text::AnnotatedString::from("t").render_string()),
18240 color: Color::WHITE,
18241 text_style: cranpose_ui::TextStyle::default(),
18242 font_size: 12.0,
18243 scale: 1.0,
18244 layout_options: cranpose_ui::TextLayoutOptions::default(),
18245 z_index,
18246 clip: None,
18247 }
18248 }
18249
18250 #[test]
18251 fn text_draw_visibility_rejects_text_outside_clip_before_rasterization() {
18252 let viewport = ViewportUniformParams {
18253 width: 320,
18254 height: 240,
18255 offset: [0.0, 0.0],
18256 };
18257 let text_rect = Rect {
18258 x: 0.0,
18259 y: 260.0,
18260 width: 200.0,
18261 height: 40.0,
18262 };
18263 let clip = Some(Rect {
18264 x: 0.0,
18265 y: 0.0,
18266 width: 320.0,
18267 height: 200.0,
18268 });
18269
18270 assert!(
18271 !text_draw_is_visible_in_viewport(text_rect, clip, viewport, 1.0),
18272 "lazy-list beyond-bound text outside the clip must not be rasterized"
18273 );
18274 }
18275
18276 #[test]
18277 fn text_draw_prewarm_accepts_clipped_text_near_viewport() {
18278 let viewport = ViewportUniformParams {
18279 width: 320,
18280 height: 240,
18281 offset: [0.0, 0.0],
18282 };
18283 let text_rect = Rect {
18284 x: 0.0,
18285 y: 260.0,
18286 width: 200.0,
18287 height: 40.0,
18288 };
18289 let clip = Some(Rect {
18290 x: 0.0,
18291 y: 0.0,
18292 width: 320.0,
18293 height: 200.0,
18294 });
18295
18296 assert!(!text_draw_is_visible_in_viewport(
18297 text_rect, clip, viewport, 1.0
18298 ));
18299 assert!(text_draw_should_prewarm_in_viewport(
18300 text_rect, clip, viewport, 1.0
18301 ));
18302 }
18303
18304 #[test]
18305 fn text_draw_prewarm_rejects_far_clipped_text() {
18306 let viewport = ViewportUniformParams {
18307 width: 320,
18308 height: 240,
18309 offset: [0.0, 0.0],
18310 };
18311 let text_rect = Rect {
18312 x: 0.0,
18313 y: 1600.0,
18314 width: 200.0,
18315 height: 40.0,
18316 };
18317 let clip = Some(Rect {
18318 x: 0.0,
18319 y: 0.0,
18320 width: 320.0,
18321 height: 200.0,
18322 });
18323
18324 assert!(!text_draw_should_prewarm_in_viewport(
18325 text_rect, clip, viewport, 1.0
18326 ));
18327 }
18328
18329 #[test]
18330 fn text_draw_visibility_rejects_unclipped_text_outside_viewport() {
18331 let viewport = ViewportUniformParams {
18332 width: 320,
18333 height: 240,
18334 offset: [0.0, 0.0],
18335 };
18336 let text_rect = Rect {
18337 x: 0.0,
18338 y: 241.0,
18339 width: 200.0,
18340 height: 40.0,
18341 };
18342
18343 assert!(
18344 !text_draw_is_visible_in_viewport(text_rect, None, viewport, 1.0),
18345 "unclipped text outside the target viewport must not be rasterized"
18346 );
18347 }
18348
18349 #[test]
18350 fn text_draw_visibility_keeps_partially_visible_text() {
18351 let viewport = ViewportUniformParams {
18352 width: 320,
18353 height: 240,
18354 offset: [0.0, 0.0],
18355 };
18356 let text_rect = Rect {
18357 x: 0.0,
18358 y: 220.0,
18359 width: 200.0,
18360 height: 40.0,
18361 };
18362
18363 assert!(text_draw_is_visible_in_viewport(
18364 text_rect, None, viewport, 1.0
18365 ));
18366 }
18367
18368 fn test_draw_ops(
18369 shapes: &[DrawShape],
18370 images: &[ImageDraw],
18371 texts: &[TextDraw],
18372 shadows: &[ShadowDraw],
18373 ) -> Vec<DrawOp> {
18374 let mut ops = Vec::new();
18375 ops.extend(shapes.iter().enumerate().map(|(index, shape)| DrawOp {
18376 z_index: shape.z_index,
18377 kind: DrawOpKind::Shape(index),
18378 }));
18379 ops.extend(images.iter().enumerate().map(|(index, image)| DrawOp {
18380 z_index: image.z_index,
18381 kind: DrawOpKind::Image(index),
18382 }));
18383 ops.extend(texts.iter().enumerate().map(|(index, text)| DrawOp {
18384 z_index: text.z_index,
18385 kind: DrawOpKind::Text(index),
18386 }));
18387 ops.extend(shadows.iter().enumerate().map(|(index, shadow)| DrawOp {
18388 z_index: shadow.z_index,
18389 kind: DrawOpKind::Shadow(index),
18390 }));
18391 ops.sort_by_key(|op| op.z_index);
18392 ops
18393 }
18394
18395 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
18396 crate::test_support::layer_node(
18397 local_bounds,
18398 ProjectiveTransform::identity(),
18399 GraphicsLayer::default(),
18400 children,
18401 )
18402 }
18403
18404 fn cacheable_layer(
18405 node_id: cranpose_core::NodeId,
18406 local_bounds: Rect,
18407 children: Vec<RenderNode>,
18408 ) -> LayerNode {
18409 let mut layer = test_layer(local_bounds, children);
18410 layer.node_id = Some(node_id);
18411 layer.cache_policy = cranpose_render_common::graph::CachePolicy::Auto;
18412 layer.recompute_raster_cache_hashes();
18413 layer
18414 }
18415
18416 fn text_layer_with_style(text: AnnotatedString, text_style: TextStyle) -> LayerNode {
18417 test_layer(
18418 Rect {
18419 x: 0.0,
18420 y: 0.0,
18421 width: 64.0,
18422 height: 32.0,
18423 },
18424 vec![RenderNode::Primitive(PrimitiveEntry {
18425 phase: PrimitivePhase::BeforeChildren,
18426 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
18427 node_id: 1,
18428 rect: Rect {
18429 x: 2.0,
18430 y: 3.0,
18431 width: 48.0,
18432 height: 18.0,
18433 },
18434 text: std::rc::Rc::new(text),
18435 text_style,
18436 font_size: 14.0,
18437 layout_options: TextLayoutOptions::default(),
18438 clip: None,
18439 })),
18440 })],
18441 )
18442 }
18443
18444 fn snapped_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
18445 LayerNode {
18446 node_id: Some(77),
18447 local_bounds: Rect {
18448 x: 0.0,
18449 y: 0.0,
18450 width: 48.0,
18451 height: 24.0,
18452 },
18453 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
18454 motion_context_animated: animated,
18455 translated_content_context,
18456 translated_content_offset: Point::default(),
18457 content_offset: Point::default(),
18458 scene_children_origin: cranpose_ui_graphics::Point::default(),
18459 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
18460 graphics_layer: GraphicsLayer::default(),
18461 clip_to_bounds: false,
18462 shadow_clip: None,
18463 hit_test: None,
18464 has_hit_targets: false,
18465 isolation: IsolationReasons::default(),
18466 cache_policy: CachePolicy::None,
18467 cache_hashes: LayerRasterCacheHashes::default(),
18468 cache_hashes_valid: false,
18469 children: vec![
18470 RenderNode::Primitive(PrimitiveEntry {
18471 phase: PrimitivePhase::BeforeChildren,
18472 node: PrimitiveNode::Draw(DrawPrimitiveNode {
18473 primitive: DrawPrimitive::RoundRect {
18474 rect: Rect {
18475 x: 0.0,
18476 y: 0.0,
18477 width: 48.0,
18478 height: 24.0,
18479 },
18480 brush: Brush::solid(Color(0.28, 0.30, 0.46, 0.88)),
18481 radii: CornerRadii::uniform(6.0),
18482 stroke: None,
18483 },
18484 clip: None,
18485 }),
18486 }),
18487 RenderNode::Primitive(PrimitiveEntry {
18488 phase: PrimitivePhase::BeforeChildren,
18489 node: PrimitiveNode::Draw(DrawPrimitiveNode {
18490 primitive: DrawPrimitive::Image {
18491 rect: Rect {
18492 x: 2.0,
18493 y: 2.0,
18494 width: 12.0,
18495 height: 12.0,
18496 },
18497 image: ImageBitmap::from_rgba8(
18498 2,
18499 2,
18500 vec![
18501 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255,
18502 255,
18503 ],
18504 )
18505 .expect("image"),
18506 alpha: 1.0,
18507 color_filter: None,
18508 sampling: ImageSampling::Linear,
18509 src_rect: None,
18510 },
18511 clip: None,
18512 }),
18513 }),
18514 RenderNode::Primitive(PrimitiveEntry {
18515 phase: PrimitivePhase::BeforeChildren,
18516 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
18517 node_id: 77,
18518 rect: Rect {
18519 x: 6.0,
18520 y: 4.0,
18521 width: 36.0,
18522 height: 16.0,
18523 },
18524 text: std::rc::Rc::new(AnnotatedString::from("48 px")),
18525 text_style: TextStyle::default(),
18526 font_size: 14.0,
18527 layout_options: TextLayoutOptions::default(),
18528 clip: None,
18529 })),
18530 }),
18531 ],
18532 }
18533 }
18534
18535 fn snapped_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
18536 let text_leaf = snapped_text_leaf(animated, translated_content_context);
18537 test_layer(
18538 Rect {
18539 x: 0.0,
18540 y: 0.0,
18541 width: 96.0,
18542 height: 64.0,
18543 },
18544 vec![RenderNode::Layer(Box::new(text_leaf))],
18545 )
18546 }
18547
18548 fn translated_content_local_surface_root() -> LayerNode {
18549 let mut effectful_text = text_layer_with_style(
18550 AnnotatedString::from("shadow"),
18551 TextStyle::from_span_style(SpanStyle {
18552 shadow: Some(Shadow {
18553 color: Color::BLACK,
18554 offset: Point::new(1.0, 2.0),
18555 blur_radius: 3.0,
18556 }),
18557 ..SpanStyle::default()
18558 }),
18559 );
18560 effectful_text.translated_content_context = true;
18561
18562 let translated_content = LayerNode {
18563 node_id: Some(78),
18564 local_bounds: Rect {
18565 x: 0.0,
18566 y: 0.0,
18567 width: 96.0,
18568 height: 64.0,
18569 },
18570 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
18571 motion_context_animated: false,
18572 translated_content_context: true,
18573 translated_content_offset: Point::default(),
18574 content_offset: Point::default(),
18575 scene_children_origin: cranpose_ui_graphics::Point::default(),
18576 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
18577 graphics_layer: GraphicsLayer::default(),
18578 clip_to_bounds: false,
18579 shadow_clip: None,
18580 hit_test: None,
18581 has_hit_targets: false,
18582 isolation: IsolationReasons::default(),
18583 cache_policy: CachePolicy::None,
18584 cache_hashes: LayerRasterCacheHashes::default(),
18585 cache_hashes_valid: false,
18586 children: vec![RenderNode::Layer(Box::new(effectful_text))],
18587 };
18588
18589 test_layer(
18590 Rect {
18591 x: 0.0,
18592 y: 0.0,
18593 width: 160.0,
18594 height: 120.0,
18595 },
18596 vec![RenderNode::Layer(Box::new(translated_content))],
18597 )
18598 }
18599
18600 #[test]
18601 fn scissor_rect_for_layer_intersects_with_clip() {
18602 let rect = Rect {
18603 x: 10.0,
18604 y: 10.0,
18605 width: 30.0,
18606 height: 20.0,
18607 };
18608 let clip = Rect {
18609 x: 20.0,
18610 y: 15.0,
18611 width: 100.0,
18612 height: 100.0,
18613 };
18614
18615 let scissor = scissor_rect_for_layer(rect, Some(clip), 1.0, 200, 200);
18616 assert_eq!(scissor, Some((20, 15, 20, 15)));
18617 }
18618
18619 #[test]
18620 fn visible_draw_rect_no_clip_returns_original() {
18621 let rect = Rect {
18622 x: 100.0,
18623 y: 200.0,
18624 width: 300.0,
18625 height: 400.0,
18626 };
18627 assert_eq!(visible_draw_rect(rect, None), Some(rect));
18628 }
18629
18630 #[test]
18631 fn visible_draw_rect_with_clip_intersects() {
18632 let rect = Rect {
18633 x: 0.0,
18634 y: 0.0,
18635 width: 2000.0,
18636 height: 5000.0,
18637 };
18638 let clip = Rect {
18639 x: 0.0,
18640 y: 0.0,
18641 width: 800.0,
18642 height: 600.0,
18643 };
18644 let visible = visible_draw_rect(rect, Some(clip)).expect("should have visible area");
18645 assert_eq!(visible.width, 800.0);
18646 assert_eq!(visible.height, 600.0);
18647 }
18648
18649 #[test]
18650 fn visible_draw_rect_fully_clipped_returns_none() {
18651 let rect = Rect {
18652 x: 1000.0,
18653 y: 1000.0,
18654 width: 200.0,
18655 height: 200.0,
18656 };
18657 let clip = Rect {
18658 x: 0.0,
18659 y: 0.0,
18660 width: 800.0,
18661 height: 600.0,
18662 };
18663 assert!(visible_draw_rect(rect, Some(clip)).is_none());
18664 }
18665
18666 #[test]
18667 fn scene_bounds_respects_clip_on_shapes() {
18668 let mut scene = CompositorScene::new();
18669 scene.shapes.push(DrawShape {
18671 rect: Rect {
18672 x: 10.0,
18673 y: 10.0,
18674 width: 100.0,
18675 height: 50.0,
18676 },
18677 clip: Some(Rect {
18678 x: 0.0,
18679 y: 0.0,
18680 width: 800.0,
18681 height: 600.0,
18682 }),
18683 ..test_shape(0, BlendMode::SrcOver)
18684 });
18685 scene.shapes.push(DrawShape {
18687 rect: Rect {
18688 x: 0.0,
18689 y: 3000.0,
18690 width: 100.0,
18691 height: 50.0,
18692 },
18693 clip: Some(Rect {
18694 x: 0.0,
18695 y: 0.0,
18696 width: 800.0,
18697 height: 600.0,
18698 }),
18699 ..test_shape(1, BlendMode::SrcOver)
18700 });
18701 let bounds = scene_bounds(&scene).expect("should have bounds");
18702 assert!(bounds.y + bounds.height <= 600.0);
18705 }
18706
18707 #[test]
18708 fn scene_bounds_scroll_content_clipped_to_viewport() {
18709 let mut scene = CompositorScene::new();
18712 let viewport_clip = Rect {
18713 x: 0.0,
18714 y: 0.0,
18715 width: 800.0,
18716 height: 600.0,
18717 };
18718 for i in 0..20 {
18719 scene.shapes.push(DrawShape {
18720 rect: Rect {
18721 x: 0.0,
18722 y: i as f32 * 300.0,
18723 width: 800.0,
18724 height: 200.0,
18725 },
18726 clip: Some(viewport_clip),
18727 ..test_shape(i, BlendMode::SrcOver)
18728 });
18729 }
18730 let bounds = scene_bounds(&scene).expect("should have bounds");
18731 assert_eq!(bounds.x, 0.0);
18734 assert_eq!(bounds.y, 0.0);
18735 assert!(bounds.width <= 800.0);
18736 assert!(bounds.height <= 600.0);
18737 }
18738
18739 #[test]
18740 fn scene_bounds_stable_across_scroll_offsets() {
18741 let viewport_clip = Rect {
18744 x: 0.0,
18745 y: 0.0,
18746 width: 400.0,
18747 height: 50.0,
18748 };
18749 let compute_bounds_at_offset = |scroll_x: f32| {
18750 let mut scene = CompositorScene::new();
18751 for i in 0..10 {
18752 scene.shapes.push(DrawShape {
18753 rect: Rect {
18754 x: i as f32 * 100.0 - scroll_x,
18755 y: 0.0,
18756 width: 80.0,
18757 height: 40.0,
18758 },
18759 clip: Some(viewport_clip),
18760 ..test_shape(i, BlendMode::SrcOver)
18761 });
18762 }
18763 scene_bounds(&scene).expect("bounds")
18764 };
18765 let bounds_at_0 = compute_bounds_at_offset(0.0);
18766 let bounds_at_300 = compute_bounds_at_offset(300.0);
18767 let bounds_at_600 = compute_bounds_at_offset(600.0);
18768 assert!(
18770 (bounds_at_0.width - bounds_at_300.width).abs() < 1.0,
18771 "bounds width changed with scroll: {} vs {}",
18772 bounds_at_0.width,
18773 bounds_at_300.width
18774 );
18775 assert!(
18776 (bounds_at_0.width - bounds_at_600.width).abs() < 1.0,
18777 "bounds width changed with scroll: {} vs {}",
18778 bounds_at_0.width,
18779 bounds_at_600.width
18780 );
18781 }
18782
18783 #[test]
18784 fn collect_effect_ranges_respects_excluded_effect() {
18785 let layers = vec![effect_layer(10, 40), effect_layer(20, 30)];
18786 let mut ranges = Vec::new();
18787 collect_effect_ranges(&layers, 10, 40, Some(0), &mut ranges);
18788 assert_eq!(ranges.len(), 1);
18789 assert_eq!(ranges[0], 20..30);
18790 }
18791
18792 #[test]
18793 fn collect_layer_events_includes_nested_when_parent_excluded() {
18794 let effects = vec![effect_layer(10, 40), effect_layer(20, 30)];
18795 let backdrops = vec![backdrop_layer(25)];
18796 let mut events = Vec::new();
18797 collect_layer_events(&effects, &backdrops, 10, 40, Some(0), &mut events);
18798 assert_eq!(events.len(), 2);
18799
18800 match events[0].kind {
18801 LayerEventKind::Effect(index) => assert_eq!(index, 1),
18802 LayerEventKind::Backdrop(_) => panic!("expected nested effect as first event"),
18803 }
18804 match events[1].kind {
18805 LayerEventKind::Backdrop(index) => assert_eq!(index, 0),
18806 LayerEventKind::Effect(_) => panic!("expected backdrop as second event"),
18807 }
18808 }
18809
18810 fn pure_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
18811 LayerNode {
18812 node_id: Some(177),
18813 local_bounds: Rect {
18814 x: 0.0,
18815 y: 0.0,
18816 width: 96.0,
18817 height: 32.0,
18818 },
18819 transform_to_parent: ProjectiveTransform::translation(11.4, 23.6),
18820 motion_context_animated: animated,
18821 translated_content_context,
18822 translated_content_offset: Point::default(),
18823 content_offset: Point::default(),
18824 scene_children_origin: cranpose_ui_graphics::Point::default(),
18825 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
18826 graphics_layer: GraphicsLayer::default(),
18827 clip_to_bounds: false,
18828 shadow_clip: None,
18829 hit_test: None,
18830 has_hit_targets: false,
18831 isolation: IsolationReasons::default(),
18832 cache_policy: CachePolicy::None,
18833 cache_hashes: LayerRasterCacheHashes::default(),
18834 cache_hashes_valid: false,
18835 children: vec![RenderNode::Primitive(PrimitiveEntry {
18836 phase: PrimitivePhase::BeforeChildren,
18837 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
18838 node_id: 177,
18839 rect: Rect {
18840 x: 0.0,
18841 y: 0.0,
18842 width: 96.0,
18843 height: 24.0,
18844 },
18845 clip: None,
18846 text: std::rc::Rc::new(AnnotatedString::from("Pure text")),
18847 text_style: TextStyle::default(),
18848 font_size: 14.0,
18849 layout_options: TextLayoutOptions::default(),
18850 })),
18851 })],
18852 }
18853 }
18854
18855 fn pure_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
18856 let text_leaf = pure_text_leaf(animated, translated_content_context);
18857 test_layer(
18858 Rect {
18859 x: 0.0,
18860 y: 0.0,
18861 width: 160.0,
18862 height: 96.0,
18863 },
18864 vec![RenderNode::Layer(Box::new(text_leaf))],
18865 )
18866 }
18867
18868 #[test]
18869 fn collect_layer_events_sorts_backdrop_before_effect_at_same_z() {
18870 let effects = vec![effect_layer(10, 20)];
18871 let backdrops = vec![backdrop_layer(10)];
18872 let mut events = Vec::new();
18873 collect_layer_events(&effects, &backdrops, 0, 30, None, &mut events);
18874 assert_eq!(events.len(), 2);
18875
18876 match events[0].kind {
18877 LayerEventKind::Backdrop(_) => {}
18878 LayerEventKind::Effect(_) => panic!("expected backdrop to run before effect"),
18879 }
18880 match events[1].kind {
18881 LayerEventKind::Effect(_) => {}
18882 LayerEventKind::Backdrop(_) => panic!("expected effect as second event"),
18883 }
18884 }
18885
18886 #[test]
18887 fn collect_layer_events_prefers_outer_effect_when_same_start_z() {
18888 let effects = vec![effect_layer(10, 20), effect_layer(10, 40)];
18891 let mut events = Vec::new();
18892 collect_layer_events(&effects, &[], 0, 50, None, &mut events);
18893
18894 assert_eq!(events.len(), 2);
18895 match events[0].kind {
18896 LayerEventKind::Effect(index) => assert_eq!(index, 1),
18897 LayerEventKind::Backdrop(_) => panic!("expected outer effect first"),
18898 }
18899 match events[1].kind {
18900 LayerEventKind::Effect(index) => assert_eq!(index, 0),
18901 LayerEventKind::Backdrop(_) => panic!("expected child effect second"),
18902 }
18903 }
18904
18905 #[test]
18906 fn collect_layer_events_prefers_later_effect_when_ranges_match() {
18907 let effects = vec![effect_layer(10, 20), effect_layer(10, 20)];
18908 let mut events = Vec::new();
18909 collect_layer_events(&effects, &[], 0, 30, None, &mut events);
18910
18911 assert_eq!(events.len(), 2);
18912 match events[0].kind {
18913 LayerEventKind::Effect(index) => assert_eq!(index, 1),
18914 LayerEventKind::Backdrop(_) => panic!("expected later effect first"),
18915 }
18916 match events[1].kind {
18917 LayerEventKind::Effect(index) => assert_eq!(index, 0),
18918 LayerEventKind::Backdrop(_) => panic!("expected earlier effect second"),
18919 }
18920 }
18921
18922 #[test]
18923 fn has_backdrop_layer_in_range_detects_nested_layers() {
18924 let backdrops = vec![backdrop_layer(5), backdrop_layer(15), backdrop_layer(25)];
18925 assert!(has_backdrop_layer_in_range(&backdrops, 10, 20));
18926 assert!(has_backdrop_layer_in_range(&backdrops, 0, 6));
18927 assert!(!has_backdrop_layer_in_range(&backdrops, 20, 25));
18928 }
18929
18930 #[test]
18931 fn layer_contains_descendant_backdrop_ignores_self_backdrop() {
18932 let mut self_backdrop = test_layer(
18933 Rect {
18934 x: 0.0,
18935 y: 0.0,
18936 width: 10.0,
18937 height: 10.0,
18938 },
18939 vec![],
18940 );
18941 self_backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
18942 assert!(!layer_contains_descendant_backdrop(&self_backdrop));
18943
18944 let mut child = test_layer(
18945 Rect {
18946 x: 0.0,
18947 y: 0.0,
18948 width: 8.0,
18949 height: 8.0,
18950 },
18951 vec![],
18952 );
18953 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
18954
18955 let parent = test_layer(
18956 Rect {
18957 x: 0.0,
18958 y: 0.0,
18959 width: 20.0,
18960 height: 20.0,
18961 },
18962 vec![RenderNode::Layer(Box::new(child))],
18963 );
18964 assert!(layer_contains_descendant_backdrop(&parent));
18965 }
18966
18967 fn child_layer_composite(
18968 layer: &LayerNode,
18969 z_index: usize,
18970 rect: Rect,
18971 needs_nested_underlay: bool,
18972 ) -> crate::normalized_scene::ChildLayerComposite {
18973 let mut requirements_cache = cranpose_core::collections::map::HashMap::new();
18974 let surface_requirements =
18975 crate::surface_plan::layer_surface_requirements_cached(layer, &mut requirements_cache);
18976 crate::normalized_scene::ChildLayerComposite {
18977 z_index,
18978 logical_rect: Rect {
18979 x: 0.0,
18980 y: 0.0,
18981 width: rect.width,
18982 height: rect.height,
18983 },
18984 dest_quad: rect_to_quad(rect),
18985 snap_anchor: None,
18986 composite_snap_origin: None,
18987 backdrop_rect: rect,
18988 visual_clip: None,
18989 surface_clip: None,
18990 shadow_draws: Vec::new(),
18991 needs_nested_underlay,
18992 node_id: layer.node_id,
18993 backdrop: layer.backdrop().cloned(),
18994 has_effect: layer.effect().is_some(),
18995 effect_contains_runtime_shader: layer
18996 .effect()
18997 .is_some_and(|effect| effect.contains_runtime_shader()),
18998 target_content_hash: layer.target_content_hash(),
18999 effect_hash: layer.effect_hash(),
19000 motion_source_content_hash: Some(layer.motion_source_content_hash()),
19001 contains_descendant_backdrop: layer_contains_descendant_backdrop(layer),
19002 cache_policy: layer.cache_policy,
19003 surface_requirements,
19004 rounded_clip: crate::surface_executor::backend::LayerSurfaceRoundedClip::from_layer(
19005 layer,
19006 ),
19007 isolation: cranpose_render_common::layer_composition::effective_layer_isolation(
19008 &layer.graphics_layer,
19009 ),
19010 translated_content_context: layer.translated_content_context,
19011 own_translated_content_axes: crate::surface_plan::translated_content_axes_for_layer(
19012 layer,
19013 ),
19014 clip_rect: layer.clip_rect(),
19015 local_bounds: layer.local_bounds,
19016 surface_scale: crate::surface_plan::layer_surface_scale(layer),
19017 source: crate::normalized_scene::LoweredChildSource::default(),
19018 }
19019 }
19020
19021 #[test]
19022 fn root_direct_preflight_allows_first_translated_child_underlay() {
19023 let child = test_layer(
19024 Rect {
19025 x: 0.0,
19026 y: 0.0,
19027 width: 400.0,
19028 height: 280.0,
19029 },
19030 vec![],
19031 );
19032 let collected = CollectedLayer {
19033 scene: CompositorScene::new(),
19034 child_layers: vec![child_layer_composite(
19035 &child,
19036 3,
19037 Rect {
19038 x: 48.0,
19039 y: 96.0,
19040 width: 400.0,
19041 height: 280.0,
19042 },
19043 true,
19044 )],
19045 };
19046
19047 assert!(direct_root_child_underlays_are_supported(&collected, false));
19048 }
19049
19050 #[test]
19051 fn root_direct_preflight_allows_axis_aligned_prior_child_underlay() {
19052 let first = test_layer(
19053 Rect {
19054 x: 0.0,
19055 y: 0.0,
19056 width: 80.0,
19057 height: 40.0,
19058 },
19059 vec![],
19060 );
19061 let backdrop_child = test_layer(
19062 Rect {
19063 x: 0.0,
19064 y: 0.0,
19065 width: 400.0,
19066 height: 280.0,
19067 },
19068 vec![],
19069 );
19070 let collected = CollectedLayer {
19071 scene: CompositorScene::new(),
19072 child_layers: vec![
19073 child_layer_composite(
19074 &first,
19075 1,
19076 Rect {
19077 x: 8.0,
19078 y: 16.0,
19079 width: 80.0,
19080 height: 40.0,
19081 },
19082 false,
19083 ),
19084 child_layer_composite(
19085 &backdrop_child,
19086 4,
19087 Rect {
19088 x: 48.0,
19089 y: 96.0,
19090 width: 400.0,
19091 height: 280.0,
19092 },
19093 true,
19094 ),
19095 ],
19096 };
19097
19098 assert!(direct_root_child_underlays_are_supported(&collected, false));
19099 }
19100
19101 #[test]
19102 fn root_direct_preflight_rejects_effectful_prior_child_underlay() {
19103 let mut first = test_layer(
19104 Rect {
19105 x: 0.0,
19106 y: 0.0,
19107 width: 80.0,
19108 height: 40.0,
19109 },
19110 vec![],
19111 );
19112 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
19113 let backdrop_child = test_layer(
19114 Rect {
19115 x: 0.0,
19116 y: 0.0,
19117 width: 400.0,
19118 height: 280.0,
19119 },
19120 vec![],
19121 );
19122 let collected = CollectedLayer {
19123 scene: CompositorScene::new(),
19124 child_layers: vec![
19125 child_layer_composite(
19126 &first,
19127 1,
19128 Rect {
19129 x: 64.0,
19130 y: 112.0,
19131 width: 80.0,
19132 height: 40.0,
19133 },
19134 false,
19135 ),
19136 child_layer_composite(
19137 &backdrop_child,
19138 4,
19139 Rect {
19140 x: 48.0,
19141 y: 96.0,
19142 width: 400.0,
19143 height: 280.0,
19144 },
19145 true,
19146 ),
19147 ],
19148 };
19149
19150 assert!(!direct_root_child_underlays_are_supported(
19151 &collected, false
19152 ));
19153 }
19154
19155 #[test]
19156 fn root_direct_preflight_ignores_non_overlapping_effectful_prior_child_underlay() {
19157 let mut first = test_layer(
19158 Rect {
19159 x: 0.0,
19160 y: 0.0,
19161 width: 80.0,
19162 height: 40.0,
19163 },
19164 vec![],
19165 );
19166 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
19167 let backdrop_child = test_layer(
19168 Rect {
19169 x: 0.0,
19170 y: 0.0,
19171 width: 400.0,
19172 height: 280.0,
19173 },
19174 vec![],
19175 );
19176 let collected = CollectedLayer {
19177 scene: CompositorScene::new(),
19178 child_layers: vec![
19179 child_layer_composite(
19180 &first,
19181 1,
19182 Rect {
19183 x: 8.0,
19184 y: 16.0,
19185 width: 80.0,
19186 height: 40.0,
19187 },
19188 false,
19189 ),
19190 child_layer_composite(
19191 &backdrop_child,
19192 4,
19193 Rect {
19194 x: 48.0,
19195 y: 96.0,
19196 width: 400.0,
19197 height: 280.0,
19198 },
19199 true,
19200 ),
19201 ],
19202 };
19203
19204 assert!(direct_root_child_underlays_are_supported(&collected, false));
19205 }
19206
19207 #[test]
19208 fn root_direct_preflight_rejects_underlay_that_would_replay_prior_scene_effects() {
19209 let backdrop_child = test_layer(
19210 Rect {
19211 x: 0.0,
19212 y: 0.0,
19213 width: 400.0,
19214 height: 280.0,
19215 },
19216 vec![],
19217 );
19218 let mut scene = CompositorScene::new();
19219 scene.next_z = 1;
19220 scene.push_effect_layer(
19221 Rect {
19222 x: 0.0,
19223 y: 0.0,
19224 width: 120.0,
19225 height: 120.0,
19226 },
19227 None,
19228 Some(RenderEffect::blur(2.0)),
19229 BlendMode::SrcOver,
19230 1.0,
19231 0,
19232 1,
19233 );
19234 let collected = CollectedLayer {
19235 scene,
19236 child_layers: vec![child_layer_composite(
19237 &backdrop_child,
19238 4,
19239 Rect {
19240 x: 48.0,
19241 y: 96.0,
19242 width: 400.0,
19243 height: 280.0,
19244 },
19245 true,
19246 )],
19247 };
19248
19249 assert!(!direct_root_child_underlays_are_supported(
19250 &collected, false
19251 ));
19252 }
19253
19254 #[test]
19255 fn root_direct_eligibility_does_not_reject_descendant_backdrop() {
19256 let mut backdrop = test_layer(
19257 Rect {
19258 x: 0.0,
19259 y: 0.0,
19260 width: 40.0,
19261 height: 40.0,
19262 },
19263 vec![],
19264 );
19265 backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
19266 let child = test_layer(
19267 Rect {
19268 x: 0.0,
19269 y: 0.0,
19270 width: 120.0,
19271 height: 96.0,
19272 },
19273 vec![RenderNode::Layer(Box::new(backdrop))],
19274 );
19275 let root = test_layer(
19276 Rect {
19277 x: 0.0,
19278 y: 0.0,
19279 width: 240.0,
19280 height: 160.0,
19281 },
19282 vec![RenderNode::Layer(Box::new(child))],
19283 );
19284 let mut cache = HashMap::new();
19285
19286 assert!(root_can_render_directly_cached(&root, &mut cache));
19287 }
19288
19289 #[test]
19290 fn root_direct_scene_events_allow_root_local_effects() {
19291 let mut scene = CompositorScene::new();
19292 scene.effect_layers.push(EffectLayer {
19293 rect: Rect {
19294 x: 20.0,
19295 y: 30.0,
19296 width: 120.0,
19297 height: 80.0,
19298 },
19299 clip: None,
19300 snap_anchor: None,
19301 effect: Some(RenderEffect::blur(6.0)),
19302 blend_mode: BlendMode::SrcOver,
19303 composite_alpha: 1.0,
19304 z_start: 0,
19305 z_end: 1,
19306 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
19307 });
19308
19309 assert!(root_direct_scene_events_are_supported(&scene, false));
19310 }
19311
19312 #[test]
19313 fn root_direct_scene_events_reject_root_local_backdrops() {
19314 let mut scene = CompositorScene::new();
19315 scene.backdrop_layers.push(BackdropLayer {
19316 node_id: Some(99),
19317 rect: Rect {
19318 x: 20.0,
19319 y: 30.0,
19320 width: 120.0,
19321 height: 80.0,
19322 },
19323 clip: None,
19324 snap_anchor: None,
19325 effect: RenderEffect::blur(6.0),
19326 z_index: 1,
19327 });
19328
19329 assert!(!root_direct_scene_events_are_supported(&scene, false));
19330 }
19331
19332 fn scene_with_root_local_backdrop(z_index: usize) -> CompositorScene {
19333 let mut scene = CompositorScene::new();
19334 scene.next_z = z_index + 1;
19335 scene.backdrop_layers.push(BackdropLayer {
19336 node_id: Some(99),
19337 rect: Rect {
19338 x: 20.0,
19339 y: 30.0,
19340 width: 120.0,
19341 height: 80.0,
19342 },
19343 clip: None,
19344 snap_anchor: None,
19345 effect: RenderEffect::blur(6.0),
19346 z_index,
19347 });
19348 scene
19349 }
19350
19351 #[test]
19352 fn a_root_local_backdrop_takes_the_direct_road_when_the_target_reads() {
19353 let scene = scene_with_root_local_backdrop(1);
19354 assert!(root_direct_scene_events_are_supported(&scene, true));
19355 }
19356
19357 #[test]
19358 fn a_backdrop_inside_an_effect_layer_stays_off_the_direct_road() {
19359 let mut scene = scene_with_root_local_backdrop(1);
19360 scene.next_z = 3;
19361 scene.effect_layers.push(EffectLayer {
19362 rect: Rect {
19363 x: 0.0,
19364 y: 0.0,
19365 width: 200.0,
19366 height: 200.0,
19367 },
19368 clip: None,
19369 snap_anchor: None,
19370 effect: Some(RenderEffect::blur(6.0)),
19371 blend_mode: BlendMode::SrcOver,
19372 composite_alpha: 1.0,
19373 z_start: 0,
19374 z_end: 3,
19375 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
19376 });
19377
19378 assert!(!root_direct_scene_events_are_supported(&scene, true));
19379 assert!(!root_direct_scene_events_are_supported(&scene, false));
19380 }
19381
19382 #[test]
19383 fn a_child_that_carries_a_backdrop_takes_the_direct_road_when_the_target_reads() {
19384 let mut backdrop_child = test_layer(
19385 Rect {
19386 x: 0.0,
19387 y: 0.0,
19388 width: 400.0,
19389 height: 280.0,
19390 },
19391 vec![],
19392 );
19393 backdrop_child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
19394 let collected = CollectedLayer {
19395 scene: CompositorScene::new(),
19396 child_layers: vec![child_layer_composite(
19397 &backdrop_child,
19398 1,
19399 Rect {
19400 x: 48.0,
19401 y: 96.0,
19402 width: 400.0,
19403 height: 280.0,
19404 },
19405 false,
19406 )],
19407 };
19408
19409 assert!(collected.child_layers[0].backdrop.is_some());
19410 assert!(direct_root_child_underlays_are_supported(&collected, true));
19411 assert!(!direct_root_child_underlays_are_supported(
19412 &collected, false
19413 ));
19414 }
19415
19416 fn frosted_layer(bounds: Rect, offset: Point) -> LayerNode {
19417 let mut layer = test_layer(
19418 bounds,
19419 vec![RenderNode::Primitive(PrimitiveEntry {
19420 phase: PrimitivePhase::BeforeChildren,
19421 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19422 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19423 rect: bounds,
19424 brush: Brush::solid(Color::from_rgba_u8(255, 255, 255, 60)),
19425 stroke: None,
19426 },
19427 clip: None,
19428 }),
19429 })],
19430 );
19431 layer.transform_to_parent = ProjectiveTransform::translation(offset.x, offset.y);
19432 layer.graphics_layer.backdrop_effect = Some(RenderEffect::blur(8.0));
19433 layer
19434 }
19435
19436 #[test]
19437 fn a_frosted_layer_keeps_its_own_surface() {
19438 let frosted = frosted_layer(
19439 Rect {
19440 x: 0.0,
19441 y: 0.0,
19442 width: 40.0,
19443 height: 20.0,
19444 },
19445 Point::new(10.0, 6.0),
19446 );
19447 let root = test_layer(
19448 Rect {
19449 x: 0.0,
19450 y: 0.0,
19451 width: 200.0,
19452 height: 100.0,
19453 },
19454 vec![RenderNode::Layer(Box::new(frosted))],
19455 );
19456 let mut rect_cache = HashMap::new();
19457 let mut requirements_cache = HashMap::new();
19458
19459 let collected =
19460 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19461
19462 assert_eq!(collected.child_layers.len(), 1);
19463 assert!(
19464 collected.scene.backdrop_layers.is_empty(),
19465 "a layer that keeps its surface carries its backdrop on the composite"
19466 );
19467 assert!(collected.child_layers[0].backdrop.is_some());
19468 }
19469
19470 fn row_with_clipped_glass(row_background: Color) -> LayerNode {
19471 let mut glass = frosted_layer(
19472 Rect {
19473 x: 0.0,
19474 y: 0.0,
19475 width: 40.0,
19476 height: 20.0,
19477 },
19478 Point::new(10.0, 6.0),
19479 );
19480 glass.isolation.shape_clip = true;
19481 let row_bounds = Rect {
19482 x: 0.0,
19483 y: 0.0,
19484 width: 200.0,
19485 height: 40.0,
19486 };
19487 let mut row = test_layer(
19488 row_bounds,
19489 vec![
19490 RenderNode::Primitive(PrimitiveEntry {
19491 phase: PrimitivePhase::BeforeChildren,
19492 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19493 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19494 rect: row_bounds,
19495 brush: Brush::solid(row_background),
19496 stroke: None,
19497 },
19498 clip: None,
19499 }),
19500 }),
19501 RenderNode::Layer(Box::new(glass)),
19502 ],
19503 );
19504 row.isolation.shape_clip = true;
19505 row
19506 }
19507
19508 #[test]
19509 fn a_backdrop_covered_by_its_own_row_asks_for_no_underlay() {
19510 let root = test_layer(
19511 Rect {
19512 x: 0.0,
19513 y: 0.0,
19514 width: 400.0,
19515 height: 200.0,
19516 },
19517 vec![RenderNode::Layer(Box::new(row_with_clipped_glass(
19518 Color::WHITE,
19519 )))],
19520 );
19521 let mut rect_cache = HashMap::new();
19522 let mut requirements_cache = HashMap::new();
19523
19524 let collected =
19525 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19526
19527 assert_eq!(collected.child_layers.len(), 1);
19528 assert!(collected.child_layers[0].contains_descendant_backdrop);
19529 assert!(
19530 !collected.child_layers[0].needs_nested_underlay,
19531 "an opaque row draw under the glass is all the blur reads, so no picture of the scene behind the row is needed"
19532 );
19533 }
19534
19535 #[test]
19536 fn a_backdrop_over_a_see_through_row_still_asks_for_an_underlay() {
19537 let root = test_layer(
19538 Rect {
19539 x: 0.0,
19540 y: 0.0,
19541 width: 400.0,
19542 height: 200.0,
19543 },
19544 vec![RenderNode::Layer(Box::new(row_with_clipped_glass(
19545 Color::from_rgba_u8(255, 255, 255, 40),
19546 )))],
19547 );
19548 let mut rect_cache = HashMap::new();
19549 let mut requirements_cache = HashMap::new();
19550
19551 let collected =
19552 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19553
19554 assert_eq!(collected.child_layers.len(), 1);
19555 assert!(collected.child_layers[0].needs_nested_underlay);
19556 }
19557
19558 #[test]
19559 fn estimate_layer_surface_rect_includes_transformed_child_bounds() {
19560 let mut child = test_layer(
19561 Rect {
19562 x: 0.0,
19563 y: 0.0,
19564 width: 10.0,
19565 height: 6.0,
19566 },
19567 vec![RenderNode::Primitive(PrimitiveEntry {
19568 phase: PrimitivePhase::BeforeChildren,
19569 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19570 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19571 rect: Rect {
19572 x: 0.0,
19573 y: 0.0,
19574 width: 10.0,
19575 height: 6.0,
19576 },
19577 brush: Brush::solid(Color::WHITE),
19578 stroke: None,
19579 },
19580 clip: None,
19581 }),
19582 })],
19583 );
19584 child.transform_to_parent = ProjectiveTransform::translation(18.0, 7.0);
19585
19586 let parent = test_layer(
19587 Rect {
19588 x: 0.0,
19589 y: 0.0,
19590 width: 4.0,
19591 height: 4.0,
19592 },
19593 vec![RenderNode::Layer(Box::new(child))],
19594 );
19595
19596 assert_eq!(
19597 estimate_layer_surface_rect(&parent),
19598 Rect {
19599 x: 18.0,
19600 y: 7.0,
19601 width: 10.0,
19602 height: 6.0,
19603 }
19604 );
19605 }
19606
19607 #[test]
19608 fn estimate_layer_surface_rect_clips_translated_clip_layers_without_hidden_leading_content() {
19609 let mut layer = test_layer(
19610 Rect {
19611 x: 0.0,
19612 y: 0.0,
19613 width: 120.0,
19614 height: 72.0,
19615 },
19616 vec![RenderNode::Primitive(PrimitiveEntry {
19617 phase: PrimitivePhase::BeforeChildren,
19618 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19619 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19620 rect: Rect {
19621 x: 24.0,
19622 y: 0.0,
19623 width: 200.0,
19624 height: 480.0,
19625 },
19626 brush: Brush::solid(Color::WHITE),
19627 stroke: None,
19628 },
19629 clip: None,
19630 }),
19631 })],
19632 );
19633 layer.translated_content_context = true;
19634 layer.motion_context_animated = true;
19635 layer.clip_to_bounds = true;
19636
19637 assert_eq!(
19638 estimate_layer_surface_rect(&layer),
19639 Rect {
19640 x: 24.0,
19641 y: 0.0,
19642 width: 96.0,
19643 height: 72.0,
19644 }
19645 );
19646 }
19647
19648 #[test]
19649 fn estimate_layer_surface_rect_clips_active_horizontal_scroll_content() {
19650 let mut layer = test_layer(
19651 Rect {
19652 x: 0.0,
19653 y: 0.0,
19654 width: 120.0,
19655 height: 72.0,
19656 },
19657 vec![RenderNode::Primitive(PrimitiveEntry {
19658 phase: PrimitivePhase::BeforeChildren,
19659 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19660 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19661 rect: Rect {
19662 x: -24.0,
19663 y: 0.0,
19664 width: 200.0,
19665 height: 480.0,
19666 },
19667 brush: Brush::solid(Color::WHITE),
19668 stroke: None,
19669 },
19670 clip: None,
19671 }),
19672 })],
19673 );
19674 layer.translated_content_context = true;
19675 layer.motion_context_animated = true;
19676 layer.clip_to_bounds = true;
19677
19678 assert_eq!(
19679 estimate_layer_surface_rect(&layer),
19680 Rect {
19681 x: 0.0,
19682 y: 0.0,
19683 width: 120.0,
19684 height: 72.0,
19685 }
19686 );
19687 }
19688
19689 #[test]
19690 fn estimate_layer_surface_rect_clips_active_vertical_scroll_content() {
19691 let mut layer = test_layer(
19692 Rect {
19693 x: 0.0,
19694 y: 0.0,
19695 width: 120.0,
19696 height: 72.0,
19697 },
19698 vec![RenderNode::Primitive(PrimitiveEntry {
19699 phase: PrimitivePhase::BeforeChildren,
19700 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19701 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19702 rect: Rect {
19703 x: 0.0,
19704 y: -24.0,
19705 width: 120.0,
19706 height: 200.0,
19707 },
19708 brush: Brush::solid(Color::WHITE),
19709 stroke: None,
19710 },
19711 clip: None,
19712 }),
19713 })],
19714 );
19715 layer.translated_content_context = true;
19716 layer.motion_context_animated = true;
19717 layer.clip_to_bounds = true;
19718
19719 assert_eq!(
19720 estimate_layer_surface_rect(&layer),
19721 Rect {
19722 x: 0.0,
19723 y: 0.0,
19724 width: 120.0,
19725 height: 72.0,
19726 }
19727 );
19728 }
19729
19730 #[test]
19731 fn estimate_layer_surface_rect_keeps_shallow_scroll_capture_origin_stable() {
19732 fn shallow_scroll_surface_rect(content_y: f32) -> Rect {
19733 let mut layer = test_layer(
19734 Rect {
19735 x: 0.0,
19736 y: 0.0,
19737 width: 120.0,
19738 height: 72.0,
19739 },
19740 vec![RenderNode::Primitive(PrimitiveEntry {
19741 phase: PrimitivePhase::BeforeChildren,
19742 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19743 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19744 rect: Rect {
19745 x: 0.0,
19746 y: content_y,
19747 width: 120.0,
19748 height: 200.0,
19749 },
19750 brush: Brush::solid(Color::WHITE),
19751 stroke: None,
19752 },
19753 clip: None,
19754 }),
19755 })],
19756 );
19757 layer.translated_content_context = true;
19758 layer.motion_context_animated = true;
19759 layer.clip_to_bounds = true;
19760 estimate_layer_surface_rect(&layer)
19761 }
19762
19763 assert_eq!(
19764 shallow_scroll_surface_rect(-24.0),
19765 shallow_scroll_surface_rect(-25.0),
19766 "shallow scroll capture bounds must not move the offscreen surface origin on adjacent scroll positions"
19767 );
19768 }
19769
19770 #[test]
19771 fn estimate_layer_surface_rect_clips_active_xy_scroll_content() {
19772 let mut layer = test_layer(
19773 Rect {
19774 x: 0.0,
19775 y: 0.0,
19776 width: 120.0,
19777 height: 72.0,
19778 },
19779 vec![RenderNode::Primitive(PrimitiveEntry {
19780 phase: PrimitivePhase::BeforeChildren,
19781 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19782 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19783 rect: Rect {
19784 x: -16.0,
19785 y: -24.0,
19786 width: 180.0,
19787 height: 240.0,
19788 },
19789 brush: Brush::solid(Color::WHITE),
19790 stroke: None,
19791 },
19792 clip: None,
19793 }),
19794 })],
19795 );
19796 layer.translated_content_context = true;
19797 layer.motion_context_animated = true;
19798 layer.clip_to_bounds = true;
19799
19800 assert_eq!(
19801 estimate_layer_surface_rect(&layer),
19802 Rect {
19803 x: 0.0,
19804 y: 0.0,
19805 width: 120.0,
19806 height: 72.0,
19807 }
19808 );
19809 }
19810
19811 #[test]
19812 fn estimate_layer_surface_rect_clips_deep_hidden_active_scroll_content() {
19813 let mut layer = test_layer(
19814 Rect {
19815 x: 0.0,
19816 y: 0.0,
19817 width: 120.0,
19818 height: 72.0,
19819 },
19820 vec![RenderNode::Primitive(PrimitiveEntry {
19821 phase: PrimitivePhase::BeforeChildren,
19822 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19823 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19824 rect: Rect {
19825 x: 0.0,
19826 y: -1200.0,
19827 width: 120.0,
19828 height: 1400.0,
19829 },
19830 brush: Brush::solid(Color::WHITE),
19831 stroke: None,
19832 },
19833 clip: None,
19834 }),
19835 })],
19836 );
19837 layer.translated_content_context = true;
19838 layer.motion_context_animated = true;
19839 layer.clip_to_bounds = true;
19840
19841 assert_eq!(
19842 estimate_layer_surface_rect(&layer),
19843 Rect {
19844 x: 0.0,
19845 y: 0.0,
19846 width: 120.0,
19847 height: 72.0,
19848 }
19849 );
19850 }
19851
19852 #[test]
19853 fn estimate_layer_surface_rect_keeps_deep_scroll_capture_origin_stable() {
19854 fn deep_scroll_surface_rect(content_y: f32) -> Rect {
19855 let mut layer = test_layer(
19856 Rect {
19857 x: 0.0,
19858 y: 0.0,
19859 width: 120.0,
19860 height: 72.0,
19861 },
19862 vec![RenderNode::Primitive(PrimitiveEntry {
19863 phase: PrimitivePhase::BeforeChildren,
19864 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19865 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
19866 rect: Rect {
19867 x: 0.0,
19868 y: content_y,
19869 width: 120.0,
19870 height: 1400.0,
19871 },
19872 brush: Brush::solid(Color::WHITE),
19873 stroke: None,
19874 },
19875 clip: None,
19876 }),
19877 })],
19878 );
19879 layer.translated_content_context = true;
19880 layer.motion_context_animated = true;
19881 layer.clip_to_bounds = true;
19882 estimate_layer_surface_rect(&layer)
19883 }
19884
19885 assert_eq!(
19886 deep_scroll_surface_rect(-1200.0),
19887 deep_scroll_surface_rect(-1201.0),
19888 "deep scroll capture bounds must not re-phase the offscreen surface origin on adjacent scroll positions"
19889 );
19890 }
19891
19892 #[test]
19893 fn motion_stable_capture_bounds_bounds_shadows_for_clipped_effect_layer() {
19894 let mut layer = test_layer(
19895 Rect {
19896 x: 0.0,
19897 y: 0.0,
19898 width: 120.0,
19899 height: 72.0,
19900 },
19901 vec![],
19902 );
19903 layer.clip_to_bounds = true;
19904 layer.graphics_layer.clip = true;
19905 layer.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
19906
19907 let mut shadow_shape = test_shape(0, BlendMode::SrcOver);
19908 shadow_shape.rect = Rect {
19909 x: -24.0,
19910 y: -1200.0,
19911 width: 180.0,
19912 height: 1400.0,
19913 };
19914 let mut scene = CompositorScene::new();
19915 scene
19916 .shadow_draws
19917 .push(test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]));
19918
19919 let requirements = SurfaceRequirementSet::default()
19920 .with(SurfaceRequirement::RenderEffect)
19921 .with(SurfaceRequirement::MotionStableCapture);
19922
19923 assert_eq!(
19924 motion_stable_capture_bounds(
19925 &layer,
19926 &scene,
19927 &[],
19928 requirements,
19929 TranslatedContentAxes::default(),
19930 None,
19931 ),
19932 Some(Rect {
19933 x: -360.0,
19934 y: -216.0,
19935 width: 480.0,
19936 height: 288.0,
19937 })
19938 );
19939 }
19940
19941 #[test]
19942 fn vertical_motion_stable_capture_uses_viewport_cross_axis_bounds() {
19943 let mut layer = test_layer(
19944 Rect {
19945 x: 0.0,
19946 y: 0.0,
19947 width: 200.0,
19948 height: 100.0,
19949 },
19950 vec![],
19951 );
19952 layer.clip_to_bounds = true;
19953 layer.graphics_layer.clip = true;
19954
19955 let mut shape = test_shape(0, BlendMode::SrcOver);
19956 shape.rect = Rect {
19957 x: 60.0,
19958 y: -80.0,
19959 width: 80.0,
19960 height: 220.0,
19961 };
19962 let mut scene = CompositorScene::new();
19963 scene.shapes.push(shape);
19964
19965 let requirements =
19966 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
19967
19968 assert_eq!(
19969 motion_stable_capture_bounds(
19970 &layer,
19971 &scene,
19972 &[],
19973 requirements,
19974 TranslatedContentAxes { x: false, y: true },
19975 None,
19976 ),
19977 Some(Rect {
19978 x: -96.0,
19979 y: -64.0,
19980 width: 296.0,
19981 height: 164.0,
19982 })
19983 );
19984 }
19985
19986 #[test]
19987 fn vertical_motion_stable_capture_uses_external_surface_clip() {
19988 let layer = test_layer(
19989 Rect {
19990 x: 0.0,
19991 y: 0.0,
19992 width: 200.0,
19993 height: 100.0,
19994 },
19995 vec![],
19996 );
19997
19998 let mut shape = test_shape(0, BlendMode::SrcOver);
19999 shape.rect = Rect {
20000 x: 60.0,
20001 y: -80.0,
20002 width: 80.0,
20003 height: 220.0,
20004 };
20005 let mut scene = CompositorScene::new();
20006 scene.shapes.push(shape);
20007
20008 let requirements =
20009 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
20010
20011 assert_eq!(
20012 motion_stable_capture_bounds(
20013 &layer,
20014 &scene,
20015 &[],
20016 requirements,
20017 TranslatedContentAxes { x: false, y: true },
20018 Some(Rect {
20019 x: 0.0,
20020 y: 0.0,
20021 width: 200.0,
20022 height: 100.0,
20023 }),
20024 ),
20025 Some(Rect {
20026 x: -96.0,
20027 y: -64.0,
20028 width: 296.0,
20029 height: 164.0,
20030 })
20031 );
20032 }
20033
20034 #[test]
20035 fn estimate_layer_surface_rect_expands_for_child_layer_shadow() {
20036 let mut child = test_layer(
20037 Rect {
20038 x: 0.0,
20039 y: 0.0,
20040 width: 12.0,
20041 height: 8.0,
20042 },
20043 vec![],
20044 );
20045 child.transform_to_parent = ProjectiveTransform::translation(20.0, 9.0);
20046 child.graphics_layer.shadow_elevation = 6.0;
20047
20048 let parent = test_layer(
20049 Rect {
20050 x: 0.0,
20051 y: 0.0,
20052 width: 4.0,
20053 height: 4.0,
20054 },
20055 vec![RenderNode::Layer(Box::new(child))],
20056 );
20057
20058 let rect = estimate_layer_surface_rect(&parent);
20059 assert!(rect.x < 20.0);
20060 assert!(rect.y < 9.0);
20061 assert!(rect.width > 12.0);
20062 assert!(rect.height > 8.0);
20063 }
20064
20065 #[test]
20066 fn estimate_layer_surface_rect_respects_local_bounds_for_effect_layers() {
20067 let mut layer = test_layer(
20068 Rect {
20069 x: 0.0,
20070 y: 0.0,
20071 width: 28.0,
20072 height: 28.0,
20073 },
20074 vec![RenderNode::Primitive(PrimitiveEntry {
20075 phase: PrimitivePhase::BeforeChildren,
20076 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20077 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20078 rect: Rect {
20079 x: 10.0,
20080 y: 10.0,
20081 width: 10.0,
20082 height: 10.0,
20083 },
20084 brush: Brush::solid(Color::WHITE),
20085 stroke: None,
20086 },
20087 clip: None,
20088 }),
20089 })],
20090 );
20091 layer.graphics_layer.render_effect = Some(RenderEffect::blur(12.0));
20092
20093 assert_eq!(
20094 estimate_layer_surface_rect(&layer),
20095 Rect {
20096 x: 0.0,
20097 y: 0.0,
20098 width: 28.0,
20099 height: 28.0,
20100 }
20101 );
20102 }
20103
20104 #[test]
20105 fn layer_raster_cache_candidate_ignores_parent_transform() {
20106 let primitive = PrimitiveEntry {
20107 phase: PrimitivePhase::BeforeChildren,
20108 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20109 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20110 rect: Rect {
20111 x: 2.0,
20112 y: 3.0,
20113 width: 6.0,
20114 height: 4.0,
20115 },
20116 brush: Brush::solid(Color::BLACK),
20117 stroke: None,
20118 },
20119 clip: None,
20120 }),
20121 };
20122 let base = cacheable_layer(
20123 41,
20124 Rect {
20125 x: 0.0,
20126 y: 0.0,
20127 width: 20.0,
20128 height: 20.0,
20129 },
20130 vec![RenderNode::Primitive(primitive.clone())],
20131 );
20132 let mut moved = base.clone();
20133 moved.transform_to_parent = ProjectiveTransform::translation(32.0, 18.0);
20134
20135 assert_eq!(
20136 layer_raster_cache_candidate(&base, 1.25, false, false),
20137 layer_raster_cache_candidate(&moved, 1.25, false, false)
20138 );
20139 }
20140
20141 #[test]
20142 fn layer_raster_cache_candidate_changes_for_translated_content_offset() {
20143 let primitive = PrimitiveEntry {
20144 phase: PrimitivePhase::BeforeChildren,
20145 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20146 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
20147 rect: Rect {
20148 x: 2.0,
20149 y: 3.0,
20150 width: 6.0,
20151 height: 4.0,
20152 },
20153 brush: Brush::solid(Color::BLACK),
20154 stroke: None,
20155 },
20156 clip: None,
20157 }),
20158 };
20159 let mut base = cacheable_layer(
20160 42,
20161 Rect {
20162 x: 0.0,
20163 y: 0.0,
20164 width: 20.0,
20165 height: 20.0,
20166 },
20167 vec![RenderNode::Primitive(primitive)],
20168 );
20169 base.translated_content_context = true;
20170 base.translated_content_offset = Point::new(0.0, -8.0);
20171 base.recompute_raster_cache_hashes();
20172
20173 let mut moved = base.clone();
20174 moved.translated_content_offset = Point::new(0.0, -16.0);
20175 moved.recompute_raster_cache_hashes();
20176
20177 assert_ne!(
20178 layer_raster_cache_candidate(&base, 1.25, false, false),
20179 layer_raster_cache_candidate(&moved, 1.25, false, false),
20180 "full-surface layer cache candidates must not alias different scroll offsets"
20181 );
20182 }
20183
20184 #[test]
20185 fn layer_raster_cache_candidate_changes_for_child_transform() {
20186 let mut child = cacheable_layer(
20187 8,
20188 Rect {
20189 x: 0.0,
20190 y: 0.0,
20191 width: 12.0,
20192 height: 10.0,
20193 },
20194 vec![],
20195 );
20196 child.transform_to_parent = ProjectiveTransform::translation(4.0, 6.0);
20197 let base = cacheable_layer(
20198 7,
20199 Rect {
20200 x: 0.0,
20201 y: 0.0,
20202 width: 20.0,
20203 height: 20.0,
20204 },
20205 vec![RenderNode::Layer(Box::new(child.clone()))],
20206 );
20207 let mut moved_child = child;
20208 moved_child.transform_to_parent = ProjectiveTransform::translation(9.0, 6.0);
20209 let moved = cacheable_layer(
20210 7,
20211 Rect {
20212 x: 0.0,
20213 y: 0.0,
20214 width: 20.0,
20215 height: 20.0,
20216 },
20217 vec![RenderNode::Layer(Box::new(moved_child))],
20218 );
20219
20220 assert_ne!(
20221 layer_raster_cache_candidate(&base, 1.0, false, false),
20222 layer_raster_cache_candidate(&moved, 1.0, false, false)
20223 );
20224 }
20225
20226 #[test]
20227 fn layer_raster_cache_candidate_rejects_external_backdrop_dependency() {
20228 let mut child = cacheable_layer(
20229 12,
20230 Rect {
20231 x: 0.0,
20232 y: 0.0,
20233 width: 8.0,
20234 height: 8.0,
20235 },
20236 vec![],
20237 );
20238 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
20239 let parent = cacheable_layer(
20240 11,
20241 Rect {
20242 x: 0.0,
20243 y: 0.0,
20244 width: 16.0,
20245 height: 16.0,
20246 },
20247 vec![RenderNode::Layer(Box::new(child))],
20248 );
20249
20250 assert!(layer_raster_cache_candidate(&parent, 1.0, false, false).is_some());
20251 assert!(layer_raster_cache_candidate(&parent, 1.0, true, false).is_none());
20252 }
20253
20254 #[test]
20255 fn layer_raster_cache_candidate_does_not_force_translation_only_text_surfaces() {
20256 let text = RenderNode::Primitive(PrimitiveEntry {
20257 phase: PrimitivePhase::BeforeChildren,
20258 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
20259 node_id: 77,
20260 rect: Rect {
20261 x: 2.0,
20262 y: 3.0,
20263 width: 48.0,
20264 height: 18.0,
20265 },
20266 text: std::rc::Rc::new(AnnotatedString::from("runtime cache")),
20267 text_style: TextStyle::default(),
20268 font_size: 14.0,
20269 layout_options: TextLayoutOptions::default(),
20270 clip: None,
20271 })),
20272 });
20273 let mut layer = test_layer(
20274 Rect {
20275 x: 0.0,
20276 y: 0.0,
20277 width: 64.0,
20278 height: 32.0,
20279 },
20280 vec![text],
20281 );
20282 layer.node_id = Some(77);
20283 layer.recompute_raster_cache_hashes();
20284
20285 assert!(
20286 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
20287 "root path should not isolate plain translation-only text layers"
20288 );
20289 assert!(
20290 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
20291 "child path should also render plain translation-only text layers directly"
20292 );
20293 }
20294
20295 #[test]
20296 fn layer_raster_cache_candidate_allows_stable_runtime_child_effect_surfaces() {
20297 let mut layer = test_layer(
20298 Rect {
20299 x: 0.0,
20300 y: 0.0,
20301 width: 64.0,
20302 height: 32.0,
20303 },
20304 vec![RenderNode::Primitive(PrimitiveEntry {
20305 phase: PrimitivePhase::BeforeChildren,
20306 node: PrimitiveNode::Draw(DrawPrimitiveNode {
20307 primitive: DrawPrimitive::Rect {
20308 rect: Rect {
20309 x: 0.0,
20310 y: 0.0,
20311 width: 64.0,
20312 height: 32.0,
20313 },
20314 brush: Brush::solid(Color::WHITE),
20315 stroke: None,
20316 },
20317 clip: None,
20318 }),
20319 })],
20320 );
20321 layer.node_id = Some(78);
20322 layer.graphics_layer.render_effect = Some(RenderEffect::blur(4.0));
20323 layer.recompute_raster_cache_hashes();
20324
20325 assert!(
20326 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
20327 "root direct path should not force-cache ordinary stable effects"
20328 );
20329 assert!(
20330 layer_raster_cache_candidate(&layer, 1.0, false, true).is_some(),
20331 "child surface rendering should retain stable non-runtime effects"
20332 );
20333 }
20334
20335 #[test]
20336 fn layer_raster_cache_candidate_rejects_runtime_shader_child_effect_surfaces() {
20337 let mut layer = test_layer(
20338 Rect {
20339 x: 0.0,
20340 y: 0.0,
20341 width: 64.0,
20342 height: 32.0,
20343 },
20344 vec![],
20345 );
20346 layer.node_id = Some(79);
20347 layer.graphics_layer.render_effect = Some(RenderEffect::runtime_shader(
20348 RuntimeShader::new("runtime shader"),
20349 ));
20350 layer.recompute_raster_cache_hashes();
20351
20352 assert!(
20353 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
20354 "runtime shaders must not fill the retained layer cache with per-frame uniform variants"
20355 );
20356 }
20357
20358 #[test]
20359 fn layer_surface_requirements_keep_plain_text_on_direct_path() {
20360 let layer = text_layer_with_style(AnnotatedString::from("plain"), TextStyle::default());
20361
20362 let requirements = layer_surface_requirements(&layer);
20363
20364 assert_eq!(requirements.direct_translation, Some(Point::default()));
20365 assert!(requirements
20366 .surface_requirements
20367 .contains(SurfaceRequirement::PixelStableComposite));
20368 assert!(!requirements
20369 .surface_requirements
20370 .has_isolating_requirement());
20371 }
20372
20373 #[test]
20374 fn layer_surface_requirements_keep_translated_plain_text_leaf_on_direct_path() {
20375 let layer = pure_text_leaf(false, true);
20376
20377 let requirements = layer_surface_requirements(&layer);
20378
20379 assert_eq!(
20380 requirements.direct_translation,
20381 Some(Point::new(11.4, 23.6))
20382 );
20383 assert!(
20384 requirements
20385 .surface_requirements
20386 .contains(SurfaceRequirement::PixelStableComposite)
20387 && !requirements
20388 .surface_requirements
20389 .has_isolating_requirement(),
20390 "translated plain text should stay on the direct path and isolate only the glyph draw"
20391 );
20392 }
20393
20394 #[test]
20395 fn layer_surface_requirements_keep_translated_text_leaf_with_background_on_direct_path() {
20396 let layer = snapped_text_leaf(false, true);
20397
20398 let requirements = layer_surface_requirements(&layer);
20399
20400 assert_eq!(
20401 requirements.direct_translation,
20402 Some(Point::new(14.25, 16.5))
20403 );
20404 assert!(
20405 requirements
20406 .surface_requirements
20407 .contains(SurfaceRequirement::PixelStableComposite)
20408 && !requirements
20409 .surface_requirements
20410 .has_isolating_requirement(),
20411 "translated text with direct sibling decoration/background should keep the layer direct"
20412 );
20413 }
20414
20415 #[test]
20416 fn translated_plain_text_uses_bounded_snap_surface() {
20417 let root = pure_text_leaf_root(true, true);
20418 let mut rect_cache = HashMap::new();
20419 let mut requirements_cache = HashMap::new();
20420 let collected =
20421 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20422
20423 assert_eq!(collected.child_layers.len(), 1);
20424 assert!(collected.scene.texts.is_empty());
20425 assert!(collected.scene.effect_layers.is_empty());
20426 assert_snap_anchor_close(
20427 collected.child_layers[0].snap_anchor,
20428 Point::new(11.4, 23.6),
20429 "translated plain text's bounded local surface should composite at the content-origin snap phase",
20430 );
20431 }
20432
20433 #[test]
20437 #[ignore]
20438 fn shape_run_collect_timing_harness() {
20439 use cranpose_render_common::graph::DrawPrimitiveNode;
20440 use cranpose_render_common::layer_composition::local_content_layer_for;
20441 use cranpose_ui_graphics::Stroke;
20442
20443 let bounds = Rect {
20444 x: 0.0,
20445 y: 0.0,
20446 width: 1080.0,
20447 height: 2244.0,
20448 };
20449 let graphics_layer = GraphicsLayer::default();
20450
20451 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
20454 for i in 0..3000u32 {
20455 let f = i as f32;
20456 let brush = if i % 8 == 0 {
20457 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
20458 } else {
20459 Brush::Solid(Color(0.5, 0.2, 0.8, 1.0))
20460 };
20461 let center = Point::new(540.0 + (f % 400.0), 1122.0 + (f % 350.0));
20462 let radius = 8.0 + (i % 23) as f32;
20463 let half = radius + 4.0;
20464 nodes.push(DrawPrimitiveNode {
20465 primitive: DrawPrimitive::Arc {
20466 rect: Rect {
20467 x: center.x - half,
20468 y: center.y - half,
20469 width: half * 2.0,
20470 height: half * 2.0,
20471 },
20472 brush,
20473 center,
20474 radius,
20475 start_angle: f * 0.07,
20476 sweep_angle: 0.5 + (i % 5) as f32,
20477 stroke: (i % 3 != 0).then(|| Stroke::new(4.0)),
20478 inner_radius: if i % 3 == 0 { radius * 0.6 } else { 0.0 },
20479 },
20480 clip: None,
20481 });
20482 }
20483
20484 let children: Vec<RenderNode> = nodes
20485 .iter()
20486 .map(|node| {
20487 RenderNode::Primitive(PrimitiveEntry {
20488 phase: PrimitivePhase::BeforeChildren,
20489 node: PrimitiveNode::Draw(node.clone()),
20490 })
20491 })
20492 .collect();
20493 let layer = crate::test_support::layer_node(
20494 bounds,
20495 ProjectiveTransform::identity(),
20496 graphics_layer,
20497 children,
20498 );
20499
20500 const ITERS: usize = 300;
20501
20502 let local_layer = local_content_layer_for(&layer.graphics_layer);
20504 let start = Instant::now();
20505 let mut sink_shapes = 0usize;
20506 for _ in 0..ITERS {
20507 let mut scene = CompositorScene::new();
20508 for node in &nodes {
20509 crate::pipeline::push_draw_primitive(
20510 &node.primitive,
20511 bounds,
20512 &local_layer,
20513 None,
20514 &mut scene,
20515 None,
20516 false,
20517 );
20518 }
20519 sink_shapes = scene.shapes.len();
20520 }
20521 let serial = start.elapsed();
20522
20523 let mut rect_cache = HashMap::new();
20524 let mut requirements_cache = HashMap::new();
20525 let start = Instant::now();
20526 let mut run_shapes = 0usize;
20527 for _ in 0..ITERS {
20528 let collected = collect_layer_contents(
20529 &layer,
20530 None,
20531 None,
20532 &mut rect_cache,
20533 &mut requirements_cache,
20534 );
20535 run_shapes = collected.scene.shapes.len();
20536 }
20537 let run = start.elapsed();
20538
20539 println!(
20540 "per-primitive: {:?}/iter ({sink_shapes} shapes) shape-run: {:?}/iter ({run_shapes} shapes)",
20541 serial / ITERS as u32,
20542 run / ITERS as u32,
20543 );
20544 }
20545
20546 fn assert_shape_run_collect_matches_per_primitive_emission() {
20548 use cranpose_render_common::graph::DrawPrimitiveNode;
20549 use cranpose_render_common::layer_composition::local_content_layer_for;
20550 use cranpose_render_common::primitive_emit::{resolve_primitive_clip, PrimitiveClipSpace};
20551 use cranpose_ui_graphics::{CornerRadii, Stroke};
20552
20553 let bounds = Rect {
20554 x: 0.0,
20555 y: 0.0,
20556 width: 800.0,
20557 height: 800.0,
20558 };
20559 let graphics_layer = GraphicsLayer {
20562 scale: 1.25,
20563 translation_x: 3.5,
20564 translation_y: -2.0,
20565 alpha: 0.9,
20566 rotation_z: 0.35,
20567 ..GraphicsLayer::default()
20568 };
20569
20570 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
20571 for i in 0..600u32 {
20572 let f = i as f32;
20573 let brush = if i % 11 == 0 {
20574 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
20575 } else {
20576 Brush::Solid(Color(0.1 + (i % 7) as f32 * 0.1, 0.5, 0.9, 1.0))
20577 };
20578 let stroke = (i % 5 == 0).then(|| Stroke::new(1.0 + (i % 3) as f32));
20579 let primitive = match i % 3 {
20580 0 => DrawPrimitive::Rect {
20581 rect: Rect {
20582 x: f % 37.0,
20583 y: f % 53.0,
20584 width: 8.0 + f % 9.0,
20585 height: 6.0 + f % 5.0,
20586 },
20587 brush,
20588 stroke,
20589 },
20590 1 => DrawPrimitive::RoundRect {
20591 rect: Rect {
20592 x: f % 41.0,
20593 y: f % 43.0,
20594 width: 12.0,
20595 height: 10.0,
20596 },
20597 brush,
20598 radii: CornerRadii::uniform(2.0 + (i % 4) as f32),
20599 stroke,
20600 },
20601 _ => {
20602 let center = Point::new(60.0 + f % 71.0, 60.0 + f % 67.0);
20603 let radius = 5.0 + (i % 13) as f32;
20604 let sweep_angle = if i == 302 { 0.0 } else { 0.4 + (i % 6) as f32 };
20606 let half = radius + 4.0;
20607 DrawPrimitive::Arc {
20608 rect: Rect {
20609 x: center.x - half,
20610 y: center.y - half,
20611 width: half * 2.0,
20612 height: half * 2.0,
20613 },
20614 brush,
20615 center,
20616 radius,
20617 start_angle: f * 0.11,
20618 sweep_angle,
20619 stroke: (i % 2 == 0).then(|| Stroke::new(3.0)),
20620 inner_radius: if i % 4 == 2 { radius * 0.5 } else { 0.0 },
20621 }
20622 }
20623 };
20624 let primitive = if i == 300 {
20625 DrawPrimitive::Blend {
20629 primitive: Box::new(DrawPrimitive::Blend {
20630 primitive: Box::new(primitive),
20631 blend_mode: BlendMode::SrcOver,
20632 }),
20633 blend_mode: BlendMode::DstOut,
20634 }
20635 } else if i % 7 == 3 {
20636 DrawPrimitive::Blend {
20637 primitive: Box::new(primitive),
20638 blend_mode: BlendMode::DstOut,
20639 }
20640 } else {
20641 primitive
20642 };
20643 let clip = (i % 31 == 7).then_some(Rect {
20644 x: 0.0,
20645 y: 0.0,
20646 width: 30.0,
20647 height: 30.0,
20648 });
20649 nodes.push(DrawPrimitiveNode { primitive, clip });
20650 }
20651
20652 let children: Vec<RenderNode> = nodes
20653 .iter()
20654 .map(|node| {
20655 RenderNode::Primitive(PrimitiveEntry {
20656 phase: PrimitivePhase::BeforeChildren,
20657 node: PrimitiveNode::Draw(node.clone()),
20658 })
20659 })
20660 .collect();
20661 let layer = crate::test_support::layer_node(
20662 bounds,
20663 ProjectiveTransform::identity(),
20664 graphics_layer,
20665 children,
20666 );
20667
20668 let mut rect_cache = HashMap::new();
20669 let mut requirements_cache = HashMap::new();
20670 let collected =
20671 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
20672
20673 let local_layer = local_content_layer_for(&layer.graphics_layer);
20676 let mut expected = CompositorScene::new();
20677 for node in &nodes {
20678 let clip = resolve_primitive_clip(
20679 node.clip,
20680 bounds,
20681 &local_layer,
20682 None,
20683 PrimitiveClipSpace::Local,
20684 );
20685 if node.clip.is_some() && clip.is_none() {
20686 continue;
20687 }
20688 crate::pipeline::push_draw_primitive(
20689 &node.primitive,
20690 bounds,
20691 &local_layer,
20692 clip,
20693 &mut expected,
20694 None,
20695 false,
20696 );
20697 }
20698
20699 assert!(
20700 collected.scene.shapes.len() >= 590,
20701 "the runs should engage the parallel branch: got {} shapes",
20702 collected.scene.shapes.len()
20703 );
20704 assert_eq!(collected.scene.shapes.len(), expected.shapes.len());
20705 assert_eq!(collected.scene.draw_ops, expected.draw_ops);
20706 assert_eq!(collected.scene.next_z, expected.next_z);
20707 assert!(
20708 collected
20709 .scene
20710 .shapes
20711 .iter()
20712 .all(|s| s.snap_anchor.is_none()),
20713 "a rotated layer must not rigid-snap; the reference scene assumes it"
20714 );
20715 for (index, (got, want)) in collected
20716 .scene
20717 .shapes
20718 .iter()
20719 .zip(&expected.shapes)
20720 .enumerate()
20721 {
20722 assert_eq!(got.rect, want.rect, "shape {index} rect");
20723 assert_eq!(got.local_rect, want.local_rect, "shape {index} local_rect");
20724 assert_eq!(got.quad, want.quad, "shape {index} quad");
20725 assert_eq!(got.snap_anchor, want.snap_anchor, "shape {index} snap");
20726 assert_eq!(got.brush, want.brush, "shape {index} brush");
20727 assert_eq!(got.shape, want.shape, "shape {index} shape");
20728 assert_eq!(got.stroke, want.stroke, "shape {index} stroke");
20729 assert_eq!(got.arc, want.arc, "shape {index} arc");
20730 assert_eq!(got.z_index, want.z_index, "shape {index} z");
20731 assert_eq!(got.clip, want.clip, "shape {index} clip");
20732 assert_eq!(got.blend_mode, want.blend_mode, "shape {index} blend");
20733 assert_eq!(
20734 got.motion_context_animated, want.motion_context_animated,
20735 "shape {index} motion flag"
20736 );
20737 }
20738 }
20739
20740 #[test]
20745 fn shape_run_collect_matches_per_primitive_emission_exactly() {
20746 assert_shape_run_collect_matches_per_primitive_emission();
20747 crate::normalized_scene::force_shape_run_parallel_for_tests(true);
20748 let outcome =
20749 std::panic::catch_unwind(assert_shape_run_collect_matches_per_primitive_emission);
20750 crate::normalized_scene::force_shape_run_parallel_for_tests(false);
20751 if let Err(payload) = outcome {
20752 std::panic::resume_unwind(payload);
20753 }
20754 }
20755
20756 #[test]
20757 fn non_translated_text_local_surface_keeps_linear_composite_resolve() {
20758 let layer = text_layer_with_style(
20759 AnnotatedString::from("gradient"),
20760 TextStyle::from_span_style(SpanStyle {
20761 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
20762 ..SpanStyle::default()
20763 }),
20764 );
20765 let requirements = layer_surface_requirements(&layer);
20766
20767 assert!(requirements
20768 .surface_requirements
20769 .contains(SurfaceRequirement::TextMaterialMask));
20770 assert_eq!(
20771 composite_sample_mode_for_requirements(false, false, requirements),
20772 CompositeSampleMode::Linear
20773 );
20774 }
20775
20776 #[test]
20777 fn inherited_translated_text_local_surface_uses_box4_layer_surface() {
20778 let layer = text_layer_with_style(
20779 AnnotatedString::from("shadow"),
20780 TextStyle::from_span_style(SpanStyle {
20781 shadow: Some(Shadow {
20782 color: Color::BLACK,
20783 offset: Point::new(1.0, 2.0),
20784 blur_radius: 3.0,
20785 }),
20786 ..SpanStyle::default()
20787 }),
20788 );
20789 let requirements = layer_surface_requirements(&layer);
20790
20791 assert!(requirements
20792 .surface_requirements
20793 .contains(SurfaceRequirement::TextMaterialMask));
20794 assert_eq!(
20795 composite_sample_mode_for_requirements(true, false, requirements),
20796 CompositeSampleMode::Box4
20797 );
20798 assert_eq!(
20799 layer_surface_target_scale(
20800 true,
20801 false,
20802 requirements,
20803 1.25,
20804 layer_surface_scale(&layer)
20805 ),
20806 SurfaceRequirementSet::default()
20807 .with(SurfaceRequirement::TextMaterialMask)
20808 .with(SurfaceRequirement::MotionStableCapture)
20809 .target_scale(1.25, 1.0)
20810 );
20811 }
20812
20813 #[test]
20814 fn translated_text_local_surface_inside_capture_keeps_parent_scale() {
20815 let layer = text_layer_with_style(
20816 AnnotatedString::from("shadow"),
20817 TextStyle::from_span_style(SpanStyle {
20818 shadow: Some(Shadow {
20819 color: Color::BLACK,
20820 offset: Point::new(1.0, 2.0),
20821 blur_radius: 3.0,
20822 }),
20823 ..SpanStyle::default()
20824 }),
20825 );
20826 let requirements = layer_surface_requirements(&layer);
20827
20828 assert_eq!(
20829 composite_sample_mode_for_requirements(true, true, requirements),
20830 CompositeSampleMode::Linear
20831 );
20832 assert_eq!(
20833 layer_surface_target_scale(true, true, requirements, 10.0, layer_surface_scale(&layer)),
20834 SurfaceRequirementSet::default()
20835 .with(SurfaceRequirement::TextMaterialMask)
20836 .target_scale(10.0, 1.0)
20837 );
20838 }
20839
20840 #[test]
20841 fn layer_surface_requirements_use_local_surface_for_gradient_and_stroke_text() {
20842 let cases = [
20843 (
20844 "draw_style",
20845 AnnotatedString::from("draw_style"),
20846 TextStyle::from_span_style(SpanStyle {
20847 draw_style: Some(TextDrawStyle::Stroke { width: 2.0 }),
20848 ..SpanStyle::default()
20849 }),
20850 ),
20851 (
20852 "gradient_brush",
20853 AnnotatedString::from("gradient"),
20854 TextStyle::from_span_style(SpanStyle {
20855 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
20856 ..SpanStyle::default()
20857 }),
20858 ),
20859 ];
20860
20861 for (label, text, text_style) in cases {
20862 let layer = text_layer_with_style(text, text_style);
20863 let requirements = layer_surface_requirements(&layer);
20864 assert!(
20865 requirements
20866 .surface_requirements
20867 .contains(SurfaceRequirement::TextMaterialMask),
20868 "{label} text should use a bounded local surface: {requirements:?}"
20869 );
20870 }
20871 }
20872
20873 #[test]
20874 fn layer_surface_requirements_use_local_surface_for_complex_text_effects() {
20875 let cases = [
20876 (
20877 "shadow",
20878 AnnotatedString::from("shadow"),
20879 TextStyle::from_span_style(SpanStyle {
20880 shadow: Some(Shadow {
20881 color: Color::BLACK,
20882 offset: Point::new(1.0, 2.0),
20883 blur_radius: 3.0,
20884 }),
20885 ..SpanStyle::default()
20886 }),
20887 ),
20888 (
20889 "background",
20890 AnnotatedString::from("background"),
20891 TextStyle::from_span_style(SpanStyle {
20892 background: Some(Color::BLACK),
20893 ..SpanStyle::default()
20894 }),
20895 ),
20896 (
20897 "baseline_shift",
20898 AnnotatedString::from("baseline_shift"),
20899 TextStyle::from_span_style(SpanStyle {
20900 baseline_shift: Some(BaselineShift::SUPERSCRIPT),
20901 ..SpanStyle::default()
20902 }),
20903 ),
20904 (
20905 "geometric_transform",
20906 AnnotatedString::from("geometric_transform"),
20907 TextStyle::from_span_style(SpanStyle {
20908 text_geometric_transform: Some(TextGeometricTransform {
20909 scale_x: 1.2,
20910 skew_x: 0.15,
20911 }),
20912 ..SpanStyle::default()
20913 }),
20914 ),
20915 (
20916 "letter_spacing",
20917 AnnotatedString::from("letter_spacing"),
20918 TextStyle::from_span_style(SpanStyle {
20919 letter_spacing: TextUnit::Em(0.2),
20920 ..SpanStyle::default()
20921 }),
20922 ),
20923 ];
20924
20925 for (label, text, text_style) in cases {
20926 let layer = text_layer_with_style(text, text_style);
20927 let requirements = layer_surface_requirements(&layer);
20928 assert!(
20929 requirements
20930 .surface_requirements
20931 .contains(SurfaceRequirement::TextMaterialMask),
20932 "{label} text should use a bounded local surface: {requirements:?}"
20933 );
20934 assert_eq!(
20935 requirements.direct_translation,
20936 Some(Point::default()),
20937 "{label} text should still classify as a direct translation"
20938 );
20939 }
20940 }
20941
20942 #[test]
20943 fn layer_surface_requirements_color_only_span_styles_use_direct_path() {
20944 let layer = text_layer_with_style(
20945 AnnotatedString {
20946 text: "styled".to_string(),
20947 span_styles: vec![RangeStyle {
20948 item: SpanStyle {
20949 color: Some(Color::BLACK),
20950 ..SpanStyle::default()
20951 },
20952 range: 0..3,
20953 }],
20954 ..AnnotatedString::default()
20955 },
20956 TextStyle::default(),
20957 );
20958 let requirements = layer_surface_requirements(&layer);
20959 assert!(
20960 !requirements
20961 .surface_requirements
20962 .contains(SurfaceRequirement::TextMaterialMask),
20963 "color-only span styles should render directly via software text raster colors"
20964 );
20965 }
20966
20967 #[test]
20968 fn layer_surface_requirements_keep_decoration_only_text_on_direct_path() {
20969 let layer = text_layer_with_style(
20970 AnnotatedString::from("decoration"),
20971 TextStyle::from_span_style(SpanStyle {
20972 text_decoration: Some(TextDecoration::UNDERLINE),
20973 ..SpanStyle::default()
20974 }),
20975 );
20976
20977 let requirements = layer_surface_requirements(&layer);
20978
20979 assert_eq!(requirements.direct_translation, Some(Point::default()));
20980 assert!(
20981 requirements
20982 .surface_requirements
20983 .contains(SurfaceRequirement::PixelStableComposite)
20984 && !requirements
20985 .surface_requirements
20986 .has_isolating_requirement(),
20987 "decoration-only text should not force an isolating layer surface: {requirements:?}"
20988 );
20989 }
20990
20991 #[test]
20992 fn direct_text_leaf_snaps_modifier_background_and_text_with_one_anchor() {
20993 let root = snapped_text_leaf_root(false, false);
20994 let mut rect_cache = HashMap::new();
20995 let mut requirements_cache = HashMap::new();
20996
20997 let collected =
20998 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
20999
21000 assert_eq!(collected.scene.shapes.len(), 1);
21001 assert_eq!(collected.scene.images.len(), 1);
21002 assert_eq!(collected.scene.texts.len(), 1);
21003 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
21004 assert_eq!(collected.scene.shapes[0].snap_anchor, expected_anchor);
21005 assert_eq!(collected.scene.images[0].snap_anchor, expected_anchor);
21006 assert_eq!(collected.scene.texts[0].snap_anchor, expected_anchor);
21007 }
21008
21009 #[test]
21010 fn animated_translated_content_text_leaf_uses_bounded_content_snap() {
21011 let root = snapped_text_leaf_root(true, true);
21012 let mut rect_cache = HashMap::new();
21013 let mut requirements_cache = HashMap::new();
21014
21015 let collected =
21016 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21017
21018 assert_eq!(collected.child_layers.len(), 1);
21019 assert!(collected.scene.shapes.is_empty());
21020 assert!(collected.scene.images.is_empty());
21021 assert!(collected.scene.texts.is_empty());
21022 assert!(collected.scene.effect_layers.is_empty());
21023 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
21024 assert_eq!(
21025 collected.child_layers[0].snap_anchor, expected_anchor,
21026 "active translated leaf surface should keep the content-origin snap phase"
21027 );
21028 }
21029
21030 #[test]
21031 fn translated_content_assigns_motion_anchor_to_rotated_child_surface() {
21032 let mut child = snapped_text_leaf(false, false);
21033 child.graphics_layer.rotation_z = 5.0;
21034 child.transform_to_parent =
21035 cranpose_render_common::layer_transform::layer_transform_to_parent(
21036 child.local_bounds,
21037 Point::new(108.0, 3.0),
21038 &child.graphics_layer,
21039 );
21040 child.recompute_raster_cache_hashes();
21041 let mut root = test_layer(
21042 Rect {
21043 x: 0.0,
21044 y: 0.0,
21045 width: 320.0,
21046 height: 180.0,
21047 },
21048 vec![RenderNode::Layer(Box::new(child))],
21049 );
21050 root.translated_content_context = true;
21051 root.translated_content_offset = Point::new(0.0, -80.8);
21052 root.recompute_raster_cache_hashes();
21053 let mut rect_cache = HashMap::new();
21054 let mut requirements_cache = HashMap::new();
21055
21056 let collected =
21057 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21058
21059 assert_eq!(collected.child_layers.len(), 1);
21060 assert!(
21061 collected.child_layers[0].snap_anchor.is_some(),
21062 "a projective child still translates rigidly with its scrolling parent"
21063 );
21064 }
21065
21066 #[test]
21067 fn rested_translated_content_context_text_leaf_snaps_for_crisp_scroll_rest() {
21068 let root = snapped_text_leaf_root(false, true);
21069 let mut rect_cache = HashMap::new();
21070 let mut requirements_cache = HashMap::new();
21071
21072 let collected =
21073 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21074
21075 assert_eq!(collected.child_layers.len(), 0);
21076 assert_eq!(collected.scene.shapes.len(), 1);
21077 assert_eq!(collected.scene.images.len(), 1);
21078 assert_eq!(collected.scene.texts.len(), 1);
21079 assert_eq!(collected.scene.effect_layers.len(), 0);
21080 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
21081 assert_eq!(
21082 collected.scene.shapes[0].snap_anchor, expected_anchor,
21083 "rested scroll content should snap back to device pixels"
21084 );
21085 assert_eq!(
21086 collected.scene.images[0].snap_anchor, expected_anchor,
21087 "rested scroll images should snap back to device pixels"
21088 );
21089 assert_eq!(
21090 collected.scene.texts[0].snap_anchor, expected_anchor,
21091 "rested scroll text should snap back to device pixels"
21092 );
21093 }
21094
21095 #[test]
21096 fn complex_text_uses_local_surface() {
21097 let root = translated_content_local_surface_root();
21098 let mut rect_cache = HashMap::new();
21099 let mut requirements_cache = HashMap::new();
21100
21101 let collected =
21102 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21103
21104 assert!(
21105 !collected.child_layers.is_empty(),
21106 "translated-content effectful text should render through a bounded local surface"
21107 );
21108 assert!(collected.scene.texts.is_empty());
21109 assert!(collected.scene.shadow_draws.is_empty());
21110 }
21111
21112 #[test]
21113 fn translated_content_surface_composite_uses_scroll_content_snap_anchor() {
21114 let mut root = translated_content_local_surface_root();
21115 let scroll_offset = Point::new(0.0, -18.5);
21116 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
21117 panic!("expected translated content layer");
21118 };
21119 translated_content.translated_content_offset = scroll_offset;
21120 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
21121 panic!("expected effectful text layer");
21122 };
21123 effectful_text.transform_to_parent =
21124 effectful_text
21125 .transform_to_parent
21126 .then(ProjectiveTransform::translation(
21127 scroll_offset.x,
21128 scroll_offset.y,
21129 ));
21130
21131 let mut rect_cache = HashMap::new();
21132 let mut requirements_cache = HashMap::new();
21133 let collected =
21134 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21135
21136 assert_eq!(collected.child_layers.len(), 1);
21137 assert_eq!(
21138 collected.child_layers[0].snap_anchor,
21139 Some(SnapAnchor::rigid(Point::new(14.25, -2.0))),
21140 "isolated scrolled descendants must composite with the same content-origin snap phase"
21141 );
21142 }
21143
21144 #[test]
21145 fn animated_translated_content_surface_composite_uses_scroll_content_snap_anchor() {
21146 let mut root = translated_content_local_surface_root();
21147 let scroll_offset = Point::new(0.0, -18.5);
21148 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
21149 panic!("expected translated content layer");
21150 };
21151 translated_content.motion_context_animated = true;
21152 translated_content.translated_content_offset = scroll_offset;
21153 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
21154 panic!("expected effectful text layer");
21155 };
21156 effectful_text.transform_to_parent =
21157 effectful_text
21158 .transform_to_parent
21159 .then(ProjectiveTransform::translation(
21160 scroll_offset.x,
21161 scroll_offset.y,
21162 ));
21163
21164 let mut rect_cache = HashMap::new();
21165 let mut requirements_cache = HashMap::new();
21166 let collected =
21167 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21168
21169 assert_eq!(collected.child_layers.len(), 1);
21170 assert_eq!(
21171 collected.child_layers[0].snap_anchor,
21172 Some(SnapAnchor::rigid(Point::new(14.25, 16.5))),
21173 "animated translated content should composite the stable local surface at the viewport-origin snap phase"
21174 );
21175 }
21176
21177 #[test]
21178 fn translated_text_material_effect_layer_uses_scroll_content_snap_anchor() {
21179 let mut layer = text_layer_with_style(
21180 AnnotatedString::from("gradient"),
21181 TextStyle::from_span_style(SpanStyle {
21182 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
21183 ..SpanStyle::default()
21184 }),
21185 );
21186 layer.translated_content_context = true;
21187 layer.translated_content_offset = Point::new(0.0, -18.5);
21188 let mut rect_cache = HashMap::new();
21189 let mut requirements_cache = HashMap::new();
21190
21191 let collected =
21192 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
21193
21194 assert_eq!(collected.scene.effect_layers.len(), 1);
21195 assert_eq!(
21196 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
21197 CompositeSampleMode::Box4
21198 );
21199 assert_eq!(
21200 collected.scene.effect_layers[0].snap_anchor,
21201 Some(SnapAnchor::rigid(Point::new(0.0, -18.5))),
21202 "text material surfaces must composite with the scroll content-origin snap phase"
21203 );
21204 }
21205
21206 #[test]
21207 fn translated_layer_surface_capture_does_not_restart_local_picture_for_shadow_text() {
21208 let mut layer = text_layer_with_style(
21209 AnnotatedString::from("shadow"),
21210 TextStyle::from_span_style(SpanStyle {
21211 shadow: Some(Shadow {
21212 color: Color::BLACK,
21213 offset: Point::new(1.0, 2.0),
21214 blur_radius: 3.0,
21215 }),
21216 ..SpanStyle::default()
21217 }),
21218 );
21219 layer.translated_content_context = true;
21220 let mut rect_cache = HashMap::new();
21221 let mut requirements_cache = HashMap::new();
21222
21223 let collected = collect_layer_contents_with_translation_context(
21224 &layer,
21225 None,
21226 None,
21227 TranslationRenderContext {
21228 inherited_content_translation: false,
21229 surface_capture_active: true,
21230 local_picture_capture_active: true,
21231 ..TranslationRenderContext::default()
21232 },
21233 &mut rect_cache,
21234 &mut requirements_cache,
21235 );
21236
21237 assert!(
21238 collected.scene.effect_layers.is_empty(),
21239 "a translated layer surface already provides the stable local capture"
21240 );
21241 assert_eq!(collected.scene.shadow_draws.len(), 1);
21242 assert_eq!(collected.scene.texts.len(), 1);
21243 assert!(
21244 !collected.scene.texts[0].translated_content_context,
21245 "text inside an active motion-stable capture must raster in capture-local coordinates"
21246 );
21247 }
21248
21249 #[test]
21250 fn translated_layer_surface_capture_keeps_only_material_effect_layers() {
21251 let mut layer = text_layer_with_style(
21252 AnnotatedString::from("gradient"),
21253 TextStyle::from_span_style(SpanStyle {
21254 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
21255 ..SpanStyle::default()
21256 }),
21257 );
21258 layer.translated_content_context = true;
21259 let mut rect_cache = HashMap::new();
21260 let mut requirements_cache = HashMap::new();
21261
21262 let collected = collect_layer_contents_with_translation_context(
21263 &layer,
21264 None,
21265 None,
21266 TranslationRenderContext {
21267 inherited_content_translation: false,
21268 surface_capture_active: true,
21269 local_picture_capture_active: true,
21270 ..TranslationRenderContext::default()
21271 },
21272 &mut rect_cache,
21273 &mut requirements_cache,
21274 );
21275
21276 assert_eq!(collected.scene.effect_layers.len(), 1);
21277 assert!(
21278 collected.scene.effect_layers[0]
21279 .requirements
21280 .contains(SurfaceRequirement::MotionStableCapture),
21281 "translated text materials still need motion-stable resolve semantics inside a stable capture"
21282 );
21283 assert_eq!(
21284 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
21285 CompositeSampleMode::Box4
21286 );
21287 assert_eq!(
21288 effect_layer_target_scale(&collected.scene.effect_layers[0], 10.0),
21289 10.0
21290 );
21291 assert!(collected.scene.effect_layers[0].effect.is_some());
21292 }
21293
21294 #[test]
21295 fn translated_viewport_surface_does_not_add_plain_local_picture_capture() {
21296 let mut layer = text_layer_with_style(
21297 AnnotatedString::from("shadow"),
21298 TextStyle::from_span_style(SpanStyle {
21299 shadow: Some(Shadow {
21300 color: Color::BLACK,
21301 offset: Point::new(1.0, 2.0),
21302 blur_radius: 3.0,
21303 }),
21304 ..SpanStyle::default()
21305 }),
21306 );
21307 layer.translated_content_context = true;
21308 layer.motion_context_animated = true;
21309 let mut rect_cache = HashMap::new();
21310 let mut requirements_cache = HashMap::new();
21311
21312 let collected = collect_layer_contents_with_translation_context(
21313 &layer,
21314 None,
21315 None,
21316 TranslationRenderContext {
21317 surface_capture_active: true,
21318 ..TranslationRenderContext::default()
21319 },
21320 &mut rect_cache,
21321 &mut requirements_cache,
21322 );
21323
21324 assert_eq!(
21325 collected.scene.effect_layers.len(),
21326 0,
21327 "plain translated content inside a viewport surface should not be captured again"
21328 );
21329 assert_eq!(collected.scene.shadow_draws.len(), 1);
21330 assert_eq!(collected.scene.texts.len(), 1);
21331 }
21332
21333 #[test]
21334 fn static_pure_text_leaf_snaps_without_sibling_draw_primitives() {
21335 let root = pure_text_leaf_root(false, false);
21336 let mut rect_cache = HashMap::new();
21337 let mut requirements_cache = HashMap::new();
21338
21339 let collected =
21340 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21341
21342 assert_eq!(collected.scene.texts.len(), 1);
21343 assert!(
21344 collected.scene.texts[0].snap_anchor.is_some(),
21345 "idle pure text leaves should participate in rigid snap anchoring"
21346 );
21347 }
21348
21349 #[test]
21350 fn animated_pure_text_leaf_stays_unsnapped() {
21351 let root = pure_text_leaf_root(true, false);
21352 let mut rect_cache = HashMap::new();
21353 let mut requirements_cache = HashMap::new();
21354
21355 let collected =
21356 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21357
21358 assert_eq!(collected.scene.texts.len(), 1);
21359 assert_eq!(collected.scene.texts[0].snap_anchor, None);
21360 }
21361
21362 #[test]
21363 fn animated_translated_pure_text_uses_bounded_content_snap() {
21364 let root = pure_text_leaf_root(true, true);
21365 let mut rect_cache = HashMap::new();
21366 let mut requirements_cache = HashMap::new();
21367
21368 let collected =
21369 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21370
21371 assert_eq!(collected.child_layers.len(), 1);
21372 assert!(collected.scene.texts.is_empty());
21373 assert!(collected.scene.effect_layers.is_empty());
21374 assert_snap_anchor_close(
21375 collected.child_layers[0].snap_anchor,
21376 Point::new(11.4, 23.6),
21377 "animated translated pure text should use the bounded content snap phase",
21378 );
21379 }
21380
21381 #[test]
21382 fn rested_translated_pure_text_leaf_snaps_for_crisp_scroll_rest() {
21383 let root = pure_text_leaf_root(false, true);
21384 let mut rect_cache = HashMap::new();
21385 let mut requirements_cache = HashMap::new();
21386
21387 let collected =
21388 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21389
21390 assert_eq!(collected.child_layers.len(), 0);
21391 assert_eq!(collected.scene.texts.len(), 1);
21392 assert_eq!(collected.scene.effect_layers.len(), 0);
21393 assert_snap_anchor_close(
21394 collected.scene.texts[0].snap_anchor,
21395 Point::new(11.4, 23.6),
21396 "rested translated text should snap to device pixels",
21397 );
21398 }
21399
21400 #[test]
21401 fn static_gpu_effect_text_leaf_stays_unsnapped() {
21402 let root = text_layer_with_style(
21403 AnnotatedString::from("Gradient"),
21404 TextStyle::from_span_style(SpanStyle {
21405 brush: Some(Brush::linear_gradient(vec![
21406 Color(0.2, 0.8, 1.0, 1.0),
21407 Color(1.0, 0.7, 0.4, 1.0),
21408 ])),
21409 draw_style: Some(TextDrawStyle::Stroke { width: 2.5 }),
21410 ..SpanStyle::default()
21411 }),
21412 );
21413 let mut rect_cache = HashMap::new();
21414 let mut requirements_cache = HashMap::new();
21415
21416 let collected =
21417 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
21418
21419 assert_eq!(collected.scene.texts.len(), 1);
21420 assert_eq!(
21421 collected.scene.texts[0].snap_anchor, None,
21422 "gpu text-effect leaves must not take the rigid text snap path"
21423 );
21424 assert_eq!(
21425 collected.scene.effect_layers.len(),
21426 1,
21427 "gradient stroke text should still emit a runtime shader effect layer"
21428 );
21429 }
21430
21431 #[test]
21432 fn layer_surface_requirements_keep_shape_plus_direct_child_on_direct_path() {
21433 let mut child = test_layer(
21434 Rect {
21435 x: 0.0,
21436 y: 0.0,
21437 width: 40.0,
21438 height: 20.0,
21439 },
21440 vec![RenderNode::Primitive(PrimitiveEntry {
21441 phase: PrimitivePhase::BeforeChildren,
21442 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21443 primitive: DrawPrimitive::Rect {
21444 rect: Rect {
21445 x: 0.0,
21446 y: 0.0,
21447 width: 40.0,
21448 height: 20.0,
21449 },
21450 brush: Brush::solid(Color::WHITE),
21451 stroke: None,
21452 },
21453 clip: None,
21454 }),
21455 })],
21456 );
21457 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
21458
21459 let layer = test_layer(
21460 Rect {
21461 x: 0.0,
21462 y: 0.0,
21463 width: 64.0,
21464 height: 32.0,
21465 },
21466 vec![
21467 RenderNode::Primitive(PrimitiveEntry {
21468 phase: PrimitivePhase::BeforeChildren,
21469 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21470 primitive: DrawPrimitive::Rect {
21471 rect: Rect {
21472 x: 0.0,
21473 y: 0.0,
21474 width: 64.0,
21475 height: 32.0,
21476 },
21477 brush: Brush::solid(Color::BLACK),
21478 stroke: None,
21479 },
21480 clip: None,
21481 }),
21482 }),
21483 RenderNode::Layer(Box::new(child)),
21484 ],
21485 );
21486
21487 let requirements = layer_surface_requirements(&layer);
21488
21489 assert_eq!(requirements.direct_translation, Some(Point::default()));
21490 assert!(!requirements
21491 .surface_requirements
21492 .contains(SurfaceRequirement::MixedDirectContent));
21493 assert!(!requirements
21494 .surface_requirements
21495 .has_isolating_requirement());
21496 }
21497
21498 #[test]
21499 fn collect_layer_contents_translates_direct_text_rects_into_parent_space() {
21500 let mut child = text_layer_with_style(
21501 AnnotatedString::from("direct"),
21502 TextStyle::from_span_style(SpanStyle {
21503 text_decoration: Some(TextDecoration::UNDERLINE),
21504 ..SpanStyle::default()
21505 }),
21506 );
21507 child.transform_to_parent = ProjectiveTransform::translation(9.0, 7.0);
21508
21509 let parent = test_layer(
21510 Rect {
21511 x: 0.0,
21512 y: 0.0,
21513 width: 64.0,
21514 height: 32.0,
21515 },
21516 vec![RenderNode::Layer(Box::new(child))],
21517 );
21518
21519 let mut rect_cache = HashMap::new();
21520 let mut requirements_cache = HashMap::new();
21521 let collected = with_test_app_context(|| {
21522 collect_layer_contents(
21523 &parent,
21524 None,
21525 None,
21526 &mut rect_cache,
21527 &mut requirements_cache,
21528 )
21529 });
21530
21531 assert!(
21532 collected.child_layers.is_empty(),
21533 "decoration-only text child should collapse directly into the parent scene"
21534 );
21535 assert_eq!(collected.scene.texts.len(), 1, "expected one text draw");
21536 let text = &collected.scene.texts[0];
21537 assert!(
21538 text.rect.x >= 9.0 && text.rect.y >= 7.0,
21539 "collapsed text rect should be translated into parent space, got {:?}",
21540 text.rect
21541 );
21542 assert!(
21543 collected
21544 .scene
21545 .shapes
21546 .iter()
21547 .any(|shape| shape.rect.y >= 7.0),
21548 "collapsed underline geometry should also be translated into parent space"
21549 );
21550 }
21551
21552 #[test]
21553 fn normalized_scene_keeps_lazy_after_bound_text_for_prewarm() {
21554 use std::cell::RefCell;
21555
21556 fn collect_graph_text_labels(layer: &LayerNode, labels: &mut Vec<String>) {
21557 for child in &layer.children {
21558 match child {
21559 RenderNode::Primitive(PrimitiveEntry {
21560 node: PrimitiveNode::Text(text),
21561 ..
21562 }) => labels.push(text.text.text.clone()),
21563 RenderNode::Layer(child_layer) => {
21564 collect_graph_text_labels(child_layer, labels)
21565 }
21566 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
21567 }
21568 }
21569 }
21570
21571 let state_holder: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
21572 let state_holder_for_comp = state_holder.clone();
21573 let mut composition = cranpose_ui::run_test_composition(move || {
21574 let list_state = remember_lazy_list_state();
21575 *state_holder_for_comp.borrow_mut() = Some(list_state);
21576 let mut spec = LazyColumnSpec::new()
21577 .vertical_arrangement(cranpose_ui::LinearArrangement::SpacedBy(6.0));
21578 spec.beyond_bounds_item_count = 0;
21579 LazyColumn(Modifier::empty().height(96.0), list_state, spec, |scope| {
21580 scope.items(
21581 12,
21582 None::<fn(usize) -> u64>,
21583 None::<fn(usize) -> u64>,
21584 |index| {
21585 Text(
21586 format!("WarmRow {index}"),
21587 Modifier::empty().height(32.0),
21588 TextStyle::default(),
21589 );
21590 },
21591 );
21592 });
21593 });
21594
21595 let list_state = (*state_holder.borrow()).expect("lazy list state should be captured");
21596 list_state.scroll_to_item(4, 0.0);
21597
21598 let root = composition.root().expect("lazy column root");
21599 let handle = composition.runtime_handle();
21600 let mut applier = composition.applier_mut();
21601 applier.set_runtime_handle(handle);
21602 let _ = applier
21603 .compute_layout(
21604 root,
21605 Size {
21606 width: 240.0,
21607 height: 240.0,
21608 },
21609 )
21610 .expect("lazy column layout");
21611 let graph = build_graph_from_applier(&mut applier, root, 1.0).expect("lazy column graph");
21612 applier.clear_runtime_handle();
21613 let mut graph_labels = Vec::new();
21614 collect_graph_text_labels(&graph.root, &mut graph_labels);
21615
21616 let visible_indices: Vec<_> = list_state
21617 .layout_info()
21618 .visible_items_info
21619 .iter()
21620 .map(|item| item.index)
21621 .collect();
21622 assert_eq!(
21623 visible_indices,
21624 vec![4, 5, 6],
21625 "test setup expects exactly three viewport-visible rows"
21626 );
21627
21628 let mut rect_cache = HashMap::new();
21629 let mut requirements_cache = HashMap::new();
21630 let collected = with_test_app_context(|| {
21631 collect_layer_contents(
21632 &graph.root,
21633 None,
21634 None,
21635 &mut rect_cache,
21636 &mut requirements_cache,
21637 )
21638 });
21639 let root_text_labels: Vec<_> = collected
21640 .scene
21641 .texts
21642 .iter()
21643 .map(|text| text.text.text.clone())
21644 .collect();
21645 let child_layer_count = collected.child_layers.len();
21646 let warm_text = collected
21647 .scene
21648 .texts
21649 .iter()
21650 .find(|text| text.text.text == "WarmRow 7")
21651 .unwrap_or_else(|| {
21652 panic!(
21653 "after-bound lazy text should reach WGPU scene collection; graph_texts={graph_labels:?} root_texts={root_text_labels:?} child_layers={child_layer_count}"
21654 )
21655 });
21656
21657 assert!(
21658 warm_text.rect.y >= 96.0,
21659 "after-bound text should be below the viewport, got {:?}",
21660 warm_text.rect
21661 );
21662 assert_eq!(
21663 visible_draw_rect(warm_text.rect, warm_text.clip),
21664 None,
21665 "after-bound text should remain clipped away for drawing while staying available for glyph prewarm"
21666 );
21667 assert!(
21668 text_draw_should_prewarm_in_viewport(
21669 warm_text.rect,
21670 warm_text.clip,
21671 ViewportUniformParams {
21672 width: 240,
21673 height: 96,
21674 offset: [0.0, 0.0],
21675 },
21676 1.0,
21677 ),
21678 "after-bound text inside the warm window must be selected by WGPU prewarm"
21679 );
21680 }
21681
21682 #[test]
21683 fn direct_translation_accepts_nearly_identity_axis_scale_noise() {
21684 let local_bounds = Rect {
21685 x: 0.0,
21686 y: 0.0,
21687 width: 393.3,
21688 height: 16.8,
21689 };
21690 let quad = [
21691 [10.0, 78.399_994],
21692 [403.3, 78.399_994],
21693 [10.0, 95.2],
21694 [403.3, 95.2],
21695 ];
21696 let transform = ProjectiveTransform::from_rect_to_quad(local_bounds, quad);
21697
21698 assert_eq!(
21699 direct_translation(transform),
21700 Some(Point::new(10.0, 78.399_994)),
21701 );
21702 }
21703
21704 #[test]
21705 fn layer_surface_requirements_keep_shape_plus_isolating_child_as_mixed_content() {
21706 let mut child = test_layer(
21707 Rect {
21708 x: 0.0,
21709 y: 0.0,
21710 width: 24.0,
21711 height: 18.0,
21712 },
21713 vec![RenderNode::Primitive(PrimitiveEntry {
21714 phase: PrimitivePhase::BeforeChildren,
21715 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21716 primitive: DrawPrimitive::Rect {
21717 rect: Rect {
21718 x: 0.0,
21719 y: 0.0,
21720 width: 24.0,
21721 height: 18.0,
21722 },
21723 brush: Brush::solid(Color::WHITE),
21724 stroke: None,
21725 },
21726 clip: None,
21727 }),
21728 })],
21729 );
21730 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
21731 child.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
21732
21733 let layer = test_layer(
21734 Rect {
21735 x: 0.0,
21736 y: 0.0,
21737 width: 64.0,
21738 height: 32.0,
21739 },
21740 vec![
21741 RenderNode::Primitive(PrimitiveEntry {
21742 phase: PrimitivePhase::BeforeChildren,
21743 node: PrimitiveNode::Draw(DrawPrimitiveNode {
21744 primitive: DrawPrimitive::Rect {
21745 rect: Rect {
21746 x: 0.0,
21747 y: 0.0,
21748 width: 64.0,
21749 height: 32.0,
21750 },
21751 brush: Brush::solid(Color::BLACK),
21752 stroke: None,
21753 },
21754 clip: None,
21755 }),
21756 }),
21757 RenderNode::Layer(Box::new(child)),
21758 ],
21759 );
21760
21761 let requirements = layer_surface_requirements(&layer);
21762
21763 assert!(requirements
21764 .surface_requirements
21765 .contains(SurfaceRequirement::MixedDirectContent));
21766 assert!(!requirements
21767 .surface_requirements
21768 .has_isolating_requirement());
21769 }
21770
21771 #[test]
21772 fn build_scene_window_filters_and_translates_items() {
21773 let mut shape = test_shape(6, BlendMode::SrcOver);
21774 shape.rect.x = 12.0;
21775 shape.rect.y = 25.0;
21776 shape.local_rect.x = 12.0;
21777 shape.local_rect.y = 25.0;
21778 shape.quad = [[12.0, 25.0], [20.0, 25.0], [12.0, 33.0], [20.0, 33.0]];
21779 shape.clip = Some(Rect {
21780 x: 11.0,
21781 y: 24.0,
21782 width: 10.0,
21783 height: 10.0,
21784 });
21785
21786 let mut image = test_image(8, BlendMode::SrcOver);
21787 image.rect.x = 18.0;
21788 image.rect.y = 27.0;
21789 image.local_rect.x = 18.0;
21790 image.local_rect.y = 27.0;
21791 image.quad = [[18.0, 27.0], [26.0, 27.0], [18.0, 35.0], [26.0, 35.0]];
21792
21793 let mut text = test_text(9);
21794 text.rect.x = 16.0;
21795 text.rect.y = 29.0;
21796 text.clip = Some(Rect {
21797 x: 15.0,
21798 y: 28.0,
21799 width: 9.0,
21800 height: 6.0,
21801 });
21802
21803 let mut shadow_shape = test_shape(7, BlendMode::SrcOver);
21804 shadow_shape.rect.x = 14.0;
21805 shadow_shape.rect.y = 26.0;
21806 shadow_shape.local_rect.x = 14.0;
21807 shadow_shape.local_rect.y = 26.0;
21808 shadow_shape.quad = [[14.0, 26.0], [22.0, 26.0], [14.0, 34.0], [22.0, 34.0]];
21809 let mut shadow = test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]);
21810 shadow.z_index = 7;
21811
21812 let mut nested_effect = effect_layer(6, 10);
21813 nested_effect.rect.x = 13.0;
21814 nested_effect.rect.y = 24.0;
21815 nested_effect.clip = Some(Rect {
21816 x: 15.0,
21817 y: 25.0,
21818 width: 4.0,
21819 height: 5.0,
21820 });
21821
21822 let mut nested_backdrop = backdrop_layer(8);
21823 nested_backdrop.rect.x = 17.0;
21824 nested_backdrop.rect.y = 26.0;
21825 nested_backdrop.clip = Some(Rect {
21826 x: 18.0,
21827 y: 27.0,
21828 width: 3.0,
21829 height: 4.0,
21830 });
21831
21832 let window = build_scene_window(
21833 SceneWindowSource {
21834 shapes: &[test_shape(4, BlendMode::SrcOver), shape],
21835 brushes: &[],
21836 images: &[image],
21837 texts: &[text],
21838 shadow_draws: &[shadow],
21839 draw_ops: &[],
21840 effect_layers: &[effect_layer(2, 4), nested_effect.clone()],
21841 backdrop_layers: &[backdrop_layer(4), nested_backdrop.clone()],
21842 },
21843 5,
21844 10,
21845 Rect {
21846 x: 10.0,
21847 y: 20.0,
21848 width: 20.0,
21849 height: 20.0,
21850 },
21851 );
21852
21853 assert_eq!(window.shapes.len(), 1);
21854 assert_eq!(
21855 window.shapes[0].rect,
21856 Rect {
21857 x: 2.0,
21858 y: 5.0,
21859 width: 8.0,
21860 height: 8.0,
21861 }
21862 );
21863 assert_eq!(
21864 window.shapes[0].clip,
21865 Some(Rect {
21866 x: 1.0,
21867 y: 4.0,
21868 width: 10.0,
21869 height: 10.0,
21870 })
21871 );
21872 assert_eq!(window.images.len(), 1);
21873 assert_eq!(window.images[0].rect.x, 8.0);
21874 assert_eq!(window.images[0].rect.y, 7.0);
21875 assert_eq!(window.texts.len(), 1);
21876 assert_eq!(window.texts[0].rect.x, 6.0);
21877 assert_eq!(window.texts[0].rect.y, 9.0);
21878 assert_eq!(
21879 window.texts[0].clip,
21880 Some(Rect {
21881 x: 5.0,
21882 y: 8.0,
21883 width: 9.0,
21884 height: 6.0,
21885 })
21886 );
21887 assert_eq!(window.shadow_draws.len(), 1);
21888 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.x, 4.0);
21889 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.y, 6.0);
21890 assert_eq!(window.effect_layers.len(), 1);
21891 assert_eq!(
21892 window.effect_layers[0].rect,
21893 Rect {
21894 x: 3.0,
21895 y: 4.0,
21896 width: 10.0,
21897 height: 10.0,
21898 }
21899 );
21900 assert_eq!(
21901 window.effect_layers[0].clip,
21902 Some(Rect {
21903 x: 5.0,
21904 y: 5.0,
21905 width: 4.0,
21906 height: 5.0,
21907 })
21908 );
21909 assert_eq!(window.backdrop_layers.len(), 1);
21910 assert_eq!(
21911 window.backdrop_layers[0].rect,
21912 Rect {
21913 x: 7.0,
21914 y: 6.0,
21915 width: 10.0,
21916 height: 10.0,
21917 }
21918 );
21919 assert_eq!(
21920 window.backdrop_layers[0].clip,
21921 Some(Rect {
21922 x: 8.0,
21923 y: 7.0,
21924 width: 3.0,
21925 height: 4.0,
21926 })
21927 );
21928 }
21929
21930 #[test]
21931 fn filtered_effect_layer_index_counts_only_window_members() {
21932 let effects = vec![
21933 effect_layer(0, 2),
21934 effect_layer(5, 12),
21935 effect_layer(6, 10),
21936 effect_layer(14, 20),
21937 ];
21938
21939 assert_eq!(filtered_effect_layer_index(&effects, 1, 5, 12), Some(0));
21940 assert_eq!(filtered_effect_layer_index(&effects, 2, 5, 12), Some(1));
21941 assert_eq!(filtered_effect_layer_index(&effects, 3, 5, 12), None);
21942 }
21943
21944 #[test]
21945 fn blend_mode_support_matrix_is_explicit() {
21946 assert!(is_blend_mode_supported(BlendMode::SrcOver));
21947 assert!(is_blend_mode_supported(BlendMode::DstOut));
21948 assert!(!is_blend_mode_supported(BlendMode::Clear));
21949 assert!(!is_blend_mode_supported(BlendMode::Multiply));
21950 }
21951
21952 #[test]
21953 fn collect_non_effect_segment_items_preserves_global_z_order() {
21954 let shapes = vec![
21955 test_shape(3, BlendMode::SrcOver),
21956 test_shape(1, BlendMode::DstOut),
21957 ];
21958 let images = vec![test_image(2, BlendMode::SrcOver)];
21959 let texts = vec![test_text(0)];
21960 let shadows: Vec<ShadowDraw> = Vec::new();
21961 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
21962
21963 let mut scratch = Vec::new();
21964 collect_non_effect_segment_items(
21965 &shapes,
21966 &images,
21967 &texts,
21968 &shadows,
21969 &draw_ops,
21970 0,
21971 4,
21972 &[],
21973 100,
21974 100,
21975 1.0,
21976 &mut scratch,
21977 );
21978 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
21979 assert_eq!(
21980 items,
21981 vec![
21982 SegmentDrawItem::Text(0),
21983 SegmentDrawItem::Shape(1),
21984 SegmentDrawItem::Image(0),
21985 SegmentDrawItem::Shape(0),
21986 ]
21987 );
21988 }
21989
21990 #[test]
21991 fn collect_non_effect_segment_items_filters_effect_ranges() {
21992 let shapes = vec![
21993 test_shape(1, BlendMode::SrcOver),
21994 test_shape(3, BlendMode::DstOut),
21995 ];
21996 let images = vec![test_image(2, BlendMode::SrcOver)];
21997 let texts = vec![test_text(4)];
21998 let shadows: Vec<ShadowDraw> = Vec::new();
21999 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
22000 let effect_ranges = [std::ops::Range { start: 2, end: 4 }];
22001
22002 let mut scratch = Vec::new();
22003 collect_non_effect_segment_items(
22004 &shapes,
22005 &images,
22006 &texts,
22007 &shadows,
22008 &draw_ops,
22009 0,
22010 5,
22011 &effect_ranges,
22012 100,
22013 100,
22014 1.0,
22015 &mut scratch,
22016 );
22017 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
22018 assert_eq!(
22019 items,
22020 vec![SegmentDrawItem::Shape(0), SegmentDrawItem::Text(0)]
22021 );
22022 }
22023
22024 #[test]
22025 fn collect_non_effect_segment_items_culls_offscreen_shapes_but_keeps_text_prewarm() {
22026 let mut shape = test_shape(0, BlendMode::SrcOver);
22027 shape.rect.y = 160.0;
22028 shape.local_rect.y = 160.0;
22029 shape.quad = [[0.0, 160.0], [8.0, 160.0], [0.0, 168.0], [8.0, 168.0]];
22030
22031 let shapes = vec![shape];
22032 let images = Vec::new();
22033 let mut text = test_text(1);
22034 text.rect.y = 160.0;
22035 let texts = vec![text];
22036 let shadows: Vec<ShadowDraw> = Vec::new();
22037 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
22038
22039 let mut scratch = Vec::new();
22040 collect_non_effect_segment_items(
22041 &shapes,
22042 &images,
22043 &texts,
22044 &shadows,
22045 &draw_ops,
22046 0,
22047 2,
22048 &[],
22049 100,
22050 100,
22051 1.0,
22052 &mut scratch,
22053 );
22054
22055 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
22056 assert_eq!(items, vec![SegmentDrawItem::Text(0)]);
22057 }
22058
22059 #[test]
22060 fn segment_command_iter_merges_non_conflicting_batches_into_one_chunk() {
22061 let ordered_items = vec![
22062 (0, SegmentDrawItem::Shape(0)),
22063 (1, SegmentDrawItem::Image(0)),
22064 (2, SegmentDrawItem::Text(0)),
22065 ];
22066 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
22067 let images = vec![test_image(1, BlendMode::DstOut)];
22068
22069 let commands: Vec<_> = SegmentCommandIter::new(
22070 &ordered_items,
22071 &shapes,
22072 &images,
22073 ShapeBatchLimits::desktop(),
22074 )
22075 .collect();
22076
22077 assert_eq!(
22078 commands,
22079 vec![SegmentRenderCommand::DrawChunk(chunk(&[
22080 SegmentBatchPlan::Shape {
22081 start: 0,
22082 end: 1,
22083 blend_mode: BlendMode::SrcOver,
22084 },
22085 SegmentBatchPlan::Image {
22086 start: 1,
22087 end: 2,
22088 blend_mode: BlendMode::DstOut,
22089 },
22090 SegmentBatchPlan::Text { start: 2, end: 3 },
22091 ]))]
22092 );
22093 }
22094
22095 #[test]
22096 fn segment_command_iter_keeps_layer_composites_in_ordered_draw_chunk() {
22097 let ordered_items = vec![
22098 (0, SegmentDrawItem::Shape(0)),
22099 (1, SegmentDrawItem::Composite(0)),
22100 (2, SegmentDrawItem::Image(0)),
22101 (3, SegmentDrawItem::Composite(1)),
22102 (4, SegmentDrawItem::Text(0)),
22103 ];
22104 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
22105 let images = vec![test_image(2, BlendMode::SrcOver)];
22106
22107 let commands: Vec<_> = SegmentCommandIter::new(
22108 &ordered_items,
22109 &shapes,
22110 &images,
22111 ShapeBatchLimits::desktop(),
22112 )
22113 .collect();
22114
22115 assert_eq!(
22116 commands,
22117 vec![SegmentRenderCommand::DrawChunk(chunk(&[
22118 SegmentBatchPlan::Shape {
22119 start: 0,
22120 end: 1,
22121 blend_mode: BlendMode::SrcOver,
22122 },
22123 SegmentBatchPlan::Composite { start: 1, end: 2 },
22124 SegmentBatchPlan::Image {
22125 start: 2,
22126 end: 3,
22127 blend_mode: BlendMode::SrcOver,
22128 },
22129 SegmentBatchPlan::Composite { start: 3, end: 4 },
22130 SegmentBatchPlan::Text { start: 4, end: 5 },
22131 ]))]
22132 );
22133 }
22134
22135 #[test]
22136 fn retain_renderable_shadow_items_culls_invisible_shadow_boundaries() {
22137 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
22138 let images = vec![test_image(2, BlendMode::SrcOver)];
22139 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
22140 shadow_shape.rect = Rect {
22141 x: 500.0,
22142 y: 500.0,
22143 width: 12.0,
22144 height: 12.0,
22145 };
22146 let shadow_draws = vec![ShadowDraw {
22147 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
22148 brushes: vec![],
22149 texts: Vec::new(),
22150 blur_radius: 8.0,
22151 clip: None,
22152 z_index: 1,
22153 }];
22154 let mut ordered_items = vec![
22155 (0, SegmentDrawItem::Shape(0)),
22156 (1, SegmentDrawItem::Shadow(0)),
22157 (2, SegmentDrawItem::Image(0)),
22158 ];
22159
22160 let culled =
22161 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
22162 let commands: Vec<_> = SegmentCommandIter::new(
22163 &ordered_items,
22164 &shapes,
22165 &images,
22166 ShapeBatchLimits::desktop(),
22167 )
22168 .collect();
22169
22170 assert_eq!(culled, 1);
22171 assert_eq!(
22172 commands,
22173 vec![SegmentRenderCommand::DrawChunk(chunk(&[
22174 SegmentBatchPlan::Shape {
22175 start: 0,
22176 end: 1,
22177 blend_mode: BlendMode::SrcOver,
22178 },
22179 SegmentBatchPlan::Image {
22180 start: 1,
22181 end: 2,
22182 blend_mode: BlendMode::SrcOver,
22183 },
22184 ]))]
22185 );
22186 }
22187
22188 #[test]
22189 fn retain_renderable_shadow_items_keeps_visible_shadow_boundaries() {
22190 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
22191 shadow_shape.rect = Rect {
22192 x: 20.0,
22193 y: 20.0,
22194 width: 12.0,
22195 height: 12.0,
22196 };
22197 let shadow_draws = vec![ShadowDraw {
22198 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
22199 brushes: vec![],
22200 texts: Vec::new(),
22201 blur_radius: 8.0,
22202 clip: None,
22203 z_index: 1,
22204 }];
22205 let mut ordered_items = vec![(1, SegmentDrawItem::Shadow(0))];
22206
22207 let culled =
22208 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
22209
22210 assert_eq!(culled, 0);
22211 assert_eq!(ordered_items, vec![(1, SegmentDrawItem::Shadow(0))]);
22212 }
22213
22214 #[test]
22215 fn shape_data_layout_matches_the_wgsl_mirror() {
22216 assert_eq!(std::mem::size_of::<ShapeData>(), 160);
22220 assert_eq!(std::mem::size_of::<ShapeData>() % 16, 0);
22221 assert_eq!(std::mem::size_of::<GradientStop>(), 32);
22222 }
22223
22224 #[test]
22225 fn shape_flags_pack_kind_cap_and_join_without_collision() {
22226 assert_eq!(
22227 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
22228 0.0
22229 );
22230 assert_eq!(
22231 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
22232 1.0
22233 );
22234 assert_eq!(
22235 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter),
22236 2.0
22237 );
22238 assert_eq!(
22240 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Round, StrokeJoin::Miter),
22241 2.0 + 4.0
22242 );
22243 assert_eq!(
22244 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Square, StrokeJoin::Miter),
22245 2.0 + 8.0
22246 );
22247 assert_eq!(
22248 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Round),
22249 1.0 + 16.0
22250 );
22251 assert_eq!(
22252 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Bevel),
22253 1.0 + 32.0
22254 );
22255 for kind in [SHAPE_KIND_FILL, SHAPE_KIND_STROKE, SHAPE_KIND_ARC] {
22257 for cap in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square] {
22258 for join in [StrokeJoin::Miter, StrokeJoin::Round, StrokeJoin::Bevel] {
22259 let packed = pack_shape_flags(kind, cap, join);
22260 let bits = packed as u32;
22261 assert_eq!(bits & 3, kind);
22262 assert_eq!((bits >> 2) & 3, stroke_cap_code(cap));
22263 assert_eq!((bits >> 4) & 3, stroke_join_code(join));
22264 assert_eq!(packed, bits as f32, "flags must be exact in f32");
22265 }
22266 }
22267 }
22268 }
22269
22270 #[cfg(not(target_arch = "wasm32"))]
22271 #[test]
22272 fn mesh_vertex_layout_matches_the_wgsl_input() {
22273 assert_eq!(std::mem::size_of::<MeshVertex>(), 20);
22276 }
22277
22278 #[cfg(not(target_arch = "wasm32"))]
22282 #[allow(clippy::too_many_arguments)]
22283 fn sdf_arc_band_reference(
22284 p: [f32; 2],
22285 center: [f32; 2],
22286 inner: f32,
22287 outer: f32,
22288 mid_sin_cos: [f32; 2],
22289 half_sin_cos: [f32; 2],
22290 cap: u32,
22291 ) -> f32 {
22292 let ra = (outer + inner) * 0.5;
22293 let rb = ((outer - inner) * 0.5).max(0.0);
22294 let sm = mid_sin_cos[0];
22295 let cm = mid_sin_cos[1];
22296 let d = [p[0] - center[0], p[1] - center[1]];
22297 let mut q = [-sm * d[0] + cm * d[1], cm * d[0] + sm * d[1]];
22298 q[0] = q[0].abs();
22299 let sc = half_sin_cos;
22300 let mut dist = if sc[1] * q[0] > sc[0] * q[1] {
22301 let dx = q[0] - sc[0] * ra;
22302 let dy = q[1] - sc[1] * ra;
22303 (dx * dx + dy * dy).sqrt() - rb
22304 } else {
22305 ((q[0] * q[0] + q[1] * q[1]).sqrt() - ra).abs() - rb
22306 };
22307 let plane = sc[1] * q[0] - sc[0] * q[1];
22308 if cap == 0 {
22310 dist = dist.max(plane);
22311 } else if cap == 2 {
22312 dist = dist.max(plane - rb);
22313 }
22314 dist
22315 }
22316
22317 #[cfg(not(target_arch = "wasm32"))]
22318 fn point_in_triangle(p: [f64; 2], tri: &[[f64; 2]; 3]) -> bool {
22319 let side = |a: [f64; 2], b: [f64; 2]| {
22320 (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
22321 };
22322 let d0 = side(tri[0], tri[1]);
22323 let d1 = side(tri[1], tri[2]);
22324 let d2 = side(tri[2], tri[0]);
22325 let has_neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
22326 let has_pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
22327 !(has_neg && has_pos)
22328 }
22329
22330 #[cfg(not(target_arch = "wasm32"))]
22331 fn converted_arc_shape(arc: cranpose_ui_graphics::ArcGeometry, root_scale: f32) -> ShapeData {
22332 let bounds = arc.bounds();
22333 let mut shape = test_shape(0, BlendMode::SrcOver);
22334 shape.rect = bounds;
22335 shape.local_rect = bounds;
22336 shape.quad = [
22337 [bounds.x, bounds.y],
22338 [bounds.x + bounds.width, bounds.y],
22339 [bounds.x, bounds.y + bounds.height],
22340 [bounds.x + bounds.width, bounds.y + bounds.height],
22341 ];
22342 shape.arc = Some(arc);
22343 let mut converted = ShapeData::zeroed();
22344 convert_shape_into_slots(&shape, &[], root_scale, 0, &mut converted, &mut []);
22345 converted
22346 }
22347
22348 #[cfg(not(target_arch = "wasm32"))]
22353 #[test]
22354 fn arc_mesh_contains_every_band_pixel() {
22355 use cranpose_ui_graphics::ArcGeometry;
22356 let tau = cranpose_ui_graphics::TAU;
22357 let center = Point::new(250.0, 250.0);
22358 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
22359 (90.0, 100.0, 0.0, tau, StrokeCap::Round),
22361 (80.0, 100.0, 1.0, 10.0, StrokeCap::Butt),
22363 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
22365 (30.0, 80.0, 0.7, 2.5, StrokeCap::Butt),
22367 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
22368 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
22369 (99.0, 101.0, 3.0, 4.0, StrokeCap::Round),
22371 (0.6, 2.0, 0.3, 1.2, StrokeCap::Butt),
22373 (1900.0, 1904.0, 0.1, 0.35, StrokeCap::Square),
22375 (40.0, 60.0, 5.0, 1e-3, StrokeCap::Round),
22377 (40.0, 60.0, 0.2, tau - 1e-3, StrokeCap::Butt),
22379 (0.0, 3.0, 1.0, 2.0, StrokeCap::Round),
22381 (20.0, 60.0, 4.5, 1.9, StrokeCap::Butt),
22383 ];
22384 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
22385 for root_scale in [1.0f32, 2.0, 2.75] {
22390 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
22391 assert!(!arc.is_degenerate(), "case {case} must be drawable");
22392 let converted = converted_arc_shape(arc, root_scale);
22393 let band = arc_mesh_band(&converted)
22394 .unwrap_or_else(|| panic!("case {case} must qualify for meshing"));
22395 let mut vertices = Vec::new();
22396 let mut indices = Vec::new();
22397 let segments =
22398 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
22399 .unwrap_or_else(|| panic!("case {case} must produce a mesh"));
22400 assert!(segments >= ARC_MESH_MIN_SEGMENTS);
22401 let position = |index: u32| {
22404 let p = vertices[index as usize].position;
22405 [p[0] as f64, p[1] as f64]
22406 };
22407 let triangles: Vec<[[f64; 2]; 3]> = indices
22408 .as_chunks::<3>()
22409 .0
22410 .iter()
22411 .map(|tri| [position(tri[0]), position(tri[1]), position(tri[2])])
22412 .collect();
22413
22414 let [qx, qy, ..] = converted.quad01;
22418 let [_, _, qr, qb] = converted.quad23;
22419 let (rw, rh) = (qr - qx, qb - qy);
22420 let cap_bits = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
22421 let step = (rw.max(rh) / 400.0).clamp(0.25, 2.0);
22422 let mut band_points = 0usize;
22423 let mut y = qy;
22424 while y <= qb {
22425 let mut x = qx;
22426 while x <= qr {
22427 let dist = sdf_arc_band_reference(
22428 [x, y],
22429 [converted.arc_params[0], converted.arc_params[1]],
22430 converted.stroke_params[3],
22431 converted.stroke_params[2],
22432 [converted.radii[0], converted.radii[1]],
22433 [converted.radii[2], converted.radii[3]],
22434 cap_bits,
22435 );
22436 if dist <= 0.5 {
22437 band_points += 1;
22438 let p = [x as f64, y as f64];
22439 assert!(
22440 triangles.iter().any(|tri| point_in_triangle(p, tri)),
22441 "case {case} scale {root_scale}: band point ({x}, {y}) \
22442 dist {dist} escapes the mesh"
22443 );
22444 }
22445 x += step;
22446 }
22447 y += step;
22448 }
22449 assert!(
22450 band_points > 0,
22451 "case {case} scale {root_scale}: the sampling grid never hit the band"
22452 );
22453 }
22454 }
22455 }
22456
22457 #[cfg(not(target_arch = "wasm32"))]
22463 #[test]
22464 fn unmeshed_shapes_leave_no_geometry_and_empty_index_ranges() {
22465 let shape = test_shape(0, BlendMode::SrcOver);
22466 let mut converted = ShapeData::zeroed();
22467 convert_shape_into_slots(&shape, &[], 1.0, 0, &mut converted, &mut []);
22468 let build = build_arc_mesh_vertices(
22469 std::slice::from_ref(&converted),
22470 RETAINED_MESH_MIN_PX2_DEFAULT as f64,
22471 )
22472 .expect("within budget");
22473 assert_eq!(build.meshed_arcs, 0);
22474 assert_eq!(build.meshed_rims, 0);
22475 assert_eq!(build.passthrough, 1);
22476 assert_eq!(build.meshed_stretches, 0);
22477 assert!(build.vertices.is_empty());
22478 assert!(build.indices.is_empty());
22479 assert_eq!(build.index_prefix, vec![0, 0]);
22480 assert_eq!(build.mesh_area, build.quad_area);
22483 }
22484
22485 #[cfg(not(target_arch = "wasm32"))]
22491 #[test]
22492 fn arc_mesh_indices_share_boundary_vertices_and_wrap_closed_rings() {
22493 use cranpose_ui_graphics::ArcGeometry;
22494 let tau = cranpose_ui_graphics::TAU;
22495 for (sweep, closed) in [(tau, true), (1.9f32, false)] {
22498 let arc = ArcGeometry::new(
22499 Point::new(250.0, 250.0),
22500 80.0,
22501 100.0,
22502 0.7,
22503 sweep,
22504 StrokeCap::Round,
22505 );
22506 let mut converted = converted_arc_shape(arc, 1.0);
22507 converted.rect = [0.0, 0.0, 500.0, 500.0];
22511 converted.quad01 = [0.0, 0.0, 500.0, 0.0];
22512 converted.quad23 = [0.0, 500.0, 500.0, 500.0];
22513 let band = arc_mesh_band(&converted).expect("arc must qualify");
22514 let mut vertices = Vec::new();
22515 let mut indices = Vec::new();
22516 let segments = emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
22517 .expect("arc must mesh");
22518 let boundary_count = if closed { segments } else { segments + 1 };
22519 assert_eq!(
22520 vertices.len(),
22521 2 * boundary_count,
22522 "closed={closed}: every boundary owns exactly one (inner, outer) pair"
22523 );
22524 assert_eq!(indices.len(), 6 * segments);
22525 for j in 0..segments {
22529 let jb = (j + 1) % boundary_count;
22530 let (in_a, out_a) = (2 * j as u32, 2 * j as u32 + 1);
22531 let (in_b, out_b) = (2 * jb as u32, 2 * jb as u32 + 1);
22532 assert_eq!(
22533 indices[6 * j..6 * j + 6],
22534 [in_a, out_a, out_b, in_a, out_b, in_b],
22535 "closed={closed}: segment {j} must share its boundary pairs"
22536 );
22537 }
22538 if closed {
22539 assert_eq!(indices[6 * segments - 1], 0);
22542 }
22543 for pair in vertices.as_chunks::<2>().0 {
22547 let radius = |v: &MeshVertex| {
22548 let dx = v.position[0] - 250.0;
22549 let dy = v.position[1] - 250.0;
22550 (dx * dx + dy * dy).sqrt()
22551 };
22552 assert!(radius(&pair[0]) < radius(&pair[1]));
22553 }
22554 }
22555 }
22556
22557 #[cfg(not(target_arch = "wasm32"))]
22563 #[test]
22564 fn arc_mesh_clipped_segments_fan_over_private_vertices() {
22565 use cranpose_ui_graphics::ArcGeometry;
22566 let arc = ArcGeometry::new(
22567 Point::new(250.0, 250.0),
22568 80.0,
22569 100.0,
22570 0.0,
22571 cranpose_ui_graphics::TAU,
22572 StrokeCap::Round,
22573 );
22574 let converted = converted_arc_shape(arc, 1.0);
22575 let band = arc_mesh_band(&converted).expect("ring must qualify");
22576 let mut vertices = Vec::new();
22577 let mut indices = Vec::new();
22578 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
22579 .expect("ring must mesh");
22580 let mut uses = vec![0usize; vertices.len()];
22583 for &index in &indices {
22584 uses[index as usize] += 1;
22585 }
22586 assert!(
22587 uses.iter().any(|&count| count >= 3),
22588 "some boundary vertices must be shared across trapezoids"
22589 );
22590 let [left, top, ..] = converted.quad01;
22596 let [.., right, bottom] = converted.quad23;
22597 let clipped: Vec<&MeshVertex> = vertices
22598 .iter()
22599 .filter(|vertex| {
22600 let [x, y] = vertex.position;
22601 x == left || x == right || y == top || y == bottom
22602 })
22603 .collect();
22604 assert!(
22605 !clipped.is_empty(),
22606 "the tight box must clip the pushed-out chord vertices"
22607 );
22608 assert!(
22611 vertices.len() < indices.len(),
22612 "{} unique vertices should undercut {} triangle corners",
22613 vertices.len(),
22614 indices.len()
22615 );
22616 }
22617
22618 #[cfg(not(target_arch = "wasm32"))]
22619 #[test]
22620 fn arc_mesh_budget_overflow_falls_back_to_whole_slot_passthrough() {
22621 use cranpose_ui_graphics::ArcGeometry;
22622 let arc = ArcGeometry::new(
22627 Point::new(2000.0, 2000.0),
22628 1690.0,
22629 1710.0,
22630 0.0,
22631 cranpose_ui_graphics::TAU,
22632 StrokeCap::Round,
22633 );
22634 let converted = converted_arc_shape(arc, 1.0);
22635 let shapes = vec![converted; 100];
22636 assert!(build_arc_mesh_vertices(&shapes, RETAINED_MESH_MIN_PX2_DEFAULT as f64).is_none());
22637 }
22638
22639 #[cfg(not(target_arch = "wasm32"))]
22644 #[test]
22645 fn retained_mesh_size_gate_engages_exactly_per_threshold() {
22646 use cranpose_ui_graphics::ArcGeometry;
22647 let big_ring = converted_arc_shape(
22651 ArcGeometry::new(
22652 Point::new(204.0, 204.0),
22653 140.0,
22654 160.0,
22655 0.0,
22656 cranpose_ui_graphics::TAU,
22657 StrokeCap::Butt,
22658 ),
22659 1.0,
22660 );
22661 let small_arc = converted_arc_shape(
22662 ArcGeometry::new(
22663 Point::new(204.0, 204.0),
22664 12.0,
22665 18.0,
22666 0.3,
22667 0.5,
22668 StrokeCap::Butt,
22669 ),
22670 1.0,
22671 );
22672 let rim = rim_test_shape_data();
22673 let shapes = [big_ring, small_arc, rim];
22674 let big_px2 = quad_shoelace_area(&shapes[0]);
22675 let small_px2 = quad_shoelace_area(&shapes[1]);
22676 let rim_px2 = quad_shoelace_area(&shapes[2]);
22677 assert!(small_px2 < 1024.0 && big_px2 > rim_px2 && rim_px2 > 16384.0);
22678
22679 let build = build_arc_mesh_vertices(&shapes, RETAINED_MESH_MIN_PX2_DEFAULT as f64)
22683 .expect("within budget");
22684 assert_eq!(
22685 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22686 (1, 1, 1)
22687 );
22688 assert_eq!(build.meshed_stretches, 2);
22689 assert_eq!(build.index_prefix[1], build.index_prefix[2]);
22692 assert!(build.index_prefix[1] > build.index_prefix[0]);
22693 assert!(build.index_prefix[3] > build.index_prefix[2]);
22694
22695 let build = build_arc_mesh_vertices(&shapes, big_px2).expect("within budget");
22698 assert_eq!(
22699 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22700 (1, 0, 2)
22701 );
22702 let build =
22704 build_arc_mesh_vertices(&shapes, big_px2 + big_px2 * f64::EPSILON).expect("budget");
22705 assert_eq!(
22706 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22707 (0, 0, 3)
22708 );
22709
22710 let build = build_arc_mesh_vertices(&shapes, (rim_px2 + big_px2) * 0.5).expect("budget");
22712 assert_eq!(
22713 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22714 (1, 0, 2)
22715 );
22716
22717 let everything_gated =
22720 build_arc_mesh_vertices(&shapes, big_px2 * 2.0).expect("within budget");
22721 assert_eq!(everything_gated.passthrough, 3);
22722 assert_eq!(everything_gated.meshed_stretches, 0);
22723 assert!(everything_gated.vertices.is_empty());
22724 assert_eq!(everything_gated.index_prefix, vec![0, 0, 0, 0]);
22725 }
22726
22727 #[cfg(not(target_arch = "wasm32"))]
22732 #[test]
22733 fn meshed_stretches_count_maximal_runs_of_consecutive_meshed_shapes() {
22734 use cranpose_ui_graphics::ArcGeometry;
22735 let big = converted_arc_shape(
22736 ArcGeometry::new(
22737 Point::new(204.0, 204.0),
22738 140.0,
22739 160.0,
22740 0.0,
22741 cranpose_ui_graphics::TAU,
22742 StrokeCap::Butt,
22743 ),
22744 1.0,
22745 );
22746 let small = converted_arc_shape(
22747 ArcGeometry::new(
22748 Point::new(204.0, 204.0),
22749 12.0,
22750 18.0,
22751 0.3,
22752 0.5,
22753 StrokeCap::Butt,
22754 ),
22755 1.0,
22756 );
22757 let shapes = [big, big, small, big, small, small, big, big];
22759 let build = build_arc_mesh_vertices(&shapes, RETAINED_MESH_MIN_PX2_DEFAULT as f64)
22760 .expect("within budget");
22761 assert_eq!(build.meshed_arcs, 5);
22762 assert_eq!(build.passthrough, 3);
22763 assert_eq!(build.meshed_stretches, 3);
22764 assert!(build.meshed_stretches <= MESH_SLOT_MAX_STRETCHES);
22767 }
22768
22769 #[cfg(not(target_arch = "wasm32"))]
22772 #[test]
22773 fn retained_mesh_px2_override_parses_and_clamps() {
22774 assert_eq!(
22775 parse_retained_mesh_min_px2(None),
22776 RETAINED_MESH_MIN_PX2_DEFAULT as f64
22777 );
22778 assert_eq!(
22779 parse_retained_mesh_min_px2(Some("not a number")),
22780 RETAINED_MESH_MIN_PX2_DEFAULT as f64
22781 );
22782 assert_eq!(
22783 parse_retained_mesh_min_px2(Some("-5")),
22784 RETAINED_MESH_MIN_PX2_DEFAULT as f64
22785 );
22786 assert_eq!(parse_retained_mesh_min_px2(Some(" 40000 ")), 40000.0);
22787 assert_eq!(
22788 parse_retained_mesh_min_px2(Some("0")),
22789 *RETAINED_MESH_MIN_PX2_RANGE.start() as f64
22790 );
22791 assert_eq!(
22792 parse_retained_mesh_min_px2(Some("99999999")),
22793 *RETAINED_MESH_MIN_PX2_RANGE.end() as f64
22794 );
22795 }
22796
22797 #[cfg(not(target_arch = "wasm32"))]
22802 #[test]
22803 fn retained_capture_meshes_big_stroked_circle_rims_as_annuli() {
22804 let rim = rim_test_shape_data();
22805 let build = build_arc_mesh_vertices(
22806 std::slice::from_ref(&rim),
22807 RETAINED_MESH_MIN_PX2_DEFAULT as f64,
22808 )
22809 .expect("within budget");
22810 assert_eq!(
22811 (build.meshed_arcs, build.meshed_rims, build.passthrough),
22812 (0, 1, 0)
22813 );
22814 assert!(build.meshed_segments >= ARC_MESH_MIN_SEGMENTS);
22815 assert!(build.mesh_area < 0.2 * build.quad_area);
22820 let band = rim_band_geometry(&rim).expect("rim must qualify");
22821 for vertex in &build.vertices {
22822 let dx = vertex.position[0] - band.center[0];
22823 let dy = vertex.position[1] - band.center[1];
22824 let radius = (dx * dx + dy * dy).sqrt();
22825 assert!(
22826 radius >= band.inner - ARC_MESH_MARGIN - 1e-3,
22827 "vertex at radius {radius} fell inside the annulus hole"
22828 );
22829 }
22830 }
22831
22832 #[cfg(not(target_arch = "wasm32"))]
22836 fn rim_test_shape_data() -> ShapeData {
22837 let mut shape = ShapeData::zeroed();
22838 shape.rect = [40.0, 40.0, 300.0, 300.0];
22839 shape.radii = [146.0; 4];
22840 shape.stroke_params = [
22841 8.0,
22842 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
22843 0.0,
22844 0.0,
22845 ];
22846 shape.quad01 = [40.0, 40.0, 340.0, 40.0];
22847 shape.quad23 = [40.0, 340.0, 340.0, 340.0];
22848 shape.color = [1.0, 1.0, 1.0, 1.0];
22849 shape
22850 }
22851
22852 #[cfg(not(target_arch = "wasm32"))]
22855 fn offscreen_test_viewport() -> ViewportUniformParams {
22856 ViewportUniformParams {
22857 width: 64,
22858 height: 64,
22859 offset: [7.0, 7.0],
22860 }
22861 }
22862
22863 #[cfg(not(target_arch = "wasm32"))]
22864 #[test]
22865 fn fill_diag_buckets_shape_quads_by_decoded_sdf_class() {
22866 let diag = FillAreaDiag::default();
22867 let mut arc = ShapeData::zeroed();
22868 arc.stroke_params[1] = pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter);
22869 arc.radii = [0.5; 4];
22872 arc.quad01 = [0.0, 0.0, 10.0, 0.0];
22873 arc.quad23 = [0.0, 10.0, 10.0, 10.0];
22874 let mut rounded = ShapeData::zeroed();
22875 rounded.stroke_params[1] =
22876 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
22877 rounded.radii = [2.0; 4];
22878 rounded.quad01 = [0.0, 0.0, 4.0, 0.0];
22879 rounded.quad23 = [0.0, 5.0, 4.0, 5.0];
22880 let mut plain = ShapeData::zeroed();
22881 plain.stroke_params[1] =
22882 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
22883 plain.quad01 = [0.0, 0.0, 2.0, 0.0];
22884 plain.quad23 = [0.0, 3.0, 2.0, 3.0];
22885 diag.add_shape_quads(
22886 &[rim_test_shape_data(), arc, rounded, plain],
22887 offscreen_test_viewport(),
22888 );
22889 assert_eq!(diag.frame[FillAreaDiag::RRECT_STROKE].get(), 300.0 * 300.0);
22890 assert_eq!(diag.frame[FillAreaDiag::ARC].get(), 100.0);
22891 assert_eq!(diag.frame[FillAreaDiag::RRECT_FILL].get(), 20.0);
22892 assert_eq!(diag.frame[FillAreaDiag::RECT].get(), 6.0);
22893 assert_eq!(diag.frame_corner.get(), 0.0);
22895 for (lit, quad) in diag.frame_lit.iter().zip(&diag.frame) {
22897 assert!(lit.get() <= quad.get() + 1e-9);
22898 }
22899 }
22900
22901 #[cfg(not(target_arch = "wasm32"))]
22902 #[test]
22903 fn fill_diag_rim_mesh_moves_quad_area_to_the_mesh_bucket() {
22904 let diag = FillAreaDiag::default();
22905 diag.add_shape_quads(&[rim_test_shape_data()], offscreen_test_viewport());
22906 diag.note_rim_mesh(&rim_test_shape_data(), 1234.5);
22907 assert_eq!(diag.frame[FillAreaDiag::RRECT_STROKE].get(), 0.0);
22908 assert_eq!(diag.frame[FillAreaDiag::MESH].get(), 1234.5);
22909 assert_eq!(diag.frame_lit[FillAreaDiag::RRECT_STROKE].get(), 0.0);
22912 assert!(diag.frame_lit[FillAreaDiag::MESH].get() <= 1234.5);
22913 assert!(diag.frame_lit[FillAreaDiag::MESH].get() > 0.0);
22914 }
22915
22916 #[cfg(not(target_arch = "wasm32"))]
22917 #[test]
22918 fn fill_diag_image_and_glyph_quads_share_one_bucket() {
22919 let diag = FillAreaDiag::default();
22920 diag.add_image_quad(&[[0.0, 0.0], [8.0, 0.0], [0.0, 4.0], [8.0, 4.0]]);
22921 let quad = CachedTextGlyphQuad {
22922 x: 0,
22923 y: 0,
22924 width: 5,
22925 height: 7,
22926 color: (1.0, 1.0, 1.0, 1.0),
22927 uv: ImageUvRect {
22928 min: [0.0, 0.0],
22929 max: [1.0, 1.0],
22930 sample_bounds: [0.0, 0.0, 1.0, 1.0],
22931 },
22932 };
22933 diag.add_glyph_quad(&quad);
22934 assert_eq!(diag.frame[FillAreaDiag::IMAGE_GLYPH].get(), 32.0 + 35.0);
22935 assert_eq!(diag.frame_lit[FillAreaDiag::IMAGE_GLYPH].get(), 32.0 + 35.0);
22937 }
22938
22939 #[cfg(not(target_arch = "wasm32"))]
22942 fn numeric_area(bounds: [f64; 4], steps: usize, inside: impl Fn(f64, f64) -> bool) -> f64 {
22943 let dx = (bounds[2] - bounds[0]) / steps as f64;
22944 let dy = (bounds[3] - bounds[1]) / steps as f64;
22945 let mut area = 0.0;
22946 for column in 0..steps {
22947 let x = bounds[0] + (column as f64 + 0.5) * dx;
22948 for row in 0..steps {
22949 let y = bounds[1] + (row as f64 + 0.5) * dy;
22950 if inside(x, y) {
22951 area += dx * dy;
22952 }
22953 }
22954 }
22955 area
22956 }
22957
22958 #[cfg(not(target_arch = "wasm32"))]
22961 fn sdf_rounded_rect_reference(
22962 p: [f64; 2],
22963 center: [f64; 2],
22964 half: [f64; 2],
22965 radius: f64,
22966 ) -> f64 {
22967 let qx = (p[0] - center[0]).abs() - (half[0] - radius);
22968 let qy = (p[1] - center[1]).abs() - (half[1] - radius);
22969 qx.max(0.0).hypot(qy.max(0.0)) + qx.max(qy).min(0.0) - radius
22970 }
22971
22972 #[cfg(not(target_arch = "wasm32"))]
22973 #[test]
22974 fn fill_truth_arc_lit_matches_the_sdf_covered_area() {
22975 use cranpose_ui_graphics::ArcGeometry;
22976 let tau = cranpose_ui_graphics::TAU;
22977 let center = Point::new(250.0, 250.0);
22978 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
22981 (90.0, 100.0, 0.7, 2.5, StrokeCap::Butt),
22982 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
22983 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
22984 (80.0, 100.0, 0.0, tau, StrokeCap::Round),
22985 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
22986 ];
22987 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
22988 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
22989 let converted = converted_arc_shape(arc, 1.0);
22990 let cap_code = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
22991 let arc_center = [converted.arc_params[0], converted.arc_params[1]];
22992 let mid = [converted.radii[0], converted.radii[1]];
22993 let half = [converted.radii[2], converted.radii[3]];
22994 let aabb = quad_aabb(&converted);
22995 let bounds = [aabb[0] - 2.0, aabb[1] - 2.0, aabb[2] + 2.0, aabb[3] + 2.0];
22998 let numeric = numeric_area(bounds, 1000, |x, y| {
22999 sdf_arc_band_reference(
23000 [x as f32, y as f32],
23001 arc_center,
23002 converted.stroke_params[3],
23003 converted.stroke_params[2],
23004 mid,
23005 half,
23006 cap_code,
23007 ) < 0.0
23008 });
23009 let analytic = analytic_covered_area(&converted);
23010 let error = (analytic - numeric).abs() / numeric.max(1.0);
23011 assert!(
23012 error < 0.02,
23013 "case {case}: analytic {analytic:.1} vs sdf {numeric:.1} \
23014 ({:.2}% off)",
23015 error * 100.0
23016 );
23017 }
23018 }
23019
23020 #[cfg(not(target_arch = "wasm32"))]
23021 #[test]
23022 fn fill_truth_circle_and_rrect_fill_lit_match_references() {
23023 let mut circle = ShapeData::zeroed();
23025 circle.stroke_params[1] =
23026 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23027 circle.rect = [10.0, 10.0, 200.0, 200.0];
23028 circle.radii = [100.0; 4];
23029 let analytic = analytic_covered_area(&circle);
23030 let exact = std::f64::consts::PI * 100.0 * 100.0;
23031 assert!(
23032 (analytic - exact).abs() / exact < 1e-9,
23033 "circle: {analytic} vs {exact}"
23034 );
23035
23036 let mut rounded = ShapeData::zeroed();
23038 rounded.stroke_params[1] =
23039 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23040 rounded.rect = [50.0, 80.0, 200.0, 120.0];
23041 rounded.radii = [40.0; 4];
23042 let numeric = numeric_area([48.0, 78.0, 252.0, 202.0], 1000, |x, y| {
23043 sdf_rounded_rect_reference([x, y], [150.0, 140.0], [100.0, 60.0], 40.0) < 0.0
23044 });
23045 let analytic = analytic_covered_area(&rounded);
23046 let error = (analytic - numeric).abs() / numeric;
23047 assert!(
23048 error < 0.02,
23049 "rrect fill: analytic {analytic:.1} vs sdf {numeric:.1}"
23050 );
23051 }
23052
23053 #[cfg(not(target_arch = "wasm32"))]
23054 #[test]
23055 fn fill_truth_stroked_rrect_lit_matches_the_band_area() {
23056 let rim = rim_test_shape_data();
23059 let analytic = analytic_covered_area(&rim);
23060 let exact = std::f64::consts::PI * (150.0 * 150.0 - 142.0 * 142.0);
23061 assert!(
23062 (analytic - exact).abs() / exact < 1e-9,
23063 "circle rim: {analytic} vs {exact}"
23064 );
23065
23066 let mut square_ring = rim_test_shape_data();
23069 square_ring.radii = [60.0; 4];
23070 let numeric = numeric_area([38.0, 38.0, 342.0, 342.0], 1000, |x, y| {
23071 sdf_rounded_rect_reference([x, y], [190.0, 190.0], [146.0, 146.0], 60.0).abs() < 4.0
23072 });
23073 let analytic = analytic_covered_area(&square_ring);
23074 let error = (analytic - numeric).abs() / numeric;
23075 assert!(
23076 error < 0.02,
23077 "square ring: analytic {analytic:.1} vs sdf {numeric:.1}"
23078 );
23079 }
23080
23081 #[cfg(not(target_arch = "wasm32"))]
23082 #[test]
23083 fn fill_truth_corner_counter_prices_the_area_outside_the_inscribed_circle() {
23084 let full = area_outside_inscribed_circle([0.0, 0.0, 454.0, 454.0], (454, 454));
23087 let exact = (1.0 - std::f64::consts::FRAC_PI_4) * 454.0 * 454.0;
23088 assert!(
23089 (full - exact).abs() / exact < 0.01,
23090 "full quad: {full} vs {exact}"
23091 );
23092 assert_eq!(
23094 area_outside_inscribed_circle([127.0, 127.0, 327.0, 327.0], (454, 454)),
23095 0.0
23096 );
23097 let corner = area_outside_inscribed_circle([0.0, 0.0, 40.0, 40.0], (454, 454));
23099 assert!((corner - 1600.0).abs() < 1e-6, "corner box: {corner}");
23100 }
23101
23102 #[cfg(not(target_arch = "wasm32"))]
23103 #[test]
23104 fn fill_truth_opacity_histogram_classifies_solid_alpha_exactly() {
23105 let diag = FillAreaDiag::default();
23106 diag.reset_frame(454, 454);
23107 let full_frame = ViewportUniformParams {
23108 width: 454,
23109 height: 454,
23110 offset: [0.0, 0.0],
23111 };
23112 let mut opaque = ShapeData::zeroed();
23113 opaque.stroke_params[1] =
23114 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23115 opaque.rect = [0.0, 0.0, 100.0, 50.0];
23116 opaque.quad01 = [0.0, 0.0, 100.0, 0.0];
23117 opaque.quad23 = [0.0, 50.0, 100.0, 50.0];
23118 opaque.color = [1.0, 1.0, 1.0, 1.0];
23119 let mut faded = opaque;
23120 faded.color[3] = 0.82;
23121 let mut gradient = opaque;
23122 gradient.brush_type = 1;
23123 diag.add_shape_quads(&[opaque, faded, gradient], full_frame);
23124 let lit = |class: FillOpacityClass| diag.frame_opacity[class as usize].get();
23126 assert_eq!(lit(FillOpacityClass::Opaque), 5000.0);
23127 assert_eq!(lit(FillOpacityClass::Translucent), 5000.0);
23128 assert_eq!(lit(FillOpacityClass::NonSolid), 5000.0);
23129 assert!(diag.frame_corner.get() > 0.0);
23131
23132 let offscreen = FillAreaDiag::default();
23135 offscreen.reset_frame(454, 454);
23136 offscreen.add_shape_quads(&[opaque], offscreen_test_viewport());
23137 assert_eq!(offscreen.frame_corner.get(), 0.0);
23138 }
23139
23140 #[cfg(not(target_arch = "wasm32"))]
23141 #[test]
23142 fn fill_truth_retained_records_price_ranges_and_identity_corners() {
23143 let mut plain = ShapeData::zeroed();
23144 plain.stroke_params[1] =
23145 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23146 plain.rect = [200.0, 200.0, 20.0, 10.0];
23147 plain.quad01 = [200.0, 200.0, 220.0, 200.0];
23148 plain.quad23 = [200.0, 210.0, 220.0, 210.0];
23149 plain.color = [1.0, 1.0, 1.0, 1.0];
23150 let shapes = vec![rim_test_shape_data(), plain];
23151 let records = fill_diag_capture_records(&shapes, None);
23152 assert_eq!(records.len(), 2);
23153 assert_eq!(records[0].bucket, FillAreaDiag::RRECT_STROKE);
23154 assert_eq!(records[0].drawn_px2, 300.0 * 300.0);
23155 assert!(records[0].lit_px2 < records[0].drawn_px2, "a rim has slack");
23156 assert_eq!(records[1].bucket, FillAreaDiag::RECT);
23158 assert_eq!(records[1].lit_px2, records[1].drawn_px2);
23159
23160 let diag = FillAreaDiag::default();
23161 diag.reset_frame(454, 454);
23162 let scaled = SimilarityTransform::new([0.0, 0.0], 0.0, 2.0);
23165 diag.add_retained_range(&records, 0, 2, &scaled);
23166 let drawn: f64 = records.iter().map(|record| record.drawn_px2).sum();
23167 assert!((diag.frame[FillAreaDiag::RETAINED].get() - drawn * 4.0).abs() < 1e-6);
23168 assert_eq!(diag.frame_corner.get(), 0.0);
23169
23170 let identity_diag = FillAreaDiag::default();
23173 identity_diag.reset_frame(454, 454);
23174 identity_diag.add_retained_range(&records, 0, 2, &SimilarityTransform::IDENTITY);
23175 assert!(identity_diag.frame_corner.get() > 0.0);
23176 let tail = FillAreaDiag::default();
23178 tail.reset_frame(454, 454);
23179 tail.add_retained_range(&records, 1, 2, &SimilarityTransform::IDENTITY);
23180 assert_eq!(
23181 tail.frame[FillAreaDiag::RETAINED].get(),
23182 records[1].drawn_px2
23183 );
23184 }
23185
23186 #[cfg(not(target_arch = "wasm32"))]
23187 #[test]
23188 fn fill_truth_top_slack_dump_keeps_the_worst_ten() {
23189 let mut diag = FillAreaDiag::default();
23190 let records: Vec<FillDiagShapeRecord> = (0..12)
23191 .map(|index| FillDiagShapeRecord {
23192 drawn_px2: 1000.0 * (index + 1) as f64,
23193 lit_px2: 100.0,
23194 bucket: FillAreaDiag::ARC,
23195 opacity: FillOpacityClass::Opaque,
23196 aabb: [0.0, 0.0, 10.0, 10.0],
23197 })
23198 .collect();
23199 diag.note_retained_capture(3, &records);
23200 assert_eq!(diag.slack_top.len(), FILL_DIAG_SLACK_TOP);
23201 assert_eq!(diag.slack_top[0].drawn_px2, 12000.0);
23203 assert_eq!(diag.slack_top[0].slot, 3);
23204 assert_eq!(diag.slack_top[0].shape, 11);
23205 for pair in diag.slack_top.windows(2) {
23206 assert!(pair[0].drawn_px2 - pair[0].lit_px2 >= pair[1].drawn_px2 - pair[1].lit_px2);
23207 }
23208 assert!(diag
23209 .slack_top
23210 .iter()
23211 .all(|entry| entry.drawn_px2 - entry.lit_px2 > 2000.0 - 100.0));
23212 }
23213
23214 #[cfg(not(target_arch = "wasm32"))]
23215 #[test]
23216 fn rim_mesh_band_accepts_only_huge_solid_unclipped_circle_rims() {
23217 let band = rim_mesh_band(&rim_test_shape_data()).expect("circle rim must qualify");
23218 assert_eq!(band.center, [190.0, 190.0]);
23219 assert_eq!(band.inner, 142.0);
23220 assert_eq!(band.outer, 150.0);
23221 assert_eq!(band.start, 0.0);
23222 assert!(
23223 band.sweep >= cranpose_ui_graphics::TAU,
23224 "a rim band is a closed ring"
23225 );
23226 let mut vertices = Vec::new();
23228 let mut indices = Vec::new();
23229 emit_arc_band_mesh(
23230 &rim_test_shape_data(),
23231 7,
23232 &band,
23233 &mut vertices,
23234 &mut indices,
23235 )
23236 .expect("rim must mesh");
23237 assert!(vertices.iter().all(|vertex| vertex.shape_idx == 7));
23238
23239 let mut square = rim_test_shape_data();
23243 square.radii = [100.0; 4];
23244 assert!(rim_mesh_band(&square).is_none());
23245
23246 let mut oblong = rim_test_shape_data();
23248 oblong.rect = [40.0, 40.0, 300.0, 200.0];
23249 assert!(rim_mesh_band(&oblong).is_none());
23250
23251 let mut gradient = rim_test_shape_data();
23253 gradient.brush_type = 1;
23254 assert!(rim_mesh_band(&gradient).is_none());
23255
23256 let mut clipped = rim_test_shape_data();
23258 clipped.clip_rect = [0.0, 0.0, 400.0, 400.0];
23259 assert!(rim_mesh_band(&clipped).is_none());
23260
23261 let mut small = rim_test_shape_data();
23263 small.rect = [40.0, 40.0, 100.0, 100.0];
23264 small.quad01 = [40.0, 40.0, 140.0, 40.0];
23265 small.quad23 = [40.0, 140.0, 140.0, 140.0];
23266 small.radii = [46.0; 4];
23267 assert!(rim_mesh_band(&small).is_none());
23268
23269 let mut fill = rim_test_shape_data();
23271 fill.stroke_params[1] =
23272 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter);
23273 assert!(rim_mesh_band(&fill).is_none());
23274
23275 let mut hairline = rim_test_shape_data();
23277 hairline.stroke_params[0] = 0.0;
23278 assert!(rim_mesh_band(&hairline).is_none());
23279
23280 let mut uneven = rim_test_shape_data();
23282 uneven.radii[2] = 145.0;
23283 assert!(rim_mesh_band(&uneven).is_none());
23284 }
23285
23286 #[cfg(not(target_arch = "wasm32"))]
23287 #[test]
23288 fn shape_batch_limits_follow_uniform_binding_size() {
23289 let desktop_shapes = 65536 / std::mem::size_of::<ShapeData>();
23292 assert_eq!(desktop_shapes, 409);
23293 assert_eq!(
23294 ShapeBatchLimits::desktop(),
23295 ShapeBatchLimits {
23296 max_shapes_per_batch: desktop_shapes.min(MAX_SHAPES_PER_BATCH),
23297 max_gradient_stops: MAX_GRADIENT_STOPS,
23298 storage: false,
23299 }
23300 );
23301
23302 let downlevel = ShapeBatchLimits::for_uniform_binding_size(16384);
23305 assert_eq!(downlevel.max_shapes_per_batch, 16384 / 160);
23306 assert_eq!(downlevel.max_shapes_per_batch, 102);
23307 assert_eq!(downlevel.max_gradient_stops, 512.min(MAX_GRADIENT_STOPS));
23308 assert!(downlevel.max_shapes_per_batch * std::mem::size_of::<ShapeData>() <= 16384);
23309 assert!(downlevel.max_gradient_stops * std::mem::size_of::<GradientStop>() <= 16384);
23310
23311 let tiny = ShapeBatchLimits::for_uniform_binding_size(1);
23313 assert_eq!(tiny.max_shapes_per_batch, 1);
23314 assert_eq!(tiny.max_gradient_stops, 1);
23315 }
23316
23317 #[test]
23318 fn storage_shape_batch_limits_uncap_the_batch_and_start_small() {
23319 let storage = ShapeBatchLimits::for_storage_binding_size(128 << 20);
23322 assert!(storage.storage);
23323 assert_eq!(storage.max_shapes_per_batch, MAX_SHAPES_PER_STORAGE_BATCH);
23324 assert_eq!(
23325 storage.max_gradient_stops,
23326 MAX_GRADIENT_STOPS_PER_STORAGE_BATCH
23327 );
23328
23329 assert_eq!(
23332 storage.initial_shape_capacity(),
23333 INITIAL_STORAGE_BATCH_CAPACITY
23334 );
23335 assert_eq!(
23336 storage.initial_gradient_capacity(),
23337 INITIAL_STORAGE_BATCH_CAPACITY
23338 );
23339 assert_eq!(
23340 storage.data_binding_type(),
23341 wgpu::BufferBindingType::Storage { read_only: true }
23342 );
23343 assert!(storage
23344 .data_buffer_usage()
23345 .contains(wgpu::BufferUsages::STORAGE));
23346
23347 let uniform = ShapeBatchLimits::desktop();
23350 assert_eq!(
23351 uniform.initial_shape_capacity(),
23352 uniform.max_shapes_per_batch
23353 );
23354 assert_eq!(
23355 uniform.initial_gradient_capacity(),
23356 uniform.max_gradient_stops
23357 );
23358 assert_eq!(
23359 uniform.data_binding_type(),
23360 wgpu::BufferBindingType::Uniform
23361 );
23362 assert!(uniform
23363 .data_buffer_usage()
23364 .contains(wgpu::BufferUsages::UNIFORM));
23365 }
23366
23367 #[test]
23368 fn storage_shape_shader_swaps_the_arrays_to_runtime_sized_storage() {
23369 let source =
23370 shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20), false);
23371 assert!(
23372 source.contains("var<storage, read> shape_data: array<ShapeData>;"),
23373 "storage-mode shader must declare a runtime-sized shape array"
23374 );
23375 assert!(
23376 source.contains("var<storage, read> gradient_stops: array<GradientStop>;"),
23377 "storage-mode shader must declare a runtime-sized gradient array"
23378 );
23379 assert!(
23380 !source.contains("var<uniform> shape_data"),
23381 "the uniform shape declaration must be fully replaced"
23382 );
23383 assert!(
23384 !source.contains("var<uniform> gradient_stops"),
23385 "the uniform gradient declaration must be fully replaced"
23386 );
23387 assert!(
23388 source.contains("var<storage, read> paint: array<vec4<f32>>;"),
23389 "storage-mode shader must declare the retained paint array"
23390 );
23391 assert!(
23392 source.contains("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)"),
23393 "storage-mode shader must read paint under the paint_select flag"
23394 );
23395 assert!(
23396 source.contains("fn vs_mesh("),
23397 "the storage rewrite must leave the retained-mesh vertex entry intact"
23398 );
23399 assert!(
23400 source.contains("fn vs_shape_instanced("),
23401 "the storage rewrite must leave the instanced-quad vertex entry intact"
23402 );
23403 assert_eq!(
23404 source
23405 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
23406 .count(),
23407 3,
23408 "vs_main, vs_shape_instanced and vs_mesh must all read paint under \
23409 the paint_select flag (meshless retained draws ride the instanced \
23410 entry when the selection is latched on)"
23411 );
23412
23413 let module = naga::front::wgsl::parse_str(&source)
23416 .expect("storage-mode shape shader must parse as WGSL");
23417 naga::valid::Validator::new(
23418 naga::valid::ValidationFlags::all(),
23419 naga::valid::Capabilities::all(),
23420 )
23421 .validate(&module)
23422 .expect("storage-mode shape shader must validate for WebGPU");
23423 }
23424
23425 #[test]
23426 fn solid_trim_keeps_the_full_struct_locations_with_the_dropped_slots_vacant() {
23427 let appendix = shaders::SOLID_TRIM_APPENDIX;
23433 for line in [
23434 "@location(0) color: vec4<f32>,",
23435 "@location(1) uv: vec2<f32>,",
23436 "@location(2) world_pos: vec2<f32>,",
23437 "@location(3) @interpolate(flat) rect: vec4<f32>,",
23438 "@location(4) @interpolate(flat) radii: vec4<f32>,",
23439 "@location(6) @interpolate(flat) clip_rect: vec4<f32>,",
23440 "@location(7) @interpolate(flat) stroke_params: vec4<f32>,",
23441 "@location(8) @interpolate(flat) arc_params: vec4<f32>,",
23442 ] {
23443 assert!(
23444 shaders::SHADER.contains(line),
23445 "`{line}` drifted out of VertexOutput; realign the trimmed \
23446 struct line for line before touching anything else"
23447 );
23448 assert!(
23449 appendix.contains(line),
23450 "`{line}` must appear verbatim in VertexOutputSolid — the \
23451 surviving varyings keep the full struct's location indices"
23452 );
23453 }
23454 assert!(
23455 !appendix.contains("@location(5)"),
23456 "location 5 is gradient_params' slot and must stay VACANT — \
23457 dense renumbering is the reverted attempt's suspect #1"
23458 );
23459 assert!(
23460 !appendix.contains("@location(9)"),
23461 "location 9 is brush's slot and must stay VACANT — dense \
23462 renumbering is the reverted attempt's suspect #1"
23463 );
23464 assert!(
23465 !appendix.contains("output.gradient_params") && !appendix.contains("output.brush"),
23466 "the trimmed vertex entries must not write the dropped varyings"
23467 );
23468 }
23469
23470 #[test]
23471 fn solid_trim_source_reaches_every_injection_and_validates() {
23472 let storage =
23477 shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20), true);
23478 for entry in [
23479 "fn vs_solid(",
23480 "fn vs_solid_instanced(",
23481 "fn fs_solid_trim(",
23482 ] {
23483 assert!(
23484 storage.contains(entry),
23485 "trimmed storage source must carry `{entry}`"
23486 );
23487 }
23488 assert_eq!(
23489 storage
23490 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
23491 .count(),
23492 5,
23493 "vs_main, vs_shape_instanced, vs_mesh, vs_solid and \
23494 vs_solid_instanced must all read paint under the paint_select \
23495 flag"
23496 );
23497
23498 let uniform = shape_shader_source(ShapeBatchLimits::desktop(), true);
23501 for source in [&storage, &uniform] {
23502 for depth in [false, true] {
23503 let text = display_clip::with_content_z(Cow::Owned(source.to_string()), depth);
23504 let module = naga::front::wgsl::parse_str(&text)
23505 .expect("trimmed shape shader must parse as WGSL");
23506 naga::valid::Validator::new(
23507 naga::valid::ValidationFlags::all(),
23508 naga::valid::Capabilities::all(),
23509 )
23510 .validate(&module)
23511 .expect("trimmed shape shader must validate for WebGPU");
23512 }
23513 }
23514 }
23515
23516 #[test]
23517 fn solid_trim_flag_reads_the_documented_variable() {
23518 std::env::remove_var("CRANPOSE_SOLID_TRIM_VARYINGS");
23521 assert!(!solid_trim_varyings_enabled(), "the trim must default OFF");
23522 std::env::set_var("CRANPOSE_SOLID_TRIM_VARYINGS", "1");
23523 assert!(solid_trim_varyings_enabled());
23524 std::env::set_var("CRANPOSE_SOLID_TRIM_VARYINGS", "0");
23525 assert!(!solid_trim_varyings_enabled());
23526 std::env::remove_var("CRANPOSE_SOLID_TRIM_VARYINGS");
23527 }
23528
23529 #[test]
23530 fn uniform_shape_shader_keeps_the_in_record_color_and_no_paint_binding() {
23531 for source in [
23535 Cow::Borrowed(shaders::SHADER),
23536 shape_shader_source(ShapeBatchLimits::desktop(), false),
23537 ] {
23538 assert!(
23539 !source.contains("paint: array"),
23540 "the uniform variant must not declare a paint array"
23541 );
23542 assert!(
23543 source.contains("output.color = shape.color;"),
23544 "the uniform variant must read the color from ShapeData \
23545 (this literal is also what `shape_shader_source` rewrites)"
23546 );
23547 assert!(
23548 source.contains("paint_select: f32"),
23549 "SimilarityTransform must name the flag field in both \
23550 variants; the Rust mirror is Pod and uploads raw bytes"
23551 );
23552 }
23553 }
23554
23555 #[test]
23556 fn shipped_shape_shader_array_length_fits_the_downlevel_uniform_floor() {
23557 assert!(
23561 shaders::SHADER.contains("array<ShapeData, 102>"),
23562 "shape.wgsl array length must stay in sync with \
23563 `shape_shader_source`'s replace string and MAX_SHAPES_PER_BATCH"
23564 );
23565 assert!(102 * std::mem::size_of::<ShapeData>() <= 16384);
23566 assert!(103 * std::mem::size_of::<ShapeData>() > 16384);
23567 }
23568
23569 #[test]
23570 fn glyph_atlas_doubles_on_overflow_and_stops_at_the_device_ceiling() {
23571 assert_eq!(
23574 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MIN_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
23575 1024
23576 );
23577 assert_eq!(
23578 next_glyph_atlas_size(2048, TEXT_GLYPH_ATLAS_MAX_SIZE),
23579 TEXT_GLYPH_ATLAS_MAX_SIZE
23580 );
23581 assert_eq!(
23582 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MAX_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
23583 TEXT_GLYPH_ATLAS_MAX_SIZE
23584 );
23585
23586 assert_eq!(next_glyph_atlas_size(1024, 2048), 2048);
23589 assert_eq!(next_glyph_atlas_size(2048, 2048), 2048);
23590
23591 assert_eq!(next_glyph_atlas_size(u32::MAX, 4096), 4096);
23593 assert_eq!(next_glyph_atlas_size(0, 0), 1);
23594 }
23595
23596 #[test]
23597 fn glyph_atlas_uv_rect_normalizes_against_the_atlas_it_was_placed_in() {
23598 let entry = GlyphAtlasEntry {
23602 x: 128,
23603 y: 256,
23604 width: 16,
23605 height: 32,
23606 };
23607
23608 let small = glyph_atlas_uv_rect(entry, 512);
23609 let large = glyph_atlas_uv_rect(entry, 4096);
23610
23611 assert_eq!(small.min, [128.0 / 512.0, 256.0 / 512.0]);
23612 assert_eq!(large.min, [128.0 / 4096.0, 256.0 / 4096.0]);
23613 assert_eq!(small.max, [144.0 / 512.0, 288.0 / 512.0]);
23614 assert_eq!(large.max, [144.0 / 4096.0, 288.0 / 4096.0]);
23615 }
23616
23617 #[test]
23618 fn native_shape_shader_source_uses_native_batch_limits() {
23619 let limits = ShapeBatchLimits::desktop();
23620 let source = shape_shader_source(limits, false);
23621
23622 assert!(source.contains(&format!(
23623 "array<ShapeData, {}>",
23624 limits.max_shapes_per_batch
23625 )));
23626 assert!(source.contains(&format!(
23627 "array<GradientStop, {}>",
23628 limits.max_gradient_stops
23629 )));
23630 assert!(!source.contains("array<ShapeData, 146>"));
23633 }
23634
23635 #[test]
23636 fn stroked_and_arc_shapes_batch_together_with_fills() {
23637 let fill = test_shape(0, BlendMode::SrcOver);
23643 let mut stroked = test_shape(1, BlendMode::SrcOver);
23644 stroked.stroke = Some(
23645 cranpose_ui_graphics::Stroke::new(3.0)
23646 .with_cap(StrokeCap::Round)
23647 .with_join(StrokeJoin::Bevel),
23648 );
23649 let mut arc = test_shape(2, BlendMode::SrcOver);
23650 arc.arc = Some(cranpose_ui_graphics::ArcGeometry::new(
23651 Point::new(4.0, 4.0),
23652 2.0,
23653 4.0,
23654 0.0,
23655 1.0,
23656 StrokeCap::Round,
23657 ));
23658 let trailing_fill = test_shape(3, BlendMode::SrcOver);
23659
23660 assert!(!fill.has_stroke_or_arc());
23661 assert!(stroked.has_stroke_or_arc());
23662 assert!(arc.has_stroke_or_arc());
23663 assert!(!trailing_fill.has_stroke_or_arc());
23664
23665 let shapes = vec![fill, stroked, arc, trailing_fill];
23666 let ordered_items: Vec<_> = (0..shapes.len())
23667 .map(|index| (index, SegmentDrawItem::Shape(index)))
23668 .collect();
23669 let images = Vec::new();
23670
23671 let commands: Vec<_> = SegmentCommandIter::new(
23672 &ordered_items,
23673 &shapes,
23674 &images,
23675 ShapeBatchLimits::desktop(),
23676 )
23677 .collect();
23678
23679 assert_eq!(
23680 commands,
23681 vec![SegmentRenderCommand::DrawChunk(chunk(&[
23682 SegmentBatchPlan::Shape {
23683 start: 0,
23684 end: 4,
23685 blend_mode: BlendMode::SrcOver,
23686 }
23687 ]))],
23688 "mixed fill/stroke/arc runs must stay one batch"
23689 );
23690 }
23691
23692 #[cfg(not(target_arch = "wasm32"))]
23693 #[test]
23694 fn native_segment_fusion_budget_allows_small_interleaved_chunks() {
23695 let ordered_items = vec![
23696 (0, SegmentDrawItem::Shape(0)),
23697 (1, SegmentDrawItem::Image(0)),
23698 (2, SegmentDrawItem::Text(0)),
23699 (3, SegmentDrawItem::Shape(1)),
23700 ];
23701 let shapes = vec![
23702 test_shape(0, BlendMode::SrcOver),
23703 test_shape(3, BlendMode::DstOut),
23704 ];
23705 let segment = chunk(&[
23706 SegmentBatchPlan::Shape {
23707 start: 0,
23708 end: 1,
23709 blend_mode: BlendMode::SrcOver,
23710 },
23711 SegmentBatchPlan::Image {
23712 start: 1,
23713 end: 2,
23714 blend_mode: BlendMode::SrcOver,
23715 },
23716 SegmentBatchPlan::Text { start: 2, end: 3 },
23717 SegmentBatchPlan::Shape {
23718 start: 3,
23719 end: 4,
23720 blend_mode: BlendMode::DstOut,
23721 },
23722 ]);
23723
23724 let budget = native_segment_fusion_budget(
23725 &ordered_items,
23726 &shapes,
23727 &[],
23728 &segment,
23729 ShapeBatchLimits::desktop(),
23730 )
23731 .expect("budget should be valid")
23732 .expect("chunk should fit native fusion budget");
23733
23734 assert_eq!(
23735 budget,
23736 NativeSegmentFusionBudget {
23737 shape_count: 2,
23738 gradient_stop_count: 0,
23739 }
23740 );
23741 }
23742
23743 #[cfg(not(target_arch = "wasm32"))]
23744 #[test]
23745 fn native_segment_fusion_budget_rejects_shape_uniform_overflow() {
23746 let ordered_items: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
23747 .map(|index| (index, SegmentDrawItem::Shape(index)))
23748 .collect();
23749 let shapes: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
23750 .map(|index| test_shape(index, BlendMode::SrcOver))
23751 .collect();
23752 let segment = chunk(&[
23753 SegmentBatchPlan::Shape {
23754 start: 0,
23755 end: MAX_SHAPES_PER_BATCH,
23756 blend_mode: BlendMode::SrcOver,
23757 },
23758 SegmentBatchPlan::Shape {
23759 start: MAX_SHAPES_PER_BATCH,
23760 end: MAX_SHAPES_PER_BATCH + 1,
23761 blend_mode: BlendMode::SrcOver,
23762 },
23763 ]);
23764
23765 let budget = native_segment_fusion_budget(
23766 &ordered_items,
23767 &shapes,
23768 &[],
23769 &segment,
23770 ShapeBatchLimits::desktop(),
23771 )
23772 .expect("valid plan");
23773
23774 assert_eq!(budget, None);
23775 }
23776
23777 #[cfg(not(target_arch = "wasm32"))]
23778 #[test]
23779 fn native_segment_fusion_budget_rejects_gradient_uniform_overflow() {
23780 let ordered_items = vec![(0, SegmentDrawItem::Shape(0))];
23781 let mut shape = test_shape(0, BlendMode::SrcOver);
23782 let brushes = vec![Brush::linear_gradient(vec![
23783 Color::BLACK;
23784 MAX_GRADIENT_STOPS + 1
23785 ])];
23786 shape.brush = SceneBrush::Gradient(0);
23787 let shapes = vec![shape];
23788 let segment = chunk(&[SegmentBatchPlan::Shape {
23789 start: 0,
23790 end: 1,
23791 blend_mode: BlendMode::SrcOver,
23792 }]);
23793
23794 let budget = native_segment_fusion_budget(
23795 &ordered_items,
23796 &shapes,
23797 &brushes,
23798 &segment,
23799 ShapeBatchLimits::desktop(),
23800 )
23801 .expect("valid plan");
23802
23803 assert_eq!(budget, None);
23804 }
23805
23806 #[cfg(not(target_arch = "wasm32"))]
23807 #[test]
23808 fn native_segment_fusion_partitions_shape_uniform_overflow() {
23809 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
23812 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
23813 .map(|index| (index, SegmentDrawItem::Shape(index)))
23814 .collect();
23815 let shapes: Vec<_> = (0..=desktop_batch_cap)
23816 .map(|index| test_shape(index, BlendMode::SrcOver))
23817 .collect();
23818 let segment = chunk(&[
23819 SegmentBatchPlan::Shape {
23820 start: 0,
23821 end: desktop_batch_cap,
23822 blend_mode: BlendMode::SrcOver,
23823 },
23824 SegmentBatchPlan::Shape {
23825 start: desktop_batch_cap,
23826 end: desktop_batch_cap + 1,
23827 blend_mode: BlendMode::SrcOver,
23828 },
23829 ]);
23830
23831 let partitions = native_segment_fusion_partitions(
23832 &ordered_items,
23833 &shapes,
23834 &[],
23835 &segment,
23836 ShapeBatchLimits::desktop(),
23837 )
23838 .expect("valid plan")
23839 .expect("overflowing segment should be partitionable");
23840
23841 assert_eq!(partitions.len(), 2);
23842 assert_eq!(
23843 partitions[0],
23844 NativeSegmentFusionPartition {
23845 chunk: chunk(&[SegmentBatchPlan::Shape {
23846 start: 0,
23847 end: desktop_batch_cap,
23848 blend_mode: BlendMode::SrcOver,
23849 }]),
23850 budget: NativeSegmentFusionBudget {
23851 shape_count: desktop_batch_cap,
23852 gradient_stop_count: 0,
23853 },
23854 }
23855 );
23856 assert_eq!(
23857 partitions[1],
23858 NativeSegmentFusionPartition {
23859 chunk: chunk(&[SegmentBatchPlan::Shape {
23860 start: desktop_batch_cap,
23861 end: desktop_batch_cap + 1,
23862 blend_mode: BlendMode::SrcOver,
23863 }]),
23864 budget: NativeSegmentFusionBudget {
23865 shape_count: 1,
23866 gradient_stop_count: 0,
23867 },
23868 }
23869 );
23870 }
23871
23872 #[cfg(not(target_arch = "wasm32"))]
23873 #[test]
23874 fn native_segment_fusion_partitions_gradient_uniform_overflow() {
23875 const STOPS_PER_SHAPE: usize = MAX_GRADIENT_STOPS / 2;
23876 let ordered_items = vec![
23877 (0, SegmentDrawItem::Shape(0)),
23878 (1, SegmentDrawItem::Shape(1)),
23879 (2, SegmentDrawItem::Shape(2)),
23880 ];
23881 let mut shapes = Vec::new();
23882 let brushes = vec![Brush::linear_gradient(vec![Color::BLACK; STOPS_PER_SHAPE])];
23883 for index in 0..3 {
23884 let mut shape = test_shape(index, BlendMode::SrcOver);
23885 shape.brush = SceneBrush::Gradient(0);
23886 shapes.push(shape);
23887 }
23888 let segment = chunk(&[SegmentBatchPlan::Shape {
23889 start: 0,
23890 end: 3,
23891 blend_mode: BlendMode::SrcOver,
23892 }]);
23893
23894 let partitions = native_segment_fusion_partitions(
23895 &ordered_items,
23896 &shapes,
23897 &brushes,
23898 &segment,
23899 ShapeBatchLimits::desktop(),
23900 )
23901 .expect("valid plan")
23902 .expect("overflowing gradient segment should be partitionable");
23903
23904 assert_eq!(partitions.len(), 2);
23905 assert_eq!(
23906 partitions[0],
23907 NativeSegmentFusionPartition {
23908 chunk: chunk(&[SegmentBatchPlan::Shape {
23909 start: 0,
23910 end: 2,
23911 blend_mode: BlendMode::SrcOver,
23912 }]),
23913 budget: NativeSegmentFusionBudget {
23914 shape_count: 2,
23915 gradient_stop_count: MAX_GRADIENT_STOPS,
23916 },
23917 }
23918 );
23919 assert_eq!(
23920 partitions[1],
23921 NativeSegmentFusionPartition {
23922 chunk: chunk(&[SegmentBatchPlan::Shape {
23923 start: 2,
23924 end: 3,
23925 blend_mode: BlendMode::SrcOver,
23926 }]),
23927 budget: NativeSegmentFusionBudget {
23928 shape_count: 1,
23929 gradient_stop_count: STOPS_PER_SHAPE,
23930 },
23931 }
23932 );
23933 }
23934
23935 #[cfg(not(target_arch = "wasm32"))]
23936 #[test]
23937 fn native_segment_fusion_accepts_layer_composite_chunks() {
23938 let ordered_items = vec![
23939 (0, SegmentDrawItem::Shape(0)),
23940 (1, SegmentDrawItem::Composite(0)),
23941 (2, SegmentDrawItem::ShaderComposite(0)),
23942 (3, SegmentDrawItem::Shape(1)),
23943 ];
23944 let shapes = vec![
23945 test_shape(0, BlendMode::SrcOver),
23946 test_shape(1, BlendMode::SrcOver),
23947 ];
23948 let segment = chunk(&[
23949 SegmentBatchPlan::Shape {
23950 start: 0,
23951 end: 1,
23952 blend_mode: BlendMode::SrcOver,
23953 },
23954 SegmentBatchPlan::Composite { start: 1, end: 2 },
23955 SegmentBatchPlan::ShaderComposite { start: 2, end: 3 },
23956 SegmentBatchPlan::Shape {
23957 start: 3,
23958 end: 4,
23959 blend_mode: BlendMode::SrcOver,
23960 },
23961 ]);
23962
23963 let partitions = native_segment_fusion_partitions(
23964 &ordered_items,
23965 &shapes,
23966 &[],
23967 &segment,
23968 ShapeBatchLimits::desktop(),
23969 )
23970 .expect("valid plan")
23971 .expect("composites are drawable inside the native fused pass");
23972
23973 assert_eq!(
23974 partitions,
23975 vec![NativeSegmentFusionPartition {
23976 chunk: segment,
23977 budget: NativeSegmentFusionBudget {
23978 shape_count: 2,
23979 gradient_stop_count: 0,
23980 },
23981 }],
23982 "layer composites and shader composites must preserve order without forcing separate render passes"
23983 );
23984 }
23985
23986 #[cfg(not(target_arch = "wasm32"))]
23987 #[test]
23988 fn native_segment_fusion_partitions_preserve_non_shape_order_at_budget_boundary() {
23989 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
23992 let ordered_items: Vec<_> = (0..desktop_batch_cap)
23993 .map(|index| (index, SegmentDrawItem::Shape(index)))
23994 .chain([
23995 (desktop_batch_cap, SegmentDrawItem::Image(0)),
23996 (
23997 desktop_batch_cap + 1,
23998 SegmentDrawItem::Shape(desktop_batch_cap),
23999 ),
24000 ])
24001 .collect();
24002 let shapes: Vec<_> = (0..=desktop_batch_cap)
24003 .map(|index| test_shape(index, BlendMode::SrcOver))
24004 .collect();
24005 let segment = chunk(&[
24006 SegmentBatchPlan::Shape {
24007 start: 0,
24008 end: desktop_batch_cap,
24009 blend_mode: BlendMode::SrcOver,
24010 },
24011 SegmentBatchPlan::Image {
24012 start: desktop_batch_cap,
24013 end: desktop_batch_cap + 1,
24014 blend_mode: BlendMode::SrcOver,
24015 },
24016 SegmentBatchPlan::Shape {
24017 start: desktop_batch_cap + 1,
24018 end: desktop_batch_cap + 2,
24019 blend_mode: BlendMode::SrcOver,
24020 },
24021 ]);
24022
24023 let partitions = native_segment_fusion_partitions(
24024 &ordered_items,
24025 &shapes,
24026 &[],
24027 &segment,
24028 ShapeBatchLimits::desktop(),
24029 )
24030 .expect("valid plan")
24031 .expect("overflowing segment should be partitionable");
24032
24033 assert_eq!(partitions.len(), 2);
24034 assert_eq!(
24035 partitions[0].chunk,
24036 chunk(&[
24037 SegmentBatchPlan::Shape {
24038 start: 0,
24039 end: desktop_batch_cap,
24040 blend_mode: BlendMode::SrcOver,
24041 },
24042 SegmentBatchPlan::Image {
24043 start: desktop_batch_cap,
24044 end: desktop_batch_cap + 1,
24045 blend_mode: BlendMode::SrcOver,
24046 },
24047 ])
24048 );
24049 assert_eq!(
24050 partitions[1].chunk,
24051 chunk(&[SegmentBatchPlan::Shape {
24052 start: desktop_batch_cap + 1,
24053 end: desktop_batch_cap + 2,
24054 blend_mode: BlendMode::SrcOver,
24055 }])
24056 );
24057 }
24058
24059 #[test]
24060 fn segment_command_iter_keeps_repeated_batch_kinds_in_one_chunk() {
24061 let ordered_items = vec![
24062 (0, SegmentDrawItem::Shape(0)),
24063 (1, SegmentDrawItem::Image(0)),
24064 (2, SegmentDrawItem::Shape(1)),
24065 ];
24066 let shapes = vec![
24067 test_shape(0, BlendMode::SrcOver),
24068 test_shape(2, BlendMode::DstOut),
24069 ];
24070 let images = vec![test_image(1, BlendMode::SrcOver)];
24071
24072 let commands: Vec<_> = SegmentCommandIter::new(
24073 &ordered_items,
24074 &shapes,
24075 &images,
24076 ShapeBatchLimits::desktop(),
24077 )
24078 .collect();
24079
24080 assert_eq!(
24081 commands,
24082 vec![SegmentRenderCommand::DrawChunk(chunk(&[
24083 SegmentBatchPlan::Shape {
24084 start: 0,
24085 end: 1,
24086 blend_mode: BlendMode::SrcOver,
24087 },
24088 SegmentBatchPlan::Image {
24089 start: 1,
24090 end: 2,
24091 blend_mode: BlendMode::SrcOver,
24092 },
24093 SegmentBatchPlan::Shape {
24094 start: 2,
24095 end: 3,
24096 blend_mode: BlendMode::DstOut,
24097 },
24098 ]))]
24099 );
24100 }
24101
24102 #[test]
24103 fn segment_command_iter_splits_contiguous_shape_runs_at_uniform_batch_limit() {
24104 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
24107 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
24108 .map(|index| (index, SegmentDrawItem::Shape(index)))
24109 .collect();
24110 let shapes: Vec<_> = (0..=desktop_batch_cap)
24111 .map(|index| test_shape(index, BlendMode::SrcOver))
24112 .collect();
24113 let images = Vec::new();
24114
24115 let commands: Vec<_> = SegmentCommandIter::new(
24116 &ordered_items,
24117 &shapes,
24118 &images,
24119 ShapeBatchLimits::desktop(),
24120 )
24121 .collect();
24122
24123 assert_eq!(
24124 commands,
24125 vec![SegmentRenderCommand::DrawChunk(chunk(&[
24126 SegmentBatchPlan::Shape {
24127 start: 0,
24128 end: desktop_batch_cap,
24129 blend_mode: BlendMode::SrcOver,
24130 },
24131 SegmentBatchPlan::Shape {
24132 start: desktop_batch_cap,
24133 end: desktop_batch_cap + 1,
24134 blend_mode: BlendMode::SrcOver,
24135 },
24136 ]))]
24137 );
24138 }
24139
24140 #[test]
24141 fn segment_command_iter_keeps_shadows_as_explicit_boundaries() {
24142 let ordered_items = vec![
24143 (0, SegmentDrawItem::Shape(0)),
24144 (1, SegmentDrawItem::Shadow(0)),
24145 (2, SegmentDrawItem::Image(0)),
24146 (3, SegmentDrawItem::Text(0)),
24147 ];
24148 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
24149 let images = vec![test_image(2, BlendMode::SrcOver)];
24150
24151 let commands: Vec<_> = SegmentCommandIter::new(
24152 &ordered_items,
24153 &shapes,
24154 &images,
24155 ShapeBatchLimits::desktop(),
24156 )
24157 .collect();
24158
24159 assert_eq!(
24160 commands,
24161 vec![
24162 SegmentRenderCommand::DrawChunk(chunk(&[SegmentBatchPlan::Shape {
24163 start: 0,
24164 end: 1,
24165 blend_mode: BlendMode::SrcOver,
24166 }])),
24167 SegmentRenderCommand::Shadow(0),
24168 SegmentRenderCommand::DrawChunk(chunk(&[
24169 SegmentBatchPlan::Image {
24170 start: 2,
24171 end: 3,
24172 blend_mode: BlendMode::SrcOver,
24173 },
24174 SegmentBatchPlan::Text { start: 3, end: 4 },
24175 ])),
24176 ]
24177 );
24178 }
24179
24180 #[test]
24181 fn staged_buffer_uploads_align_new_copies_to_copy_buffer_alignment() {
24182 let mut uploads = StagedBufferUploads::default();
24183 uploads.bytes.extend_from_slice(&[1, 2]);
24184
24185 uploads.stage(UploadTarget::ImageIndex, &[3, 4, 5, 6]);
24186
24187 assert_eq!(uploads.bytes, vec![1, 2, 0, 0, 3, 4, 5, 6]);
24188 assert_eq!(
24189 uploads.copies,
24190 vec![PendingBufferCopy {
24191 source_offset: 4,
24192 target_offset: 0,
24193 size: 4,
24194 target: UploadTarget::ImageIndex,
24195 }]
24196 );
24197 }
24198
24199 #[test]
24200 fn staged_buffer_uploads_ignore_empty_payloads() {
24201 let mut uploads = StagedBufferUploads::default();
24202
24203 uploads.stage(UploadTarget::Uniform, &[]);
24204
24205 assert!(uploads.is_empty());
24206 assert!(uploads.bytes.is_empty());
24207 }
24208
24209 #[test]
24210 fn staged_buffer_uploads_return_exact_payload_slice_for_copy() {
24211 let mut uploads = StagedBufferUploads::default();
24212 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
24213 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
24214
24215 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
24216 assert_eq!(uploads.payload_for_copy(uploads.copies[1]), &[5, 6, 7, 8]);
24217 }
24218
24219 #[test]
24220 fn staged_buffer_uploads_record_destination_offsets() {
24221 let mut uploads = StagedBufferUploads::default();
24222
24223 uploads.stage_at(UploadTarget::ImageIndex, 256, &[1, 2, 3, 4]);
24224
24225 assert_eq!(uploads.copies[0].target_offset, 256);
24226 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
24227 }
24228
24229 #[test]
24230 fn staged_buffer_uploads_truncate_restores_previous_state() {
24231 let mut uploads = StagedBufferUploads::default();
24232 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
24233 let bytes_len = uploads.bytes.len();
24234 let copies_len = uploads.copies.len();
24235 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
24236
24237 uploads.truncate(bytes_len, copies_len);
24238
24239 assert_eq!(uploads.bytes, vec![1, 2, 3, 4]);
24240 assert_eq!(uploads.copies.len(), 1);
24241 }
24242
24243 #[test]
24244 fn inner_shadow_composite_mask_uses_fill_shape_and_scale() {
24245 let mut fill = test_shape(0, BlendMode::SrcOver);
24246 fill.local_rect = Rect {
24247 x: 10.0,
24248 y: 12.0,
24249 width: 40.0,
24250 height: 20.0,
24251 };
24252 fill.shape = Some(RoundedCornerShape::uniform(6.0));
24253
24254 let cutout = test_shape(1, BlendMode::DstOut);
24255 let shadow = test_shadow_draw(vec![
24256 (fill, BlendMode::SrcOver),
24257 (cutout, BlendMode::DstOut),
24258 ]);
24259
24260 let mask = inner_shadow_composite_mask(&shadow, 1.5).expect("inner mask expected");
24261 assert_eq!(mask.rect, [15.0, 18.0, 60.0, 30.0]);
24262 assert_eq!(mask.radii, [9.0, 9.0, 9.0, 9.0]);
24263 }
24264
24265 #[test]
24266 fn inner_shadow_composite_mask_is_none_without_dst_out() {
24267 let fill = test_shape(0, BlendMode::SrcOver);
24268 let shadow = test_shadow_draw(vec![(fill, BlendMode::SrcOver)]);
24269 assert!(inner_shadow_composite_mask(&shadow, 1.0).is_none());
24270 }
24271
24272 #[test]
24273 fn render_effect_support_matrix_covers_all_variants() {
24274 let blur = RenderEffect::blur(4.0);
24275 let offset = RenderEffect::offset(2.0, 3.0);
24276 let shader = RenderEffect::runtime_shader(cranpose_ui_graphics::RuntimeShader::new(
24277 r#"
24278 @group(0) @binding(0) var input_texture: texture_2d<f32>;
24279 @group(0) @binding(1) var input_sampler: sampler;
24280 @group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
24281 struct VertexOutput {
24282 @builtin(position) position: vec4<f32>,
24283 @location(0) uv: vec2<f32>,
24284 }
24285 @vertex
24286 fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
24287 var output: VertexOutput;
24288 let x = f32(i32(vertex_index & 1u) * 2 - 1);
24289 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
24290 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
24291 output.position = vec4<f32>(x, y, 0.0, 1.0);
24292 return output;
24293 }
24294 @fragment
24295 fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
24296 return textureSample(input_texture, input_sampler, input.uv);
24297 }
24298 "#,
24299 ));
24300 let chain = blur.clone().then(offset.clone());
24301
24302 assert!(is_render_effect_supported(&blur));
24303 assert!(is_render_effect_supported(&offset));
24304 assert!(is_render_effect_supported(&shader));
24305 assert!(is_render_effect_supported(&chain));
24306 }
24307
24308 #[test]
24309 fn clip_to_bounds_propagates_visual_clip_to_all_descendant_shapes() {
24310 let container_local_bounds = Rect {
24313 x: 0.0,
24314 y: 0.0,
24315 width: 800.0,
24316 height: 500.0,
24317 };
24318 let container_clip_in_parent = Rect {
24320 x: 0.0,
24321 y: 50.0,
24322 width: 800.0,
24323 height: 500.0,
24324 };
24325
24326 let shape_above = RenderNode::Primitive(PrimitiveEntry {
24328 phase: PrimitivePhase::BeforeChildren,
24329 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24330 primitive: DrawPrimitive::Rect {
24331 rect: Rect {
24332 x: 10.0,
24333 y: -30.0,
24334 width: 100.0,
24335 height: 40.0,
24336 },
24337 brush: Brush::solid(Color::WHITE),
24338 stroke: None,
24339 },
24340 clip: None,
24341 }),
24342 });
24343
24344 let shape_inside = RenderNode::Primitive(PrimitiveEntry {
24346 phase: PrimitivePhase::BeforeChildren,
24347 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24348 primitive: DrawPrimitive::Rect {
24349 rect: Rect {
24350 x: 10.0,
24351 y: 100.0,
24352 width: 100.0,
24353 height: 40.0,
24354 },
24355 brush: Brush::solid(Color::WHITE),
24356 stroke: None,
24357 },
24358 clip: None,
24359 }),
24360 });
24361
24362 let shape_below = RenderNode::Primitive(PrimitiveEntry {
24364 phase: PrimitivePhase::BeforeChildren,
24365 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24366 primitive: DrawPrimitive::Rect {
24367 rect: Rect {
24368 x: 10.0,
24369 y: 600.0,
24370 width: 100.0,
24371 height: 40.0,
24372 },
24373 brush: Brush::solid(Color::WHITE),
24374 stroke: None,
24375 },
24376 clip: None,
24377 }),
24378 });
24379
24380 let mut content_layer = test_layer(
24382 Rect {
24383 x: 0.0,
24384 y: 0.0,
24385 width: 800.0,
24386 height: 1000.0,
24387 },
24388 vec![shape_above, shape_inside, shape_below],
24389 );
24390 content_layer.transform_to_parent = ProjectiveTransform::translation(0.0, -30.0);
24391 content_layer.translated_content_context = true;
24392
24393 let mut clip_container = test_layer(
24395 container_local_bounds,
24396 vec![RenderNode::Layer(Box::new(content_layer))],
24397 );
24398 clip_container.clip_to_bounds = true;
24399 clip_container.transform_to_parent = ProjectiveTransform::translation(0.0, 50.0);
24400
24401 let root = test_layer(
24403 Rect {
24404 x: 0.0,
24405 y: 0.0,
24406 width: 800.0,
24407 height: 600.0,
24408 },
24409 vec![RenderNode::Layer(Box::new(clip_container))],
24410 );
24411
24412 let mut rect_cache = HashMap::new();
24413 let mut requirements_cache = HashMap::new();
24414 let collected =
24415 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
24416
24417 assert_eq!(
24418 collected.scene.shapes.len(),
24419 3,
24420 "all three shapes should be flattened into the scene"
24421 );
24422
24423 for (i, shape) in collected.scene.shapes.iter().enumerate() {
24424 assert!(
24425 shape.clip.is_some(),
24426 "shape {} at rect {:?} must have a clip from clip_to_bounds container, but clip is None",
24427 i,
24428 shape.rect
24429 );
24430 let clip = shape.clip.unwrap();
24431 assert_eq!(
24432 clip, container_clip_in_parent,
24433 "shape {} clip should match the clip_to_bounds container bounds in parent space",
24434 i
24435 );
24436 }
24437 }
24438
24439 #[test]
24440 fn clip_to_bounds_culls_child_layers_outside_boundary() {
24441 let clip_container_bounds = Rect {
24449 x: 0.0,
24450 y: 0.0,
24451 width: 800.0,
24452 height: 500.0,
24453 };
24454
24455 let shape_in_card = RenderNode::Primitive(PrimitiveEntry {
24456 phase: PrimitivePhase::BeforeChildren,
24457 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24458 primitive: DrawPrimitive::Rect {
24459 rect: Rect {
24460 x: 0.0,
24461 y: 0.0,
24462 width: 300.0,
24463 height: 80.0,
24464 },
24465 brush: Brush::solid(Color::WHITE),
24466 stroke: None,
24467 },
24468 clip: None,
24469 }),
24470 });
24471
24472 let mut card_outside = crate::test_support::layer_node(
24474 Rect {
24475 x: 0.0,
24476 y: 0.0,
24477 width: 300.0,
24478 height: 80.0,
24479 },
24480 ProjectiveTransform::identity(),
24481 GraphicsLayer {
24482 clip: true,
24483 ..GraphicsLayer::default()
24484 },
24485 vec![shape_in_card.clone()],
24486 );
24487 card_outside.transform_to_parent = ProjectiveTransform::translation(10.0, 600.0);
24488
24489 let mut card_inside = crate::test_support::layer_node(
24491 Rect {
24492 x: 0.0,
24493 y: 0.0,
24494 width: 300.0,
24495 height: 80.0,
24496 },
24497 ProjectiveTransform::identity(),
24498 GraphicsLayer {
24499 clip: true,
24500 ..GraphicsLayer::default()
24501 },
24502 vec![shape_in_card],
24503 );
24504 card_inside.transform_to_parent = ProjectiveTransform::translation(10.0, 100.0);
24505
24506 let content = test_layer(
24508 Rect {
24509 x: 0.0,
24510 y: 0.0,
24511 width: 800.0,
24512 height: 1000.0,
24513 },
24514 vec![
24515 RenderNode::Layer(Box::new(card_inside)),
24516 RenderNode::Layer(Box::new(card_outside)),
24517 ],
24518 );
24519
24520 let mut clip_container = test_layer(
24522 clip_container_bounds,
24523 vec![RenderNode::Layer(Box::new(content))],
24524 );
24525 clip_container.clip_to_bounds = true;
24526
24527 let root = test_layer(
24529 Rect {
24530 x: 0.0,
24531 y: 0.0,
24532 width: 800.0,
24533 height: 600.0,
24534 },
24535 vec![RenderNode::Layer(Box::new(clip_container))],
24536 );
24537
24538 let mut rect_cache = HashMap::new();
24539 let mut requirements_cache = HashMap::new();
24540 let collected =
24541 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
24542
24543 assert_eq!(
24544 collected.scene.shapes.len(),
24545 1,
24546 "only the card inside the clip boundary should produce shapes; \
24547 the card outside must be culled entirely"
24548 );
24549
24550 let shape = &collected.scene.shapes[0];
24551 assert!(
24552 shape.clip.is_some(),
24553 "the visible card's shape must have a clip from clip_to_bounds"
24554 );
24555 }
24556
24557 #[test]
24558 fn flattened_layer_shadow_z_index_is_below_content() {
24559 let shape = RenderNode::Primitive(PrimitiveEntry {
24563 phase: PrimitivePhase::BeforeChildren,
24564 node: PrimitiveNode::Draw(DrawPrimitiveNode {
24565 primitive: DrawPrimitive::Rect {
24566 rect: Rect {
24567 x: 0.0,
24568 y: 0.0,
24569 width: 100.0,
24570 height: 100.0,
24571 },
24572 brush: Brush::solid(Color::WHITE),
24573 stroke: None,
24574 },
24575 clip: None,
24576 }),
24577 });
24578
24579 let child_bounds = Rect {
24580 x: 0.0,
24581 y: 0.0,
24582 width: 100.0,
24583 height: 100.0,
24584 };
24585
24586 let child = crate::test_support::layer_node(
24587 child_bounds,
24588 ProjectiveTransform::translation(50.0, 50.0),
24589 GraphicsLayer {
24590 shadow_elevation: 20.0,
24591 ..GraphicsLayer::default()
24592 },
24593 vec![shape],
24594 );
24595
24596 let root = test_layer(
24597 Rect {
24598 x: 0.0,
24599 y: 0.0,
24600 width: 800.0,
24601 height: 600.0,
24602 },
24603 vec![RenderNode::Layer(Box::new(child))],
24604 );
24605
24606 let mut rect_cache = HashMap::new();
24607 let mut requirements_cache = HashMap::new();
24608 let collected =
24609 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
24610
24611 assert!(
24612 !collected.scene.shadow_draws.is_empty(),
24613 "shadow_elevation > 0 must produce shadow draws"
24614 );
24615 let max_shadow_z = collected
24616 .scene
24617 .shadow_draws
24618 .iter()
24619 .map(|s| s.z_index)
24620 .max()
24621 .unwrap();
24622 let min_content_z = collected
24623 .scene
24624 .shapes
24625 .iter()
24626 .map(|s| s.z_index)
24627 .min()
24628 .unwrap();
24629 assert!(
24630 max_shadow_z < min_content_z,
24631 "shadow z-index ({}) must be less than content z-index ({}); \
24632 shadows must render behind their content",
24633 max_shadow_z,
24634 min_content_z
24635 );
24636 }
24637
24638 #[cfg(not(target_arch = "wasm32"))]
24641 fn bundle_op(slot: u32, epoch: Option<u64>, first: u32, last: u32) -> RetainedBundleOpKey {
24642 RetainedBundleOpKey {
24643 slot,
24644 capture_epoch: epoch,
24645 first,
24646 last,
24647 retained_index: slot,
24648 has_mesh: false,
24649 }
24650 }
24651
24652 #[cfg(not(target_arch = "wasm32"))]
24653 fn bundle_key(ops: &[RetainedBundleOpKey]) -> RetainedBundleKey {
24654 RetainedBundleKey {
24655 depth: false,
24656 ops: ops.to_vec(),
24657 }
24658 }
24659
24660 #[cfg(not(target_arch = "wasm32"))]
24663 #[test]
24664 fn retained_bundle_cache_reuses_stable_keys() {
24665 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24666 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
24667 let key = bundle_key(&ops);
24668
24669 assert!(!cache.hit(&key), "empty cache must miss");
24670 cache.insert(key.clone(), 111);
24671 assert_eq!(cache.get(&key), Some(&111));
24672 cache.end_frame();
24673
24674 for _ in 0..3 {
24675 assert!(cache.hit(&bundle_key(&ops)), "stable key must stay cached");
24676 cache.end_frame();
24677 }
24678 assert_eq!(cache.stats(), (1, 3), "one rebuild, three cached executes");
24679 }
24680
24681 #[cfg(not(target_arch = "wasm32"))]
24685 #[test]
24686 fn retained_bundle_cache_invalidates_on_any_op_change() {
24687 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
24688 let variants: [Vec<RetainedBundleOpKey>; 5] = [
24689 vec![bundle_op(3, Some(8), 0, 40), bundle_op(5, Some(9), 4, 12)],
24691 vec![bundle_op(5, Some(9), 4, 12), bundle_op(3, Some(7), 0, 40)],
24693 vec![bundle_op(3, Some(7), 0, 40)],
24695 vec![bundle_op(3, Some(7), 0, 41), bundle_op(5, Some(9), 4, 12)],
24697 vec![bundle_op(3, Some(7), 0, 40), bundle_op(5, None, 4, 12)],
24699 ];
24700 for changed in variants {
24701 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24702 cache.insert(bundle_key(&ops), 111);
24703 cache.end_frame();
24704 assert!(
24705 !cache.hit(&RetainedBundleKey {
24706 depth: false,
24707 ops: changed.clone()
24708 }),
24709 "changed key {changed:?} must not reuse the stale bundle"
24710 );
24711 }
24712 }
24713
24714 #[cfg(not(target_arch = "wasm32"))]
24718 #[test]
24719 fn retained_bundle_cache_keys_depth_variants_apart() {
24720 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24721 let ops = vec![bundle_op(3, Some(7), 0, 40)];
24722 cache.insert(
24723 RetainedBundleKey {
24724 depth: false,
24725 ops: ops.clone(),
24726 },
24727 111,
24728 );
24729 cache.end_frame();
24730 assert!(
24731 !cache.hit(&RetainedBundleKey { depth: true, ops }),
24732 "a flat bundle must not replay into the display-clip culled pass"
24733 );
24734 }
24735
24736 #[cfg(not(target_arch = "wasm32"))]
24740 #[test]
24741 fn retained_bundle_cache_evicts_unused_entries() {
24742 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
24743 let stale = bundle_key(&[bundle_op(1, Some(1), 0, 6)]);
24744 let live = bundle_key(&[bundle_op(2, Some(2), 0, 6)]);
24745 cache.insert(stale.clone(), 1);
24746 cache.insert(live.clone(), 2);
24747 cache.end_frame();
24748
24749 assert!(cache.hit(&live));
24750 cache.end_frame();
24751
24752 assert!(
24753 !cache.hit(&stale),
24754 "entry unused for a frame must have been evicted"
24755 );
24756 assert!(cache.hit(&live), "used entry must survive eviction");
24757
24758 cache.clear();
24759 assert!(!cache.hit(&live), "clear must drop every entry");
24760 }
24761}