1use crate::effect_renderer::{
4 projective_dest_bounds_rect, CompositeBatchItem, CompositeSampleMode, EffectRenderer,
5 EffectScratchTargetProvider, ProjectiveSurfaceComposite, RoundedCompositeMask,
6 ShaderCompositeBatchItem,
7};
8use crate::frame_graph::{
9 FrameCommandRecorder, FrameTextureDescriptor, WgpuFrameGraph, WgpuFrameGraphExecutor,
10};
11use crate::frame_packet::{
12 CancelReason, FramePacket, PacketRoot, PresentOutcome, RenderReturns, RootSurfacePacket,
13};
14use crate::layer_events::{
15 collect_effect_ranges, collect_layer_events, LayerEvent, LayerEventKind,
16};
17use crate::layer_surface_cache::LayerSurfaceCache;
18use crate::lazy_resource::LazyGpuResource;
19#[cfg(test)]
20use crate::normalized_scene::{
21 build_scene_window, collect_layer_contents, collect_layer_contents_with_translation_context,
22 filtered_effect_layer_index, scene_bounds, SceneWindowSource,
23};
24#[cfg(test)]
25use crate::normalized_scene::{estimate_layer_surface_rect, motion_stable_capture_bounds};
26use crate::normalized_scene::{translate_quad, ChildLayerComposite, CollectedLayer};
27use crate::offscreen::OffscreenTarget;
28use crate::rect_to_quad;
29use crate::scene::{
30 BackdropLayer, CompositorScene, DrawOp, DrawOpKind, DrawShape, EffectLayer, ImageDraw,
31 RetainedDraw, ShadowDraw, SimilarityTransform, SnapAnchor, TextDraw,
32};
33use crate::shaders;
34#[cfg(test)]
35use crate::surface_executor::surface_target_size;
36use crate::surface_executor::{
37 apply_backdrop_layer_to_target as execute_apply_backdrop_layer_to_target,
38 axis_aligned_quad_rect, backdrop_underlay_is_covered_by_local_content,
39 canonicalize_device_coordinate, canonicalized_scaled_quad, canonicalized_scaled_rect,
40 composite_surface_to_view as execute_composite_surface_to_view, device_pixel_bounds_for_rect,
41 offscreen_byte_size, render_effect_layer_to_target as execute_render_effect_layer_to_target,
42 render_layer_surface as execute_render_layer_surface,
43 render_root_direct as execute_render_root_direct, root_direct_scene_events_are_supported,
44 scaled_quad, snap_delta_for_anchor, snap_motion_stable_dest_quad,
45 translation_stable_anchored_device_pixel_bounds, DevicePixelBounds, LayerSurfaceTexture,
46 SurfaceExecutionBackend,
47};
48#[cfg(test)]
49use crate::surface_executor::{clamp_effect_surface_scale, visible_layer_rect};
50#[cfg(test)]
51use crate::surface_plan::root_can_render_directly_cached;
52#[cfg(test)]
53use crate::surface_plan::{
54 composite_sample_mode_for_effect_layer, composite_sample_mode_for_requirements,
55 direct_translation, effect_layer_target_scale, layer_contains_descendant_backdrop,
56 layer_surface_requirements, layer_surface_requirements_cached, layer_surface_scale,
57 layer_surface_target_scale, layer_uses_external_backdrop_input, TranslatedContentAxes,
58};
59use crate::surface_plan::{LayerSurfaceRequest, TranslationRenderContext};
60#[cfg(test)]
61use crate::surface_requirements::SurfaceRequirement;
62use crate::surface_requirements::SurfaceRequirementSet;
63use crate::DebugCpuAllocationStats;
64use bytemuck::{Pod, Zeroable};
65#[cfg(any(not(target_arch = "wasm32"), test))]
66use cranpose_core::collections::map::HashMap;
67use cranpose_core::{hash::default as default_hash, NodeId};
68use cranpose_render_common::bounded_lru_cache::BoundedLruCache;
69use cranpose_render_common::geometry::blur_extent_margin;
70use cranpose_render_common::graph::quad_bounds;
71#[cfg(test)]
72use cranpose_render_common::graph::{
73 CachePolicy, LayerNode, PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform,
74 RenderNode,
75};
76use cranpose_render_common::raster_cache::LayerRasterCacheKey;
77#[cfg(test)]
78use cranpose_render_common::raster_cache::ScaleBucket;
79use cranpose_render_common::software_text_raster::{
80 collect_solid_text_atlas_run, measure_text_with_font,
81 rasterize_annotated_text_to_image_with_glyph_cache, rasterize_text_to_image_with_glyph_cache,
82 SoftwareGlyphAtlasGlyph, SoftwareGlyphAtlasKey, SoftwareGlyphAtlasPlacement,
83 SoftwareGlyphAtlasRunGlyph, SoftwareGlyphRasterCache, SoftwareTextFontSet,
84};
85#[cfg(test)]
86use cranpose_ui_graphics::GraphicsLayer;
87use cranpose_ui_graphics::{
88 BlendMode, Brush, Color, ColorFilter, FxHasher, ImageBitmap, ImageSampling, Point, Rect,
89 RenderEffect, RenderHash, RuntimeShader, StrokeCap, StrokeJoin, TileMode,
90};
91use std::borrow::Cow;
92use std::cell::Cell;
93use std::hash::{Hash, Hasher};
94use std::ops::Range;
95use std::rc::Rc;
96#[cfg(not(target_arch = "wasm32"))]
97use std::sync::atomic::{AtomicUsize, Ordering};
98use std::sync::{mpsc, Arc};
99use std::time::Duration;
100use web_time::Instant;
101
102use crate::gpu_stats;
103use crate::gpu_stats::gpu_stats_enabled;
104use crate::pipeline::push_layer_shadow;
105
106#[cfg(target_arch = "wasm32")]
111const MAX_SHAPES_PER_BATCH: usize = 102;
112#[cfg(not(target_arch = "wasm32"))]
113const MAX_SHAPES_PER_BATCH: usize = 768;
114#[cfg(target_arch = "wasm32")]
115const MAX_GRADIENT_STOPS: usize = 256;
116#[cfg(not(target_arch = "wasm32"))]
117const MAX_GRADIENT_STOPS: usize = 1024;
118
119#[cfg(not(target_arch = "wasm32"))]
126const MAX_SHAPES_PER_STORAGE_BATCH: usize = 1 << 16;
127#[cfg(not(target_arch = "wasm32"))]
128const MAX_GRADIENT_STOPS_PER_STORAGE_BATCH: usize = 1 << 16;
129
130#[cfg(not(target_arch = "wasm32"))]
136const INITIAL_STORAGE_BATCH_CAPACITY: usize = 1024;
137
138#[derive(Clone, Copy, Debug, Eq, PartialEq)]
150struct ShapeBatchLimits {
151 max_shapes_per_batch: usize,
152 max_gradient_stops: usize,
153 storage: bool,
154}
155
156impl ShapeBatchLimits {
157 fn for_device(device: &wgpu::Device) -> Self {
158 let limits = device.limits();
159 #[cfg(not(target_arch = "wasm32"))]
160 if limits.max_storage_buffers_per_shader_stage >= 2 {
161 return Self::for_storage_binding_size(limits.max_storage_buffer_binding_size);
162 }
163 Self::for_uniform_binding_size(limits.max_uniform_buffer_binding_size)
164 }
165
166 fn for_uniform_binding_size(max_uniform_buffer_binding_size: u64) -> Self {
167 let binding = max_uniform_buffer_binding_size as usize;
168 Self {
169 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
170 .clamp(1, MAX_SHAPES_PER_BATCH),
171 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
172 .clamp(1, MAX_GRADIENT_STOPS),
173 storage: false,
174 }
175 }
176
177 #[cfg(not(target_arch = "wasm32"))]
178 fn for_storage_binding_size(max_storage_buffer_binding_size: u64) -> Self {
179 let binding = max_storage_buffer_binding_size as usize;
180 Self {
181 max_shapes_per_batch: (binding / std::mem::size_of::<ShapeData>())
182 .clamp(1, MAX_SHAPES_PER_STORAGE_BATCH),
183 max_gradient_stops: (binding / std::mem::size_of::<GradientStop>())
184 .clamp(1, MAX_GRADIENT_STOPS_PER_STORAGE_BATCH),
185 storage: true,
186 }
187 }
188
189 fn initial_shape_capacity(&self) -> usize {
190 #[cfg(not(target_arch = "wasm32"))]
191 if self.storage {
192 return self
193 .max_shapes_per_batch
194 .min(INITIAL_STORAGE_BATCH_CAPACITY);
195 }
196 self.max_shapes_per_batch
197 }
198
199 fn initial_gradient_capacity(&self) -> usize {
200 #[cfg(not(target_arch = "wasm32"))]
201 if self.storage {
202 return self.max_gradient_stops.min(INITIAL_STORAGE_BATCH_CAPACITY);
203 }
204 self.max_gradient_stops
205 }
206
207 fn data_buffer_usage(&self) -> wgpu::BufferUsages {
208 if self.storage {
209 wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST
210 } else {
211 wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST
212 }
213 }
214
215 fn data_binding_type(&self) -> wgpu::BufferBindingType {
216 if self.storage {
217 wgpu::BufferBindingType::Storage { read_only: true }
218 } else {
219 wgpu::BufferBindingType::Uniform
220 }
221 }
222
223 #[cfg(test)]
224 fn desktop() -> Self {
225 Self::for_uniform_binding_size(wgpu::Limits::default().max_uniform_buffer_binding_size)
226 }
227}
228#[cfg(target_arch = "wasm32")]
229const HARD_MAX_BUFFER_MB: usize = 64; const MAX_SHADOW_SURFACE_CACHE_ITEMS: usize = 512;
231const MAX_SHADOW_SURFACE_CACHE_BYTES: u64 = 192 * 1024 * 1024;
235const MAX_TEXT_IMAGE_CACHE_ITEMS: usize = 1024;
236const MAX_TEXT_GLYPH_MASK_CACHE_ITEMS: usize = 8192;
237const MAX_TEXT_GLYPH_ATLAS_ITEMS: usize = 8192;
238const MAX_TEXT_GLYPH_RUN_CACHE_ITEMS: usize = 1024;
239#[cfg(not(target_arch = "wasm32"))]
240const MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS: usize = 1024;
241#[cfg(not(target_arch = "wasm32"))]
242const MIN_RETAINED_TEXT_GLYPH_QUADS: usize = 192;
243#[cfg(not(target_arch = "wasm32"))]
244const OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS: f64 = 0.75;
245#[cfg(not(target_arch = "wasm32"))]
246const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES: usize = 2;
247#[cfg(not(target_arch = "wasm32"))]
248const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS: usize = 160;
249#[cfg(not(target_arch = "wasm32"))]
250const MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS: usize = 160;
251const TEXT_GLYPH_ATLAS_MIN_SIZE: u32 = 512;
263const TEXT_GLYPH_ATLAS_MAX_SIZE: u32 = 4096;
264const TEXT_GLYPH_ATLAS_PADDING: u32 = 1;
265const MAX_TEXT_LINE_INDEX_CACHE_ITEMS: usize = 512;
266const MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER: usize = 2;
267const MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES: usize = 128;
268const CACHE_MISS_WARMUP_FRAMES: u8 = 1;
269pub(crate) const CLEAR_COLOR: wgpu::Color = wgpu::Color {
270 r: cranpose_render_common::FRAME_CLEAR_COLOR[0] as f64,
271 g: cranpose_render_common::FRAME_CLEAR_COLOR[1] as f64,
272 b: cranpose_render_common::FRAME_CLEAR_COLOR[2] as f64,
273 a: cranpose_render_common::FRAME_CLEAR_COLOR[3] as f64,
274};
275#[cfg(not(target_arch = "wasm32"))]
276const INITIAL_UPLOAD_BUFFER_BYTES: u64 = 4 * 1024;
277#[cfg(not(target_arch = "wasm32"))]
278const INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS: usize = 128;
279const MAX_TEXTURE_CACHE_ITEMS: usize = 256;
280const MAX_IMAGE_TEXTURE_CACHE_BYTES: usize = 256 * 1024 * 1024;
284const RETAINED_STAGED_UPLOAD_BYTES: usize = 256 * 1024;
285const RETAINED_STAGED_UPLOAD_COPIES: usize = 128;
286pub(crate) const RETAINED_LAYER_REQUIREMENTS_CAPACITY: usize = 512;
287const DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS: f64 = 4.0;
288#[cfg(not(target_arch = "wasm32"))]
289static SEGMENT_DIAG_LINES: AtomicUsize = AtomicUsize::new(0);
290fn wgpu_render_stage_telemetry_threshold_ms() -> Option<f64> {
294 static THRESHOLD_MS: std::sync::OnceLock<Option<f64>> = std::sync::OnceLock::new();
295 *THRESHOLD_MS.get_or_init(|| {
296 let explicit = std::env::var("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY_MS")
297 .ok()
298 .and_then(|value| value.parse::<f64>().ok())
299 .filter(|value| value.is_finite() && *value >= 0.0);
300 explicit.or_else(|| {
301 std::env::var_os("CRANPOSE_WGPU_RENDER_STAGE_TELEMETRY")
302 .is_some()
303 .then_some(DEFAULT_WGPU_RENDER_STAGE_TELEMETRY_THRESHOLD_MS)
304 })
305 })
306}
307
308pub(crate) fn instant_ms(start: Instant, end: Instant) -> f64 {
309 end.duration_since(start).as_secs_f64() * 1000.0
310}
311
312pub(crate) fn should_log_wgpu_render_stage(start: Instant, end: Instant) -> Option<f64> {
313 let threshold_ms = wgpu_render_stage_telemetry_threshold_ms()?;
314 let total_ms = instant_ms(start, end);
315 (total_ms >= threshold_ms).then_some(total_ms)
316}
317
318fn admit_layer_surface_cache_miss_impl(
319 key: &LayerRasterCacheKey,
320 observed_scene_range_misses: &mut BoundedLruCache<LayerRasterCacheKey, ()>,
321) -> bool {
322 if !key.is_scene_range() {
323 return true;
324 }
325 if observed_scene_range_misses.contains(key) {
326 return true;
327 }
328 observed_scene_range_misses.put(*key, ());
329 false
330}
331
332#[cfg(test)]
333fn first_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
334 let mut observed_scene_range_misses =
335 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
336 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
337}
338
339#[cfg(test)]
340fn repeated_cache_miss_admission(key: &LayerRasterCacheKey) -> bool {
341 let mut observed_scene_range_misses =
342 BoundedLruCache::with_capacity_at_least_one(MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES);
343 let _ = admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses);
344 admit_layer_surface_cache_miss_impl(key, &mut observed_scene_range_misses)
345}
346
347pub static PRESENTED_FRAMES: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
348
349pub fn frames_presented() -> u64 {
350 PRESENTED_FRAMES.load(std::sync::atomic::Ordering::Relaxed)
351}
352
353fn frame_stats_need_warmup_frame(snapshot: &gpu_stats::FrameStatsSnapshot) -> bool {
354 snapshot.layer_cache_misses > 0
355 || snapshot.shadow_shape_cache_misses > 0
356 || snapshot.text_image_cache_misses > 0
357 || snapshot.text_glyph_atlas_misses > 0
358}
359
360fn update_frame_warmup_budget(pending_frames: &mut u8, snapshot: &gpu_stats::FrameStatsSnapshot) {
361 if *pending_frames > 0 {
362 *pending_frames = pending_frames.saturating_sub(1);
363 } else if frame_stats_need_warmup_frame(snapshot) {
364 *pending_frames = CACHE_MISS_WARMUP_FRAMES;
365 }
366}
367
368fn text_atlas_fallback_diag_enabled() -> bool {
369 cranpose_core::env_flag!("CRANPOSE_TEXT_ATLAS_FALLBACK_DIAG")
370}
371
372fn text_glyph_run_diag_enabled() -> bool {
373 cranpose_core::env_flag!("CRANPOSE_TEXT_GLYPH_RUN_DIAG")
374}
375
376fn root_direct_diag_enabled() -> bool {
377 cranpose_core::env_flag!("CRANPOSE_ROOT_DIRECT_DIAG")
378}
379
380fn scene_layer_events_precede_z(scene: &CompositorScene, z_index: usize) -> bool {
381 scene
382 .effect_layers
383 .iter()
384 .any(|layer| layer.z_start < z_index && 0 < layer.z_end)
385 || scene
386 .backdrop_layers
387 .iter()
388 .any(|layer| layer.z_index < z_index)
389}
390
391fn direct_root_child_can_be_replayed_into_later_underlay(child: &ChildLayerComposite) -> bool {
392 child.backdrop.is_none()
393 && !child.has_effect
394 && child.shadow_draws.is_empty()
395 && axis_aligned_quad_rect(child.dest_quad).is_some()
396}
397
398fn rects_overlap(a: Rect, b: Rect) -> bool {
399 let a_right = a.x + a.width;
400 let a_bottom = a.y + a.height;
401 let b_right = b.x + b.width;
402 let b_bottom = b.y + b.height;
403 a.x < b_right && b.x < a_right && a.y < b_bottom && b.y < a_bottom
404}
405
406pub(crate) fn direct_root_child_underlays_are_supported(collected: &CollectedLayer) -> bool {
407 for (child_index, child) in collected.child_layers.iter().enumerate() {
408 if child.backdrop.is_some() {
409 if root_direct_diag_enabled() {
410 log::warn!(
411 "[root-direct-diag] reject self-backdrop child node={:?}",
412 child.node_id
413 );
414 }
415 return false;
416 }
417 if child.needs_nested_underlay {
418 let Some(dest_rect) = axis_aligned_quad_rect(child.dest_quad) else {
419 if root_direct_diag_enabled() {
420 log::warn!(
421 "[root-direct-diag] reject projective underlay child node={:?}",
422 child.node_id
423 );
424 }
425 return false;
426 };
427 let translation_only = (dest_rect.width - child.logical_rect.width).abs() <= 0.001
428 && (dest_rect.height - child.logical_rect.height).abs() <= 0.001;
429 let unsupported_preceding_child_layer = collected.child_layers[..child_index]
430 .iter()
431 .any(|preceding| {
432 if direct_root_child_can_be_replayed_into_later_underlay(preceding) {
433 return false;
434 }
435 axis_aligned_quad_rect(preceding.dest_quad)
436 .is_none_or(|preceding_rect| rects_overlap(preceding_rect, dest_rect))
437 });
438 let preceding_scene_events =
439 scene_layer_events_precede_z(&collected.scene, child.z_index);
440 if unsupported_preceding_child_layer || preceding_scene_events || !translation_only {
441 if root_direct_diag_enabled() {
442 log::warn!(
443 "[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})",
444 child.node_id,
445 unsupported_preceding_child_layer,
446 preceding_scene_events,
447 translation_only,
448 dest_rect.x,
449 dest_rect.y,
450 dest_rect.width,
451 dest_rect.height,
452 child.logical_rect.x,
453 child.logical_rect.y,
454 child.logical_rect.width,
455 child.logical_rect.height
456 );
457 }
458 return false;
459 }
460 }
461 }
462 true
463}
464
465#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
466struct ShadowSurfaceCacheKey {
467 content_hash: u64,
468 pixel_size: [u32; 2],
469 root_scale_bits: u32,
470 blur_radius_bits: u32,
471}
472
473struct CachedShadowSurface {
474 target: Rc<OffscreenTarget>,
475 byte_size: u64,
476}
477
478struct CachedShadowComposite {
479 source: Rc<OffscreenTarget>,
480 scissor: Option<(u32, u32, u32, u32)>,
481 rounded_mask: Option<RoundedCompositeMask>,
482 dest_viewport: Option<(f32, f32, f32, f32)>,
483}
484
485impl CachedShadowComposite {
486 fn batch_item(&self) -> CompositeBatchItem<'_> {
487 CompositeBatchItem {
488 source: &self.source,
489 alpha: 1.0,
490 scissor: self.scissor,
491 rounded_mask: self.rounded_mask,
492 blend_mode: BlendMode::SrcOver,
493 dest_viewport: self.dest_viewport,
494 source_viewport: None,
495 sample_mode: CompositeSampleMode::Nearest,
496 }
497 }
498}
499
500#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
501struct TextImageCacheKey(u64);
502
503struct CachedTextImage {
504 image: ImageBitmap,
505}
506
507#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
508struct TextGlyphRunCacheKey(u64);
509
510#[derive(Clone, Copy)]
511struct CachedTextGlyphQuad {
512 x: i32,
513 y: i32,
514 width: usize,
515 height: usize,
516 color: (f32, f32, f32, f32),
517 uv: ImageUvRect,
518}
519
520struct CachedTextGlyphRun {
521 glyphs: Rc<[SoftwareGlyphAtlasPlacement]>,
522 quads: Option<Rc<[CachedTextGlyphQuad]>>,
523 atlas_generation: u64,
524}
525
526const TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER: f32 = 2.0;
527
528#[cfg(not(target_arch = "wasm32"))]
529struct CachedGpuTextGlyphRun {
530 vertex_buffer: wgpu::Buffer,
531 index_buffer: wgpu::Buffer,
532 index_count: u32,
533 atlas_generation: u64,
534}
535
536#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
537struct TextLineIndexCacheKey(usize);
538
539struct CachedTextLineIndex {
540 text: std::sync::Weak<cranpose_ui::text::RenderString>,
541 len: usize,
542 starts: Rc<[usize]>,
543}
544
545struct TextLineIndexCache {
546 entries: BoundedLruCache<TextLineIndexCacheKey, CachedTextLineIndex>,
547}
548
549impl TextLineIndexCache {
550 fn new(capacity: usize) -> Self {
551 Self {
552 entries: BoundedLruCache::with_capacity_at_least_one(capacity),
553 }
554 }
555
556 fn line_starts(&mut self, text: &Arc<cranpose_ui::text::RenderString>) -> Rc<[usize]> {
557 let key = TextLineIndexCacheKey(Arc::as_ptr(text) as usize);
558 if let Some(cached) = self.entries.get(&key) {
559 if cached.len == text.text.len()
560 && cached
561 .text
562 .upgrade()
563 .is_some_and(|cached_text| Arc::ptr_eq(&cached_text, text))
564 {
565 return cached.starts.clone();
566 }
567 }
568
569 let starts = Rc::<[usize]>::from(line_start_offsets(text.text.as_str()));
570 self.entries.put(
571 key,
572 CachedTextLineIndex {
573 text: Arc::downgrade(text),
574 len: text.text.len(),
575 starts: starts.clone(),
576 },
577 );
578 starts
579 }
580}
581
582#[derive(Clone, Copy, Debug, PartialEq)]
583struct ShapeShadowSurfacePlan {
584 source_device_bounds: DevicePixelBounds,
585 processing_scissor: Option<(u32, u32, u32, u32)>,
586 pixel_radius: f32,
587}
588
589#[derive(Default)]
590struct RendererWarningState {
591 unsupported_effect_reported: Cell<bool>,
592}
593
594impl RendererWarningState {
595 fn warn_unsupported_effect_once(&self) {
596 if !self.unsupported_effect_reported.replace(true) {
597 log::warn!(
598 "WGPU renderer received an unsupported RenderEffect variant; falling back to passthrough compositing"
599 );
600 }
601 }
602}
603
604fn is_blend_mode_supported(mode: BlendMode) -> bool {
605 matches!(mode, BlendMode::SrcOver | BlendMode::DstOut)
606}
607
608fn blend_state_for_mode(mode: BlendMode) -> wgpu::BlendState {
609 match mode {
610 BlendMode::DstOut => wgpu::BlendState {
611 color: wgpu::BlendComponent {
612 src_factor: wgpu::BlendFactor::Zero,
613 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
614 operation: wgpu::BlendOperation::Add,
615 },
616 alpha: wgpu::BlendComponent {
617 src_factor: wgpu::BlendFactor::Zero,
618 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
619 operation: wgpu::BlendOperation::Add,
620 },
621 },
622 _ => wgpu::BlendState::ALPHA_BLENDING,
623 }
624}
625
626fn supported_blend_mode(mode: BlendMode) -> BlendMode {
627 if is_blend_mode_supported(mode) {
628 return mode;
629 }
630
631 BlendMode::SrcOver
632}
633
634fn direct_shader_composite_viewport(
635 alpha: f32,
636 blend_mode: BlendMode,
637 dest_viewport: Option<(f32, f32, f32, f32)>,
638 sample_mode: CompositeSampleMode,
639 source_size: (u32, u32),
640) -> Option<(f32, f32, f32, f32)> {
641 if alpha != 1.0 || supported_blend_mode(blend_mode) != BlendMode::SrcOver {
642 return None;
643 }
644 let viewport = dest_viewport?;
645 if viewport.2 <= 0.0 || viewport.3 <= 0.0 {
646 return None;
647 }
648 match sample_mode {
649 CompositeSampleMode::Linear | CompositeSampleMode::Nearest => Some(viewport),
650 CompositeSampleMode::Box4
651 if shader_composite_preserves_source_pixel_grid(viewport, source_size) =>
652 {
653 Some(viewport)
654 }
655 CompositeSampleMode::Box4 => None,
656 }
657}
658
659fn shader_composite_preserves_source_pixel_grid(
660 viewport: (f32, f32, f32, f32),
661 source_size: (u32, u32),
662) -> bool {
663 const EPSILON: f32 = 0.01;
664 let (x, y, width, height) = viewport;
665 let (source_width, source_height) = source_size;
666 (x - x.round()).abs() <= EPSILON
667 && (y - y.round()).abs() <= EPSILON
668 && (width - source_width as f32).abs() <= EPSILON
669 && (height - source_height as f32).abs() <= EPSILON
670}
671
672type DirectShaderTailComposite<'a> = (&'a RenderEffect, &'a RuntimeShader, (f32, f32, f32, f32));
673
674fn direct_shader_tail_composite(
675 effect: &RenderEffect,
676 alpha: f32,
677 blend_mode: BlendMode,
678 dest_viewport: Option<(f32, f32, f32, f32)>,
679 sample_mode: CompositeSampleMode,
680 source_size: (u32, u32),
681) -> Option<DirectShaderTailComposite<'_>> {
682 let viewport = direct_shader_composite_viewport(
683 alpha,
684 blend_mode,
685 dest_viewport,
686 sample_mode,
687 source_size,
688 )?;
689 let RenderEffect::Chain { first, second } = effect else {
690 return None;
691 };
692 let RenderEffect::Shader { shader } = second.as_ref() else {
693 return None;
694 };
695 Some((first.as_ref(), shader, viewport))
696}
697
698fn hash_f32_for_cache<H: Hasher>(value: f32, state: &mut H) {
699 value.to_bits().hash(state);
700}
701
702fn hash_text_raster_geometry_for_cache<H: Hasher>(
703 rect: Rect,
704 static_text_motion: bool,
705 state: &mut H,
706) {
707 hash_f32_for_cache(rect.width, state);
708 hash_f32_for_cache(rect.height, state);
709 static_text_motion.hash(state);
710 if !static_text_motion {
711 hash_f32_for_cache(rect.x.fract(), state);
712 hash_f32_for_cache(rect.y.fract(), state);
713 }
714}
715
716fn text_raster_geometry_for_draw(
717 text_draw: &TextDraw,
718 root_scale: f32,
719) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
720 if text_draw.text.is_empty()
721 || text_draw.rect.width <= 0.0
722 || text_draw.rect.height <= 0.0
723 || !root_scale.is_finite()
724 || root_scale <= 0.0
725 {
726 return None;
727 }
728
729 let text_scale = text_draw.scale * root_scale;
730 if !text_scale.is_finite() || text_scale <= 0.0 {
731 return None;
732 }
733
734 let static_text_motion = text_draw
735 .text_style
736 .paragraph_style
737 .text_motion
738 .unwrap_or(cranpose_ui::text::TextMotion::Static)
739 == cranpose_ui::text::TextMotion::Static;
740 let snap_delta = text_draw
741 .snap_anchor
742 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
743 .unwrap_or_default();
744 let logical_rect = text_draw.rect.translate(snap_delta.x, snap_delta.y);
745 let clip = text_draw.clip;
748 let mut raster_rect = Rect {
749 x: logical_rect.x * root_scale,
750 y: logical_rect.y * root_scale,
751 width: logical_rect.width * root_scale,
752 height: logical_rect.height * root_scale,
753 };
754 if text_draw.snap_anchor.is_some() {
755 raster_rect.x = canonicalize_device_coordinate(raster_rect.x);
756 raster_rect.y = canonicalize_device_coordinate(raster_rect.y);
757 }
758 if static_text_motion {
759 raster_rect.x = raster_rect.x.round();
760 raster_rect.y = raster_rect.y.round();
761 }
762 raster_rect.width = raster_rect.width.ceil().max(1.0);
763 raster_rect.height = raster_rect.height.ceil().max(1.0);
764 Some((
765 logical_rect,
766 raster_rect,
767 clip,
768 text_scale,
769 static_text_motion,
770 ))
771}
772
773fn text_draw_is_visible_in_viewport(
774 logical_rect: Rect,
775 clip: Option<Rect>,
776 viewport: ViewportUniformParams,
777 root_scale: f32,
778) -> bool {
779 draw_rect_is_visible_in_viewport(logical_rect, clip, viewport, root_scale)
780}
781
782fn text_draw_should_prewarm_in_viewport(
783 logical_rect: Rect,
784 clip: Option<Rect>,
785 viewport: ViewportUniformParams,
786 root_scale: f32,
787) -> bool {
788 if !root_scale.is_finite() || root_scale <= 0.0 {
789 return false;
790 }
791 let viewport_rect = Rect {
792 x: viewport.offset[0] / root_scale,
793 y: viewport.offset[1] / root_scale,
794 width: viewport.width as f32 / root_scale,
795 height: viewport.height as f32 / root_scale,
796 };
797 let margin_x = viewport_rect.width * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
798 let margin_y = viewport_rect.height * TEXT_GLYPH_PREWARM_VIEWPORT_MULTIPLIER;
799 let prewarm_viewport = expand_rect(viewport_rect, margin_x, margin_y);
800 let prewarm_rect = match clip {
801 Some(clip) => expand_rect(clip, margin_x, margin_y).intersect(prewarm_viewport),
802 None => Some(prewarm_viewport),
803 };
804 prewarm_rect.is_some_and(|rect| logical_rect.intersect(rect).is_some())
805}
806
807fn expand_rect(rect: Rect, margin_x: f32, margin_y: f32) -> Rect {
808 Rect {
809 x: rect.x - margin_x,
810 y: rect.y - margin_y,
811 width: rect.width + margin_x * 2.0,
812 height: rect.height + margin_y * 2.0,
813 }
814}
815
816fn draw_rect_is_visible_in_viewport(
817 rect: Rect,
818 clip: Option<Rect>,
819 viewport: ViewportUniformParams,
820 root_scale: f32,
821) -> bool {
822 if !root_scale.is_finite() || root_scale <= 0.0 {
823 return false;
824 }
825 let viewport_rect = Rect {
826 x: viewport.offset[0] / root_scale,
827 y: viewport.offset[1] / root_scale,
828 width: viewport.width as f32 / root_scale,
829 height: viewport.height as f32 / root_scale,
830 };
831 let visible_rect = match clip {
832 Some(clip) => clip.intersect(viewport_rect),
833 None => Some(viewport_rect),
834 };
835 visible_rect.is_some_and(|visible| rect.intersect(visible).is_some())
836}
837
838fn shape_draw_is_visible_in_viewport(
839 shape: &DrawShape,
840 viewport: ViewportUniformParams,
841 root_scale: f32,
842) -> bool {
843 let snap_delta = shape
844 .snap_anchor
845 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
846 .unwrap_or_default();
847 let rect = quad_bounds(translate_quad(shape.quad, snap_delta));
848 let clip = shape.clip;
849 draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale)
850}
851
852fn cached_text_glyph_quad(
853 glyph: &SoftwareGlyphAtlasPlacement,
854 entry: GlyphAtlasEntry,
855 atlas_size: u32,
856) -> CachedTextGlyphQuad {
857 CachedTextGlyphQuad {
858 x: glyph.x,
859 y: glyph.y,
860 width: glyph.width,
861 height: glyph.height,
862 color: (
863 glyph.color.0.clamp(0.0, 1.0),
864 glyph.color.1.clamp(0.0, 1.0),
865 glyph.color.2.clamp(0.0, 1.0),
866 glyph.color.3.clamp(0.0, 1.0),
867 ),
868 uv: glyph_atlas_uv_rect(entry, atlas_size),
869 }
870}
871
872fn append_cached_text_glyph_quad(
873 source_raster_rect: Rect,
874 quad: &CachedTextGlyphQuad,
875 image_vertices: &mut Vec<Vertex>,
876 image_indices: &mut Vec<u32>,
877) -> bool {
878 if quad.width == 0 || quad.height == 0 || quad.color.3 <= 0.0 {
879 return false;
880 }
881
882 let base_vertex = image_vertices.len() as u32;
883 image_indices.extend_from_slice(&[
884 base_vertex,
885 base_vertex + 1,
886 base_vertex + 2,
887 base_vertex + 2,
888 base_vertex + 1,
889 base_vertex + 3,
890 ]);
891
892 let x0 = source_raster_rect.x + quad.x as f32;
893 let y0 = source_raster_rect.y + quad.y as f32;
894 let x1 = x0 + quad.width as f32;
895 let y1 = y0 + quad.height as f32;
896 let color = [quad.color.0, quad.color.1, quad.color.2, quad.color.3];
897
898 image_vertices.extend_from_slice(&[
899 Vertex {
900 position: [x0, y0],
901 color,
902 uv: [quad.uv.min[0], quad.uv.min[1]],
903 uv_bounds: quad.uv.sample_bounds,
904 },
905 Vertex {
906 position: [x1, y0],
907 color,
908 uv: [quad.uv.max[0], quad.uv.min[1]],
909 uv_bounds: quad.uv.sample_bounds,
910 },
911 Vertex {
912 position: [x0, y1],
913 color,
914 uv: [quad.uv.min[0], quad.uv.max[1]],
915 uv_bounds: quad.uv.sample_bounds,
916 },
917 Vertex {
918 position: [x1, y1],
919 color,
920 uv: [quad.uv.max[0], quad.uv.max[1]],
921 uv_bounds: quad.uv.sample_bounds,
922 },
923 ]);
924 true
925}
926
927fn cached_text_glyph_quad_logical_rect(
928 source_raster_rect: Rect,
929 quad: &CachedTextGlyphQuad,
930 root_scale: f32,
931) -> Option<Rect> {
932 if !root_scale.is_finite() || root_scale <= 0.0 {
933 return None;
934 }
935 Some(Rect {
936 x: (source_raster_rect.x + quad.x as f32) / root_scale,
937 y: (source_raster_rect.y + quad.y as f32) / root_scale,
938 width: quad.width as f32 / root_scale,
939 height: quad.height as f32 / root_scale,
940 })
941}
942
943fn cached_text_glyph_quad_is_visible_in_viewport(
944 source_raster_rect: Rect,
945 quad: &CachedTextGlyphQuad,
946 clip: Option<Rect>,
947 viewport: ViewportUniformParams,
948 root_scale: f32,
949) -> bool {
950 cached_text_glyph_quad_logical_rect(source_raster_rect, quad, root_scale)
951 .is_some_and(|rect| draw_rect_is_visible_in_viewport(rect, clip, viewport, root_scale))
952}
953
954#[derive(Clone, Copy, Debug, Eq, PartialEq)]
955enum TextGlyphDrawAction {
956 DrawVisible,
957 PrewarmOffscreen,
958 Skip,
959}
960
961fn text_glyph_draw_action(
962 is_visible: bool,
963 is_prewarm_candidate: bool,
964 allow_offscreen_prewarm: bool,
965) -> TextGlyphDrawAction {
966 if is_visible {
967 TextGlyphDrawAction::DrawVisible
968 } else if allow_offscreen_prewarm && is_prewarm_candidate {
969 TextGlyphDrawAction::PrewarmOffscreen
970 } else {
971 TextGlyphDrawAction::Skip
972 }
973}
974
975#[cfg(not(target_arch = "wasm32"))]
976fn should_use_retained_text_glyph_run(quads_len: usize, clip: Option<Rect>) -> bool {
977 clip.is_none() && quads_len >= MIN_RETAINED_TEXT_GLYPH_QUADS
978}
979
980#[cfg(not(target_arch = "wasm32"))]
981fn offscreen_text_glyph_prewarm_work_is_bounded(
982 cached_glyphs: Option<usize>,
983 text_len: usize,
984) -> bool {
985 match cached_glyphs {
986 Some(glyphs) => glyphs <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS,
987 None => text_len <= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
988 }
989}
990
991#[cfg(not(target_arch = "wasm32"))]
992fn offscreen_text_glyph_prewarm_budget_exhausted(
993 start: Instant,
994 admitted_candidates: usize,
995) -> bool {
996 admitted_candidates >= MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES
997 || instant_ms(start, Instant::now()) >= OFFSCREEN_TEXT_GLYPH_PREWARM_BUDGET_MS
998}
999
1000fn text_draws_for_ordered_range<'a>(
1001 ordered_items: &'a [(usize, SegmentDrawItem)],
1002 texts: &'a [TextDraw],
1003 start: usize,
1004 end: usize,
1005) -> Result<impl Iterator<Item = &'a TextDraw>, String> {
1006 let range_items = ordered_items
1007 .get(start..end)
1008 .ok_or_else(|| format!("text batch range {start}..{end} is outside ordered draw items"))?;
1009 for (_, item) in range_items {
1010 match item {
1011 SegmentDrawItem::Text(text_index) if *text_index < texts.len() => {}
1012 SegmentDrawItem::Text(text_index) => {
1013 return Err(format!(
1014 "text batch references missing text draw index: {text_index}"
1015 ));
1016 }
1017 _ => return Err(format!("text batch contains non-text draw item: {item:?}")),
1018 }
1019 }
1020
1021 Ok(range_items.iter().filter_map(move |(_, item)| match item {
1022 SegmentDrawItem::Text(text_index) => texts.get(*text_index),
1023 _ => None,
1024 }))
1025}
1026
1027const SHADOW_CACHE_DEVICE_QUANT: f32 = 16.0;
1032
1033fn hash_shadow_device_offset<H: Hasher>(value: f32, origin: f32, root_scale: f32, state: &mut H) {
1034 let quantized = ((value - origin) * root_scale * SHADOW_CACHE_DEVICE_QUANT).round();
1035 (quantized as i64).hash(state);
1036}
1037
1038fn hash_shadow_device_rect<H: Hasher>(
1039 rect: Rect,
1040 origin_x: f32,
1041 origin_y: f32,
1042 root_scale: f32,
1043 state: &mut H,
1044) {
1045 hash_shadow_device_offset(rect.x, origin_x, root_scale, state);
1046 hash_shadow_device_offset(rect.y, origin_y, root_scale, state);
1047 hash_shadow_device_offset(rect.width, 0.0, root_scale, state);
1048 hash_shadow_device_offset(rect.height, 0.0, root_scale, state);
1049}
1050
1051fn hash_shape_shadow_item<H: Hasher>(
1052 shape: &DrawShape,
1053 blend_mode: BlendMode,
1054 origin_x: f32,
1055 origin_y: f32,
1056 root_scale: f32,
1057 state: &mut H,
1058) {
1059 hash_shadow_device_rect(shape.rect, origin_x, origin_y, root_scale, state);
1060 hash_shadow_device_rect(shape.local_rect, origin_x, origin_y, root_scale, state);
1061 for point in shape.quad {
1062 hash_shadow_device_offset(point[0], origin_x, root_scale, state);
1063 hash_shadow_device_offset(point[1], origin_y, root_scale, state);
1064 }
1065 match shape.snap_anchor {
1066 Some(anchor) => {
1067 1u8.hash(state);
1068 hash_shadow_device_offset(anchor.origin.x, origin_x, root_scale, state);
1069 hash_shadow_device_offset(anchor.origin.y, origin_y, root_scale, state);
1070 hash_f32_for_cache(anchor.device_pixel_step, state);
1071 }
1072 None => 0u8.hash(state),
1073 }
1074 shape.brush.render_hash().hash(state);
1075 match shape.shape {
1076 Some(corner_shape) => {
1077 1u8.hash(state);
1078 corner_shape.radii().render_hash().hash(state);
1079 }
1080 None => 0u8.hash(state),
1081 }
1082 match shape.clip {
1083 Some(clip) => {
1084 1u8.hash(state);
1085 hash_shadow_device_rect(clip, origin_x, origin_y, root_scale, state);
1086 }
1087 None => 0u8.hash(state),
1088 }
1089 blend_mode.hash(state);
1090 shape.blend_mode.hash(state);
1091}
1092
1093fn shape_shadow_content_hash(shapes: &[(DrawShape, BlendMode)], root_scale: f32) -> u64 {
1094 let mut hasher = FxHasher::default();
1095 let origin = shape_shadow_bounds(shapes).unwrap_or(Rect {
1100 x: 0.0,
1101 y: 0.0,
1102 width: 0.0,
1103 height: 0.0,
1104 });
1105
1106 shapes.len().hash(&mut hasher);
1107 for (shape, blend_mode) in shapes {
1108 hash_shape_shadow_item(
1109 shape,
1110 *blend_mode,
1111 origin.x,
1112 origin.y,
1113 root_scale,
1114 &mut hasher,
1115 );
1116 }
1117 hasher.finish()
1118}
1119
1120fn shape_shadow_surface_cache_key(
1121 shapes: &[(DrawShape, BlendMode)],
1122 device_bounds: DevicePixelBounds,
1123 pixel_radius: f32,
1124 root_scale: f32,
1125) -> Option<ShadowSurfaceCacheKey> {
1126 (root_scale.is_finite() && root_scale > 0.0).then(|| ShadowSurfaceCacheKey {
1127 content_hash: shape_shadow_content_hash(shapes, root_scale),
1128 pixel_size: [device_bounds.width, device_bounds.height],
1129 root_scale_bits: root_scale.to_bits(),
1130 blur_radius_bits: pixel_radius.to_bits(),
1131 })
1132}
1133
1134fn shape_shadow_bounds(shapes: &[(DrawShape, BlendMode)]) -> Option<Rect> {
1135 shapes
1136 .iter()
1137 .map(|(shape, _)| shape.rect)
1138 .reduce(|a, b| Rect {
1139 x: a.x.min(b.x),
1140 y: a.y.min(b.y),
1141 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1142 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1143 })
1144}
1145
1146fn shared_shape_shadow_snap_anchor(shapes: &[(DrawShape, BlendMode)]) -> Option<SnapAnchor> {
1147 let anchor = shapes.first()?.0.snap_anchor?;
1148 shapes
1149 .iter()
1150 .all(|(shape, _)| shape.snap_anchor == Some(anchor))
1151 .then_some(anchor)
1152}
1153
1154fn shadow_draw_bounds(shadow: &ShadowDraw) -> Option<Rect> {
1155 shadow
1156 .shapes
1157 .iter()
1158 .map(|(shape, _)| shape.rect)
1159 .chain(shadow.texts.iter().map(|text| text.rect))
1160 .reduce(|a, b| Rect {
1161 x: a.x.min(b.x),
1162 y: a.y.min(b.y),
1163 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
1164 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
1165 })
1166}
1167
1168fn shadow_draw_may_render(
1169 shadow: &ShadowDraw,
1170 width: u32,
1171 height: u32,
1172 root_scale: f32,
1173 max_texture_dim: u32,
1174) -> bool {
1175 if shadow.texts.is_empty() && !shadow.shapes.is_empty() && shadow.blur_radius > 0.0 {
1176 return shape_shadow_surface_plan(
1177 &shadow.shapes,
1178 shadow.clip,
1179 shadow.blur_radius,
1180 width,
1181 height,
1182 root_scale,
1183 max_texture_dim,
1184 )
1185 .is_some();
1186 }
1187
1188 let Some(bounds) = shadow_draw_bounds(shadow) else {
1189 return false;
1190 };
1191 let blur_margin = blur_extent_margin(shadow.blur_radius);
1192 let mut visible_bounds = Rect {
1193 x: bounds.x - blur_margin,
1194 y: bounds.y - blur_margin,
1195 width: bounds.width + blur_margin * 2.0,
1196 height: bounds.height + blur_margin * 2.0,
1197 };
1198 if let Some(clip) = shadow.clip {
1199 let clip_expanded = Rect {
1200 x: clip.x - blur_margin,
1201 y: clip.y - blur_margin,
1202 width: clip.width + blur_margin * 2.0,
1203 height: clip.height + blur_margin * 2.0,
1204 };
1205 let Some(intersection) = visible_bounds.intersect(clip_expanded) else {
1206 return false;
1207 };
1208 visible_bounds = intersection;
1209 }
1210
1211 scissor_rect_for_rect(visible_bounds, root_scale, width, height).is_some()
1212}
1213
1214fn shape_shadow_surface_plan(
1215 shapes: &[(DrawShape, BlendMode)],
1216 clip: Option<Rect>,
1217 blur_radius: f32,
1218 width: u32,
1219 height: u32,
1220 root_scale: f32,
1221 max_texture_dim: u32,
1222) -> Option<ShapeShadowSurfacePlan> {
1223 let shape_bounds = shape_shadow_bounds(shapes)?;
1224 let blur_margin = blur_extent_margin(blur_radius);
1225 let source_blur_bounds = Rect {
1226 x: shape_bounds.x - blur_margin,
1227 y: shape_bounds.y - blur_margin,
1228 width: shape_bounds.width + blur_margin * 2.0,
1229 height: shape_bounds.height + blur_margin * 2.0,
1230 };
1231
1232 let mut visible_blur_bounds = source_blur_bounds;
1233 if let Some(clip) = clip {
1234 let clip_expanded = Rect {
1235 x: clip.x - blur_margin,
1236 y: clip.y - blur_margin,
1237 width: clip.width + blur_margin * 2.0,
1238 height: clip.height + blur_margin * 2.0,
1239 };
1240 visible_blur_bounds = visible_blur_bounds.intersect(clip_expanded)?;
1241 }
1242
1243 let processing_scissor = scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
1244 processing_scissor?;
1245 let visible_device_bounds =
1246 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)?;
1247 let source_device_bounds = translation_stable_anchored_device_pixel_bounds(
1248 source_blur_bounds,
1249 shared_shape_shadow_snap_anchor(shapes),
1250 root_scale,
1251 max_texture_dim,
1252 )
1253 .unwrap_or(visible_device_bounds);
1254
1255 Some(ShapeShadowSurfacePlan {
1256 source_device_bounds,
1257 processing_scissor,
1258 pixel_radius: blur_radius * root_scale,
1259 })
1260}
1261
1262fn is_render_effect_supported(effect: &RenderEffect) -> bool {
1263 match effect {
1264 RenderEffect::Blur { .. } => true,
1265 RenderEffect::Offset { .. } => true,
1266 RenderEffect::Shader { .. } => true,
1267 RenderEffect::Chain { first, second } => {
1268 is_render_effect_supported(first) && is_render_effect_supported(second)
1269 }
1270 }
1271}
1272
1273fn resolve_gradient_point(origin: f32, extent: f32, value: f32) -> f32 {
1274 if value.is_finite() {
1275 origin + value
1276 } else if value.is_sign_positive() {
1277 origin + extent
1278 } else {
1279 origin
1280 }
1281}
1282
1283fn gradient_tile_mode_value(tile_mode: TileMode) -> u32 {
1284 match tile_mode {
1285 TileMode::Clamp => 0,
1286 TileMode::Repeated => 1,
1287 TileMode::Mirror => 2,
1288 TileMode::Decal => 3,
1289 }
1290}
1291
1292#[cfg(not(target_arch = "wasm32"))]
1293fn shape_shader_source(batch_limits: ShapeBatchLimits) -> Cow<'static, str> {
1294 if batch_limits.storage {
1298 return Cow::Owned(
1299 shaders::SHADER
1300 .replace(
1301 "var<uniform> shape_data: array<ShapeData, 102>;",
1302 "var<storage, read> shape_data: array<ShapeData>;",
1303 )
1304 .replace(
1305 "var<uniform> gradient_stops: array<GradientStop, 256>;",
1306 "var<storage, read> gradient_stops: array<GradientStop>;\n\n\
1313 @group(1) @binding(3)\n\
1314 var<storage, read> paint: array<vec4<f32>>;",
1315 )
1316 .replace(
1317 "output.color = shape.color;",
1318 "output.color = \
1319 select(shape.color, paint[shape_idx], similarity.paint_select > 0.5);",
1320 ),
1321 );
1322 }
1323 Cow::Owned(
1324 shaders::SHADER
1325 .replace(
1326 "array<ShapeData, 102>",
1327 &format!("array<ShapeData, {}>", batch_limits.max_shapes_per_batch),
1328 )
1329 .replace(
1330 "array<GradientStop, 256>",
1331 &format!("array<GradientStop, {}>", batch_limits.max_gradient_stops),
1332 ),
1333 )
1334}
1335
1336#[cfg(target_arch = "wasm32")]
1337fn shape_shader_source(_batch_limits: ShapeBatchLimits) -> Cow<'static, str> {
1338 Cow::Borrowed(shaders::SHADER)
1339}
1340
1341fn create_shape_pipeline(
1342 device: &wgpu::Device,
1343 surface_format: wgpu::TextureFormat,
1344 uniform_layout: &wgpu::BindGroupLayout,
1345 shape_layout: &wgpu::BindGroupLayout,
1346 blend_mode: BlendMode,
1347 batch_limits: ShapeBatchLimits,
1348) -> wgpu::RenderPipeline {
1349 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1350 label: Some("Shape Shader"),
1351 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1352 });
1353
1354 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1355 label: Some("Render Pipeline Layout"),
1356 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1357 immediate_size: 0,
1358 });
1359
1360 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1361 label: Some("Render Pipeline"),
1362 layout: Some(&pipeline_layout),
1363 vertex: wgpu::VertexState {
1364 module: &shader,
1365 entry_point: Some("vs_main"),
1366 compilation_options: wgpu::PipelineCompilationOptions::default(),
1367 buffers: &[],
1370 },
1371 fragment: Some(wgpu::FragmentState {
1372 module: &shader,
1373 entry_point: Some("fs_main"),
1374 compilation_options: wgpu::PipelineCompilationOptions::default(),
1375 targets: &[Some(wgpu::ColorTargetState {
1376 format: surface_format,
1377 blend: Some(blend_state_for_mode(blend_mode)),
1378 write_mask: wgpu::ColorWrites::ALL,
1379 })],
1380 }),
1381 primitive: wgpu::PrimitiveState {
1382 topology: wgpu::PrimitiveTopology::TriangleList,
1383 strip_index_format: None,
1384 front_face: wgpu::FrontFace::Ccw,
1385 cull_mode: None,
1386 unclipped_depth: false,
1387 polygon_mode: wgpu::PolygonMode::Fill,
1388 conservative: false,
1389 },
1390 depth_stencil: None,
1391 multisample: wgpu::MultisampleState::default(),
1392 multiview_mask: None,
1393 cache: None,
1394 })
1395}
1396
1397#[cfg(not(target_arch = "wasm32"))]
1404fn create_mesh_shape_pipeline(
1405 device: &wgpu::Device,
1406 surface_format: wgpu::TextureFormat,
1407 uniform_layout: &wgpu::BindGroupLayout,
1408 shape_layout: &wgpu::BindGroupLayout,
1409 batch_limits: ShapeBatchLimits,
1410) -> wgpu::RenderPipeline {
1411 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1412 label: Some("Shape Mesh Shader"),
1413 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1414 });
1415
1416 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1417 label: Some("Mesh Render Pipeline Layout"),
1418 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1419 immediate_size: 0,
1420 });
1421
1422 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1423 label: Some("Retained Mesh Pipeline"),
1424 layout: Some(&pipeline_layout),
1425 vertex: wgpu::VertexState {
1426 module: &shader,
1427 entry_point: Some("vs_mesh"),
1428 compilation_options: wgpu::PipelineCompilationOptions::default(),
1429 buffers: &[MeshVertex::desc()],
1430 },
1431 fragment: Some(wgpu::FragmentState {
1432 module: &shader,
1433 entry_point: Some("fs_main"),
1434 compilation_options: wgpu::PipelineCompilationOptions::default(),
1435 targets: &[Some(wgpu::ColorTargetState {
1436 format: surface_format,
1437 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1438 write_mask: wgpu::ColorWrites::ALL,
1439 })],
1440 }),
1441 primitive: wgpu::PrimitiveState {
1442 topology: wgpu::PrimitiveTopology::TriangleList,
1443 strip_index_format: None,
1444 front_face: wgpu::FrontFace::Ccw,
1445 cull_mode: None,
1446 unclipped_depth: false,
1447 polygon_mode: wgpu::PolygonMode::Fill,
1448 conservative: false,
1449 },
1450 depth_stencil: None,
1451 multisample: wgpu::MultisampleState::default(),
1452 multiview_mask: None,
1453 cache: None,
1454 })
1455}
1456
1457#[cfg(not(target_arch = "wasm32"))]
1465fn create_instanced_shape_pipeline(
1466 device: &wgpu::Device,
1467 surface_format: wgpu::TextureFormat,
1468 uniform_layout: &wgpu::BindGroupLayout,
1469 shape_layout: &wgpu::BindGroupLayout,
1470 blend_mode: BlendMode,
1471 batch_limits: ShapeBatchLimits,
1472) -> wgpu::RenderPipeline {
1473 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1474 label: Some("Shape Instanced Shader"),
1475 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1476 });
1477
1478 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1479 label: Some("Instanced Render Pipeline Layout"),
1480 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1481 immediate_size: 0,
1482 });
1483
1484 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1485 label: Some("Instanced Render Pipeline"),
1486 layout: Some(&pipeline_layout),
1487 vertex: wgpu::VertexState {
1488 module: &shader,
1489 entry_point: Some("vs_shape_instanced"),
1490 compilation_options: wgpu::PipelineCompilationOptions::default(),
1491 buffers: &[],
1495 },
1496 fragment: Some(wgpu::FragmentState {
1497 module: &shader,
1498 entry_point: Some("fs_main"),
1499 compilation_options: wgpu::PipelineCompilationOptions::default(),
1500 targets: &[Some(wgpu::ColorTargetState {
1501 format: surface_format,
1502 blend: Some(blend_state_for_mode(blend_mode)),
1503 write_mask: wgpu::ColorWrites::ALL,
1504 })],
1505 }),
1506 primitive: wgpu::PrimitiveState {
1507 topology: wgpu::PrimitiveTopology::TriangleList,
1508 strip_index_format: None,
1509 front_face: wgpu::FrontFace::Ccw,
1510 cull_mode: None,
1511 unclipped_depth: false,
1512 polygon_mode: wgpu::PolygonMode::Fill,
1513 conservative: false,
1514 },
1515 depth_stencil: None,
1516 multisample: wgpu::MultisampleState::default(),
1517 multiview_mask: None,
1518 cache: None,
1519 })
1520}
1521
1522fn create_image_pipeline(
1523 device: &wgpu::Device,
1524 surface_format: wgpu::TextureFormat,
1525 uniform_layout: &wgpu::BindGroupLayout,
1526 image_layout: &wgpu::BindGroupLayout,
1527 blend_mode: BlendMode,
1528) -> wgpu::RenderPipeline {
1529 let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1530 label: Some("Image Shader"),
1531 source: wgpu::ShaderSource::Wgsl(shaders::IMAGE_SHADER.into()),
1532 });
1533
1534 let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1535 label: Some("Image Pipeline Layout"),
1536 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1537 immediate_size: 0,
1538 });
1539
1540 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1541 label: Some("Image Pipeline"),
1542 layout: Some(&image_pipeline_layout),
1543 vertex: wgpu::VertexState {
1544 module: &image_shader,
1545 entry_point: Some("image_vs_main"),
1546 compilation_options: wgpu::PipelineCompilationOptions::default(),
1547 buffers: &[Vertex::desc()],
1548 },
1549 fragment: Some(wgpu::FragmentState {
1550 module: &image_shader,
1551 entry_point: Some("image_fs_main"),
1552 compilation_options: wgpu::PipelineCompilationOptions::default(),
1553 targets: &[Some(wgpu::ColorTargetState {
1554 format: surface_format,
1555 blend: Some(blend_state_for_mode(blend_mode)),
1556 write_mask: wgpu::ColorWrites::ALL,
1557 })],
1558 }),
1559 primitive: wgpu::PrimitiveState {
1560 topology: wgpu::PrimitiveTopology::TriangleList,
1561 strip_index_format: None,
1562 front_face: wgpu::FrontFace::Ccw,
1563 cull_mode: None,
1564 unclipped_depth: false,
1565 polygon_mode: wgpu::PolygonMode::Fill,
1566 conservative: false,
1567 },
1568 depth_stencil: None,
1569 multisample: wgpu::MultisampleState::default(),
1570 multiview_mask: None,
1571 cache: None,
1572 })
1573}
1574
1575fn create_glyph_atlas_pipeline(
1576 device: &wgpu::Device,
1577 surface_format: wgpu::TextureFormat,
1578 uniform_layout: &wgpu::BindGroupLayout,
1579 image_layout: &wgpu::BindGroupLayout,
1580) -> wgpu::RenderPipeline {
1581 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1582 label: Some("Glyph Atlas Shader"),
1583 source: wgpu::ShaderSource::Wgsl(shaders::GLYPH_ATLAS_SHADER.into()),
1584 });
1585
1586 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1587 label: Some("Glyph Atlas Pipeline Layout"),
1588 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1589 immediate_size: 0,
1590 });
1591
1592 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1593 label: Some("Glyph Atlas Pipeline"),
1594 layout: Some(&pipeline_layout),
1595 vertex: wgpu::VertexState {
1596 module: &shader,
1597 entry_point: Some("glyph_atlas_vs_main"),
1598 compilation_options: wgpu::PipelineCompilationOptions::default(),
1599 buffers: &[Vertex::desc()],
1600 },
1601 fragment: Some(wgpu::FragmentState {
1602 module: &shader,
1603 entry_point: Some("glyph_atlas_fs_main"),
1604 compilation_options: wgpu::PipelineCompilationOptions::default(),
1605 targets: &[Some(wgpu::ColorTargetState {
1606 format: surface_format,
1607 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1608 write_mask: wgpu::ColorWrites::ALL,
1609 })],
1610 }),
1611 primitive: wgpu::PrimitiveState {
1612 topology: wgpu::PrimitiveTopology::TriangleList,
1613 strip_index_format: None,
1614 front_face: wgpu::FrontFace::Ccw,
1615 cull_mode: None,
1616 unclipped_depth: false,
1617 polygon_mode: wgpu::PolygonMode::Fill,
1618 conservative: false,
1619 },
1620 depth_stencil: None,
1621 multisample: wgpu::MultisampleState::default(),
1622 multiview_mask: None,
1623 cache: None,
1624 })
1625}
1626
1627#[repr(C)]
1628#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1629struct Vertex {
1630 position: [f32; 2],
1631 color: [f32; 4],
1632 uv: [f32; 2],
1633 uv_bounds: [f32; 4],
1634}
1635
1636impl Vertex {
1637 const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
1638 0 => Float32x2,
1639 1 => Float32x4,
1640 2 => Float32x2,
1641 3 => Float32x4
1642 ];
1643
1644 fn desc() -> wgpu::VertexBufferLayout<'static> {
1645 wgpu::VertexBufferLayout {
1646 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
1647 step_mode: wgpu::VertexStepMode::Vertex,
1648 attributes: &Self::ATTRIBS,
1649 }
1650 }
1651}
1652
1653#[repr(C)]
1654#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1655struct Uniforms {
1656 viewport: [f32; 2],
1657 viewport_offset: [f32; 2],
1658}
1659
1660#[repr(C)]
1666#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1667struct ShapeData {
1668 rect: [f32; 4], radii: [f32; 4],
1673 gradient_params: [f32; 4], clip_rect: [f32; 4], stroke_params: [f32; 4],
1678 arc_params: [f32; 4],
1680 quad01: [f32; 4],
1682 quad23: [f32; 4],
1684 color: [f32; 4],
1686 brush_type: u32, gradient_start: u32, gradient_count: u32, gradient_tile_mode: u32, }
1691
1692const SHAPE_KIND_FILL: u32 = 0;
1694const SHAPE_KIND_STROKE: u32 = 1;
1695const SHAPE_KIND_ARC: u32 = 2;
1696
1697fn stroke_cap_code(cap: StrokeCap) -> u32 {
1698 match cap {
1699 StrokeCap::Butt => 0,
1700 StrokeCap::Round => 1,
1701 StrokeCap::Square => 2,
1702 }
1703}
1704
1705fn stroke_join_code(join: StrokeJoin) -> u32 {
1706 match join {
1707 StrokeJoin::Miter => 0,
1708 StrokeJoin::Round => 1,
1709 StrokeJoin::Bevel => 2,
1710 }
1711}
1712
1713fn pack_shape_flags(kind: u32, cap: StrokeCap, join: StrokeJoin) -> f32 {
1719 ((kind & 3) | (stroke_cap_code(cap) << 2) | (stroke_join_code(join) << 4)) as f32
1720}
1721
1722#[cfg(not(target_arch = "wasm32"))]
1730static SHAPE_CONVERT_TUNER: crate::cost_tuner::CostTuner =
1731 crate::cost_tuner::CostTuner::new("shape-convert", 256, 400_000);
1732
1733#[cfg(not(target_arch = "wasm32"))]
1734pub(crate) fn shape_convert_worker_count() -> usize {
1735 static WORKERS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
1736 *WORKERS.get_or_init(|| {
1737 let cpus = std::thread::available_parallelism()
1738 .map(|count| count.get())
1739 .unwrap_or(1);
1740 let workers = cpus.clamp(1, 4);
1741 log::info!("[shape-convert] fan-out width {workers} (available parallelism {cpus})");
1745 workers
1746 })
1747}
1748
1749#[cfg(target_arch = "wasm32")]
1750pub(crate) fn shape_convert_worker_count() -> usize {
1751 1
1752}
1753
1754fn shape_gradient_stop_count(shape: &DrawShape) -> usize {
1755 match &shape.brush {
1756 Brush::Solid(_) => 0,
1757 Brush::LinearGradient { colors, .. }
1758 | Brush::RadialGradient { colors, .. }
1759 | Brush::SweepGradient { colors, .. } => colors.len(),
1760 }
1761}
1762
1763fn convert_shape_into_slots(
1768 shape: &DrawShape,
1769 root_scale: f32,
1770 gradient_start: u32,
1771 shape_out: &mut ShapeData,
1772 gradient_out: &mut [GradientStop],
1773) {
1774 let snap_delta = shape
1775 .snap_anchor
1776 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
1777 .unwrap_or_default();
1778 let local_rect = shape.local_rect.translate(snap_delta.x, snap_delta.y);
1779 let quad = translate_quad(shape.quad, snap_delta);
1780 let clip = shape.clip;
1783 let canonicalize = shape.snap_anchor.is_some();
1784 let device_local_rect = if canonicalize {
1785 canonicalized_scaled_rect(local_rect, root_scale)
1786 } else {
1787 Rect {
1788 x: local_rect.x * root_scale,
1789 y: local_rect.y * root_scale,
1790 width: local_rect.width * root_scale,
1791 height: local_rect.height * root_scale,
1792 }
1793 };
1794 let device_quad = if canonicalize {
1795 canonicalized_scaled_quad(quad, root_scale)
1796 } else {
1797 scaled_quad(quad, root_scale)
1798 };
1799 let canonicalize_brush_coordinate = |value| {
1800 if canonicalize {
1801 canonicalize_device_coordinate(value)
1802 } else {
1803 value
1804 }
1805 };
1806
1807 let clip_rect = if let Some(clip) = clip {
1809 let device_clip = if canonicalize {
1810 canonicalized_scaled_rect(clip, root_scale)
1811 } else {
1812 Rect {
1813 x: clip.x * root_scale,
1814 y: clip.y * root_scale,
1815 width: clip.width * root_scale,
1816 height: clip.height * root_scale,
1817 }
1818 };
1819 [
1820 device_clip.x,
1821 device_clip.y,
1822 device_clip.width,
1823 device_clip.height,
1824 ]
1825 } else {
1826 [0.0, 0.0, 0.0, 0.0]
1827 };
1828
1829 let mut fill_gradient_entries = |colors: &[Color], stops: Option<&[f32]>| {
1831 let count = colors.len();
1832 let explicit_stops = stops.filter(|values| values.len() == count);
1833 for (index, color) in colors.iter().enumerate() {
1834 let position = explicit_stops
1835 .map(|values| values[index])
1836 .unwrap_or_else(|| {
1837 if count <= 1 {
1838 0.0
1839 } else {
1840 index as f32 / (count - 1) as f32
1841 }
1842 });
1843 gradient_out[index] = GradientStop {
1844 color: [color.r(), color.g(), color.b(), color.a()],
1845 position: [position, 0.0, 0.0, 0.0],
1846 };
1847 }
1848 count as u32
1849 };
1850 let mut gradient_params = [0.0f32; 4];
1851 let (brush_type, gradient_count, gradient_tile_mode) = match &shape.brush {
1852 Brush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
1853 Brush::LinearGradient {
1854 colors,
1855 stops,
1856 start,
1857 end,
1858 tile_mode,
1859 } => {
1860 let count = fill_gradient_entries(colors, stops.as_deref());
1861 gradient_params = [
1862 canonicalize_brush_coordinate(resolve_gradient_point(
1863 device_local_rect.x,
1864 device_local_rect.width,
1865 start.x * root_scale,
1866 )),
1867 canonicalize_brush_coordinate(resolve_gradient_point(
1868 device_local_rect.y,
1869 device_local_rect.height,
1870 start.y * root_scale,
1871 )),
1872 canonicalize_brush_coordinate(resolve_gradient_point(
1873 device_local_rect.x,
1874 device_local_rect.width,
1875 end.x * root_scale,
1876 )),
1877 canonicalize_brush_coordinate(resolve_gradient_point(
1878 device_local_rect.y,
1879 device_local_rect.height,
1880 end.y * root_scale,
1881 )),
1882 ];
1883 (1u32, count, gradient_tile_mode_value(*tile_mode))
1884 }
1885 Brush::RadialGradient {
1886 colors,
1887 stops,
1888 center,
1889 radius,
1890 tile_mode,
1891 } => {
1892 let count = fill_gradient_entries(colors, stops.as_deref());
1893 gradient_params = [
1894 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
1895 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
1896 (radius * root_scale).max(f32::EPSILON),
1897 0.0,
1898 ];
1899 (2u32, count, gradient_tile_mode_value(*tile_mode))
1900 }
1901 Brush::SweepGradient {
1902 colors,
1903 stops,
1904 center,
1905 } => {
1906 let count = fill_gradient_entries(colors, stops.as_deref());
1907 gradient_params = [
1908 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
1909 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
1910 0.0,
1911 0.0,
1912 ];
1913 (3u32, count, gradient_tile_mode_value(TileMode::Clamp))
1914 }
1915 };
1916
1917 let stroke_outset = shape
1922 .stroke
1923 .map(|stroke| stroke.half_width())
1924 .unwrap_or(0.0);
1925 let geometry_width = (local_rect.width - stroke_outset * 2.0).max(0.0);
1926 let geometry_height = (local_rect.height - stroke_outset * 2.0).max(0.0);
1927
1928 let radii = if let Some(arc) = shape.arc {
1929 if arc.sweep_angle >= cranpose_ui_graphics::TAU && arc.start_angle == 0.0 {
1939 [0.0, -1.0, 0.0, -1.0]
1940 } else {
1941 let half_sweep = arc.sweep_angle.clamp(0.0, cranpose_ui_graphics::TAU) * 0.5;
1942 let (mid_sin, mid_cos) = (arc.start_angle + half_sweep).sin_cos();
1943 let (half_sin, half_cos) = half_sweep.sin_cos();
1944 [mid_sin, mid_cos, half_sin.max(0.0), half_cos]
1945 }
1946 } else if let Some(rounded) = shape.shape {
1947 let resolved = rounded.resolve(geometry_width, geometry_height);
1948 [
1949 resolved.top_left * root_scale,
1950 resolved.top_right * root_scale,
1951 resolved.bottom_left * root_scale,
1952 resolved.bottom_right * root_scale,
1953 ]
1954 } else {
1955 [0.0, 0.0, 0.0, 0.0]
1956 };
1957
1958 let device_rect = [
1959 device_local_rect.x,
1960 device_local_rect.y,
1961 device_local_rect.width,
1962 device_local_rect.height,
1963 ];
1964
1965 let (stroke_params, arc_params) = match (shape.arc, shape.stroke) {
1969 (Some(arc), _) => (
1970 [
1971 0.0,
1972 pack_shape_flags(SHAPE_KIND_ARC, arc.cap, StrokeJoin::Miter),
1973 arc.outer_radius * root_scale,
1974 arc.inner_radius * root_scale,
1975 ],
1976 [
1977 (arc.center.x + snap_delta.x) * root_scale,
1978 (arc.center.y + snap_delta.y) * root_scale,
1979 arc.start_angle,
1980 arc.sweep_angle,
1981 ],
1982 ),
1983 (None, Some(stroke)) => (
1984 [
1985 stroke.width.max(0.0) * root_scale,
1986 pack_shape_flags(SHAPE_KIND_STROKE, stroke.cap, stroke.join),
1987 0.0,
1988 0.0,
1989 ],
1990 [0.0; 4],
1991 ),
1992 (None, None) => (
1993 [
1994 0.0,
1995 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
1996 0.0,
1997 0.0,
1998 ],
1999 [0.0; 4],
2000 ),
2001 };
2002
2003 let color = match &shape.brush {
2004 Brush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2005 Brush::LinearGradient { colors, .. } => {
2006 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2007 [first.r(), first.g(), first.b(), first.a()]
2008 }
2009 Brush::RadialGradient { colors, .. } | Brush::SweepGradient { colors, .. } => {
2010 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2011 [first.r(), first.g(), first.b(), first.a()]
2012 }
2013 };
2014
2015 *shape_out = ShapeData {
2016 rect: device_rect,
2017 radii,
2018 gradient_params,
2019 clip_rect,
2020 stroke_params,
2021 arc_params,
2022 quad01: [
2023 device_quad[0][0],
2024 device_quad[0][1],
2025 device_quad[1][0],
2026 device_quad[1][1],
2027 ],
2028 quad23: [
2029 device_quad[2][0],
2030 device_quad[2][1],
2031 device_quad[3][0],
2032 device_quad[3][1],
2033 ],
2034 color,
2035 brush_type,
2036 gradient_start,
2037 gradient_count,
2038 gradient_tile_mode,
2039 };
2040}
2041
2042fn quad_area_diag_enabled() -> bool {
2049 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2050 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_QUAD_AREA_DIAG").is_some())
2051}
2052
2053fn convert_shapes_into_outputs(
2059 shape_refs: &[&DrawShape],
2060 gradient_offsets: &[u32],
2061 root_scale: f32,
2062 shape_data_out: &mut [ShapeData],
2063 gradients_out: &mut [GradientStop],
2064) {
2065 let shape_count = shape_refs.len();
2066 #[cfg(not(target_arch = "wasm32"))]
2067 let convert_started = Instant::now();
2068 #[cfg(not(target_arch = "wasm32"))]
2069 let parallel =
2070 SHAPE_CONVERT_TUNER.choose_parallel(shape_count) && shape_convert_worker_count() > 1;
2071 if quad_area_diag_enabled() {
2072 let quad_area = |q: [[f32; 2]; 4]| {
2073 let poly = [q[0], q[1], q[3], q[2]];
2075 let mut twice = 0.0f64;
2076 for i in 0..4 {
2077 let a = poly[i];
2078 let b = poly[(i + 1) % 4];
2079 twice += a[0] as f64 * b[1] as f64 - b[0] as f64 * a[1] as f64;
2080 }
2081 twice.abs() * 0.5
2082 };
2083 let mut arc_quad = 0.0f64; let mut arc_band = 0.0f64; let mut arc_count = 0usize;
2086 let mut ring_count = 0usize;
2087 let mut other_quad = 0.0f64;
2088 let mut other_count = 0usize;
2089 let mut top_other: Vec<(f64, usize)> = Vec::new();
2092 for (index, shape) in shape_refs.iter().enumerate() {
2093 let area = quad_area(shape.quad);
2094 if let Some(arc) = shape.arc {
2095 arc_quad += area;
2096 arc_count += 1;
2097 if arc.sweep_angle >= cranpose_ui_graphics::TAU {
2098 ring_count += 1;
2099 }
2100 let ra = arc.mid_radius() as f64;
2101 let rb = arc.half_thickness() as f64;
2102 arc_band +=
2103 arc.sweep_angle as f64 * ra * (2.0 * rb) + std::f64::consts::PI * rb * rb;
2104 } else {
2105 other_quad += area;
2106 other_count += 1;
2107 top_other.push((area, index));
2108 }
2109 }
2110 let scale2 = (root_scale as f64) * (root_scale as f64);
2111 eprintln!(
2112 "[quad-area] arcs={arc_count} (rings={ring_count}) arc_quad_px={:.0} arc_band_px={:.0} | other={other_count} other_px={:.0}",
2113 arc_quad * scale2,
2114 arc_band * scale2,
2115 other_quad * scale2,
2116 );
2117 top_other.sort_by(|a, b| b.0.total_cmp(&a.0));
2118 for &(area, index) in top_other.iter().take(4) {
2119 let shape = shape_refs[index];
2120 let brush = match &shape.brush {
2121 cranpose_ui_graphics::Brush::Solid(color) => format!("solid a={:.2}", color.3),
2122 cranpose_ui_graphics::Brush::LinearGradient { colors, .. } => {
2123 format!("linear n={}", colors.len())
2124 }
2125 cranpose_ui_graphics::Brush::RadialGradient { colors, .. } => {
2126 format!("radial n={}", colors.len())
2127 }
2128 cranpose_ui_graphics::Brush::SweepGradient { colors, .. } => {
2129 format!("sweep n={}", colors.len())
2130 }
2131 };
2132 eprintln!(
2133 "[quad-area] top other: {:.0}px {}x{} at ({:.0},{:.0}) {} shape={} stroke={} clip={} blend={:?} z={}",
2134 area * scale2,
2135 shape.rect.width.round(),
2136 shape.rect.height.round(),
2137 shape.rect.x,
2138 shape.rect.y,
2139 brush,
2140 shape.shape.is_some(),
2141 shape.stroke.is_some(),
2142 shape.clip.is_some(),
2143 shape.blend_mode,
2144 shape.z_index,
2145 );
2146 }
2147 }
2148 #[cfg(target_arch = "wasm32")]
2149 let parallel = false;
2150 let workers = if parallel {
2151 shape_convert_worker_count()
2152 } else {
2153 1
2154 };
2155 if workers <= 1 {
2156 for (idx, shape) in shape_refs.iter().enumerate() {
2157 let gradient_start = gradient_offsets[idx];
2158 let gradient_end = gradient_offsets[idx + 1];
2159 convert_shape_into_slots(
2160 shape,
2161 root_scale,
2162 gradient_start,
2163 &mut shape_data_out[idx],
2164 &mut gradients_out[gradient_start as usize..gradient_end as usize],
2165 );
2166 }
2167 #[cfg(not(target_arch = "wasm32"))]
2168 SHAPE_CONVERT_TUNER.record(
2169 false,
2170 shape_count,
2171 convert_started.elapsed().as_nanos() as u64,
2172 );
2173 return;
2174 }
2175
2176 let chunk_len = shape_count.div_ceil(workers);
2177 let mut shape_data_rest = shape_data_out;
2178 let mut gradients_rest = gradients_out;
2179 std::thread::scope(|scope| {
2180 let mut chunk_start = 0usize;
2181 while chunk_start < shape_count {
2182 let chunk_end = (chunk_start + chunk_len).min(shape_count);
2183 let count = chunk_end - chunk_start;
2184 let gradient_base = gradient_offsets[chunk_start];
2185 let gradient_span = (gradient_offsets[chunk_end] - gradient_base) as usize;
2186 let (shape_data_chunk, rest) = std::mem::take(&mut shape_data_rest).split_at_mut(count);
2187 shape_data_rest = rest;
2188 let (gradient_chunk, rest) =
2189 std::mem::take(&mut gradients_rest).split_at_mut(gradient_span);
2190 gradients_rest = rest;
2191 let chunk_refs = &shape_refs[chunk_start..chunk_end];
2192 let chunk_offsets = &gradient_offsets[chunk_start..=chunk_end];
2193 let mut convert_chunk = move || {
2194 for (j, shape) in chunk_refs.iter().enumerate() {
2195 let gradient_start = chunk_offsets[j];
2196 let local_start = (gradient_start - gradient_base) as usize;
2197 let local_end = (chunk_offsets[j + 1] - gradient_base) as usize;
2198 convert_shape_into_slots(
2199 shape,
2200 root_scale,
2201 gradient_start,
2202 &mut shape_data_chunk[j],
2203 &mut gradient_chunk[local_start..local_end],
2204 );
2205 }
2206 };
2207 if chunk_end == shape_count {
2208 convert_chunk();
2212 } else {
2213 scope.spawn(convert_chunk);
2214 }
2215 chunk_start = chunk_end;
2216 }
2217 });
2218 #[cfg(not(target_arch = "wasm32"))]
2219 SHAPE_CONVERT_TUNER.record(
2220 true,
2221 shape_count,
2222 convert_started.elapsed().as_nanos() as u64,
2223 );
2224}
2225
2226#[repr(C)]
2227#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2228struct GradientStop {
2229 color: [f32; 4],
2230 position: [f32; 4],
2231}
2232
2233#[cfg(not(target_arch = "wasm32"))]
2237const MAX_REPLAY_SLOTS: u32 = 128;
2238#[cfg(not(target_arch = "wasm32"))]
2239const REPLAY_TRANSFORM_STRIDE: u64 = 256;
2240
2241#[cfg(not(target_arch = "wasm32"))]
2248struct ReplaySlot {
2249 paint_buffer: wgpu::Buffer,
2254 bind_group: wgpu::BindGroup,
2255 shape_count: u32,
2256 paint_mirror: Vec<[f32; 4]>,
2262 mesh: Option<ReplaySlotMesh>,
2267 capture_epoch: u64,
2272}
2273
2274#[cfg(not(target_arch = "wasm32"))]
2279struct ReplaySlotMesh {
2280 vertex_buffer: wgpu::Buffer,
2281 index_buffer: wgpu::Buffer,
2287 index_prefix: Vec<u32>,
2292}
2293
2294#[cfg(not(target_arch = "wasm32"))]
2298#[repr(C)]
2299#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2300struct MeshVertex {
2301 position: [f32; 2],
2302 uv: [f32; 2],
2303 shape_idx: u32,
2304}
2305
2306#[cfg(not(target_arch = "wasm32"))]
2307impl MeshVertex {
2308 const ATTRIBS: [wgpu::VertexAttribute; 3] =
2309 wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32];
2310
2311 fn desc() -> wgpu::VertexBufferLayout<'static> {
2312 wgpu::VertexBufferLayout {
2313 array_stride: std::mem::size_of::<MeshVertex>() as wgpu::BufferAddress,
2314 step_mode: wgpu::VertexStepMode::Vertex,
2315 attributes: &Self::ATTRIBS,
2316 }
2317 }
2318}
2319
2320#[cfg(not(target_arch = "wasm32"))]
2325fn arc_mesh_enabled() -> bool {
2326 matches!(std::env::var("CRANPOSE_ARC_MESH").as_deref(), Ok(v) if v != "0")
2334}
2335
2336#[cfg(not(target_arch = "wasm32"))]
2342const ARC_MESH_MARGIN: f32 = 1.0;
2343
2344#[cfg(not(target_arch = "wasm32"))]
2348const ARC_MESH_OVERSHOOT: f32 = 2.0;
2349
2350#[cfg(not(target_arch = "wasm32"))]
2351const ARC_MESH_MIN_SEGMENTS: usize = 4;
2352#[cfg(not(target_arch = "wasm32"))]
2353const ARC_MESH_MAX_SEGMENTS: usize = 64;
2354
2355#[cfg(not(target_arch = "wasm32"))]
2364const ARC_MESH_BUDGET_BYTES_PER_SHAPE: usize = 48 * std::mem::size_of::<MeshVertex>();
2365#[cfg(not(target_arch = "wasm32"))]
2366const ARC_MESH_BUDGET_FLOOR_BYTES: usize = 4096 * std::mem::size_of::<MeshVertex>();
2367
2368#[cfg(not(target_arch = "wasm32"))]
2371fn arc_mesh_bytes(vertices: usize, indices: usize) -> usize {
2372 vertices * std::mem::size_of::<MeshVertex>() + indices * std::mem::size_of::<u32>()
2373}
2374
2375#[cfg(not(target_arch = "wasm32"))]
2380struct ArcMeshBand {
2381 center: [f32; 2],
2382 inner: f32,
2383 outer: f32,
2384 start: f32,
2385 sweep: f32,
2386}
2387
2388#[cfg(not(target_arch = "wasm32"))]
2389fn arc_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
2390 let flags = shape.stroke_params[1].max(0.0) as u32;
2392 if flags & 3 != SHAPE_KIND_ARC {
2393 return None;
2394 }
2395 if shape.brush_type != 0 {
2399 return None;
2400 }
2401 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
2406 return None;
2407 }
2408 let [_, _, w, h] = shape.rect;
2409 if !(w > 0.0 && h > 0.0) {
2410 return None;
2411 }
2412 let [left, top, right, _] = shape.quad01;
2420 let [bl_x, bottom, br_x, br_y] = shape.quad23;
2421 let axis_aligned = shape.quad01[3] == top
2422 && bl_x == left
2423 && br_x == right
2424 && br_y == bottom
2425 && left < right
2426 && top < bottom;
2427 if !axis_aligned {
2428 return None;
2429 }
2430 let center = [shape.arc_params[0], shape.arc_params[1]];
2431 let start = shape.arc_params[2];
2432 let sweep = shape.arc_params[3];
2433 let outer = shape.stroke_params[2];
2434 let inner = shape.stroke_params[3];
2435 let finite = center[0].is_finite()
2436 && center[1].is_finite()
2437 && start.is_finite()
2438 && sweep.is_finite()
2439 && outer.is_finite()
2440 && inner.is_finite();
2441 if !finite || outer <= 0.0 || sweep <= 0.0 {
2442 return None;
2443 }
2444 Some(ArcMeshBand {
2445 center,
2446 inner,
2447 outer,
2448 start,
2449 sweep,
2450 })
2451}
2452
2453#[cfg(not(target_arch = "wasm32"))]
2459fn emit_passthrough_quad(
2460 shape: &ShapeData,
2461 shape_idx: u32,
2462 vertices: &mut Vec<MeshVertex>,
2463 indices: &mut Vec<u32>,
2464) {
2465 let base = vertices.len() as u32;
2466 let corners = [
2467 ([shape.quad01[0], shape.quad01[1]], [0.0, 0.0]),
2468 ([shape.quad01[2], shape.quad01[3]], [1.0, 0.0]),
2469 ([shape.quad23[0], shape.quad23[1]], [0.0, 1.0]),
2470 ([shape.quad23[2], shape.quad23[3]], [1.0, 1.0]),
2471 ];
2472 for (position, uv) in corners {
2473 vertices.push(MeshVertex {
2474 position,
2475 uv,
2476 shape_idx,
2477 });
2478 }
2479 indices.extend([0u32, 1, 2, 2, 1, 3].map(|corner| base + corner));
2480}
2481
2482#[cfg(not(target_arch = "wasm32"))]
2493fn clip_polygon_axis(
2494 input: &[[f32; 2]],
2495 axis: usize,
2496 bound: f32,
2497 keep_at_most: bool,
2498 output: &mut Vec<[f32; 2]>,
2499) {
2500 output.clear();
2501 let inside = |p: [f32; 2]| {
2502 if keep_at_most {
2503 p[axis] <= bound
2504 } else {
2505 p[axis] >= bound
2506 }
2507 };
2508 let intersect = |a: [f32; 2], b: [f32; 2]| {
2509 let (p, q) = if (b[0], b[1]) < (a[0], a[1]) {
2510 (b, a)
2511 } else {
2512 (a, b)
2513 };
2514 let t = (bound - p[axis]) / (q[axis] - p[axis]);
2515 let mut point = [0.0f32; 2];
2516 point[axis] = bound;
2517 point[1 - axis] = p[1 - axis] + t * (q[1 - axis] - p[1 - axis]);
2518 point
2519 };
2520 for (index, ¤t) in input.iter().enumerate() {
2521 let previous = input[(index + input.len() - 1) % input.len()];
2522 match (inside(previous), inside(current)) {
2523 (true, true) => output.push(current),
2524 (true, false) => output.push(intersect(previous, current)),
2525 (false, true) => {
2526 output.push(intersect(previous, current));
2527 output.push(current);
2528 }
2529 (false, false) => {}
2530 }
2531 }
2532}
2533
2534#[cfg(not(target_arch = "wasm32"))]
2569fn emit_arc_band_mesh(
2570 shape: &ShapeData,
2571 shape_idx: u32,
2572 band: &ArcMeshBand,
2573 vertices: &mut Vec<MeshVertex>,
2574 indices: &mut Vec<u32>,
2575) -> Option<usize> {
2576 let [cx, cy] = band.center;
2577 let ra = (band.outer + band.inner) * 0.5;
2578 let rb = ((band.outer - band.inner) * 0.5).max(0.0);
2579 let rb_m = rb + ARC_MESH_MARGIN;
2580 let ro = ra + rb_m;
2581 let ri = (ra - rb_m).max(0.0);
2582 let tau = cranpose_ui_graphics::TAU;
2583
2584 let (range_start, range) = if band.sweep >= tau {
2585 (0.0, tau)
2586 } else {
2587 let pad = if rb_m < ra {
2588 (rb_m / ra).asin() + 0.05
2589 } else {
2590 std::f32::consts::PI
2593 };
2594 let padded = band.sweep + pad + pad;
2595 if padded >= tau {
2596 (0.0, tau)
2597 } else {
2598 (band.start - pad, padded)
2599 }
2600 };
2601 let closed = range >= tau;
2602
2603 let dtheta = (2.0 * (ro / (ro + ARC_MESH_OVERSHOOT)).acos()).clamp(tau / 64.0, tau / 6.0);
2604 let segments =
2605 ((range / dtheta).ceil() as usize).clamp(ARC_MESH_MIN_SEGMENTS, ARC_MESH_MAX_SEGMENTS);
2606 let step = range / segments as f32;
2607 let rc = ro / (step * 0.5).cos();
2608
2609 let boundary_count = if closed { segments } else { segments + 1 };
2613 let mut boundaries = Vec::with_capacity(boundary_count);
2614 for j in 0..boundary_count {
2615 let (sin, cos) = (range_start + step * j as f32).sin_cos();
2616 boundaries.push((
2617 [cx + cos * ri, cy + sin * ri],
2618 [cx + cos * rc, cy + sin * rc],
2619 ));
2620 }
2621
2622 let quad_min = [shape.quad01[0], shape.quad01[1]];
2623 let quad_max = [shape.quad23[2], shape.quad23[3]];
2624
2625 enum SegmentGeometry {
2630 Shared,
2631 Fan(Vec<[f32; 2]>),
2632 Empty,
2633 }
2634
2635 let mut polygon: Vec<[f32; 2]> = Vec::with_capacity(8);
2637 let mut scratch: Vec<[f32; 2]> = Vec::with_capacity(8);
2638 let mut segment_geometry = Vec::with_capacity(segments);
2639 let mut boundary_used = vec![false; boundary_count];
2640 for j in 0..segments {
2641 let jb = (j + 1) % boundary_count;
2642 let (inner_a, outer_a) = boundaries[j];
2643 let (inner_b, outer_b) = boundaries[jb];
2644 polygon.clear();
2645 polygon.extend_from_slice(&[inner_a, outer_a, outer_b, inner_b]);
2646 clip_polygon_axis(&polygon, 0, quad_min[0], false, &mut scratch);
2647 clip_polygon_axis(&scratch, 0, quad_max[0], true, &mut polygon);
2648 clip_polygon_axis(&polygon, 1, quad_min[1], false, &mut scratch);
2649 clip_polygon_axis(&scratch, 1, quad_max[1], true, &mut polygon);
2650 scratch.clear();
2653 for &point in polygon.iter() {
2654 if scratch.last() != Some(&point) {
2655 scratch.push(point);
2656 }
2657 }
2658 while scratch.len() > 1 && scratch.first() == scratch.last() {
2659 scratch.pop();
2660 }
2661 if scratch.len() < 3 {
2662 segment_geometry.push(SegmentGeometry::Empty);
2663 } else if scratch[..] == [inner_a, outer_a, outer_b, inner_b] {
2664 boundary_used[j] = true;
2665 boundary_used[jb] = true;
2666 segment_geometry.push(SegmentGeometry::Shared);
2667 } else {
2668 segment_geometry.push(SegmentGeometry::Fan(scratch.clone()));
2669 }
2670 }
2671
2672 let push_vertex = |vertices: &mut Vec<MeshVertex>, position: [f32; 2]| -> u32 {
2673 let index = vertices.len() as u32;
2674 vertices.push(MeshVertex {
2675 position,
2676 uv: [
2677 (position[0] - shape.rect[0]) / shape.rect[2],
2678 (position[1] - shape.rect[1]) / shape.rect[3],
2679 ],
2680 shape_idx,
2681 });
2682 index
2683 };
2684
2685 let mut boundary_vertex = vec![[0u32; 2]; boundary_count];
2688 for (j, used) in boundary_used.iter().enumerate() {
2689 if *used {
2690 let (inner, outer) = boundaries[j];
2691 boundary_vertex[j] = [push_vertex(vertices, inner), push_vertex(vertices, outer)];
2692 }
2693 }
2694
2695 let start_len = indices.len();
2698 for (j, geometry) in segment_geometry.iter().enumerate() {
2699 match geometry {
2700 SegmentGeometry::Empty => {}
2701 SegmentGeometry::Shared => {
2702 let jb = (j + 1) % boundary_count;
2703 let [in_a, out_a] = boundary_vertex[j];
2704 let [in_b, out_b] = boundary_vertex[jb];
2705 indices.extend_from_slice(&[in_a, out_a, out_b, in_a, out_b, in_b]);
2709 }
2710 SegmentGeometry::Fan(points) => {
2711 let base = vertices.len() as u32;
2712 for &point in points {
2713 push_vertex(vertices, point);
2714 }
2715 for i in 1..points.len() as u32 - 1 {
2716 indices.extend_from_slice(&[base, base + i, base + i + 1]);
2717 }
2718 }
2719 }
2720 }
2721 if indices.len() == start_len {
2722 return None;
2723 }
2724 Some(segments)
2725}
2726
2727#[cfg(not(target_arch = "wasm32"))]
2730fn triangles_shoelace_area(vertices: &[MeshVertex], indices: &[u32]) -> f64 {
2731 indices
2732 .chunks_exact(3)
2733 .map(|tri| {
2734 let [a, b, c] = [
2735 vertices[tri[0] as usize].position,
2736 vertices[tri[1] as usize].position,
2737 vertices[tri[2] as usize].position,
2738 ];
2739 let cross = (b[0] as f64 - a[0] as f64) * (c[1] as f64 - a[1] as f64)
2740 - (b[1] as f64 - a[1] as f64) * (c[0] as f64 - a[0] as f64);
2741 cross.abs() * 0.5
2742 })
2743 .sum()
2744}
2745
2746#[cfg(not(target_arch = "wasm32"))]
2749fn quad_shoelace_area(shape: &ShapeData) -> f64 {
2750 let corners = [
2751 [shape.quad01[0] as f64, shape.quad01[1] as f64],
2752 [shape.quad01[2] as f64, shape.quad01[3] as f64],
2753 [shape.quad23[0] as f64, shape.quad23[1] as f64],
2754 [shape.quad23[2] as f64, shape.quad23[3] as f64],
2755 ];
2756 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
2757 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
2758 };
2759 tri(corners[0], corners[1], corners[2]) + tri(corners[2], corners[1], corners[3])
2760}
2761
2762#[cfg(not(target_arch = "wasm32"))]
2763struct ArcMeshBuild {
2764 vertices: Vec<MeshVertex>,
2765 indices: Vec<u32>,
2767 index_prefix: Vec<u32>,
2770 meshed_arcs: usize,
2771 meshed_segments: usize,
2772 passthrough: usize,
2773 quad_area: f64,
2774 mesh_area: f64,
2775}
2776
2777#[cfg(not(target_arch = "wasm32"))]
2784fn build_arc_mesh_vertices(shape_data: &[ShapeData]) -> Option<ArcMeshBuild> {
2785 let budget_bytes =
2786 (shape_data.len() * ARC_MESH_BUDGET_BYTES_PER_SHAPE).max(ARC_MESH_BUDGET_FLOOR_BYTES);
2787 let mut build = ArcMeshBuild {
2788 vertices: Vec::new(),
2789 indices: Vec::new(),
2790 index_prefix: Vec::with_capacity(shape_data.len() + 1),
2791 meshed_arcs: 0,
2792 meshed_segments: 0,
2793 passthrough: 0,
2794 quad_area: 0.0,
2795 mesh_area: 0.0,
2796 };
2797 build.index_prefix.push(0);
2798 for (index, shape) in shape_data.iter().enumerate() {
2799 let start = build.indices.len();
2800 let meshed = arc_mesh_band(shape).and_then(|band| {
2801 emit_arc_band_mesh(
2802 shape,
2803 index as u32,
2804 &band,
2805 &mut build.vertices,
2806 &mut build.indices,
2807 )
2808 });
2809 match meshed {
2810 Some(segments) => {
2811 build.meshed_arcs += 1;
2812 build.meshed_segments += segments;
2813 }
2814 None => {
2815 emit_passthrough_quad(shape, index as u32, &mut build.vertices, &mut build.indices);
2816 build.passthrough += 1;
2817 }
2818 }
2819 if arc_mesh_bytes(build.vertices.len(), build.indices.len()) > budget_bytes {
2820 return None;
2821 }
2822 build.index_prefix.push(build.indices.len() as u32);
2823 build.quad_area += quad_shoelace_area(shape);
2824 build.mesh_area += triangles_shoelace_area(&build.vertices, &build.indices[start..]);
2825 }
2826 Some(build)
2827}
2828
2829#[cfg(not(target_arch = "wasm32"))]
2832struct ReplaySlotStore {
2833 slots: std::collections::HashMap<u32, ReplaySlot, cranpose_ui_graphics::FxBuildHasher>,
2834 transform_buffer: wgpu::Buffer,
2835 free_ids: Vec<u32>,
2836 next_capture_epoch: u64,
2840}
2841
2842#[cfg(not(target_arch = "wasm32"))]
2843impl ReplaySlotStore {
2844 fn new(device: &wgpu::Device) -> Self {
2845 let transform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2846 label: Some("Replay Transform Buffer"),
2847 size: MAX_REPLAY_SLOTS as u64 * REPLAY_TRANSFORM_STRIDE,
2848 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2849 mapped_at_creation: false,
2850 });
2851 Self {
2852 slots: std::collections::HashMap::default(),
2853 transform_buffer,
2854 free_ids: (0..MAX_REPLAY_SLOTS).rev().collect(),
2855 next_capture_epoch: 1,
2856 }
2857 }
2858}
2859
2860#[cfg(not(target_arch = "wasm32"))]
2866fn retained_bundles_enabled() -> bool {
2867 std::env::var("CRANPOSE_RETAINED_BUNDLES").as_deref() != Ok("0")
2868}
2869
2870#[cfg(not(target_arch = "wasm32"))]
2878fn instanced_quads_enabled() -> bool {
2879 std::env::var("CRANPOSE_INSTANCED_QUADS").as_deref() != Ok("0")
2880}
2881
2882#[cfg(not(target_arch = "wasm32"))]
2886const INSTANCED_QUAD_INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
2887
2888#[cfg(not(target_arch = "wasm32"))]
2894struct InstancedQuadPipelines {
2895 pipeline: LazyGpuResource<wgpu::RenderPipeline>,
2896 pipeline_dst_out: LazyGpuResource<wgpu::RenderPipeline>,
2897 index_buffer: wgpu::Buffer,
2900}
2901
2902#[cfg(not(target_arch = "wasm32"))]
2910#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2911struct RetainedBundleOpKey {
2912 slot: u32,
2913 capture_epoch: Option<u64>,
2918 first: u32,
2919 last: u32,
2920 retained_index: u32,
2921 has_mesh: bool,
2922}
2923
2924#[cfg(not(target_arch = "wasm32"))]
2928#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
2929struct RetainedBundleKey {
2930 ops: Vec<RetainedBundleOpKey>,
2931}
2932
2933#[cfg(not(target_arch = "wasm32"))]
2934struct RetainedBundleCacheEntry<B> {
2935 bundle: B,
2936 last_used_frame: u64,
2937}
2938
2939#[cfg(not(target_arch = "wasm32"))]
2948struct RetainedBundleCacheImpl<B> {
2949 entries: HashMap<RetainedBundleKey, RetainedBundleCacheEntry<B>>,
2950 frame: u64,
2951 rebuilds: u64,
2952 cached_executes: u64,
2953 window_rebuilds: u64,
2954 window_executes: u64,
2955}
2956
2957#[cfg(not(target_arch = "wasm32"))]
2958type RetainedBundleCache = RetainedBundleCacheImpl<wgpu::RenderBundle>;
2959
2960#[cfg(not(target_arch = "wasm32"))]
2961impl<B> RetainedBundleCacheImpl<B> {
2962 fn new() -> Self {
2963 Self {
2964 entries: HashMap::default(),
2965 frame: 0,
2966 rebuilds: 0,
2967 cached_executes: 0,
2968 window_rebuilds: 0,
2969 window_executes: 0,
2970 }
2971 }
2972
2973 fn hit(&mut self, key: &RetainedBundleKey) -> bool {
2976 let frame = self.frame;
2977 match self.entries.get_mut(key) {
2978 Some(entry) => {
2979 entry.last_used_frame = frame;
2980 self.cached_executes += 1;
2981 self.window_executes += 1;
2982 true
2983 }
2984 None => false,
2985 }
2986 }
2987
2988 fn insert(&mut self, key: RetainedBundleKey, bundle: B) {
2990 self.rebuilds += 1;
2991 self.window_rebuilds += 1;
2992 self.entries.insert(
2993 key,
2994 RetainedBundleCacheEntry {
2995 bundle,
2996 last_used_frame: self.frame,
2997 },
2998 );
2999 }
3000
3001 fn get(&self, key: &RetainedBundleKey) -> Option<&B> {
3002 self.entries.get(key).map(|entry| &entry.bundle)
3003 }
3004
3005 fn clear(&mut self) {
3010 self.entries.clear();
3011 }
3012
3013 fn end_frame(&mut self) {
3017 let frame = self.frame;
3018 self.entries
3019 .retain(|_, entry| entry.last_used_frame >= frame);
3020 self.frame = self.frame.wrapping_add(1);
3021 let due = self.frame.is_multiple_of(1024)
3026 || (cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG")
3027 && self.frame.is_multiple_of(120));
3028 if due && self.window_rebuilds + self.window_executes > 0 {
3029 log::warn!(
3030 "[retained-bundles] {} stretches, {} rebuilds, {} cached executes ({} live bundles)",
3031 self.window_rebuilds + self.window_executes,
3032 self.window_rebuilds,
3033 self.window_executes,
3034 self.entries.len(),
3035 );
3036 self.window_rebuilds = 0;
3037 self.window_executes = 0;
3038 }
3039 }
3040
3041 fn stats(&self) -> (u64, u64) {
3043 (self.rebuilds, self.cached_executes)
3044 }
3045}
3046
3047struct CachedImageTexture {
3048 _texture: wgpu::Texture,
3049 _view: wgpu::TextureView,
3050 nearest_bind_group: wgpu::BindGroup,
3051 linear_bind_group: wgpu::BindGroup,
3052 bytes: usize,
3059}
3060
3061impl CachedImageTexture {
3062 fn bind_group(&self, sampling: ImageSampling) -> &wgpu::BindGroup {
3063 match sampling {
3064 ImageSampling::Nearest => &self.nearest_bind_group,
3065 ImageSampling::Linear => &self.linear_bind_group,
3066 }
3067 }
3068}
3069
3070#[derive(Clone, Copy)]
3071struct GlyphAtlasEntry {
3072 x: u32,
3073 y: u32,
3074 width: u32,
3075 height: u32,
3076}
3077
3078fn next_glyph_atlas_size(current: u32, max: u32) -> u32 {
3085 current.saturating_mul(2).clamp(1, max.max(1))
3086}
3087
3088struct TextGlyphAtlas {
3089 texture: wgpu::Texture,
3090 _view: wgpu::TextureView,
3091 bind_group: wgpu::BindGroup,
3092 entries: BoundedLruCache<SoftwareGlyphAtlasKey, GlyphAtlasEntry>,
3093 generation: u64,
3094 size: u32,
3098 max_size: u32,
3104 cursor_x: u32,
3105 cursor_y: u32,
3106 row_height: u32,
3107 upload_scratch: Vec<u8>,
3108}
3109
3110impl TextGlyphAtlas {
3111 fn new(
3112 device: &wgpu::Device,
3113 image_layout: &wgpu::BindGroupLayout,
3114 sampler: &wgpu::Sampler,
3115 size: u32,
3116 ) -> Self {
3117 let max_size = TEXT_GLYPH_ATLAS_MAX_SIZE.min(device.limits().max_texture_dimension_2d);
3118 let size = size.clamp(TEXT_GLYPH_ATLAS_MIN_SIZE.min(max_size), max_size);
3119 let texture = Self::create_texture(device, size);
3120 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3121 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3122 label: Some("Text Glyph Atlas Bind Group"),
3123 layout: image_layout,
3124 entries: &[
3125 wgpu::BindGroupEntry {
3126 binding: 0,
3127 resource: wgpu::BindingResource::TextureView(&view),
3128 },
3129 wgpu::BindGroupEntry {
3130 binding: 1,
3131 resource: wgpu::BindingResource::Sampler(sampler),
3132 },
3133 ],
3134 });
3135 Self {
3136 texture,
3137 _view: view,
3138 bind_group,
3139 entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
3140 generation: 0,
3141 size,
3142 max_size,
3143 cursor_x: TEXT_GLYPH_ATLAS_PADDING,
3144 cursor_y: TEXT_GLYPH_ATLAS_PADDING,
3145 row_height: 0,
3146 upload_scratch: Vec::new(),
3147 }
3148 }
3149
3150 fn create_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
3151 device.create_texture(&wgpu::TextureDescriptor {
3152 label: Some("Text Glyph Atlas Texture"),
3153 size: wgpu::Extent3d {
3154 width: size,
3155 height: size,
3156 depth_or_array_layers: 1,
3157 },
3158 mip_level_count: 1,
3159 sample_count: 1,
3160 dimension: wgpu::TextureDimension::D2,
3161 format: wgpu::TextureFormat::R8Unorm,
3162 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3163 view_formats: &[],
3164 })
3165 }
3166
3167 fn reset(
3183 &mut self,
3184 device: &wgpu::Device,
3185 image_layout: &wgpu::BindGroupLayout,
3186 sampler: &wgpu::Sampler,
3187 ) {
3188 let generation = self.generation.wrapping_add(1);
3189 let grown = next_glyph_atlas_size(self.size, self.max_size);
3190 let mut next = Self::new(device, image_layout, sampler, grown);
3191 next.generation = generation;
3192 *self = next;
3193 }
3194
3195 fn generation(&self) -> u64 {
3196 self.generation
3197 }
3198
3199 fn size(&self) -> u32 {
3200 self.size
3201 }
3202
3203 fn entry(&mut self, key: &SoftwareGlyphAtlasKey) -> Option<GlyphAtlasEntry> {
3204 self.entries.get(key).copied()
3205 }
3206
3207 fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
3208 if width == 0
3209 || height == 0
3210 || width + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
3211 || height + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
3212 {
3213 return None;
3214 }
3215
3216 if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > self.size {
3217 self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
3218 self.cursor_y = self
3219 .cursor_y
3220 .saturating_add(self.row_height)
3221 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
3222 self.row_height = 0;
3223 }
3224 if self.cursor_y + height + TEXT_GLYPH_ATLAS_PADDING > self.size {
3225 return None;
3226 }
3227
3228 let entry = GlyphAtlasEntry {
3229 x: self.cursor_x,
3230 y: self.cursor_y,
3231 width,
3232 height,
3233 };
3234 self.cursor_x = self
3235 .cursor_x
3236 .saturating_add(width)
3237 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
3238 self.row_height = self.row_height.max(height);
3239 Some(entry)
3240 }
3241
3242 fn upload_glyph(
3243 &mut self,
3244 key: SoftwareGlyphAtlasKey,
3245 glyph: &SoftwareGlyphAtlasGlyph,
3246 queue: &wgpu::Queue,
3247 executor: &mut WgpuFrameGraphExecutor,
3248 frame_stats: &mut gpu_stats::FrameStats,
3249 ) -> Option<GlyphAtlasEntry> {
3250 if let Some(entry) = self.entry(&key) {
3251 frame_stats.record_text_glyph_atlas_hit();
3252 return Some(entry);
3253 }
3254
3255 let width = u32::try_from(glyph.mask.width).ok()?;
3256 let height = u32::try_from(glyph.mask.height).ok()?;
3257 let entry = self.allocate(width, height)?;
3258 self.upload_scratch.clear();
3259 self.upload_scratch.reserve(
3260 glyph
3261 .mask
3262 .alpha
3263 .len()
3264 .saturating_sub(self.upload_scratch.capacity()),
3265 );
3266 self.upload_scratch.extend(
3267 glyph
3268 .mask
3269 .alpha
3270 .iter()
3271 .map(|alpha| (alpha.clamp(0.0, 1.0) * 255.0).round() as u8),
3272 );
3273
3274 let upload_stats = executor.upload_texture(
3275 queue,
3276 wgpu::TexelCopyTextureInfo {
3277 texture: &self.texture,
3278 mip_level: 0,
3279 origin: wgpu::Origin3d {
3280 x: entry.x,
3281 y: entry.y,
3282 z: 0,
3283 },
3284 aspect: wgpu::TextureAspect::All,
3285 },
3286 &self.upload_scratch,
3287 wgpu::TexelCopyBufferLayout {
3288 offset: 0,
3289 bytes_per_row: Some(entry.width),
3290 rows_per_image: Some(entry.height),
3291 },
3292 wgpu::Extent3d {
3293 width: entry.width,
3294 height: entry.height,
3295 depth_or_array_layers: 1,
3296 },
3297 );
3298 frame_stats.record_command_stats(upload_stats);
3299 frame_stats.record_text_glyph_atlas_miss(entry.width, entry.height);
3300 self.entries.put(key, entry);
3301 Some(entry)
3302 }
3303}
3304
3305struct ImageDrawCmd {
3306 index_start: u32,
3307 scissor: (u32, u32, u32, u32),
3308 image_id: u64,
3309 sampling: ImageSampling,
3310}
3311
3312#[derive(Clone, Copy)]
3313enum GlyphDrawSource {
3314 Shared {
3315 index_start: u32,
3316 index_count: u32,
3317 },
3318 #[cfg(not(target_arch = "wasm32"))]
3319 Retained {
3320 cache_key: TextGlyphRunCacheKey,
3321 uniform_slot: usize,
3322 },
3323}
3324
3325#[derive(Clone, Copy)]
3326struct GlyphDrawCmd {
3327 source: GlyphDrawSource,
3328 scissor: (u32, u32, u32, u32),
3329}
3330
3331impl GlyphDrawCmd {
3332 fn shared(index_start: u32, index_count: u32, scissor: (u32, u32, u32, u32)) -> Self {
3333 Self {
3334 source: GlyphDrawSource::Shared {
3335 index_start,
3336 index_count,
3337 },
3338 scissor,
3339 }
3340 }
3341
3342 #[cfg(not(target_arch = "wasm32"))]
3343 fn retained(
3344 cache_key: TextGlyphRunCacheKey,
3345 uniform_slot: usize,
3346 scissor: (u32, u32, u32, u32),
3347 ) -> Self {
3348 Self {
3349 source: GlyphDrawSource::Retained {
3350 cache_key,
3351 uniform_slot,
3352 },
3353 scissor,
3354 }
3355 }
3356}
3357
3358#[derive(Clone, Copy, Debug, PartialEq)]
3359struct ImageUvRect {
3360 min: [f32; 2],
3361 max: [f32; 2],
3362 sample_bounds: [f32; 4],
3363}
3364
3365struct ShapeBatchBuffers {
3372 shape_buffer: wgpu::Buffer,
3373 gradient_buffer: wgpu::Buffer,
3374 bind_group: wgpu::BindGroup,
3375 shape_capacity: usize,
3376 gradient_capacity: usize,
3377 batch_limits: ShapeBatchLimits,
3378}
3379
3380#[cfg(target_arch = "wasm32")]
3381struct UniformBatchBuffer {
3382 buffer: wgpu::Buffer,
3383 bind_group: wgpu::BindGroup,
3384}
3385
3386#[cfg(target_arch = "wasm32")]
3387struct ImageBatchBuffers {
3388 vertex_buffer: wgpu::Buffer,
3389 index_buffer: wgpu::Buffer,
3390 vertex_capacity: usize,
3391 index_capacity: usize,
3392}
3393
3394#[derive(Clone, Copy, Debug, PartialEq)]
3395struct ViewportUniformParams {
3396 width: u32,
3397 height: u32,
3398 offset: [f32; 2],
3399}
3400
3401#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3402#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
3403enum UploadTarget {
3404 Uniform,
3405 ShapeData,
3406 ShapeGradient,
3407 ImageVertex,
3408 ImageIndex,
3409 #[cfg(not(target_arch = "wasm32"))]
3410 RetainedGlyphUniform,
3411 #[cfg(not(target_arch = "wasm32"))]
3414 ReplayTransform,
3415 #[cfg(not(target_arch = "wasm32"))]
3417 ReplayPaintData(u32),
3418}
3419
3420#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3421#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
3422struct PendingBufferCopy {
3423 source_offset: u64,
3424 target_offset: u64,
3425 size: u64,
3426 target: UploadTarget,
3427}
3428
3429#[derive(Default)]
3430struct StagedBufferUploads {
3431 bytes: Vec<u8>,
3432 copies: Vec<PendingBufferCopy>,
3433}
3434
3435impl StagedBufferUploads {
3436 fn clear(&mut self) {
3437 self.bytes.clear();
3438 self.copies.clear();
3439 }
3440
3441 fn shrink_retained_capacity(&mut self, max_bytes: usize, max_copies: usize) -> bool {
3442 let mut shrunk = false;
3443 if self.bytes.len() <= max_bytes && self.bytes.capacity() > max_bytes {
3444 self.bytes.shrink_to(max_bytes);
3445 shrunk = true;
3446 }
3447 if self.copies.len() <= max_copies && self.copies.capacity() > max_copies {
3448 self.copies.shrink_to(max_copies);
3449 shrunk = true;
3450 }
3451 shrunk
3452 }
3453
3454 fn is_empty(&self) -> bool {
3455 self.copies.is_empty()
3456 }
3457
3458 #[cfg(test)]
3459 fn payload_for_copy(&self, copy: PendingBufferCopy) -> &[u8] {
3460 let start = copy.source_offset as usize;
3461 let end = start + copy.size as usize;
3462 &self.bytes[start..end]
3463 }
3464
3465 #[cfg(not(target_arch = "wasm32"))]
3466 fn stage(&mut self, target: UploadTarget, bytes: &[u8]) {
3467 self.stage_at(target, 0, bytes);
3468 }
3469
3470 #[cfg(not(target_arch = "wasm32"))]
3475 fn record_upload_copy(
3476 &mut self,
3477 target: UploadTarget,
3478 source_offset: u64,
3479 target_offset: u64,
3480 size: u64,
3481 ) {
3482 if size == 0 {
3483 return;
3484 }
3485 self.copies.push(PendingBufferCopy {
3486 source_offset,
3487 target_offset,
3488 size,
3489 target,
3490 });
3491 }
3492
3493 #[cfg(not(target_arch = "wasm32"))]
3494 fn stage_at(&mut self, target: UploadTarget, target_offset: u64, bytes: &[u8]) {
3495 if bytes.is_empty() {
3496 return;
3497 }
3498
3499 debug_assert_eq!(
3500 bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize,
3501 0,
3502 "buffer uploads must be aligned to copy requirements"
3503 );
3504
3505 let aligned_offset = align_usize_to(self.bytes.len(), wgpu::COPY_BUFFER_ALIGNMENT as usize);
3506 if aligned_offset > self.bytes.len() {
3507 self.bytes.resize(aligned_offset, 0);
3508 }
3509
3510 let source_offset = self.bytes.len() as u64;
3511 self.bytes.extend_from_slice(bytes);
3512 self.copies.push(PendingBufferCopy {
3513 source_offset,
3514 target_offset,
3515 size: bytes.len() as u64,
3516 target,
3517 });
3518 }
3519
3520 fn truncate(&mut self, bytes_len: usize, copies_len: usize) {
3521 self.bytes.truncate(bytes_len);
3522 self.copies.truncate(copies_len);
3523 }
3524}
3525
3526fn shape_batch_bind_group_entries<'a>(
3531 shape_buffer: &'a wgpu::Buffer,
3532 gradient_buffer: &'a wgpu::Buffer,
3533 similarity_buffer: &'a wgpu::Buffer,
3534 paint_buffer: Option<&'a wgpu::Buffer>,
3535) -> Vec<wgpu::BindGroupEntry<'a>> {
3536 let mut entries = vec![
3537 wgpu::BindGroupEntry {
3538 binding: 0,
3539 resource: shape_buffer.as_entire_binding(),
3540 },
3541 wgpu::BindGroupEntry {
3542 binding: 1,
3543 resource: gradient_buffer.as_entire_binding(),
3544 },
3545 wgpu::BindGroupEntry {
3546 binding: 2,
3547 resource: similarity_buffer.as_entire_binding(),
3548 },
3549 ];
3550 if let Some(paint_buffer) = paint_buffer {
3551 entries.push(wgpu::BindGroupEntry {
3552 binding: 3,
3553 resource: paint_buffer.as_entire_binding(),
3554 });
3555 }
3556 entries
3557}
3558
3559impl ShapeBatchBuffers {
3560 fn new(
3561 device: &wgpu::Device,
3562 bind_group_layout: &wgpu::BindGroupLayout,
3563 similarity_buffer: &wgpu::Buffer,
3564 paint_buffer: Option<&wgpu::Buffer>,
3565 batch_limits: ShapeBatchLimits,
3566 ) -> Self {
3567 debug_assert_eq!(
3568 paint_buffer.is_some(),
3569 batch_limits.storage,
3570 "the paint binding exists exactly when the layout is in storage mode"
3571 );
3572 let initial_shape_cap = batch_limits.initial_shape_capacity();
3573 let initial_gradient_cap = batch_limits.initial_gradient_capacity();
3574
3575 let shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3576 label: Some("Shape Data Buffer"),
3577 size: (std::mem::size_of::<ShapeData>() * initial_shape_cap) as u64,
3578 usage: batch_limits.data_buffer_usage(),
3579 mapped_at_creation: false,
3580 });
3581
3582 let gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3583 label: Some("Gradient Buffer"),
3584 size: (std::mem::size_of::<GradientStop>() * initial_gradient_cap) as u64,
3585 usage: batch_limits.data_buffer_usage(),
3586 mapped_at_creation: false,
3587 });
3588
3589 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3590 label: Some("Shape Bind Group"),
3591 layout: bind_group_layout,
3592 entries: &shape_batch_bind_group_entries(
3593 &shape_buffer,
3594 &gradient_buffer,
3595 similarity_buffer,
3596 paint_buffer,
3597 ),
3598 });
3599
3600 Self {
3601 shape_buffer,
3602 gradient_buffer,
3603 bind_group,
3604 shape_capacity: initial_shape_cap,
3605 gradient_capacity: initial_gradient_cap,
3606 batch_limits,
3607 }
3608 }
3609
3610 fn ensure_capacity(
3613 &mut self,
3614 device: &wgpu::Device,
3615 bind_group_layout: &wgpu::BindGroupLayout,
3616 similarity_buffer: &wgpu::Buffer,
3617 paint_buffer: Option<&wgpu::Buffer>,
3618 shapes_needed: usize,
3619 gradients_needed: usize,
3620 ) {
3621 let mut need_bind_group_update = false;
3622
3623 if shapes_needed > self.shape_capacity
3627 && self.shape_capacity < self.batch_limits.max_shapes_per_batch
3628 {
3629 let new_cap = shapes_needed
3630 .next_power_of_two()
3631 .min(self.batch_limits.max_shapes_per_batch);
3632 self.shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3633 label: Some("Shape Data Buffer"),
3634 size: (std::mem::size_of::<ShapeData>() * new_cap) as u64,
3635 usage: self.batch_limits.data_buffer_usage(),
3636 mapped_at_creation: false,
3637 });
3638 self.shape_capacity = new_cap;
3639 need_bind_group_update = true;
3640 }
3641
3642 if gradients_needed > self.gradient_capacity
3643 && self.gradient_capacity < self.batch_limits.max_gradient_stops
3644 {
3645 let new_cap = gradients_needed
3646 .max(1)
3647 .next_power_of_two()
3648 .min(self.batch_limits.max_gradient_stops);
3649 self.gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3650 label: Some("Gradient Buffer"),
3651 size: (std::mem::size_of::<GradientStop>() * new_cap) as u64,
3652 usage: self.batch_limits.data_buffer_usage(),
3653 mapped_at_creation: false,
3654 });
3655 self.gradient_capacity = new_cap;
3656 need_bind_group_update = true;
3657 }
3658
3659 if need_bind_group_update {
3660 self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3661 label: Some("Shape Bind Group"),
3662 layout: bind_group_layout,
3663 entries: &shape_batch_bind_group_entries(
3664 &self.shape_buffer,
3665 &self.gradient_buffer,
3666 similarity_buffer,
3667 paint_buffer,
3668 ),
3669 });
3670 }
3671 }
3672}
3673
3674#[cfg(target_arch = "wasm32")]
3675impl UniformBatchBuffer {
3676 fn new(device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout) -> Self {
3677 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
3678 label: Some("Viewport Uniform Batch Buffer"),
3679 size: std::mem::size_of::<Uniforms>() as u64,
3680 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3681 mapped_at_creation: false,
3682 });
3683 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3684 label: Some("Viewport Uniform Batch Bind Group"),
3685 layout: bind_group_layout,
3686 entries: &[wgpu::BindGroupEntry {
3687 binding: 0,
3688 resource: buffer.as_entire_binding(),
3689 }],
3690 });
3691 Self { buffer, bind_group }
3692 }
3693}
3694
3695#[cfg(target_arch = "wasm32")]
3696impl ImageBatchBuffers {
3697 fn new(device: &wgpu::Device) -> Self {
3698 let vertex_capacity = 4;
3699 let index_capacity = 6;
3700 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3701 label: Some("Image Vertex Batch Buffer"),
3702 size: (std::mem::size_of::<Vertex>() * vertex_capacity) as u64,
3703 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3704 mapped_at_creation: false,
3705 });
3706 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3707 label: Some("Image Index Batch Buffer"),
3708 size: (std::mem::size_of::<u32>() * index_capacity) as u64,
3709 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3710 mapped_at_creation: false,
3711 });
3712 Self {
3713 vertex_buffer,
3714 index_buffer,
3715 vertex_capacity,
3716 index_capacity,
3717 }
3718 }
3719
3720 fn ensure_capacity(
3721 &mut self,
3722 device: &wgpu::Device,
3723 vertices_needed: usize,
3724 indices_needed: usize,
3725 ) {
3726 let hard_max_bytes = HARD_MAX_BUFFER_MB * 1024 * 1024;
3727 if vertices_needed > self.vertex_capacity {
3728 let desired = vertices_needed.next_power_of_two();
3729 let max_count = hard_max_bytes / std::mem::size_of::<Vertex>();
3730 let new_cap = desired.min(max_count);
3731 self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3732 label: Some("Image Vertex Batch Buffer"),
3733 size: (std::mem::size_of::<Vertex>() * new_cap) as u64,
3734 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3735 mapped_at_creation: false,
3736 });
3737 self.vertex_capacity = new_cap;
3738 }
3739 if indices_needed > self.index_capacity {
3740 let desired = indices_needed.next_power_of_two();
3741 let max_count = hard_max_bytes / std::mem::size_of::<u32>();
3742 let new_cap = desired.min(max_count);
3743 self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3744 label: Some("Image Index Batch Buffer"),
3745 size: (std::mem::size_of::<u32>() * new_cap) as u64,
3746 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3747 mapped_at_creation: false,
3748 });
3749 self.index_capacity = new_cap;
3750 }
3751 }
3752}
3753
3754pub struct GpuRenderer {
3757 pub(crate) device: Arc<wgpu::Device>,
3758 pub(crate) queue: Arc<wgpu::Queue>,
3759 renderer_epoch: u64,
3764 #[cfg(not(target_arch = "wasm32"))]
3770 store_feed_generation: u64,
3771 surface_format: wgpu::TextureFormat,
3772 adapter_backend: wgpu::Backend,
3773 shape_batch_limits: ShapeBatchLimits,
3774 pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3775 pipeline_dst_out: LazyGpuResource<wgpu::RenderPipeline>,
3776 #[cfg(not(target_arch = "wasm32"))]
3780 mesh_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3781 #[cfg(not(target_arch = "wasm32"))]
3787 instanced_quads: Option<InstancedQuadPipelines>,
3788 uniform_bind_group_layout: wgpu::BindGroupLayout,
3789 shape_bind_group_layout: wgpu::BindGroupLayout,
3790 dummy_paint_buffer: Option<wgpu::Buffer>,
3793 identity_similarity_buffer: wgpu::Buffer,
3796 #[cfg(not(target_arch = "wasm32"))]
3797 replay_slots: ReplaySlotStore,
3798 image_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3799 image_pipeline_dst_out: LazyGpuResource<wgpu::RenderPipeline>,
3800 glyph_atlas_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3801 #[cfg(not(target_arch = "wasm32"))]
3802 retained_glyph_atlas_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3803 image_bind_group_layout: wgpu::BindGroupLayout,
3804 #[cfg(not(target_arch = "wasm32"))]
3805 retained_glyph_uniform_bind_group_layout: wgpu::BindGroupLayout,
3806 image_nearest_sampler: wgpu::Sampler,
3807 image_linear_sampler: wgpu::Sampler,
3808 text_fonts: SoftwareTextFontSet,
3809 #[cfg(not(target_arch = "wasm32"))]
3811 upload_buffer: wgpu::Buffer,
3812 #[cfg(not(target_arch = "wasm32"))]
3813 uniform_buffer: wgpu::Buffer,
3814 #[cfg(not(target_arch = "wasm32"))]
3815 uniform_bind_group: wgpu::BindGroup,
3816 #[cfg(not(target_arch = "wasm32"))]
3817 shape_buffers: ShapeBatchBuffers,
3818 #[cfg(not(target_arch = "wasm32"))]
3819 image_vertex_buffer: wgpu::Buffer,
3820 #[cfg(not(target_arch = "wasm32"))]
3821 image_index_buffer: wgpu::Buffer,
3822 #[cfg(not(target_arch = "wasm32"))]
3823 retained_glyph_uniform_buffer: wgpu::Buffer,
3824 #[cfg(not(target_arch = "wasm32"))]
3825 retained_glyph_uniform_bind_group: wgpu::BindGroup,
3826 #[cfg(not(target_arch = "wasm32"))]
3827 retained_glyph_uniform_stride: u64,
3828 #[cfg(not(target_arch = "wasm32"))]
3829 retained_glyph_uniform_capacity: usize,
3830 #[cfg(not(target_arch = "wasm32"))]
3831 retained_glyph_uniform_cursor: usize,
3832 #[cfg(target_arch = "wasm32")]
3833 wasm_uniform_batches: Vec<UniformBatchBuffer>,
3834 #[cfg(target_arch = "wasm32")]
3835 wasm_uniform_batch_cursor: usize,
3836 #[cfg(target_arch = "wasm32")]
3837 wasm_shape_batches: Vec<ShapeBatchBuffers>,
3838 #[cfg(target_arch = "wasm32")]
3839 wasm_shape_batch_cursor: usize,
3840 #[cfg(target_arch = "wasm32")]
3841 wasm_image_batches: Vec<ImageBatchBuffers>,
3842 #[cfg(target_arch = "wasm32")]
3843 wasm_image_batch_cursor: usize,
3844 image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
3845 image_texture_cache_bytes: usize,
3847 text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
3848 text_glyph_atlas: TextGlyphAtlas,
3849 text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
3850 #[cfg(not(target_arch = "wasm32"))]
3851 text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedGpuTextGlyphRun>,
3852 text_glyph_mask_cache: SoftwareGlyphRasterCache,
3853 text_line_index_cache: TextLineIndexCache,
3854 scratch_shape_data: Vec<ShapeData>,
3855 scratch_gradients: Vec<GradientStop>,
3856 scratch_image_vertices: Vec<Vertex>,
3857 scratch_image_indices: Vec<u32>,
3858 scratch_image_cmds: Vec<ImageDrawCmd>,
3859 scratch_glyph_cmds: Vec<GlyphDrawCmd>,
3860 scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
3861 scratch_text_glyph_placements: Vec<SoftwareGlyphAtlasPlacement>,
3862 scratch_text_glyph_quads: Vec<CachedTextGlyphQuad>,
3863 scratch_segment_items: Vec<(usize, SegmentDrawItem)>,
3864 scratch_effect_ranges: Vec<Range<usize>>,
3865 scratch_layer_events: Vec<LayerEvent>,
3866 staged_uploads: StagedBufferUploads,
3867 frame_graph_executor: WgpuFrameGraphExecutor,
3868 deferred_offscreen_releases: Vec<OffscreenTarget>,
3869 effect_renderer: EffectRenderer,
3870 layer_surface_cache: LayerSurfaceCache,
3871 observed_scene_range_cache_misses: BoundedLruCache<LayerRasterCacheKey, ()>,
3872 shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
3873 shadow_surface_cache_bytes: u64,
3874 frame_stats: gpu_stats::FrameStats,
3875 last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
3876 pending_frame_warmup_frames: u8,
3877 frame_count: u64,
3878 gpu_stats_enabled: bool,
3879 warning_state: RendererWarningState,
3880 #[cfg(not(target_arch = "wasm32"))]
3881 replay_upload_stats: ReplayUploadStats,
3882 #[cfg(not(target_arch = "wasm32"))]
3889 replay_color_patches: Vec<crate::scene::ColorPatch>,
3890 #[cfg(not(target_arch = "wasm32"))]
3894 color_patch_scratch: Vec<crate::scene::ColorPatch>,
3895 #[cfg(not(target_arch = "wasm32"))]
3900 replay_ack_confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
3901 #[cfg(not(target_arch = "wasm32"))]
3906 replay_generation_drops: u64,
3907 #[cfg(not(target_arch = "wasm32"))]
3910 retained_bundle_cache: RetainedBundleCache,
3911}
3912
3913#[cfg(not(target_arch = "wasm32"))]
3919#[derive(Default)]
3920struct ReplayUploadStats {
3921 calls: u64,
3922 patched_calls: u64,
3923 patches: u64,
3924 slots: u64,
3925 records: u64,
3926 bytes: u64,
3927 ideal_bytes: u64,
3928 max_frame_bytes: u64,
3929}
3930
3931#[cfg(not(target_arch = "wasm32"))]
3932impl ReplayUploadStats {
3933 const REPORT_CALLS: u64 = 1024;
3943
3944 fn note_frame(&mut self, patches: u64, slots: u64, records: u64, bytes: u64, ideal: u64) {
3945 self.calls += 1;
3946 if patches > 0 {
3947 self.patched_calls += 1;
3948 self.patches += patches;
3949 self.slots += slots;
3950 self.records += records;
3951 self.bytes += bytes;
3952 self.ideal_bytes += ideal;
3953 self.max_frame_bytes = self.max_frame_bytes.max(bytes);
3954 }
3955 if self.calls >= Self::REPORT_CALLS {
3956 let patched = self.patched_calls.max(1);
3957 log::warn!(
3958 "[replay-upload] {} patched of {} drains: avg {:.1} KB/frame (max {:.1} KB), \
3959 color-only would be {:.1} KB/frame; avg {} patches over {} records in {} slots",
3960 self.patched_calls,
3961 self.calls,
3962 self.bytes as f64 / patched as f64 / 1024.0,
3963 self.max_frame_bytes as f64 / 1024.0,
3964 self.ideal_bytes as f64 / patched as f64 / 1024.0,
3965 self.patches / patched,
3966 self.records / patched,
3967 self.slots / patched,
3968 );
3969 *self = Self::default();
3970 }
3971 }
3972}
3973
3974fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
3975 let filter = match sampling {
3976 ImageSampling::Nearest => wgpu::FilterMode::Nearest,
3977 ImageSampling::Linear => wgpu::FilterMode::Linear,
3978 };
3979 wgpu::SamplerDescriptor {
3980 label: Some(match sampling {
3981 ImageSampling::Nearest => "Nearest Image Sampler",
3982 ImageSampling::Linear => "Linear Image Sampler",
3983 }),
3984 address_mode_u: wgpu::AddressMode::ClampToEdge,
3985 address_mode_v: wgpu::AddressMode::ClampToEdge,
3986 address_mode_w: wgpu::AddressMode::ClampToEdge,
3987 mag_filter: filter,
3988 min_filter: filter,
3989 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
3990 ..Default::default()
3991 }
3992}
3993
3994#[cfg(test)]
3995fn layer_raster_cache_candidate(
3996 layer: &LayerNode,
3997 root_scale: f32,
3998 has_backdrop_underlay: bool,
3999 allow_runtime_cache: bool,
4000) -> Option<(LayerRasterCacheKey, Rect)> {
4001 let mut layer_surface_requirements_cache = HashMap::new();
4002 let surface_requirements =
4003 layer_surface_requirements_cached(layer, &mut layer_surface_requirements_cache);
4004 let runtime_cache_is_safe = allow_runtime_cache
4005 && surface_requirements
4006 .surface_requirements
4007 .has_isolating_requirement()
4008 && !layer
4009 .effect()
4010 .is_some_and(RenderEffect::contains_runtime_shader);
4011 let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
4012 || (allow_runtime_cache && surface_requirements.has_renderer_forced_surface())
4013 || runtime_cache_is_safe;
4014 if !cache_is_allowed {
4015 return None;
4016 }
4017 if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
4018 return None;
4019 }
4020 if layer
4021 .effect()
4022 .is_some_and(RenderEffect::contains_runtime_shader)
4023 {
4024 return None;
4025 }
4026
4027 let logical_rect = estimate_layer_surface_rect(layer);
4028 let pixel_size = surface_target_size(logical_rect, root_scale, u32::MAX);
4029 Some((
4030 LayerRasterCacheKey::new(
4031 layer.node_id,
4032 layer.target_content_hash(),
4033 layer.effect_hash(),
4034 logical_rect,
4035 pixel_size,
4036 ScaleBucket::from_scale(root_scale),
4037 ),
4038 logical_rect,
4039 ))
4040}
4041
4042impl GpuRenderer {
4043 pub fn new(
4044 device: Arc<wgpu::Device>,
4045 queue: Arc<wgpu::Queue>,
4046 surface_format: wgpu::TextureFormat,
4047 adapter_backend: wgpu::Backend,
4048 text_fonts: SoftwareTextFontSet,
4049 renderer_epoch: u64,
4050 store_feed_generation: u64,
4051 ) -> Self {
4052 #[cfg(target_arch = "wasm32")]
4053 let _ = store_feed_generation;
4054 let shape_batch_limits = ShapeBatchLimits::for_device(&device);
4055 let uniform_bind_group_layout =
4056 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4057 label: Some("Uniform Bind Group Layout"),
4058 entries: &[wgpu::BindGroupLayoutEntry {
4059 binding: 0,
4060 visibility: wgpu::ShaderStages::VERTEX,
4061 ty: wgpu::BindingType::Buffer {
4062 ty: wgpu::BufferBindingType::Uniform,
4063 has_dynamic_offset: false,
4064 min_binding_size: None,
4065 },
4066 count: None,
4067 }],
4068 });
4069 #[cfg(not(target_arch = "wasm32"))]
4070 let retained_glyph_uniform_bind_group_layout =
4071 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4072 label: Some("Retained Glyph Dynamic Uniform Bind Group Layout"),
4073 entries: &[wgpu::BindGroupLayoutEntry {
4074 binding: 0,
4075 visibility: wgpu::ShaderStages::VERTEX,
4076 ty: wgpu::BindingType::Buffer {
4077 ty: wgpu::BufferBindingType::Uniform,
4078 has_dynamic_offset: true,
4079 min_binding_size: wgpu::BufferSize::new(
4080 std::mem::size_of::<Uniforms>() as u64
4081 ),
4082 },
4083 count: None,
4084 }],
4085 });
4086
4087 let mut shape_bind_group_layout_entries = vec![
4096 wgpu::BindGroupLayoutEntry {
4097 binding: 0,
4098 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
4099 ty: wgpu::BindingType::Buffer {
4100 ty: shape_batch_limits.data_binding_type(),
4101 has_dynamic_offset: false,
4102 min_binding_size: None,
4103 },
4104 count: None,
4105 },
4106 wgpu::BindGroupLayoutEntry {
4107 binding: 1,
4108 visibility: wgpu::ShaderStages::FRAGMENT,
4109 ty: wgpu::BindingType::Buffer {
4110 ty: shape_batch_limits.data_binding_type(),
4111 has_dynamic_offset: false,
4112 min_binding_size: None,
4113 },
4114 count: None,
4115 },
4116 wgpu::BindGroupLayoutEntry {
4121 binding: 2,
4122 visibility: wgpu::ShaderStages::VERTEX,
4123 ty: wgpu::BindingType::Buffer {
4124 ty: wgpu::BufferBindingType::Uniform,
4125 has_dynamic_offset: true,
4126 min_binding_size: wgpu::BufferSize::new(
4127 std::mem::size_of::<SimilarityTransform>() as u64,
4128 ),
4129 },
4130 count: None,
4131 },
4132 ];
4133 if shape_batch_limits.storage {
4139 shape_bind_group_layout_entries.push(wgpu::BindGroupLayoutEntry {
4140 binding: 3,
4141 visibility: wgpu::ShaderStages::VERTEX,
4142 ty: wgpu::BindingType::Buffer {
4143 ty: wgpu::BufferBindingType::Storage { read_only: true },
4144 has_dynamic_offset: false,
4145 min_binding_size: None,
4146 },
4147 count: None,
4148 });
4149 }
4150 let shape_bind_group_layout =
4151 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4152 label: Some("Shape Bind Group Layout"),
4153 entries: &shape_bind_group_layout_entries,
4154 });
4155
4156 let identity_similarity_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4157 label: Some("Identity Similarity Buffer"),
4158 size: std::mem::size_of::<SimilarityTransform>() as u64,
4159 usage: wgpu::BufferUsages::UNIFORM,
4160 mapped_at_creation: true,
4161 });
4162 identity_similarity_buffer
4163 .slice(..)
4164 .get_mapped_range_mut()
4165 .copy_from_slice(bytemuck::bytes_of(&SimilarityTransform::IDENTITY));
4166 identity_similarity_buffer.unmap();
4167
4168 let dummy_paint_buffer = shape_batch_limits.storage.then(|| {
4173 device.create_buffer(&wgpu::BufferDescriptor {
4174 label: Some("Dummy Paint Buffer"),
4175 size: std::mem::size_of::<[f32; 4]>() as u64,
4176 usage: wgpu::BufferUsages::STORAGE,
4177 mapped_at_creation: false,
4178 })
4179 });
4180 #[cfg(not(target_arch = "wasm32"))]
4181 let replay_slot_store = ReplaySlotStore::new(&device);
4182
4183 let pipeline = LazyGpuResource::new("shape/src-over");
4184 let pipeline_dst_out = LazyGpuResource::new("shape/dst-out");
4185 #[cfg(not(target_arch = "wasm32"))]
4186 let mesh_pipeline = LazyGpuResource::new("shape/mesh");
4187 #[cfg(not(target_arch = "wasm32"))]
4193 let instanced_quads =
4194 (shape_batch_limits.storage && instanced_quads_enabled()).then(|| {
4195 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4196 label: Some("Instanced Quad Index Buffer"),
4197 size: std::mem::size_of_val(&INSTANCED_QUAD_INDICES) as u64,
4198 usage: wgpu::BufferUsages::INDEX,
4199 mapped_at_creation: true,
4200 });
4201 index_buffer
4202 .slice(..)
4203 .get_mapped_range_mut()
4204 .copy_from_slice(bytemuck::cast_slice(&INSTANCED_QUAD_INDICES));
4205 index_buffer.unmap();
4206 InstancedQuadPipelines {
4207 pipeline: LazyGpuResource::new("shape/instanced-src-over"),
4208 pipeline_dst_out: LazyGpuResource::new("shape/instanced-dst-out"),
4209 index_buffer,
4210 }
4211 });
4212
4213 let image_bind_group_layout =
4214 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4215 label: Some("Image Texture Bind Group Layout"),
4216 entries: &[
4217 wgpu::BindGroupLayoutEntry {
4218 binding: 0,
4219 visibility: wgpu::ShaderStages::FRAGMENT,
4220 ty: wgpu::BindingType::Texture {
4221 multisampled: false,
4222 view_dimension: wgpu::TextureViewDimension::D2,
4223 sample_type: wgpu::TextureSampleType::Float { filterable: true },
4224 },
4225 count: None,
4226 },
4227 wgpu::BindGroupLayoutEntry {
4228 binding: 1,
4229 visibility: wgpu::ShaderStages::FRAGMENT,
4230 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
4231 count: None,
4232 },
4233 ],
4234 });
4235
4236 let image_pipeline = LazyGpuResource::new("image/src-over");
4237 let image_pipeline_dst_out = LazyGpuResource::new("image/dst-out");
4238 let glyph_atlas_pipeline = LazyGpuResource::new("glyph/shared");
4239 #[cfg(not(target_arch = "wasm32"))]
4240 let retained_glyph_atlas_pipeline = LazyGpuResource::new("glyph/retained");
4241
4242 #[cfg(not(target_arch = "wasm32"))]
4243 let upload_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4244 label: Some("Frame Upload Buffer"),
4245 size: INITIAL_UPLOAD_BUFFER_BYTES,
4246 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
4247 mapped_at_creation: false,
4248 });
4249
4250 #[cfg(not(target_arch = "wasm32"))]
4251 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4252 label: Some("Uniform Buffer"),
4253 size: std::mem::size_of::<Uniforms>() as u64,
4254 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4255 mapped_at_creation: false,
4256 });
4257
4258 #[cfg(not(target_arch = "wasm32"))]
4259 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
4260 label: Some("Uniform Bind Group"),
4261 layout: &uniform_bind_group_layout,
4262 entries: &[wgpu::BindGroupEntry {
4263 binding: 0,
4264 resource: uniform_buffer.as_entire_binding(),
4265 }],
4266 });
4267
4268 #[cfg(not(target_arch = "wasm32"))]
4269 let shape_buffers = ShapeBatchBuffers::new(
4270 &device,
4271 &shape_bind_group_layout,
4272 &identity_similarity_buffer,
4273 dummy_paint_buffer.as_ref(),
4274 shape_batch_limits,
4275 );
4276
4277 let image_nearest_sampler =
4278 device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
4279 let image_linear_sampler =
4280 device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
4281 let text_glyph_atlas = TextGlyphAtlas::new(
4282 &device,
4283 &image_bind_group_layout,
4284 &image_nearest_sampler,
4285 TEXT_GLYPH_ATLAS_MIN_SIZE,
4286 );
4287
4288 #[cfg(not(target_arch = "wasm32"))]
4289 let image_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4290 label: Some("Image Vertex Buffer"),
4291 size: (std::mem::size_of::<Vertex>() * 4) as u64,
4292 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
4293 mapped_at_creation: false,
4294 });
4295
4296 #[cfg(not(target_arch = "wasm32"))]
4297 let image_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4298 label: Some("Image Index Buffer"),
4299 size: (std::mem::size_of::<u32>() * 6) as u64,
4300 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
4301 mapped_at_creation: false,
4302 });
4303 #[cfg(not(target_arch = "wasm32"))]
4304 let retained_glyph_uniform_stride = align_usize_to(
4305 std::mem::size_of::<Uniforms>(),
4306 (device.limits().min_uniform_buffer_offset_alignment as usize)
4307 .max(wgpu::COPY_BUFFER_ALIGNMENT as usize),
4308 ) as u64;
4309 #[cfg(not(target_arch = "wasm32"))]
4310 let retained_glyph_uniform_capacity = INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS;
4311 #[cfg(not(target_arch = "wasm32"))]
4312 let retained_glyph_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4313 label: Some("Retained Glyph Uniform Buffer"),
4314 size: retained_glyph_uniform_stride * retained_glyph_uniform_capacity as u64,
4315 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4316 mapped_at_creation: false,
4317 });
4318 #[cfg(not(target_arch = "wasm32"))]
4319 let retained_glyph_uniform_bind_group =
4320 device.create_bind_group(&wgpu::BindGroupDescriptor {
4321 label: Some("Retained Glyph Uniform Bind Group"),
4322 layout: &retained_glyph_uniform_bind_group_layout,
4323 entries: &[wgpu::BindGroupEntry {
4324 binding: 0,
4325 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
4326 buffer: &retained_glyph_uniform_buffer,
4327 offset: 0,
4328 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
4329 }),
4330 }],
4331 });
4332
4333 let effect_renderer = EffectRenderer::new(&device, surface_format, adapter_backend);
4334
4335 Self {
4336 device,
4337 queue,
4338 renderer_epoch,
4339 #[cfg(not(target_arch = "wasm32"))]
4340 store_feed_generation,
4341 surface_format,
4342 adapter_backend,
4343 shape_batch_limits,
4344 pipeline,
4345 pipeline_dst_out,
4346 #[cfg(not(target_arch = "wasm32"))]
4347 mesh_pipeline,
4348 #[cfg(not(target_arch = "wasm32"))]
4349 instanced_quads,
4350 uniform_bind_group_layout,
4351 shape_bind_group_layout,
4352 dummy_paint_buffer,
4353 identity_similarity_buffer,
4354 #[cfg(not(target_arch = "wasm32"))]
4355 replay_slots: replay_slot_store,
4356 image_pipeline,
4357 image_pipeline_dst_out,
4358 glyph_atlas_pipeline,
4359 #[cfg(not(target_arch = "wasm32"))]
4360 retained_glyph_atlas_pipeline,
4361 image_bind_group_layout,
4362 #[cfg(not(target_arch = "wasm32"))]
4363 retained_glyph_uniform_bind_group_layout,
4364 image_nearest_sampler,
4365 image_linear_sampler,
4366 text_fonts,
4367 #[cfg(not(target_arch = "wasm32"))]
4368 upload_buffer,
4369 #[cfg(not(target_arch = "wasm32"))]
4370 uniform_buffer,
4371 #[cfg(not(target_arch = "wasm32"))]
4372 uniform_bind_group,
4373 #[cfg(not(target_arch = "wasm32"))]
4374 shape_buffers,
4375 #[cfg(not(target_arch = "wasm32"))]
4376 image_vertex_buffer,
4377 #[cfg(not(target_arch = "wasm32"))]
4378 image_index_buffer,
4379 #[cfg(not(target_arch = "wasm32"))]
4380 retained_glyph_uniform_buffer,
4381 #[cfg(not(target_arch = "wasm32"))]
4382 retained_glyph_uniform_bind_group,
4383 #[cfg(not(target_arch = "wasm32"))]
4384 retained_glyph_uniform_stride,
4385 #[cfg(not(target_arch = "wasm32"))]
4386 retained_glyph_uniform_capacity,
4387 #[cfg(not(target_arch = "wasm32"))]
4388 retained_glyph_uniform_cursor: 0,
4389 #[cfg(target_arch = "wasm32")]
4390 wasm_uniform_batches: Vec::new(),
4391 #[cfg(target_arch = "wasm32")]
4392 wasm_uniform_batch_cursor: 0,
4393 #[cfg(target_arch = "wasm32")]
4394 wasm_shape_batches: Vec::new(),
4395 #[cfg(target_arch = "wasm32")]
4396 wasm_shape_batch_cursor: 0,
4397 #[cfg(target_arch = "wasm32")]
4398 wasm_image_batches: Vec::new(),
4399 #[cfg(target_arch = "wasm32")]
4400 wasm_image_batch_cursor: 0,
4401 image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
4402 MAX_TEXTURE_CACHE_ITEMS,
4403 ),
4404 image_texture_cache_bytes: 0,
4405 text_image_cache: BoundedLruCache::with_capacity_at_least_one(
4406 MAX_TEXT_IMAGE_CACHE_ITEMS,
4407 ),
4408 text_glyph_atlas,
4409 text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
4410 MAX_TEXT_GLYPH_RUN_CACHE_ITEMS,
4411 ),
4412 #[cfg(not(target_arch = "wasm32"))]
4413 text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
4414 MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
4415 ),
4416 text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
4417 MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
4418 ),
4419 text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_CACHE_ITEMS),
4420 scratch_shape_data: Vec::new(),
4421 scratch_gradients: Vec::new(),
4422 scratch_image_vertices: Vec::new(),
4423 scratch_image_indices: Vec::new(),
4424 scratch_image_cmds: Vec::new(),
4425 scratch_glyph_cmds: Vec::new(),
4426 scratch_text_glyph_run: Vec::new(),
4427 scratch_text_glyph_placements: Vec::new(),
4428 scratch_text_glyph_quads: Vec::new(),
4429 scratch_segment_items: Vec::new(),
4430 scratch_effect_ranges: Vec::new(),
4431 scratch_layer_events: Vec::new(),
4432 staged_uploads: StagedBufferUploads::default(),
4433 frame_graph_executor: WgpuFrameGraphExecutor::new(),
4434 deferred_offscreen_releases: Vec::new(),
4435 effect_renderer,
4436 layer_surface_cache: LayerSurfaceCache::new(),
4437 observed_scene_range_cache_misses: BoundedLruCache::with_capacity_at_least_one(
4438 MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES,
4439 ),
4440 shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
4441 MAX_SHADOW_SURFACE_CACHE_ITEMS,
4442 ),
4443 shadow_surface_cache_bytes: 0,
4444 frame_stats: gpu_stats::FrameStats::default(),
4445 last_frame_stats: None,
4446 pending_frame_warmup_frames: 0,
4447 frame_count: 0,
4448 gpu_stats_enabled: gpu_stats_enabled(),
4449 warning_state: RendererWarningState::default(),
4450 #[cfg(not(target_arch = "wasm32"))]
4451 replay_upload_stats: ReplayUploadStats::default(),
4452 #[cfg(not(target_arch = "wasm32"))]
4453 replay_color_patches: Vec::new(),
4454 #[cfg(not(target_arch = "wasm32"))]
4455 color_patch_scratch: Vec::new(),
4456 #[cfg(not(target_arch = "wasm32"))]
4457 replay_ack_confirmations: Vec::new(),
4458 #[cfg(not(target_arch = "wasm32"))]
4459 replay_generation_drops: 0,
4460 #[cfg(not(target_arch = "wasm32"))]
4461 retained_bundle_cache: RetainedBundleCache::new(),
4462 }
4463 }
4464
4465 fn shape_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
4466 let resource = match blend_mode {
4467 BlendMode::DstOut => &self.pipeline_dst_out,
4468 _ => &self.pipeline,
4469 };
4470 resource.get_or_init(self.adapter_backend, || {
4471 create_shape_pipeline(
4472 &self.device,
4473 self.surface_format,
4474 &self.uniform_bind_group_layout,
4475 &self.shape_bind_group_layout,
4476 blend_mode,
4477 self.shape_batch_limits,
4478 )
4479 })
4480 }
4481
4482 #[cfg(not(target_arch = "wasm32"))]
4483 fn mesh_pipeline(&self) -> &wgpu::RenderPipeline {
4484 self.mesh_pipeline.get_or_init(self.adapter_backend, || {
4485 create_mesh_shape_pipeline(
4486 &self.device,
4487 self.surface_format,
4488 &self.uniform_bind_group_layout,
4489 &self.shape_bind_group_layout,
4490 self.shape_batch_limits,
4491 )
4492 })
4493 }
4494
4495 #[cfg(not(target_arch = "wasm32"))]
4496 fn instanced_pipeline<'a>(
4497 &'a self,
4498 instanced: &'a InstancedQuadPipelines,
4499 blend_mode: BlendMode,
4500 ) -> &'a wgpu::RenderPipeline {
4501 let resource = match blend_mode {
4502 BlendMode::DstOut => &instanced.pipeline_dst_out,
4503 _ => &instanced.pipeline,
4504 };
4505 resource.get_or_init(self.adapter_backend, || {
4506 create_instanced_shape_pipeline(
4507 &self.device,
4508 self.surface_format,
4509 &self.uniform_bind_group_layout,
4510 &self.shape_bind_group_layout,
4511 blend_mode,
4512 self.shape_batch_limits,
4513 )
4514 })
4515 }
4516
4517 fn image_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
4518 let resource = match blend_mode {
4519 BlendMode::DstOut => &self.image_pipeline_dst_out,
4520 _ => &self.image_pipeline,
4521 };
4522 resource.get_or_init(self.adapter_backend, || {
4523 create_image_pipeline(
4524 &self.device,
4525 self.surface_format,
4526 &self.uniform_bind_group_layout,
4527 &self.image_bind_group_layout,
4528 blend_mode,
4529 )
4530 })
4531 }
4532
4533 fn glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
4534 self.glyph_atlas_pipeline
4535 .get_or_init(self.adapter_backend, || {
4536 create_glyph_atlas_pipeline(
4537 &self.device,
4538 self.surface_format,
4539 &self.uniform_bind_group_layout,
4540 &self.image_bind_group_layout,
4541 )
4542 })
4543 }
4544
4545 #[cfg(not(target_arch = "wasm32"))]
4546 fn retained_glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
4547 self.retained_glyph_atlas_pipeline
4548 .get_or_init(self.adapter_backend, || {
4549 create_glyph_atlas_pipeline(
4550 &self.device,
4551 self.surface_format,
4552 &self.retained_glyph_uniform_bind_group_layout,
4553 &self.image_bind_group_layout,
4554 )
4555 })
4556 }
4557
4558 fn ensure_image_cached(&mut self, image: &ImageBitmap) -> Result<(), String> {
4559 if self.image_texture_cache.get(&image.id()).is_some() {
4560 return Ok(());
4561 }
4562
4563 let size = wgpu::Extent3d {
4564 width: image.width(),
4565 height: image.height(),
4566 depth_or_array_layers: 1,
4567 };
4568
4569 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
4570 label: Some("Image Texture"),
4571 size,
4572 mip_level_count: 1,
4573 sample_count: 1,
4574 dimension: wgpu::TextureDimension::D2,
4575 format: wgpu::TextureFormat::Rgba8Unorm,
4576 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4577 view_formats: &[],
4578 });
4579
4580 let upload_stats = self.frame_graph_executor.upload_texture(
4581 &self.queue,
4582 wgpu::TexelCopyTextureInfo {
4583 texture: &texture,
4584 mip_level: 0,
4585 origin: wgpu::Origin3d::ZERO,
4586 aspect: wgpu::TextureAspect::All,
4587 },
4588 image.pixels(),
4589 wgpu::TexelCopyBufferLayout {
4590 offset: 0,
4591 bytes_per_row: Some(4 * image.width()),
4592 rows_per_image: Some(image.height()),
4593 },
4594 size,
4595 );
4596 self.frame_stats.record_command_stats(upload_stats);
4597
4598 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4599 let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
4600 let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
4601
4602 let bytes = image.width() as usize * image.height() as usize * 4;
4603 if let Some(replaced) = self.image_texture_cache.put(
4604 image.id(),
4605 CachedImageTexture {
4606 _texture: texture,
4607 _view: view,
4608 nearest_bind_group,
4609 linear_bind_group,
4610 bytes,
4611 },
4612 ) {
4613 self.image_texture_cache_bytes = self
4614 .image_texture_cache_bytes
4615 .saturating_sub(replaced.bytes);
4616 }
4617 self.image_texture_cache_bytes += bytes;
4618 while self.image_texture_cache_bytes > MAX_IMAGE_TEXTURE_CACHE_BYTES
4621 && self.image_texture_cache.len() > 1
4622 {
4623 let Some((_, evicted)) = self.image_texture_cache.pop_lru() else {
4624 break;
4625 };
4626 self.image_texture_cache_bytes =
4627 self.image_texture_cache_bytes.saturating_sub(evicted.bytes);
4628 }
4629 Ok(())
4630 }
4631
4632 fn image_bind_group(
4633 &self,
4634 view: &wgpu::TextureView,
4635 sampler: &wgpu::Sampler,
4636 ) -> wgpu::BindGroup {
4637 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4638 label: Some("Image Texture Bind Group"),
4639 layout: &self.image_bind_group_layout,
4640 entries: &[
4641 wgpu::BindGroupEntry {
4642 binding: 0,
4643 resource: wgpu::BindingResource::TextureView(view),
4644 },
4645 wgpu::BindGroupEntry {
4646 binding: 1,
4647 resource: wgpu::BindingResource::Sampler(sampler),
4648 },
4649 ],
4650 })
4651 }
4652
4653 fn max_texture_dim(&self) -> u32 {
4656 self.effect_renderer.max_texture_dim()
4657 }
4658
4659 fn acquire_offscreen(&mut self, width: u32, height: u32) -> OffscreenTarget {
4660 self.effect_renderer
4661 .acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
4662 }
4663
4664 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
4665 self.acquire_offscreen(width, height)
4666 }
4667
4668 fn transient_offscreen_descriptor(
4669 &self,
4670 label: &'static str,
4671 width: u32,
4672 height: u32,
4673 ) -> FrameTextureDescriptor {
4674 let max_texture_dim = self.max_texture_dim();
4675 FrameTextureDescriptor::render_attachment(
4676 label,
4677 width.min(max_texture_dim),
4678 height.min(max_texture_dim),
4679 self.surface_format,
4680 )
4681 }
4682
4683 fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
4684 self.deferred_offscreen_releases.push(target);
4685 }
4686
4687 fn flush_deferred_offscreen_releases(&mut self) {
4688 for target in self.deferred_offscreen_releases.drain(..) {
4689 self.effect_renderer.release_offscreen(target);
4690 }
4691 }
4692
4693 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
4694 if let LayerSurfaceTexture::Owned(target) = target {
4695 self.defer_offscreen_release(target);
4696 }
4697 }
4698
4699 fn cached_layer_surface(
4700 &mut self,
4701 key: &LayerRasterCacheKey,
4702 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
4703 self.layer_surface_cache.get(key, &self.frame_stats)
4704 }
4705
4706 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
4707 admit_layer_surface_cache_miss_impl(key, &mut self.observed_scene_range_cache_misses)
4708 }
4709
4710 fn insert_cached_layer_surface(
4711 &mut self,
4712 key: LayerRasterCacheKey,
4713 target: OffscreenTarget,
4714 logical_rect: Rect,
4715 ) -> Rc<OffscreenTarget> {
4716 self.layer_surface_cache
4717 .insert(key, target, logical_rect, &self.frame_stats)
4718 }
4719
4720 fn cached_shadow_surface(
4721 &mut self,
4722 key: &ShadowSurfaceCacheKey,
4723 ) -> Option<Rc<OffscreenTarget>> {
4724 self.shadow_surface_cache
4725 .get(key)
4726 .map(|cached| cached.target.clone())
4727 }
4728
4729 fn cached_shape_shadow_composite(
4730 &mut self,
4731 shadow: &ShadowDraw,
4732 width: u32,
4733 height: u32,
4734 root_scale: f32,
4735 ) -> Option<CachedShadowComposite> {
4736 if shadow.blur_radius <= 0.0 || shadow.shapes.is_empty() || !shadow.texts.is_empty() {
4737 return None;
4738 }
4739
4740 let plan = shape_shadow_surface_plan(
4741 &shadow.shapes,
4742 shadow.clip,
4743 shadow.blur_radius,
4744 width,
4745 height,
4746 root_scale,
4747 self.max_texture_dim(),
4748 )?;
4749 let key = shape_shadow_surface_cache_key(
4750 &shadow.shapes,
4751 plan.source_device_bounds,
4752 plan.pixel_radius,
4753 root_scale,
4754 )?;
4755 let cached = self.cached_shadow_surface(&key)?;
4756 let viewport_offset = [plan.source_device_bounds.x, plan.source_device_bounds.y];
4757 self.frame_stats.record_shadow_shape_cache_hit(
4758 plan.source_device_bounds.width,
4759 plan.source_device_bounds.height,
4760 );
4761
4762 let clip_scissor = shadow
4763 .clip
4764 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
4765 let scissor = clip_scissor.or(plan.processing_scissor);
4766 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
4767 mask.rect[0] -= viewport_offset[0];
4768 mask.rect[1] -= viewport_offset[1];
4769 mask
4770 });
4771 let dest_viewport = Some((
4772 viewport_offset[0],
4773 viewport_offset[1],
4774 plan.source_device_bounds.width as f32,
4775 plan.source_device_bounds.height as f32,
4776 ));
4777
4778 Some(CachedShadowComposite {
4779 source: cached,
4780 scissor,
4781 rounded_mask,
4782 dest_viewport,
4783 })
4784 }
4785
4786 fn insert_cached_shadow_surface(
4787 &mut self,
4788 key: ShadowSurfaceCacheKey,
4789 target: OffscreenTarget,
4790 ) {
4791 let byte_size = offscreen_byte_size(target.width, target.height);
4792 while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
4793 let Some((_evicted_key, evicted_entry)) = self.shadow_surface_cache.pop_lru() else {
4794 break;
4795 };
4796 self.shadow_surface_cache_bytes = self
4797 .shadow_surface_cache_bytes
4798 .saturating_sub(evicted_entry.byte_size);
4799 }
4800
4801 let cached = CachedShadowSurface {
4802 target: Rc::new(target),
4803 byte_size,
4804 };
4805 if let Some((_replaced_key, replaced_entry)) = self.shadow_surface_cache.push(key, cached) {
4806 self.shadow_surface_cache_bytes = self
4807 .shadow_surface_cache_bytes
4808 .saturating_sub(replaced_entry.byte_size);
4809 }
4810 self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
4811 }
4812
4813 fn supports_render_effect(&self, effect: &RenderEffect) -> bool {
4814 is_render_effect_supported(effect)
4815 }
4816}
4817
4818struct RecordingSurfaceBackend<'renderer, 'recorder, C: FrameCommandRecorder> {
4819 renderer: &'renderer mut GpuRenderer,
4820 recorder: &'recorder mut C,
4821}
4822
4823impl<C: FrameCommandRecorder> RecordingSurfaceBackend<'_, '_, C> {
4824 #[allow(clippy::too_many_arguments)]
4825 fn render_range_with_layer_events_to_target_recorded(
4826 &mut self,
4827 target: &OffscreenTarget,
4828 shapes: &[DrawShape],
4829 images: &[ImageDraw],
4830 texts: &[TextDraw],
4831 shadow_draws: &[ShadowDraw],
4832 draw_ops: &[DrawOp],
4833 effect_layers: &[EffectLayer],
4834 backdrop_layers: &[BackdropLayer],
4835 z_start: usize,
4836 z_end: usize,
4837 excluded_effect_layer: Option<usize>,
4838 width: u32,
4839 height: u32,
4840 root_scale: f32,
4841 backdrop_underlay: Option<&OffscreenTarget>,
4842 initial_load_op: wgpu::LoadOp<wgpu::Color>,
4843 ) -> Result<(), String> {
4844 if z_start >= z_end {
4845 if matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
4846 self.clear_target_view_with_load_op(&target.view, initial_load_op);
4847 }
4848 return Ok(());
4849 }
4850
4851 let mut effect_z_ranges = std::mem::take(&mut self.renderer.scratch_effect_ranges);
4852 collect_effect_ranges(
4853 effect_layers,
4854 z_start,
4855 z_end,
4856 excluded_effect_layer,
4857 &mut effect_z_ranges,
4858 );
4859 let mut events = std::mem::take(&mut self.renderer.scratch_layer_events);
4860 collect_layer_events(
4861 effect_layers,
4862 backdrop_layers,
4863 z_start,
4864 z_end,
4865 excluded_effect_layer,
4866 &mut events,
4867 );
4868
4869 let result = (|| -> Result<(), String> {
4870 let mut next_load_op = initial_load_op;
4871 let mut cursor_z = z_start;
4872 for event in &events {
4873 if event.z_index > cursor_z {
4874 self.render_non_effect_segment(
4875 &target.view,
4876 shapes,
4877 images,
4878 texts,
4879 shadow_draws,
4880 &[],
4883 draw_ops,
4884 cursor_z,
4885 event.z_index,
4886 &effect_z_ranges,
4887 width,
4888 height,
4889 root_scale,
4890 next_load_op,
4891 )?;
4892 next_load_op = wgpu::LoadOp::Load;
4893 cursor_z = event.z_index;
4894 } else if event.z_index < cursor_z {
4895 continue;
4896 }
4897
4898 if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
4899 self.clear_target_view_with_load_op(&target.view, next_load_op);
4900 next_load_op = wgpu::LoadOp::Load;
4901 }
4902
4903 match event.kind {
4904 LayerEventKind::Backdrop(index) => {
4905 let layer = &backdrop_layers[index];
4906 let effective_backdrop_underlay = if backdrop_underlay.is_some()
4907 && backdrop_underlay_is_covered_by_local_content(
4908 shapes,
4909 images,
4910 shadow_draws,
4911 draw_ops,
4912 effect_layers,
4913 backdrop_layers,
4914 layer,
4915 ) {
4916 None
4917 } else {
4918 backdrop_underlay
4919 };
4920 execute_apply_backdrop_layer_to_target(
4921 self,
4922 target,
4923 layer,
4924 effective_backdrop_underlay,
4925 width,
4926 height,
4927 root_scale,
4928 None,
4929 )?;
4930 }
4931 LayerEventKind::Effect(index) => {
4932 let layer = &effect_layers[index];
4933 if layer.z_start < cursor_z {
4934 continue;
4935 }
4936 execute_render_effect_layer_to_target(
4937 self,
4938 target,
4939 shapes,
4940 images,
4941 texts,
4942 shadow_draws,
4943 draw_ops,
4944 effect_layers,
4945 backdrop_layers,
4946 index,
4947 backdrop_underlay,
4948 width,
4949 height,
4950 root_scale,
4951 )?;
4952 cursor_z = cursor_z.max(layer.z_end);
4953 }
4954 }
4955 }
4956
4957 if cursor_z < z_end {
4958 self.render_non_effect_segment(
4959 &target.view,
4960 shapes,
4961 images,
4962 texts,
4963 shadow_draws,
4964 &[],
4965 draw_ops,
4966 cursor_z,
4967 z_end,
4968 &effect_z_ranges,
4969 width,
4970 height,
4971 root_scale,
4972 next_load_op,
4973 )?;
4974 } else if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
4975 self.clear_target_view_with_load_op(&target.view, next_load_op);
4976 }
4977
4978 Ok(())
4979 })();
4980
4981 self.renderer.scratch_effect_ranges = effect_z_ranges;
4982 self.renderer.scratch_layer_events = events;
4983 result
4984 }
4985
4986 #[allow(clippy::too_many_arguments)]
4987 fn record_shader_composite(
4988 &mut self,
4989 source: &OffscreenTarget,
4990 shader: &RuntimeShader,
4991 effect_rect: [f32; 4],
4992 dest_view: &wgpu::TextureView,
4993 alpha: f32,
4994 load_op: wgpu::LoadOp<wgpu::Color>,
4995 scissor: Option<(u32, u32, u32, u32)>,
4996 blend_mode: BlendMode,
4997 dest_viewport: Option<(f32, f32, f32, f32)>,
4998 sample_mode: CompositeSampleMode,
4999 ) {
5000 let device = self.renderer.device.clone();
5001 if let Some(viewport) = direct_shader_composite_viewport(
5002 alpha,
5003 blend_mode,
5004 dest_viewport,
5005 sample_mode,
5006 (source.width, source.height),
5007 ) {
5008 let shader_applied = self
5009 .renderer
5010 .effect_renderer
5011 .encode_shader_src_over_to_view(
5012 self.recorder,
5013 &device,
5014 source,
5015 dest_view,
5016 shader,
5017 effect_rect,
5018 load_op,
5019 scissor,
5020 viewport,
5021 );
5022 if shader_applied {
5023 self.renderer
5024 .effect_renderer
5025 .debug_effects
5026 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5027 self.recorder.record_pass();
5028 self.renderer.effect_renderer.record_composite_pass();
5029 return;
5030 }
5031 }
5032 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5033 "Shader Effect Composite Scratch",
5034 source.width,
5035 source.height,
5036 );
5037 let scratch = self
5038 .recorder
5039 .acquire_transient_offscreen(&device, scratch_descriptor);
5040 let shader_applied = {
5041 self.renderer.effect_renderer.encode_shader(
5042 self.recorder,
5043 &device,
5044 source,
5045 &scratch.view,
5046 shader,
5047 effect_rect,
5048 )
5049 };
5050 let composite_source = if shader_applied {
5051 self.renderer
5052 .effect_renderer
5053 .debug_effects
5054 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5055 self.recorder.record_pass();
5056 &scratch
5057 } else {
5058 source
5059 };
5060 {
5061 self.renderer
5062 .effect_renderer
5063 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
5064 self.recorder,
5065 &device,
5066 composite_source,
5067 dest_view,
5068 alpha,
5069 load_op,
5070 scissor,
5071 None,
5072 supported_blend_mode(blend_mode),
5073 dest_viewport,
5074 sample_mode,
5075 );
5076 }
5077 self.recorder.record_pass();
5078 self.renderer.effect_renderer.record_composite_pass();
5079 self.recorder
5080 .release_transient_offscreen(scratch_descriptor, scratch);
5081 }
5082
5083 #[allow(clippy::too_many_arguments)]
5084 fn record_shader_projective_composite(
5085 &mut self,
5086 source: &OffscreenTarget,
5087 shader: &RuntimeShader,
5088 effect_rect: [f32; 4],
5089 dest_view: &wgpu::TextureView,
5090 viewport: (u32, u32),
5091 source_size: (f32, f32),
5092 inverse_matrix: [[f32; 3]; 3],
5093 dest_bounds: [[f32; 2]; 4],
5094 alpha: f32,
5095 load_op: wgpu::LoadOp<wgpu::Color>,
5096 scissor: Option<(u32, u32, u32, u32)>,
5097 blend_mode: BlendMode,
5098 sample_mode: CompositeSampleMode,
5099 ) {
5100 if projective_dest_bounds_rect(dest_bounds).is_none() {
5101 return;
5102 }
5103 let device = self.renderer.device.clone();
5104 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5105 "Shader Projective Composite Scratch",
5106 source.width,
5107 source.height,
5108 );
5109 let scratch = self
5110 .recorder
5111 .acquire_transient_offscreen(&device, scratch_descriptor);
5112 let shader_applied = {
5113 self.renderer.effect_renderer.encode_shader(
5114 self.recorder,
5115 &device,
5116 source,
5117 &scratch.view,
5118 shader,
5119 effect_rect,
5120 )
5121 };
5122 let composite_source = if shader_applied {
5123 self.renderer
5124 .effect_renderer
5125 .debug_effects
5126 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5127 self.recorder.record_pass();
5128 &scratch
5129 } else {
5130 source
5131 };
5132 let composited = {
5133 self.renderer
5134 .effect_renderer
5135 .encode_composite_to_view_projective(
5136 self.recorder,
5137 &device,
5138 composite_source,
5139 dest_view,
5140 viewport,
5141 source_size,
5142 inverse_matrix,
5143 dest_bounds,
5144 alpha,
5145 load_op,
5146 scissor,
5147 supported_blend_mode(blend_mode),
5148 sample_mode,
5149 )
5150 };
5151 if composited {
5152 self.recorder.record_pass();
5153 self.renderer.effect_renderer.record_composite_pass();
5154 }
5155 self.recorder
5156 .release_transient_offscreen(scratch_descriptor, scratch);
5157 }
5158
5159 #[allow(clippy::too_many_arguments)]
5160 fn record_effect_with_direct_shader_tail_composite(
5161 &mut self,
5162 source: &OffscreenTarget,
5163 first_effect: &RenderEffect,
5164 shader: &RuntimeShader,
5165 effect_rect: [f32; 4],
5166 dest_view: &wgpu::TextureView,
5167 load_op: wgpu::LoadOp<wgpu::Color>,
5168 scissor: Option<(u32, u32, u32, u32)>,
5169 dest_viewport: (f32, f32, f32, f32),
5170 ) -> Result<bool, String> {
5171 let device = self.renderer.device.clone();
5172 let intermediate_descriptor = self.renderer.transient_offscreen_descriptor(
5173 "Render Effect Direct Shader Tail Intermediate",
5174 source.width,
5175 source.height,
5176 );
5177 let intermediate = self
5178 .recorder
5179 .acquire_transient_offscreen(&device, intermediate_descriptor);
5180 let effect_scratch_targets = self
5181 .renderer
5182 .effect_renderer
5183 .acquire_recorded_effect_scratch_targets(
5184 self.recorder,
5185 &device,
5186 first_effect,
5187 source.width,
5188 source.height,
5189 self.renderer.surface_format,
5190 );
5191 let first_passes = {
5192 let mut effect_scratch_refs = effect_scratch_targets.refs();
5193 let pass_count = self.renderer.effect_renderer.encode_effect(
5194 self.recorder,
5195 &device,
5196 source,
5197 &intermediate.view,
5198 first_effect,
5199 effect_rect,
5200 &mut effect_scratch_refs,
5201 );
5202 match pass_count {
5203 Ok(pass_count) => effect_scratch_refs.assert_consumed().map(|()| pass_count),
5204 Err(error) => Err(error),
5205 }
5206 };
5207 let first_passes = match first_passes {
5208 Ok(pass_count) => pass_count,
5209 Err(error) => {
5210 effect_scratch_targets.release_into(self.recorder);
5211 self.recorder
5212 .release_transient_offscreen(intermediate_descriptor, intermediate);
5213 return Err(error);
5214 }
5215 };
5216 let shader_applied = self
5217 .renderer
5218 .effect_renderer
5219 .encode_shader_src_over_to_view(
5220 self.recorder,
5221 &device,
5222 &intermediate,
5223 dest_view,
5224 shader,
5225 effect_rect,
5226 load_op,
5227 scissor,
5228 dest_viewport,
5229 );
5230 self.recorder
5231 .record_passes(first_passes.saturating_add(u32::from(shader_applied)));
5232 effect_scratch_targets.release_into(self.recorder);
5233 self.recorder
5234 .release_transient_offscreen(intermediate_descriptor, intermediate);
5235 if !shader_applied {
5236 return Ok(false);
5237 }
5238 self.renderer
5239 .effect_renderer
5240 .debug_effects
5241 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5242 self.renderer.effect_renderer.record_composite_pass();
5243 Ok(true)
5244 }
5245
5246 #[allow(clippy::too_many_arguments)]
5247 fn record_effect_composite(
5248 &mut self,
5249 source: &OffscreenTarget,
5250 effect: &RenderEffect,
5251 effect_rect: [f32; 4],
5252 dest_view: &wgpu::TextureView,
5253 alpha: f32,
5254 load_op: wgpu::LoadOp<wgpu::Color>,
5255 scissor: Option<(u32, u32, u32, u32)>,
5256 blend_mode: BlendMode,
5257 dest_viewport: Option<(f32, f32, f32, f32)>,
5258 sample_mode: CompositeSampleMode,
5259 ) -> Result<(), String> {
5260 if let (
5261 RenderEffect::Chain { first, second },
5262 Some(viewport),
5263 BlendMode::SrcOver,
5264 CompositeSampleMode::Linear,
5265 ) = (
5266 effect,
5267 dest_viewport,
5268 supported_blend_mode(blend_mode),
5269 sample_mode,
5270 ) {
5271 if let (
5272 RenderEffect::Blur {
5273 radius_x,
5274 radius_y,
5275 edge_treatment,
5276 },
5277 RenderEffect::Shader { shader },
5278 ) = (first.as_ref(), second.as_ref())
5279 {
5280 if *radius_x > 0.0 || *radius_y > 0.0 {
5281 let device = self.renderer.device.clone();
5282 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5283 "Blur Rounded Mask Scratch",
5284 source.width,
5285 source.height,
5286 );
5287 let scratch = self
5288 .recorder
5289 .acquire_transient_offscreen(&device, scratch_descriptor);
5290 let fused = self
5291 .renderer
5292 .effect_renderer
5293 .encode_blur_then_rounded_mask_src_over_to_view(
5294 self.recorder,
5295 &device,
5296 source,
5297 &scratch,
5298 dest_view,
5299 *radius_x,
5300 *radius_y,
5301 *edge_treatment,
5302 shader,
5303 effect_rect,
5304 load_op,
5305 scissor,
5306 viewport,
5307 );
5308 if fused {
5309 self.recorder.record_passes(2);
5310 self.renderer.effect_renderer.record_blur_pass();
5311 self.renderer
5312 .effect_renderer
5313 .debug_effects
5314 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5315 self.renderer.effect_renderer.record_composite_pass();
5316 self.recorder
5317 .release_transient_offscreen(scratch_descriptor, scratch);
5318 return Ok(());
5319 }
5320 self.recorder
5321 .release_transient_offscreen(scratch_descriptor, scratch);
5322 }
5323 }
5324 }
5325 if let Some((first_effect, shader, viewport)) = direct_shader_tail_composite(
5326 effect,
5327 alpha,
5328 blend_mode,
5329 dest_viewport,
5330 sample_mode,
5331 (source.width, source.height),
5332 ) {
5333 if self.record_effect_with_direct_shader_tail_composite(
5334 source,
5335 first_effect,
5336 shader,
5337 effect_rect,
5338 dest_view,
5339 load_op,
5340 scissor,
5341 viewport,
5342 )? {
5343 return Ok(());
5344 }
5345 }
5346 let device = self.renderer.device.clone();
5347 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5348 "Render Effect Composite Scratch",
5349 source.width,
5350 source.height,
5351 );
5352 let scratch = self
5353 .recorder
5354 .acquire_transient_offscreen(&device, scratch_descriptor);
5355 let effect_scratch_targets = self
5356 .renderer
5357 .effect_renderer
5358 .acquire_recorded_effect_scratch_targets(
5359 self.recorder,
5360 &device,
5361 effect,
5362 source.width,
5363 source.height,
5364 self.renderer.surface_format,
5365 );
5366 let effect_passes = {
5367 let mut effect_scratch_refs = effect_scratch_targets.refs();
5368 let pass_count = self.renderer.effect_renderer.encode_effect(
5369 self.recorder,
5370 &device,
5371 source,
5372 &scratch.view,
5373 effect,
5374 effect_rect,
5375 &mut effect_scratch_refs,
5376 )?;
5377 effect_scratch_refs.assert_consumed()?;
5378 Ok(pass_count)
5379 };
5380 let effect_passes = match effect_passes {
5381 Ok(pass_count) => pass_count,
5382 Err(error) => {
5383 effect_scratch_targets.release_into(self.recorder);
5384 self.recorder
5385 .release_transient_offscreen(scratch_descriptor, scratch);
5386 return Err(error);
5387 }
5388 };
5389 {
5390 self.renderer
5391 .effect_renderer
5392 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
5393 self.recorder,
5394 &device,
5395 &scratch,
5396 dest_view,
5397 alpha,
5398 load_op,
5399 scissor,
5400 None,
5401 supported_blend_mode(blend_mode),
5402 dest_viewport,
5403 sample_mode,
5404 );
5405 }
5406 self.recorder.record_passes(effect_passes.saturating_add(1));
5407 self.renderer.effect_renderer.record_composite_pass();
5408 effect_scratch_targets.release_into(self.recorder);
5409 self.recorder
5410 .release_transient_offscreen(scratch_descriptor, scratch);
5411 Ok(())
5412 }
5413
5414 #[allow(clippy::too_many_arguments)]
5415 fn record_effect_projective_composite(
5416 &mut self,
5417 source: &OffscreenTarget,
5418 effect: &RenderEffect,
5419 effect_rect: [f32; 4],
5420 dest_view: &wgpu::TextureView,
5421 viewport: (u32, u32),
5422 source_size: (f32, f32),
5423 inverse_matrix: [[f32; 3]; 3],
5424 dest_bounds: [[f32; 2]; 4],
5425 alpha: f32,
5426 load_op: wgpu::LoadOp<wgpu::Color>,
5427 scissor: Option<(u32, u32, u32, u32)>,
5428 blend_mode: BlendMode,
5429 sample_mode: CompositeSampleMode,
5430 ) -> Result<(), String> {
5431 if projective_dest_bounds_rect(dest_bounds).is_none() {
5432 return Ok(());
5433 }
5434 let device = self.renderer.device.clone();
5435 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5436 "Render Effect Projective Composite Scratch",
5437 source.width,
5438 source.height,
5439 );
5440 let scratch = self
5441 .recorder
5442 .acquire_transient_offscreen(&device, scratch_descriptor);
5443 let effect_scratch_targets = self
5444 .renderer
5445 .effect_renderer
5446 .acquire_recorded_effect_scratch_targets(
5447 self.recorder,
5448 &device,
5449 effect,
5450 source.width,
5451 source.height,
5452 self.renderer.surface_format,
5453 );
5454 let effect_passes = {
5455 let mut effect_scratch_refs = effect_scratch_targets.refs();
5456 let pass_count = self.renderer.effect_renderer.encode_effect(
5457 self.recorder,
5458 &device,
5459 source,
5460 &scratch.view,
5461 effect,
5462 effect_rect,
5463 &mut effect_scratch_refs,
5464 )?;
5465 effect_scratch_refs.assert_consumed()?;
5466 Ok(pass_count)
5467 };
5468 let effect_passes = match effect_passes {
5469 Ok(pass_count) => pass_count,
5470 Err(error) => {
5471 effect_scratch_targets.release_into(self.recorder);
5472 self.recorder
5473 .release_transient_offscreen(scratch_descriptor, scratch);
5474 return Err(error);
5475 }
5476 };
5477 let composited = {
5478 self.renderer
5479 .effect_renderer
5480 .encode_composite_to_view_projective(
5481 self.recorder,
5482 &device,
5483 &scratch,
5484 dest_view,
5485 viewport,
5486 source_size,
5487 inverse_matrix,
5488 dest_bounds,
5489 alpha,
5490 load_op,
5491 scissor,
5492 supported_blend_mode(blend_mode),
5493 sample_mode,
5494 )
5495 };
5496 if composited {
5497 self.recorder.record_passes(effect_passes.saturating_add(1));
5498 self.renderer.effect_renderer.record_composite_pass();
5499 } else {
5500 self.recorder.record_passes(effect_passes);
5501 }
5502 effect_scratch_targets.release_into(self.recorder);
5503 self.recorder
5504 .release_transient_offscreen(scratch_descriptor, scratch);
5505 Ok(())
5506 }
5507}
5508
5509impl<C: FrameCommandRecorder> SurfaceExecutionBackend for RecordingSurfaceBackend<'_, '_, C> {
5510 fn max_texture_dim(&self) -> u32 {
5511 self.renderer.max_texture_dim()
5512 }
5513
5514 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
5515 self.renderer.acquire_retained_surface(width, height)
5516 }
5517
5518 fn acquire_frame_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
5519 let descriptor =
5520 self.renderer
5521 .transient_offscreen_descriptor("Frame Surface", width, height);
5522 self.recorder
5523 .acquire_transient_offscreen(&self.renderer.device, descriptor)
5524 }
5525
5526 fn release_frame_surface(&mut self, target: OffscreenTarget) {
5527 let descriptor = self.renderer.transient_offscreen_descriptor(
5528 "Frame Surface",
5529 target.width,
5530 target.height,
5531 );
5532 self.recorder
5533 .release_transient_offscreen(descriptor, target);
5534 }
5535
5536 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
5537 self.renderer.release_layer_surface_target(target);
5538 }
5539
5540 fn cached_layer_surface(
5541 &mut self,
5542 key: &LayerRasterCacheKey,
5543 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
5544 self.renderer.cached_layer_surface(key)
5545 }
5546
5547 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
5548 self.renderer.admit_layer_surface_cache_miss(key)
5549 }
5550
5551 fn insert_cached_layer_surface(
5552 &mut self,
5553 key: LayerRasterCacheKey,
5554 target: OffscreenTarget,
5555 logical_rect: Rect,
5556 ) -> Rc<OffscreenTarget> {
5557 self.renderer
5558 .insert_cached_layer_surface(key, target, logical_rect)
5559 }
5560
5561 fn clear_target_view_with_load_op(
5562 &mut self,
5563 target_view: &wgpu::TextureView,
5564 load_op: wgpu::LoadOp<wgpu::Color>,
5565 ) {
5566 {
5567 let _clear = self
5568 .recorder
5569 .encoder()
5570 .begin_render_pass(&wgpu::RenderPassDescriptor {
5571 label: Some("Layer Event Clear Pass"),
5572 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
5573 view: target_view,
5574 resolve_target: None,
5575 depth_slice: None,
5576 ops: wgpu::Operations {
5577 load: load_op,
5578 store: wgpu::StoreOp::Store,
5579 },
5580 })],
5581 depth_stencil_attachment: None,
5582 timestamp_writes: None,
5583 occlusion_query_set: None,
5584 multiview_mask: None,
5585 });
5586 }
5587 self.recorder.record_pass();
5588 }
5589
5590 #[allow(clippy::too_many_arguments)]
5591 fn render_non_effect_segment(
5592 &mut self,
5593 target_view: &wgpu::TextureView,
5594 shapes: &[DrawShape],
5595 images: &[ImageDraw],
5596 texts: &[TextDraw],
5597 shadow_draws: &[ShadowDraw],
5598 retained_draws: &[RetainedDraw],
5599 draw_ops: &[DrawOp],
5600 z_start: usize,
5601 z_end: usize,
5602 effect_z_ranges: &[Range<usize>],
5603 width: u32,
5604 height: u32,
5605 root_scale: f32,
5606 initial_load_op: wgpu::LoadOp<wgpu::Color>,
5607 ) -> Result<(), String> {
5608 self.render_non_effect_segment_with_composites(
5609 target_view,
5610 shapes,
5611 images,
5612 texts,
5613 shadow_draws,
5614 retained_draws,
5615 draw_ops,
5616 z_start,
5617 z_end,
5618 effect_z_ranges,
5619 &[],
5620 &[],
5621 width,
5622 height,
5623 root_scale,
5624 initial_load_op,
5625 )
5626 }
5627
5628 #[allow(clippy::too_many_arguments)]
5629 fn render_non_effect_segment_with_composites(
5630 &mut self,
5631 target_view: &wgpu::TextureView,
5632 shapes: &[DrawShape],
5633 images: &[ImageDraw],
5634 texts: &[TextDraw],
5635 shadow_draws: &[ShadowDraw],
5636 retained_draws: &[RetainedDraw],
5637 draw_ops: &[DrawOp],
5638 z_start: usize,
5639 z_end: usize,
5640 effect_z_ranges: &[Range<usize>],
5641 composites: &[(usize, CompositeBatchItem<'_>)],
5642 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
5643 width: u32,
5644 height: u32,
5645 root_scale: f32,
5646 initial_load_op: wgpu::LoadOp<wgpu::Color>,
5647 ) -> Result<(), String> {
5648 let mut ordered_items = std::mem::take(&mut self.renderer.scratch_segment_items);
5649 collect_non_effect_segment_items(
5650 shapes,
5651 images,
5652 texts,
5653 shadow_draws,
5654 draw_ops,
5655 z_start,
5656 z_end,
5657 effect_z_ranges,
5658 width,
5659 height,
5660 root_scale,
5661 &mut ordered_items,
5662 );
5663 #[cfg(not(target_arch = "wasm32"))]
5664 let raw_shadow_items = ordered_items
5665 .iter()
5666 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
5667 .count();
5668 let culled_shadow_items = retain_renderable_shadow_items(
5669 &mut ordered_items,
5670 shadow_draws,
5671 width,
5672 height,
5673 root_scale,
5674 self.renderer.max_texture_dim(),
5675 );
5676 #[cfg(target_arch = "wasm32")]
5677 let _ = culled_shadow_items;
5678 let mut cached_shadow_composites: Vec<(usize, CachedShadowComposite)> = Vec::new();
5679 ordered_items.extend(
5680 composites
5681 .iter()
5682 .enumerate()
5683 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::Composite(index))),
5684 );
5685 ordered_items.extend(
5686 shader_composites
5687 .iter()
5688 .enumerate()
5689 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::ShaderComposite(index))),
5690 );
5691 for (z_index, item) in &mut ordered_items {
5692 let SegmentDrawItem::Shadow(shadow_index) = *item else {
5693 continue;
5694 };
5695 let Some(composite) = self.renderer.cached_shape_shadow_composite(
5696 &shadow_draws[shadow_index],
5697 width,
5698 height,
5699 root_scale,
5700 ) else {
5701 continue;
5702 };
5703 let composite_index = composites.len() + cached_shadow_composites.len();
5704 cached_shadow_composites.push((*z_index, composite));
5705 *item = SegmentDrawItem::Composite(composite_index);
5706 }
5707 let mut merged_composites = Vec::with_capacity(
5708 composites
5709 .len()
5710 .saturating_add(cached_shadow_composites.len()),
5711 );
5712 merged_composites.extend(composites.iter().copied());
5713 merged_composites.extend(
5714 cached_shadow_composites
5715 .iter()
5716 .map(|(z_index, composite)| (*z_index, composite.batch_item())),
5717 );
5718 ordered_items.sort_unstable_by_key(|(z_index, _)| *z_index);
5722 #[cfg(not(target_arch = "wasm32"))]
5723 maybe_print_segment_diag(
5724 z_start..z_end,
5725 &ordered_items,
5726 shapes,
5727 images,
5728 SegmentDiagCounts {
5729 raw_shadow_items,
5730 culled_shadow_items,
5731 cached_shadow_composites: cached_shadow_composites.len(),
5732 composite_items: merged_composites.len(),
5733 shader_composite_items: shader_composites.len(),
5734 },
5735 self.renderer.shape_batch_limits,
5736 );
5737 let result = if ordered_items.is_empty() {
5738 Ok(SegmentCommandEncodeOutcome { first_batch: true })
5739 } else {
5740 self.renderer.encode_non_effect_segment_commands(
5741 self.recorder,
5742 target_view,
5743 &ordered_items,
5744 &merged_composites,
5745 shader_composites,
5746 shapes,
5747 images,
5748 texts,
5749 shadow_draws,
5750 retained_draws,
5751 initial_load_op,
5752 width,
5753 height,
5754 root_scale,
5755 )
5756 };
5757 self.renderer.scratch_segment_items = ordered_items;
5758 let outcome = result?;
5759 if outcome.first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
5760 self.clear_target_view_with_load_op(target_view, initial_load_op);
5761 }
5762 Ok(())
5763 }
5764
5765 fn render_range_with_layer_events_to_target(
5766 &mut self,
5767 target: &OffscreenTarget,
5768 shapes: &[DrawShape],
5769 images: &[ImageDraw],
5770 texts: &[TextDraw],
5771 shadow_draws: &[ShadowDraw],
5772 draw_ops: &[DrawOp],
5773 effect_layers: &[EffectLayer],
5774 backdrop_layers: &[BackdropLayer],
5775 z_start: usize,
5776 z_end: usize,
5777 excluded_effect_layer: Option<usize>,
5778 width: u32,
5779 height: u32,
5780 root_scale: f32,
5781 backdrop_underlay: Option<&OffscreenTarget>,
5782 initial_load_op: wgpu::LoadOp<wgpu::Color>,
5783 ) -> Result<(), String> {
5784 self.render_range_with_layer_events_to_target_recorded(
5785 target,
5786 shapes,
5787 images,
5788 texts,
5789 shadow_draws,
5790 draw_ops,
5791 effect_layers,
5792 backdrop_layers,
5793 z_start,
5794 z_end,
5795 excluded_effect_layer,
5796 width,
5797 height,
5798 root_scale,
5799 backdrop_underlay,
5800 initial_load_op,
5801 )
5802 }
5803
5804 fn render_shadow_draw(
5805 &mut self,
5806 target_view: &wgpu::TextureView,
5807 shadow: &ShadowDraw,
5808 width: u32,
5809 height: u32,
5810 root_scale: f32,
5811 ) {
5812 self.renderer.encode_shadow_draw(
5813 self.recorder,
5814 target_view,
5815 shadow,
5816 width,
5817 height,
5818 root_scale,
5819 );
5820 }
5821
5822 fn composite_to_view_projective(
5823 &mut self,
5824 source: &OffscreenTarget,
5825 dest_view: &wgpu::TextureView,
5826 viewport: (u32, u32),
5827 source_size: (f32, f32),
5828 inverse_matrix: [[f32; 3]; 3],
5829 dest_bounds: [[f32; 2]; 4],
5830 alpha: f32,
5831 load_op: wgpu::LoadOp<wgpu::Color>,
5832 scissor: Option<(u32, u32, u32, u32)>,
5833 blend_mode: BlendMode,
5834 sample_mode: CompositeSampleMode,
5835 ) {
5836 let device = self.renderer.device.clone();
5837 let composited = {
5838 self.renderer
5839 .effect_renderer
5840 .encode_composite_to_view_projective(
5841 self.recorder,
5842 &device,
5843 source,
5844 dest_view,
5845 viewport,
5846 source_size,
5847 inverse_matrix,
5848 dest_bounds,
5849 alpha,
5850 load_op,
5851 scissor,
5852 supported_blend_mode(blend_mode),
5853 sample_mode,
5854 )
5855 };
5856 if composited {
5857 self.recorder.record_pass();
5858 self.renderer.effect_renderer.record_composite_pass();
5859 }
5860 }
5861
5862 fn composite_projective_surfaces_to_view(
5863 &mut self,
5864 dest_view: &wgpu::TextureView,
5865 viewport: (u32, u32),
5866 composites: &[ProjectiveSurfaceComposite<'_>],
5867 ) {
5868 let device = self.renderer.device.clone();
5869 let mut composite_count = 0_u32;
5870 for composite in composites
5871 .iter()
5872 .copied()
5873 .filter(|composite| projective_dest_bounds_rect(composite.dest_bounds).is_some())
5874 {
5875 let composited = {
5876 self.renderer
5877 .effect_renderer
5878 .encode_composite_to_view_projective(
5879 self.recorder,
5880 &device,
5881 composite.source,
5882 dest_view,
5883 viewport,
5884 composite.source_size,
5885 composite.inverse_matrix,
5886 composite.dest_bounds,
5887 composite.alpha,
5888 composite.load_op,
5889 composite.scissor,
5890 supported_blend_mode(composite.blend_mode),
5891 composite.sample_mode,
5892 )
5893 };
5894 if composited {
5895 composite_count = composite_count.saturating_add(1);
5896 }
5897 }
5898 if composite_count > 0 {
5899 self.recorder.record_passes(composite_count);
5900 self.renderer
5901 .effect_renderer
5902 .debug_composites
5903 .set(self.renderer.effect_renderer.debug_composites.get() + composite_count);
5904 }
5905 }
5906
5907 fn composite_surface_batch_to_view(
5908 &mut self,
5909 dest_view: &wgpu::TextureView,
5910 viewport: (u32, u32),
5911 load_op: wgpu::LoadOp<wgpu::Color>,
5912 composites: &[CompositeBatchItem<'_>],
5913 ) {
5914 if composites.is_empty() {
5915 return;
5916 }
5917 let device = self.renderer.device.clone();
5918 self.renderer
5919 .effect_renderer
5920 .encode_composite_batch_to_view_pass(
5921 self.recorder,
5922 &device,
5923 dest_view,
5924 viewport,
5925 load_op,
5926 composites,
5927 );
5928 self.recorder.record_pass();
5929 self.renderer.effect_renderer.record_composite_pass();
5930 }
5931
5932 fn copy_texture_region_to_target(
5933 &mut self,
5934 source: &OffscreenTarget,
5935 source_origin: (u32, u32),
5936 target: &OffscreenTarget,
5937 size: (u32, u32),
5938 ) -> bool {
5939 let (width, height) = size;
5940 if width == 0 || height == 0 || width > target.width || height > target.height {
5941 return false;
5942 }
5943 let Some(source_right) = source_origin.0.checked_add(width) else {
5944 return false;
5945 };
5946 let Some(source_bottom) = source_origin.1.checked_add(height) else {
5947 return false;
5948 };
5949 if source_right > source.width || source_bottom > source.height {
5950 return false;
5951 }
5952
5953 self.recorder.encoder().copy_texture_to_texture(
5954 wgpu::TexelCopyTextureInfo {
5955 texture: source.texture(),
5956 mip_level: 0,
5957 origin: wgpu::Origin3d {
5958 x: source_origin.0,
5959 y: source_origin.1,
5960 z: 0,
5961 },
5962 aspect: wgpu::TextureAspect::All,
5963 },
5964 wgpu::TexelCopyTextureInfo {
5965 texture: target.texture(),
5966 mip_level: 0,
5967 origin: wgpu::Origin3d::ZERO,
5968 aspect: wgpu::TextureAspect::All,
5969 },
5970 wgpu::Extent3d {
5971 width,
5972 height,
5973 depth_or_array_layers: 1,
5974 },
5975 );
5976 true
5977 }
5978
5979 fn shader_composite_batch_to_view(
5980 &mut self,
5981 dest_view: &wgpu::TextureView,
5982 viewport: (u32, u32),
5983 load_op: wgpu::LoadOp<wgpu::Color>,
5984 composites: &[ShaderCompositeBatchItem<'_>],
5985 ) -> bool {
5986 if composites.is_empty() {
5987 return true;
5988 }
5989 let device = self.renderer.device.clone();
5990 let encoded = self
5991 .renderer
5992 .effect_renderer
5993 .encode_shader_batch_src_over_to_view(
5994 self.recorder,
5995 &device,
5996 dest_view,
5997 viewport,
5998 load_op,
5999 composites,
6000 );
6001 if encoded {
6002 self.recorder.record_pass();
6003 self.renderer.effect_renderer.record_composite_pass();
6004 self.renderer
6005 .effect_renderer
6006 .debug_effects
6007 .set(self.renderer.effect_renderer.debug_effects.get() + composites.len() as u32);
6008 }
6009 encoded
6010 }
6011
6012 fn composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
6013 &mut self,
6014 source: &OffscreenTarget,
6015 dest_view: &wgpu::TextureView,
6016 alpha: f32,
6017 load_op: wgpu::LoadOp<wgpu::Color>,
6018 scissor: Option<(u32, u32, u32, u32)>,
6019 rounded_mask: Option<RoundedCompositeMask>,
6020 blend_mode: BlendMode,
6021 dest_viewport: Option<(f32, f32, f32, f32)>,
6022 sample_mode: CompositeSampleMode,
6023 ) {
6024 let device = self.renderer.device.clone();
6025 {
6026 self.renderer
6027 .effect_renderer
6028 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
6029 self.recorder,
6030 &device,
6031 source,
6032 dest_view,
6033 alpha,
6034 load_op,
6035 scissor,
6036 rounded_mask,
6037 supported_blend_mode(blend_mode),
6038 dest_viewport,
6039 sample_mode,
6040 );
6041 }
6042 self.recorder.record_pass();
6043 self.renderer.effect_renderer.record_composite_pass();
6044 }
6045
6046 fn apply_effect_and_composite_to_view(
6047 &mut self,
6048 source: &OffscreenTarget,
6049 effect: &RenderEffect,
6050 effect_rect: [f32; 4],
6051 dest_view: &wgpu::TextureView,
6052 alpha: f32,
6053 load_op: wgpu::LoadOp<wgpu::Color>,
6054 scissor: Option<(u32, u32, u32, u32)>,
6055 blend_mode: BlendMode,
6056 dest_viewport: Option<(f32, f32, f32, f32)>,
6057 sample_mode: CompositeSampleMode,
6058 ) -> Result<(), String> {
6059 self.record_effect_composite(
6060 source,
6061 effect,
6062 effect_rect,
6063 dest_view,
6064 alpha,
6065 load_op,
6066 scissor,
6067 blend_mode,
6068 dest_viewport,
6069 sample_mode,
6070 )
6071 }
6072
6073 fn apply_shader_and_composite_to_view(
6074 &mut self,
6075 source: &OffscreenTarget,
6076 shader: &RuntimeShader,
6077 effect_rect: [f32; 4],
6078 dest_view: &wgpu::TextureView,
6079 alpha: f32,
6080 load_op: wgpu::LoadOp<wgpu::Color>,
6081 scissor: Option<(u32, u32, u32, u32)>,
6082 blend_mode: BlendMode,
6083 dest_viewport: Option<(f32, f32, f32, f32)>,
6084 sample_mode: CompositeSampleMode,
6085 ) {
6086 self.record_shader_composite(
6087 source,
6088 shader,
6089 effect_rect,
6090 dest_view,
6091 alpha,
6092 load_op,
6093 scissor,
6094 blend_mode,
6095 dest_viewport,
6096 sample_mode,
6097 );
6098 }
6099
6100 fn apply_shader_and_composite_to_view_projective(
6101 &mut self,
6102 source: &OffscreenTarget,
6103 shader: &RuntimeShader,
6104 effect_rect: [f32; 4],
6105 dest_view: &wgpu::TextureView,
6106 viewport: (u32, u32),
6107 source_size: (f32, f32),
6108 inverse_matrix: [[f32; 3]; 3],
6109 dest_bounds: [[f32; 2]; 4],
6110 alpha: f32,
6111 load_op: wgpu::LoadOp<wgpu::Color>,
6112 scissor: Option<(u32, u32, u32, u32)>,
6113 blend_mode: BlendMode,
6114 sample_mode: CompositeSampleMode,
6115 ) {
6116 self.record_shader_projective_composite(
6117 source,
6118 shader,
6119 effect_rect,
6120 dest_view,
6121 viewport,
6122 source_size,
6123 inverse_matrix,
6124 dest_bounds,
6125 alpha,
6126 load_op,
6127 scissor,
6128 blend_mode,
6129 sample_mode,
6130 );
6131 }
6132
6133 fn apply_effect_and_composite_to_view_projective(
6134 &mut self,
6135 source: &OffscreenTarget,
6136 effect: &RenderEffect,
6137 effect_rect: [f32; 4],
6138 dest_view: &wgpu::TextureView,
6139 viewport: (u32, u32),
6140 source_size: (f32, f32),
6141 inverse_matrix: [[f32; 3]; 3],
6142 dest_bounds: [[f32; 2]; 4],
6143 alpha: f32,
6144 load_op: wgpu::LoadOp<wgpu::Color>,
6145 scissor: Option<(u32, u32, u32, u32)>,
6146 blend_mode: BlendMode,
6147 sample_mode: CompositeSampleMode,
6148 ) -> Result<(), String> {
6149 self.record_effect_projective_composite(
6150 source,
6151 effect,
6152 effect_rect,
6153 dest_view,
6154 viewport,
6155 source_size,
6156 inverse_matrix,
6157 dest_bounds,
6158 alpha,
6159 load_op,
6160 scissor,
6161 blend_mode,
6162 sample_mode,
6163 )
6164 }
6165
6166 fn is_render_effect_supported(&self, effect: &RenderEffect) -> bool {
6167 self.renderer.supports_render_effect(effect)
6168 }
6169
6170 fn warn_unsupported_effect_once(&self) {
6171 self.renderer.warning_state.warn_unsupported_effect_once();
6172 }
6173
6174 fn record_layer_cache_miss(&self, width: u32, height: u32) {
6175 self.renderer
6176 .frame_stats
6177 .record_layer_cache_miss(width, height);
6178 }
6179
6180 fn record_isolated_layer_render(
6181 &self,
6182 width: u32,
6183 height: u32,
6184 node_id: Option<NodeId>,
6185 logical_rect: Rect,
6186 requirements: SurfaceRequirementSet,
6187 ) {
6188 self.renderer.frame_stats.record_isolated_layer_render(
6189 width,
6190 height,
6191 node_id,
6192 logical_rect,
6193 requirements.into(),
6194 );
6195 }
6196}
6197
6198impl GpuRenderer {
6199 pub fn render(
6200 &mut self,
6201 view: &wgpu::TextureView,
6202 width: u32,
6203 height: u32,
6204 packet: FramePacket,
6205 surface_epoch: u64,
6206 returns: &mut RenderReturns,
6207 ) -> Result<(), String> {
6208 let cancel_reason = if packet.renderer_epoch != self.renderer_epoch {
6214 Some(CancelReason::RendererEpoch)
6215 } else if packet.surface_epoch != surface_epoch {
6216 Some(CancelReason::SurfaceEpoch)
6217 } else if packet.viewport != (width, height) {
6218 Some(CancelReason::Viewport)
6219 } else {
6220 None
6221 };
6222 if let Some(reason) = cancel_reason {
6223 return Self::cancel_packet(packet, reason, returns);
6224 }
6225 returns.frame_id = packet.frame_id;
6226 log::trace!("🎨 Rendering graph to {}x{}", width, height);
6227 let render_start = Instant::now();
6228
6229 #[cfg(target_arch = "wasm32")]
6230 {
6231 self.wasm_uniform_batch_cursor = 0;
6232 self.wasm_shape_batch_cursor = 0;
6233 self.wasm_image_batch_cursor = 0;
6234 }
6235 #[cfg(not(target_arch = "wasm32"))]
6236 {
6237 self.retained_glyph_uniform_cursor = 0;
6238 }
6239
6240 let text_cache_len = packet.text_cache_len;
6244 let result = self.render_graph(view, packet, returns);
6245 let after_graph = Instant::now();
6246 self.flush_deferred_offscreen_releases();
6247
6248 #[cfg(target_arch = "wasm32")]
6249 {
6250 const WASM_BATCH_POOL_MARGIN: usize = 4;
6251 self.wasm_uniform_batches.truncate(
6252 self.wasm_uniform_batch_cursor
6253 .saturating_add(WASM_BATCH_POOL_MARGIN),
6254 );
6255 self.wasm_shape_batches.truncate(
6256 self.wasm_shape_batch_cursor
6257 .saturating_add(WASM_BATCH_POOL_MARGIN),
6258 );
6259 self.wasm_image_batches.truncate(
6260 self.wasm_image_batch_cursor
6261 .saturating_add(WASM_BATCH_POOL_MARGIN),
6262 );
6263 }
6264 self.staged_uploads
6265 .shrink_retained_capacity(RETAINED_STAGED_UPLOAD_BYTES, RETAINED_STAGED_UPLOAD_COPIES);
6266
6267 self.layer_surface_cache.finish_frame(&self.frame_stats);
6268 #[cfg(not(target_arch = "wasm32"))]
6269 self.retained_bundle_cache.end_frame();
6270
6271 self.frame_stats.offscreen_pool_size.set(
6272 self.effect_renderer
6273 .retained_offscreen_count()
6274 .saturating_add(self.frame_graph_executor.retained_texture_count())
6275 as u32,
6276 );
6277 self.frame_stats.offscreen_pool_bytes.set(
6278 (self.effect_renderer.retained_offscreen_bytes() as u64)
6279 .saturating_add(self.frame_graph_executor.retained_texture_bytes()),
6280 );
6281 self.frame_stats
6282 .text_pool_size
6283 .set(self.text_image_cache.len() as u32);
6284 self.frame_stats
6285 .image_cache_size
6286 .set(self.image_texture_cache.len() as u32);
6287 self.frame_stats.text_cache_size.set(text_cache_len as u32);
6288 self.effect_renderer
6289 .merge_and_reset_debug_counters(&self.frame_stats);
6290 self.frame_graph_executor.reset_upload_allocators();
6291 let snapshot = self.frame_stats.snapshot();
6292 self.last_frame_stats = Some(snapshot);
6293 PRESENTED_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6294 update_frame_warmup_budget(&mut self.pending_frame_warmup_frames, &snapshot);
6295 self.frame_stats.maybe_print_snapshot(
6296 snapshot,
6297 &mut self.frame_count,
6298 self.gpu_stats_enabled,
6299 );
6300 if self.gpu_stats_enabled && self.frame_count.is_multiple_of(60) {
6301 gpu_stats::print_gpu_memory_report(&self.device, self.frame_count);
6302 }
6303 self.frame_stats.reset();
6304 let after_stats = Instant::now();
6305 if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
6306 log::warn!(
6307 "[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
6308 instant_ms(render_start, after_graph),
6309 instant_ms(after_graph, after_stats),
6310 );
6311 }
6312 if result.is_ok() {
6313 returns.outcome = PresentOutcome::Presented;
6316 }
6317 result
6318 }
6319
6320 fn cancel_packet(
6327 packet: FramePacket,
6328 reason: CancelReason,
6329 returns: &mut RenderReturns,
6330 ) -> Result<(), String> {
6331 let FramePacket {
6332 frame_id,
6333 viewport: _,
6334 renderer_epoch: _,
6335 surface_epoch: _,
6336 root_scale: _,
6337 root,
6338 overlay: _,
6339 replay,
6340 text_cache_len: _,
6341 } = packet;
6342 match root {
6343 PacketRoot::Direct(root) => {
6344 returns.scene = Some(root.scene);
6351 #[cfg(not(target_arch = "wasm32"))]
6352 {
6353 returns.cancelled_replay = Some(replay);
6354 }
6355 }
6356 PacketRoot::Surface(_) => {}
6357 }
6358 #[cfg(target_arch = "wasm32")]
6359 let _ = replay;
6360 returns.ack = None;
6361 returns.frame_id = frame_id;
6362 returns.outcome = PresentOutcome::Cancelled(reason);
6363 Ok(())
6364 }
6365
6366 pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
6367 self.last_frame_stats
6368 }
6369
6370 pub fn needs_frame_warmup(&self) -> bool {
6371 self.pending_frame_warmup_frames > 0
6372 }
6373
6374 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
6375 let layer_surface_cache_stats = self.layer_surface_cache.debug_stats();
6376 DebugCpuAllocationStats {
6377 scene_graph_node_count: 0,
6378 scene_graph_heap_bytes: 0,
6379 scene_hits_len: 0,
6380 scene_hits_cap: 0,
6381 scene_node_index_len: 0,
6382 scene_node_index_cap: 0,
6383 text_renderer_pool_len: self.text_image_cache.len(),
6384 text_renderer_pool_cap: self.text_image_cache.cap().get(),
6385 swash_image_cache_len: 0,
6386 swash_image_cache_cap: 0,
6387 swash_outline_cache_len: 0,
6388 swash_outline_cache_cap: 0,
6389 image_texture_cache_len: self.image_texture_cache.len(),
6390 image_texture_cache_cap: self.image_texture_cache.cap().get(),
6391 scratch_shape_data_cap: self.scratch_shape_data.capacity(),
6392 scratch_gradients_cap: self.scratch_gradients.capacity(),
6393 scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
6394 scratch_image_indices_cap: self.scratch_image_indices.capacity(),
6395 scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
6396 scratch_segment_items_cap: self.scratch_segment_items.capacity(),
6397 scratch_effect_ranges_cap: self.scratch_effect_ranges.capacity(),
6398 scratch_layer_events_cap: self.scratch_layer_events.capacity(),
6399 staged_upload_bytes_cap: self.staged_uploads.bytes.capacity(),
6400 staged_upload_copies_cap: self.staged_uploads.copies.capacity(),
6401 layer_surface_cache_len: layer_surface_cache_stats.entries_len,
6402 layer_surface_cache_cap: layer_surface_cache_stats.entries_cap,
6403 layer_surface_cache_identity_len: layer_surface_cache_stats.identity_len,
6404 layer_surface_cache_identity_cap: layer_surface_cache_stats.identity_cap,
6405 layer_surface_rect_cache_len: 0,
6408 layer_surface_rect_cache_cap: 0,
6409 layer_surface_requirements_cache_len: 0,
6410 layer_surface_requirements_cache_cap: 0,
6411 layer_cache_seen_this_frame_len: layer_surface_cache_stats.seen_this_frame_len,
6412 layer_cache_seen_this_frame_cap: layer_surface_cache_stats.seen_this_frame_cap,
6413 }
6414 }
6415
6416 pub fn render_to_rgba_pixels(
6417 &mut self,
6418 width: u32,
6419 height: u32,
6420 packet: FramePacket,
6421 surface_epoch: u64,
6422 returns: &mut RenderReturns,
6423 ) -> Result<Vec<u8>, String> {
6424 if width == 0 || height == 0 {
6425 return Err("Screenshot size must be non-zero".to_string());
6426 }
6427
6428 let output_texture = self.device.create_texture(&wgpu::TextureDescriptor {
6429 label: Some("Screenshot Output Texture"),
6430 size: wgpu::Extent3d {
6431 width,
6432 height,
6433 depth_or_array_layers: 1,
6434 },
6435 mip_level_count: 1,
6436 sample_count: 1,
6437 dimension: wgpu::TextureDimension::D2,
6438 format: self.surface_format,
6439 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
6440 view_formats: &[],
6441 });
6442 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
6443
6444 self.render(&output_view, width, height, packet, surface_epoch, returns)?;
6445
6446 let bytes_per_pixel = 4u32;
6447 let unpadded_bytes_per_row = width
6448 .checked_mul(bytes_per_pixel)
6449 .ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
6450 let padded_bytes_per_row =
6451 align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
6452 let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
6453
6454 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
6455 label: Some("Screenshot Readback Buffer"),
6456 size: output_buffer_size,
6457 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
6458 mapped_at_creation: false,
6459 });
6460
6461 let device = self.device.clone();
6462 let queue = self.queue.clone();
6463 let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
6464 let source = graph.import_surface("screenshot-copy-source");
6465 graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
6466 context.encoder.copy_texture_to_buffer(
6467 wgpu::TexelCopyTextureInfo {
6468 texture: &output_texture,
6469 mip_level: 0,
6470 origin: wgpu::Origin3d::ZERO,
6471 aspect: wgpu::TextureAspect::All,
6472 },
6473 wgpu::TexelCopyBufferInfo {
6474 buffer: &output_buffer,
6475 layout: wgpu::TexelCopyBufferLayout {
6476 offset: 0,
6477 bytes_per_row: Some(padded_bytes_per_row),
6478 rows_per_image: Some(height),
6479 },
6480 },
6481 wgpu::Extent3d {
6482 width,
6483 height,
6484 depth_or_array_layers: 1,
6485 },
6486 );
6487 Ok(())
6488 });
6489 let mut executor = std::mem::take(&mut self.frame_graph_executor);
6490 let execution = executor.execute_recorded_graph(&device, &queue, graph);
6491 self.frame_graph_executor = executor;
6492 let execution = execution.map_err(|error| error.to_string())?;
6493 let submission_index = execution.submission;
6494 let copy_stats = execution.stats;
6495 self.last_frame_stats = self
6496 .last_frame_stats
6497 .map(|snapshot| snapshot.with_command_stats_added(copy_stats));
6498
6499 let buffer_slice = output_buffer.slice(..);
6500 let (tx, rx) = mpsc::channel();
6501 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
6502 let _ = tx.send(result);
6503 });
6504 let _ = self.device.poll(wgpu::PollType::Wait {
6505 submission_index: Some(submission_index),
6506 timeout: None,
6507 });
6508
6509 match rx.recv_timeout(Duration::from_secs(3)) {
6510 Ok(Ok(())) => {}
6511 Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
6512 Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
6513 }
6514
6515 let mapped = buffer_slice.get_mapped_range();
6516 let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
6517
6518 let src_row_len = padded_bytes_per_row as usize;
6519 let dst_row_len = unpadded_bytes_per_row as usize;
6520 for row in 0..height as usize {
6521 let src_offset = row * src_row_len;
6522 let dst_offset = row * dst_row_len;
6523 pixels[dst_offset..dst_offset + dst_row_len]
6524 .copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
6525 }
6526 drop(mapped);
6527 output_buffer.unmap();
6528
6529 self.convert_surface_pixels_to_rgba(&mut pixels)?;
6530 Ok(pixels)
6531 }
6532
6533 fn render_graph(
6534 &mut self,
6535 surface_view: &wgpu::TextureView,
6536 packet: FramePacket,
6537 returns: &mut RenderReturns,
6538 ) -> Result<(), String> {
6539 let device = self.device.clone();
6540 let queue = self.queue.clone();
6541 let graph_start = Instant::now();
6542
6543 #[cfg(not(target_arch = "wasm32"))]
6544 {
6545 let mut executor = std::mem::take(&mut self.frame_graph_executor);
6546 let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
6547 let surface = frame_graph.import_surface("renderer-surface");
6548 frame_graph.add_fallible_recorded_command_pass(
6549 Some("Renderer Frame Pass"),
6550 &[],
6551 &[surface],
6552 |frame_encoder| {
6553 self.render_graph_recorded(surface_view, packet, returns, frame_encoder)
6554 },
6555 );
6556 let after_build = Instant::now();
6557 let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
6558 let after_execute = Instant::now();
6559 self.frame_graph_executor = executor;
6560 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
6561 log::warn!(
6562 "[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
6563 instant_ms(graph_start, after_build),
6564 instant_ms(after_build, after_execute),
6565 );
6566 }
6567
6568 match execution {
6569 Ok(execution) => {
6570 if execution.stats.pass_count > 0 {
6571 self.frame_stats.record_command_stats(execution.stats);
6572 }
6573 Ok(())
6574 }
6575 Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => Ok(()),
6576 Err(error) => Err(error.to_string()),
6577 }
6578 }
6579
6580 #[cfg(target_arch = "wasm32")]
6581 {
6582 let mut executor = std::mem::take(&mut self.frame_graph_executor);
6583 let (result, execution) = {
6584 let mut frame_encoder =
6585 executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
6586 let initial_pass_count = frame_encoder.recorded_pass_count();
6587 let result =
6588 self.render_graph_recorded(surface_view, packet, returns, &mut frame_encoder);
6589 let execution =
6590 if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
6591 Some(frame_encoder.finish())
6592 } else {
6593 None
6594 };
6595 (result, execution)
6596 };
6597 let after_execute = Instant::now();
6598 self.frame_graph_executor = executor;
6599 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
6600 log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
6601 }
6602 if let Some(execution) = execution {
6603 self.frame_stats.record_command_stats(execution.stats);
6604 }
6605 result
6606 }
6607 }
6608
6609 fn render_graph_recorded<C: FrameCommandRecorder>(
6610 &mut self,
6611 surface_view: &wgpu::TextureView,
6612 packet: FramePacket,
6613 returns: &mut RenderReturns,
6614 frame_encoder: &mut C,
6615 ) -> Result<(), String> {
6616 let recorded_start = Instant::now();
6617
6618 #[cfg(not(target_arch = "wasm32"))]
6630 let mut packet = packet;
6631 #[cfg(not(target_arch = "wasm32"))]
6632 if let PacketRoot::Direct(root) = &packet.root {
6633 let ops = std::mem::take(&mut packet.replay);
6634 let (ack, recycled) =
6635 self.consume_replay_ops(ops, &root.scene.shapes, packet.root_scale);
6636 returns.ack = Some((ack, recycled));
6637 }
6638
6639 let FramePacket {
6640 frame_id,
6641 viewport: (width, height),
6642 renderer_epoch: _,
6643 surface_epoch: _,
6644 root_scale,
6645 root,
6646 overlay,
6647 replay: _,
6648 text_cache_len: _,
6649 } = packet;
6650
6651 let mut backend = RecordingSurfaceBackend {
6652 renderer: self,
6653 recorder: frame_encoder,
6654 };
6655
6656 let surface_packet = match root {
6657 PacketRoot::Direct(root) => {
6658 let direct_render_start = Instant::now();
6659 let result = match execute_render_root_direct(
6660 &mut backend,
6661 surface_view,
6662 *root,
6663 width,
6664 height,
6665 root_scale,
6666 wgpu::LoadOp::Clear(CLEAR_COLOR),
6667 ) {
6668 Ok(scene) => {
6673 returns.scene = Some(scene);
6674 Ok(())
6675 }
6676 Err((error, scene)) => {
6677 returns.scene = Some(scene);
6678 Err(error)
6679 }
6680 };
6681 if result.is_ok() {
6682 if let Some(overlay) = overlay {
6683 Self::render_overlay_packet(
6684 &mut backend,
6685 surface_view,
6686 overlay,
6687 width,
6688 height,
6689 root_scale,
6690 )?;
6691 }
6692 }
6693 let after_direct_render = Instant::now();
6694 if let Some(total_ms) =
6695 should_log_wgpu_render_stage(recorded_start, after_direct_render)
6696 {
6697 log::warn!(
6698 "[wgpu-render-stage:recorded-direct-root] frame={frame_id} total_ms={total_ms:.2} render_ms={:.2}",
6699 instant_ms(direct_render_start, after_direct_render),
6700 );
6701 }
6702 return result;
6703 }
6704 PacketRoot::Surface(surface_packet) => surface_packet,
6705 };
6706 let after_root_collect = Instant::now();
6707
6708 let RootSurfacePacket {
6709 lowered,
6710 source,
6711 transform_to_parent,
6712 node_id,
6713 backdrop,
6714 graphics_layer,
6715 local_bounds,
6716 clip_rect,
6717 shadow_clip,
6718 } = *surface_packet;
6719 let mut lowered = lowered;
6720 lowered.source = source;
6721
6722 let viewport_rect = Rect {
6727 x: 0.0,
6728 y: 0.0,
6729 width: width as f32 / root_scale,
6730 height: height as f32 / root_scale,
6731 };
6732 let root_surface = execute_render_layer_surface(
6733 &mut backend,
6734 &mut lowered,
6735 LayerSurfaceRequest {
6736 root_scale,
6737 backdrop_underlay: None,
6738 allow_runtime_cache: false,
6739 logical_rect_override: Some(viewport_rect),
6740 capture_clip_override: None,
6741 activates_nested_capture: false,
6742 translation_context: TranslationRenderContext::default(),
6743 },
6744 )?;
6745 let root_quad = transform_to_parent.map_rect(root_surface.logical_rect);
6746 let root_dest_quad = scaled_quad(root_quad, root_scale);
6747
6748 let needs_root_composite_target =
6749 backdrop.is_some() || graphics_layer.shadow_elevation > 0.0;
6750
6751 if needs_root_composite_target {
6752 let composite_target = backend.acquire_frame_surface(width, height);
6753 backend.clear_target_view_with_load_op(
6754 &composite_target.view,
6755 wgpu::LoadOp::Clear(CLEAR_COLOR),
6756 );
6757
6758 if let Some(backdrop) = &backdrop {
6759 execute_apply_backdrop_layer_to_target(
6760 &mut backend,
6761 &composite_target,
6762 &BackdropLayer {
6763 node_id,
6764 rect: quad_bounds(transform_to_parent.map_rect(local_bounds)),
6765 clip: clip_rect.map(|clip| quad_bounds(transform_to_parent.map_rect(clip))),
6766 snap_anchor: None,
6767 effect: backdrop.clone(),
6768 z_index: 0,
6769 },
6770 None,
6771 width,
6772 height,
6773 root_scale,
6774 None,
6775 )?;
6776 }
6777
6778 let mut root_shadow_scene = CompositorScene::new();
6779 let root_shadow_clip =
6780 shadow_clip.map(|clip| quad_bounds(transform_to_parent.map_rect(clip)));
6781 push_layer_shadow(
6782 &mut root_shadow_scene,
6783 &graphics_layer,
6784 local_bounds,
6785 quad_bounds(transform_to_parent.map_rect(local_bounds)),
6786 root_shadow_clip,
6787 );
6788 for shadow in &root_shadow_scene.shadow_draws {
6789 backend.render_shadow_draw(
6790 &composite_target.view,
6791 shadow,
6792 width,
6793 height,
6794 root_scale,
6795 );
6796 }
6797
6798 let composite_dest_quad =
6799 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
6800 execute_composite_surface_to_view(
6801 &mut backend,
6802 root_surface.target.target(),
6803 &composite_target.view,
6804 (width, height),
6805 composite_dest_quad,
6806 root_surface.composite_alpha,
6807 wgpu::LoadOp::Load,
6808 None,
6809 root_surface.blend_mode,
6810 root_surface.sample_mode,
6811 )?;
6812 backend.composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
6813 &composite_target,
6814 surface_view,
6815 1.0,
6816 wgpu::LoadOp::Clear(CLEAR_COLOR),
6817 None,
6818 None,
6819 BlendMode::SrcOver,
6820 None,
6821 CompositeSampleMode::Linear,
6822 );
6823 backend.release_frame_surface(composite_target);
6824 } else {
6825 let composite_dest_quad =
6826 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
6827 execute_composite_surface_to_view(
6828 &mut backend,
6829 root_surface.target.target(),
6830 surface_view,
6831 (width, height),
6832 composite_dest_quad,
6833 root_surface.composite_alpha,
6834 wgpu::LoadOp::Clear(CLEAR_COLOR),
6835 None,
6836 root_surface.blend_mode,
6837 root_surface.sample_mode,
6838 )?;
6839 }
6840 backend.release_layer_surface_target(root_surface.target);
6841 if let Some(overlay) = overlay {
6842 Self::render_overlay_packet(
6843 &mut backend,
6844 surface_view,
6845 overlay,
6846 width,
6847 height,
6848 root_scale,
6849 )?;
6850 }
6851 let after_layer_render = Instant::now();
6852 if let Some(total_ms) = should_log_wgpu_render_stage(recorded_start, after_layer_render) {
6853 log::warn!(
6854 "[wgpu-render-stage:recorded-layer-root] total_ms={total_ms:.2} collect_ms={:.2} render_ms={:.2}",
6855 instant_ms(recorded_start, after_root_collect),
6856 instant_ms(after_root_collect, after_layer_render),
6857 );
6858 }
6859 Ok(())
6860 }
6861
6862 fn render_overlay_packet<C: FrameCommandRecorder>(
6866 backend: &mut RecordingSurfaceBackend<'_, '_, C>,
6867 surface_view: &wgpu::TextureView,
6868 overlay: CollectedLayer,
6869 width: u32,
6870 height: u32,
6871 root_scale: f32,
6872 ) -> Result<(), String> {
6873 if !overlay.child_layers.is_empty()
6874 || !root_direct_scene_events_are_supported(&overlay.scene)
6875 || !direct_root_child_underlays_are_supported(&overlay)
6876 {
6877 return Err("dev overlay graph must stay directly renderable".to_string());
6878 }
6879 execute_render_root_direct(
6880 backend,
6881 surface_view,
6882 overlay,
6883 width,
6884 height,
6885 root_scale,
6886 wgpu::LoadOp::Load,
6887 )
6888 .map(|_overlay_scene| ())
6889 .map_err(|(error, _overlay_scene)| error)
6890 }
6891
6892 #[allow(clippy::too_many_arguments)]
6893 fn encode_non_effect_segment_commands<C: FrameCommandRecorder>(
6894 &mut self,
6895 frame_encoder: &mut C,
6896 target_view: &wgpu::TextureView,
6897 ordered_items: &[(usize, SegmentDrawItem)],
6898 composites: &[(usize, CompositeBatchItem<'_>)],
6899 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
6900 shapes: &[DrawShape],
6901 images: &[ImageDraw],
6902 texts: &[TextDraw],
6903 shadow_draws: &[ShadowDraw],
6904 retained_draws: &[RetainedDraw],
6905 initial_load_op: wgpu::LoadOp<wgpu::Color>,
6906 width: u32,
6907 height: u32,
6908 root_scale: f32,
6909 ) -> Result<SegmentCommandEncodeOutcome, String> {
6910 let mut first_batch = true;
6911 for command in
6912 SegmentCommandIter::new(ordered_items, shapes, images, self.shape_batch_limits)
6913 {
6914 match command {
6915 SegmentRenderCommand::DrawChunk(chunk) => {
6916 let load_op = if first_batch {
6917 initial_load_op
6918 } else {
6919 wgpu::LoadOp::Load
6920 };
6921 let outcome = self.render_segment_draw_chunk(
6922 frame_encoder,
6923 target_view,
6924 ordered_items,
6925 composites,
6926 shader_composites,
6927 shapes,
6928 images,
6929 texts,
6930 retained_draws,
6931 chunk,
6932 width,
6933 height,
6934 root_scale,
6935 load_op,
6936 )?;
6937 if outcome.rendered_any {
6938 frame_encoder.record_passes(outcome.pass_count);
6939 first_batch = false;
6940 }
6941 }
6942 SegmentRenderCommand::Shadow(index) => {
6943 if first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
6944 {
6945 let _clear = frame_encoder.encoder().begin_render_pass(
6946 &wgpu::RenderPassDescriptor {
6947 label: Some("Shadow Pre-Clear"),
6948 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
6949 view: target_view,
6950 resolve_target: None,
6951 depth_slice: None,
6952 ops: wgpu::Operations {
6953 load: initial_load_op,
6954 store: wgpu::StoreOp::Store,
6955 },
6956 })],
6957 depth_stencil_attachment: None,
6958 timestamp_writes: None,
6959 occlusion_query_set: None,
6960 multiview_mask: None,
6961 },
6962 );
6963 }
6964 frame_encoder.record_pass();
6965 first_batch = false;
6966 }
6967 let pass_count_before = frame_encoder.recorded_pass_count();
6968 self.encode_shadow_draw(
6969 frame_encoder,
6970 target_view,
6971 &shadow_draws[index],
6972 width,
6973 height,
6974 root_scale,
6975 );
6976 if frame_encoder.recorded_pass_count() > pass_count_before {
6977 first_batch = false;
6978 }
6979 }
6980 }
6981 }
6982 Ok(SegmentCommandEncodeOutcome { first_batch })
6983 }
6984
6985 #[cfg(not(target_arch = "wasm32"))]
6986 #[allow(clippy::too_many_arguments)]
6987 fn render_segment_draw_chunk_fused_native<C: FrameCommandRecorder>(
6988 &mut self,
6989 frame_encoder: &mut C,
6990 target_view: &wgpu::TextureView,
6991 ordered_items: &[(usize, SegmentDrawItem)],
6992 composites: &[(usize, CompositeBatchItem<'_>)],
6993 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
6994 shapes: &[DrawShape],
6995 images: &[ImageDraw],
6996 texts: &[TextDraw],
6997 retained_draws: &[RetainedDraw],
6998 chunk: &SegmentDrawChunkPlan,
6999 width: u32,
7000 height: u32,
7001 root_scale: f32,
7002 load_op: wgpu::LoadOp<wgpu::Color>,
7003 ) -> Result<Option<SegmentRenderOutcome>, String> {
7004 let Some(partitions) = native_segment_fusion_partitions(
7005 ordered_items,
7006 shapes,
7007 chunk,
7008 self.shape_batch_limits,
7009 )?
7010 else {
7011 return Ok(None);
7012 };
7013
7014 let mut rendered_any = false;
7015 let mut pass_count = 0_u32;
7016 let mut next_load_op = load_op;
7017 for partition in partitions {
7018 let outcome = self.render_segment_draw_chunk_fused_native_partition(
7019 frame_encoder,
7020 target_view,
7021 ordered_items,
7022 composites,
7023 shader_composites,
7024 shapes,
7025 images,
7026 texts,
7027 retained_draws,
7028 &partition.chunk,
7029 partition.budget,
7030 width,
7031 height,
7032 root_scale,
7033 next_load_op,
7034 )?;
7035 if outcome.rendered_any {
7036 rendered_any = true;
7037 pass_count = pass_count.saturating_add(outcome.pass_count);
7038 next_load_op = wgpu::LoadOp::Load;
7039 }
7040 }
7041
7042 Ok(Some(SegmentRenderOutcome {
7043 rendered_any,
7044 pass_count,
7045 }))
7046 }
7047
7048 #[cfg(not(target_arch = "wasm32"))]
7049 #[allow(clippy::too_many_arguments)]
7050 fn render_segment_draw_chunk_fused_native_partition<C: FrameCommandRecorder>(
7051 &mut self,
7052 frame_encoder: &mut C,
7053 target_view: &wgpu::TextureView,
7054 ordered_items: &[(usize, SegmentDrawItem)],
7055 composites: &[(usize, CompositeBatchItem<'_>)],
7056 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
7057 shapes: &[DrawShape],
7058 images: &[ImageDraw],
7059 texts: &[TextDraw],
7060 retained_draws: &[RetainedDraw],
7061 chunk: &SegmentDrawChunkPlan,
7062 budget: NativeSegmentFusionBudget,
7063 width: u32,
7064 height: u32,
7065 root_scale: f32,
7066 load_op: wgpu::LoadOp<wgpu::Color>,
7067 ) -> Result<SegmentRenderOutcome, String> {
7068 let partition_start = Instant::now();
7069 let mut staged_uploads = self.take_staged_uploads();
7070 staged_uploads.clear();
7071 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
7072 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
7073 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
7074 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
7075
7076 image_vertices.clear();
7077 image_indices.clear();
7078 image_cmds.clear();
7079 glyph_cmds.clear();
7080
7081 let result = (|| {
7082 let viewport = ViewportUniformParams {
7083 width,
7084 height,
7085 offset: [0.0, 0.0],
7086 };
7087 self.prewarm_offscreen_text_glyph_draws_in_chunk(
7088 ordered_items,
7089 texts,
7090 chunk,
7091 viewport,
7092 root_scale,
7093 &mut staged_uploads,
7094 &mut image_vertices,
7095 &mut image_indices,
7096 &mut glyph_cmds,
7097 )?;
7098 let mut shape_refs = Vec::with_capacity(budget.shape_count);
7099 for batch in chunk.iter() {
7100 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
7101 continue;
7102 };
7103 for (_, item) in &ordered_items[start..end] {
7104 let SegmentDrawItem::Shape(shape_index) = item else {
7105 return Err(format!(
7106 "shape batch contains non-shape draw item: {item:?}"
7107 ));
7108 };
7109 shape_refs.push(&shapes[*shape_index]);
7110 }
7111 }
7112 let after_shape_refs = Instant::now();
7113
7114 let mut direct_shape_uploads = StagedBufferUploads::default();
7115 let mut shape_upload_base = 0u64;
7116 if !shape_refs.is_empty() {
7117 let Some((_, upload_base)) = self.prepare_shapes_batch_direct(
7118 frame_encoder,
7119 shape_refs.iter().copied(),
7120 root_scale,
7121 viewport,
7122 &mut direct_shape_uploads,
7123 ) else {
7124 return Err(
7125 "native fused segment shape preparation produced no draw batch".to_string(),
7126 );
7127 };
7128 shape_upload_base = upload_base;
7129 }
7130 let after_shape_prepare = Instant::now();
7131
7132 let mut fused_batches = Vec::with_capacity(chunk.batches.len());
7133 let mut shape_cursor = 0_u32;
7134 let mut composite_cursor = 0usize;
7135 let mut shader_composite_cursor = 0usize;
7136 for batch in chunk.iter() {
7137 match batch {
7138 SegmentBatchPlan::Shape {
7139 start,
7140 end,
7141 blend_mode,
7142 } => {
7143 for (_, item) in &ordered_items[start..end] {
7144 if !matches!(item, SegmentDrawItem::Shape(_)) {
7145 return Err(format!(
7146 "shape batch contains non-shape draw item: {item:?}"
7147 ));
7148 }
7149 }
7150 let shape_count = end - start;
7151 if shape_count > 0 {
7152 fused_batches.push(FusedSegmentBatch::Shape {
7153 batch: PreparedShapeBatch {
7154 vertex_start: shape_cursor * 6,
7155 vertex_count: shape_count as u32 * 6,
7156 },
7157 blend_mode,
7158 });
7159 shape_cursor += shape_count as u32;
7160 }
7161 }
7162 SegmentBatchPlan::Image {
7163 start,
7164 end,
7165 blend_mode,
7166 } => {
7167 let cmd_start = image_cmds.len();
7168 for (_, item) in &ordered_items[start..end] {
7169 let SegmentDrawItem::Image(image_index) = item else {
7170 return Err(format!(
7171 "image batch contains non-image draw item: {item:?}"
7172 ));
7173 };
7174 self.append_image_draw_cmd(
7175 &images[*image_index],
7176 viewport,
7177 root_scale,
7178 &mut image_vertices,
7179 &mut image_indices,
7180 &mut image_cmds,
7181 )?;
7182 }
7183 let cmd_end = image_cmds.len();
7184 if cmd_start < cmd_end {
7185 fused_batches.push(FusedSegmentBatch::Image {
7186 cmd_range: cmd_start..cmd_end,
7187 blend_mode,
7188 });
7189 }
7190 }
7191 SegmentBatchPlan::Text { start, end } => {
7192 let glyph_cmd_start = glyph_cmds.len();
7193 let image_cmd_start = image_cmds.len();
7194 let text_draws =
7195 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7196 if !self.append_text_glyph_draws(
7197 text_draws,
7198 viewport,
7199 root_scale,
7200 false,
7201 &mut staged_uploads,
7202 &mut image_vertices,
7203 &mut image_indices,
7204 &mut glyph_cmds,
7205 )? {
7206 let text_draws =
7207 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7208 self.append_text_image_draw_cmds(
7209 text_draws,
7210 viewport,
7211 root_scale,
7212 &mut image_vertices,
7213 &mut image_indices,
7214 &mut image_cmds,
7215 )?;
7216 }
7217 let image_cmd_end = image_cmds.len();
7218 let glyph_cmd_end = glyph_cmds.len();
7219 if image_cmd_start < image_cmd_end || glyph_cmd_start < glyph_cmd_end {
7220 fused_batches.push(FusedSegmentBatch::Text {
7221 image_cmd_range: image_cmd_start..image_cmd_end,
7222 glyph_cmd_range: glyph_cmd_start..glyph_cmd_end,
7223 });
7224 }
7225 }
7226 SegmentBatchPlan::Composite { start, end } => {
7227 for (_, item) in &ordered_items[start..end] {
7228 if !matches!(item, SegmentDrawItem::Composite(_)) {
7229 return Err(format!(
7230 "composite batch contains non-composite draw item: {item:?}"
7231 ));
7232 }
7233 }
7234 let draw_count = end - start;
7235 if draw_count > 0 {
7236 let draw_start = composite_cursor;
7237 composite_cursor += draw_count;
7238 fused_batches.push(FusedSegmentBatch::Composite {
7239 draw_range: draw_start..composite_cursor,
7240 });
7241 }
7242 }
7243 SegmentBatchPlan::ShaderComposite { start, end } => {
7244 for (_, item) in &ordered_items[start..end] {
7245 if !matches!(item, SegmentDrawItem::ShaderComposite(_)) {
7246 return Err(format!(
7247 "shader composite batch contains non-shader-composite draw item: {item:?}"
7248 ));
7249 }
7250 }
7251 let draw_count = end - start;
7252 if draw_count > 0 {
7253 let draw_start = shader_composite_cursor;
7254 shader_composite_cursor += draw_count;
7255 fused_batches.push(FusedSegmentBatch::ShaderComposite {
7256 draw_range: draw_start..shader_composite_cursor,
7257 });
7258 }
7259 }
7260 SegmentBatchPlan::Retained { start, end } => {
7261 self.stage_replay_patches(&mut staged_uploads);
7262 for (_, item) in &ordered_items[start..end] {
7263 let SegmentDrawItem::Retained(index) = item else {
7264 return Err(format!(
7265 "retained batch contains non-retained draw item: {item:?}"
7266 ));
7267 };
7268 let retained = retained_draws.get(*index).ok_or_else(|| {
7269 format!("retained draw index {index} out of bounds")
7270 })?;
7271 if (*index as u32) < MAX_REPLAY_SLOTS
7272 && self.replay_slots.slots.contains_key(&retained.slot)
7273 {
7274 let transform = retained.transform.with_retained_paint();
7275 staged_uploads.stage_at(
7276 UploadTarget::ReplayTransform,
7277 *index as u64 * REPLAY_TRANSFORM_STRIDE,
7278 bytemuck::bytes_of(&transform),
7279 );
7280 }
7281 }
7282 if end > start {
7283 fused_batches.push(FusedSegmentBatch::Retained {
7284 item_range: start..end,
7285 });
7286 }
7287 }
7288 }
7289 }
7290 let after_batch_prepare = Instant::now();
7291
7292 if !image_indices.is_empty() {
7293 self.stage_native_image_buffers(
7294 &mut staged_uploads,
7295 viewport,
7296 &image_vertices,
7297 &image_indices,
7298 );
7299 }
7300
7301 let device = self.device.clone();
7302 let composite_items: Vec<_> = chunk
7303 .iter()
7304 .filter_map(|batch| match batch {
7305 SegmentBatchPlan::Composite { start, end } => Some((start, end)),
7306 _ => None,
7307 })
7308 .flat_map(|(start, end)| {
7309 ordered_items[start..end].iter().filter_map(|(_, item)| {
7310 let SegmentDrawItem::Composite(composite_index) = item else {
7311 return None;
7312 };
7313 composites
7314 .get(*composite_index)
7315 .map(|(_, composite)| *composite)
7316 })
7317 })
7318 .collect();
7319 let prepared_composites = self.effect_renderer.prepare_composite_batch_draws(
7320 frame_encoder,
7321 &device,
7322 load_op,
7323 &composite_items,
7324 );
7325 let shader_items: Vec<_> = chunk
7326 .iter()
7327 .filter_map(|batch| match batch {
7328 SegmentBatchPlan::ShaderComposite { start, end } => Some((start, end)),
7329 _ => None,
7330 })
7331 .flat_map(|(start, end)| {
7332 ordered_items[start..end].iter().filter_map(|(_, item)| {
7333 let SegmentDrawItem::ShaderComposite(composite_index) = item else {
7334 return None;
7335 };
7336 shader_composites
7337 .get(*composite_index)
7338 .map(|(_, composite)| *composite)
7339 })
7340 })
7341 .collect();
7342 let prepared_shaders = self
7343 .effect_renderer
7344 .prepare_shader_batch_draws(frame_encoder, &device, &shader_items)
7345 .ok_or_else(|| "shader composite batch preparation failed".to_string())?;
7346 if !shader_items.is_empty() {
7347 self.effect_renderer.record_composite_pass();
7348 self.effect_renderer
7349 .debug_effects
7350 .set(self.effect_renderer.debug_effects.get() + shader_items.len() as u32);
7351 }
7352 let after_composite_prepare = Instant::now();
7353
7354 if fused_batches.is_empty() {
7355 return Ok(SegmentRenderOutcome {
7356 rendered_any: false,
7357 pass_count: 0,
7358 });
7359 }
7360
7361 self.flush_staged_uploads_at(
7366 frame_encoder.encoder(),
7367 &direct_shape_uploads,
7368 shape_upload_base,
7369 );
7370 let upload_offset =
7371 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7372 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
7373 let after_upload = Instant::now();
7374
7375 let use_retained_bundles = retained_bundles_enabled();
7376 let mut retained_encode_ms = 0.0_f64;
7377 {
7378 let mut render_pass =
7379 frame_encoder
7380 .encoder()
7381 .begin_render_pass(&wgpu::RenderPassDescriptor {
7382 label: Some("Fused Segment Draw Pass"),
7383 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7384 view: target_view,
7385 resolve_target: None,
7386 depth_slice: None,
7387 ops: wgpu::Operations {
7388 load: load_op,
7389 store: wgpu::StoreOp::Store,
7390 },
7391 })],
7392 depth_stencil_attachment: None,
7393 timestamp_writes: None,
7394 occlusion_query_set: None,
7395 multiview_mask: None,
7396 });
7397
7398 for batch in &fused_batches {
7399 match batch {
7400 FusedSegmentBatch::Shape { batch, blend_mode } => {
7401 self.draw_prepared_shapes(
7402 &mut render_pass,
7403 *blend_mode,
7404 *batch,
7405 width,
7406 height,
7407 );
7408 }
7409 FusedSegmentBatch::Image {
7410 cmd_range,
7411 blend_mode,
7412 } => {
7413 self.draw_native_prepared_image_cmd_range(
7414 &mut render_pass,
7415 &image_cmds,
7416 cmd_range.clone(),
7417 *blend_mode,
7418 )?;
7419 }
7420 FusedSegmentBatch::Text {
7421 image_cmd_range,
7422 glyph_cmd_range,
7423 } => {
7424 if !image_cmd_range.is_empty() {
7425 self.draw_native_prepared_image_cmd_range(
7426 &mut render_pass,
7427 &image_cmds,
7428 image_cmd_range.clone(),
7429 BlendMode::SrcOver,
7430 )?;
7431 self.frame_stats.bump_text();
7432 }
7433 if !glyph_cmd_range.is_empty() {
7434 self.draw_native_prepared_glyph_cmd_range(
7435 &mut render_pass,
7436 &glyph_cmds,
7437 glyph_cmd_range.clone(),
7438 )?;
7439 }
7440 }
7441 FusedSegmentBatch::Composite { draw_range } => {
7442 for draw in
7443 prepared_composites.get(draw_range.clone()).ok_or_else(|| {
7444 "composite draw range is outside the prepared command buffer"
7445 .to_string()
7446 })?
7447 {
7448 self.effect_renderer.draw_prepared_composite(
7449 &mut render_pass,
7450 (width, height),
7451 draw,
7452 );
7453 }
7454 }
7455 FusedSegmentBatch::ShaderComposite { draw_range } => {
7456 for draw in prepared_shaders.get(draw_range.clone()).ok_or_else(|| {
7457 "shader composite draw range is outside the prepared command buffer"
7458 .to_string()
7459 })? {
7460 self.effect_renderer.draw_prepared_shader_src_over(
7461 &device,
7462 &mut render_pass,
7463 (width, height),
7464 draw,
7465 );
7466 }
7467 }
7468 FusedSegmentBatch::Retained { item_range } => {
7469 let retained_start = Instant::now();
7475 if use_retained_bundles {
7476 self.draw_retained_stretch_bundled(
7477 &mut render_pass,
7478 ordered_items,
7479 retained_draws,
7480 item_range.clone(),
7481 width,
7482 height,
7483 );
7484 } else {
7485 for (_, item) in &ordered_items[item_range.clone()] {
7486 if let SegmentDrawItem::Retained(index) = item {
7487 if let Some(retained) = retained_draws.get(*index) {
7488 self.draw_retained_batch(
7489 &mut render_pass,
7490 retained,
7491 *index,
7492 width,
7493 height,
7494 );
7495 }
7496 }
7497 }
7498 }
7499 retained_encode_ms += instant_ms(retained_start, Instant::now());
7500 }
7501 }
7502 }
7503 }
7504 let after_pass = Instant::now();
7505 if let Some(total_ms) = should_log_wgpu_render_stage(partition_start, after_pass) {
7506 log::warn!(
7507 "[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={}",
7508 instant_ms(partition_start, after_shape_refs),
7509 instant_ms(after_shape_refs, after_shape_prepare),
7510 instant_ms(after_shape_prepare, after_batch_prepare),
7511 instant_ms(after_batch_prepare, after_composite_prepare),
7512 instant_ms(after_composite_prepare, after_upload),
7513 instant_ms(after_upload, after_pass),
7514 fused_batches.len(),
7515 budget.shape_count,
7516 image_cmds.len(),
7517 glyph_cmds.len(),
7518 staged_uploads.bytes.len(),
7519 );
7520 }
7521
7522 Ok(SegmentRenderOutcome {
7523 rendered_any: true,
7524 pass_count: 1,
7525 })
7526 })();
7527
7528 self.scratch_image_vertices = image_vertices;
7529 self.scratch_image_indices = image_indices;
7530 self.scratch_image_cmds = image_cmds;
7531 self.scratch_glyph_cmds = glyph_cmds;
7532 self.restore_staged_uploads(staged_uploads);
7533 result
7534 }
7535
7536 #[allow(clippy::too_many_arguments)]
7537 fn render_segment_draw_chunk<C: FrameCommandRecorder>(
7538 &mut self,
7539 frame_encoder: &mut C,
7540 target_view: &wgpu::TextureView,
7541 ordered_items: &[(usize, SegmentDrawItem)],
7542 composites: &[(usize, CompositeBatchItem<'_>)],
7543 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
7544 shapes: &[DrawShape],
7545 images: &[ImageDraw],
7546 texts: &[TextDraw],
7547 retained_draws: &[RetainedDraw],
7548 chunk: SegmentDrawChunkPlan,
7549 width: u32,
7550 height: u32,
7551 root_scale: f32,
7552 load_op: wgpu::LoadOp<wgpu::Color>,
7553 ) -> Result<SegmentRenderOutcome, String> {
7554 #[cfg(target_arch = "wasm32")]
7555 let _ = retained_draws;
7556 #[cfg(not(target_arch = "wasm32"))]
7557 if let Some(outcome) = self.render_segment_draw_chunk_fused_native(
7558 frame_encoder,
7559 target_view,
7560 ordered_items,
7561 composites,
7562 shader_composites,
7563 shapes,
7564 images,
7565 texts,
7566 retained_draws,
7567 &chunk,
7568 width,
7569 height,
7570 root_scale,
7571 load_op,
7572 )? {
7573 return Ok(outcome);
7574 }
7575
7576 let mut staged_uploads = self.take_staged_uploads();
7577 let result = (|| {
7578 let mut rendered_any = false;
7579 let mut pass_count = 0_u32;
7580 let mut next_load_op = load_op;
7581 for batch in chunk.iter() {
7582 staged_uploads.clear();
7583 match batch {
7584 SegmentBatchPlan::Shape {
7585 start,
7586 end,
7587 blend_mode,
7588 } => {
7589 let slice = &ordered_items[start..end];
7590 if slice.len() > self.shape_batch_limits.max_shapes_per_batch {
7591 return Err(format!(
7592 "shape batch contains {} shapes, exceeding the renderer limit of {}",
7593 slice.len(),
7594 self.shape_batch_limits.max_shapes_per_batch
7595 ));
7596 }
7597 let viewport = ViewportUniformParams {
7598 width,
7599 height,
7600 offset: [0.0, 0.0],
7601 };
7602 for (_, item) in slice {
7603 if !matches!(item, SegmentDrawItem::Shape(_)) {
7604 return Err(format!(
7605 "shape batch contains non-shape draw item: {item:?}"
7606 ));
7607 }
7608 }
7609 let Some(prepared) = self.prepare_shapes_batch(
7610 slice.iter().filter_map(|(_, item)| match item {
7611 SegmentDrawItem::Shape(shape_index) => Some(&shapes[*shape_index]),
7612 _ => None,
7613 }),
7614 root_scale,
7615 viewport,
7616 &mut staged_uploads,
7617 ) else {
7618 continue;
7619 };
7620 let upload_offset = frame_encoder
7621 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7622 self.flush_staged_uploads_at(
7623 frame_encoder.encoder(),
7624 &staged_uploads,
7625 upload_offset,
7626 );
7627 {
7628 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7629 &wgpu::RenderPassDescriptor {
7630 label: Some("Segment Shape Pass"),
7631 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7632 view: target_view,
7633 resolve_target: None,
7634 depth_slice: None,
7635 ops: wgpu::Operations {
7636 load: next_load_op,
7637 store: wgpu::StoreOp::Store,
7638 },
7639 })],
7640 depth_stencil_attachment: None,
7641 timestamp_writes: None,
7642 occlusion_query_set: None,
7643 multiview_mask: None,
7644 },
7645 );
7646 self.draw_prepared_shapes(
7647 &mut render_pass,
7648 blend_mode,
7649 prepared,
7650 width,
7651 height,
7652 );
7653 }
7654 pass_count = pass_count.saturating_add(1);
7655 rendered_any = true;
7656 next_load_op = wgpu::LoadOp::Load;
7657 }
7658 SegmentBatchPlan::Image {
7659 start,
7660 end,
7661 blend_mode,
7662 } => {
7663 let viewport = ViewportUniformParams {
7664 width,
7665 height,
7666 offset: [0.0, 0.0],
7667 };
7668 for (_, item) in &ordered_items[start..end] {
7669 if !matches!(item, SegmentDrawItem::Image(_)) {
7670 return Err(format!(
7671 "image batch contains non-image draw item: {item:?}"
7672 ));
7673 }
7674 }
7675 let prepared_images = self.prepare_image_draw_cmds(
7676 ordered_items[start..end]
7677 .iter()
7678 .filter_map(|(_, item)| match item {
7679 SegmentDrawItem::Image(image_index) => {
7680 Some(&images[*image_index])
7681 }
7682 _ => None,
7683 }),
7684 viewport,
7685 root_scale,
7686 &mut staged_uploads,
7687 )?;
7688 if prepared_images.is_empty() {
7689 self.scratch_image_cmds = prepared_images.into_cmds();
7690 continue;
7691 }
7692 let upload_offset = frame_encoder
7693 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7694 self.flush_staged_uploads_at(
7695 frame_encoder.encoder(),
7696 &staged_uploads,
7697 upload_offset,
7698 );
7699 let draw_result = {
7700 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7701 &wgpu::RenderPassDescriptor {
7702 label: Some("Segment Image Pass"),
7703 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7704 view: target_view,
7705 resolve_target: None,
7706 depth_slice: None,
7707 ops: wgpu::Operations {
7708 load: next_load_op,
7709 store: wgpu::StoreOp::Store,
7710 },
7711 })],
7712 depth_stencil_attachment: None,
7713 timestamp_writes: None,
7714 occlusion_query_set: None,
7715 multiview_mask: None,
7716 },
7717 );
7718 self.draw_prepared_images(
7719 &mut render_pass,
7720 &prepared_images,
7721 blend_mode,
7722 )
7723 };
7724 pass_count = pass_count.saturating_add(1);
7725 self.scratch_image_cmds = prepared_images.into_cmds();
7726 draw_result?;
7727 rendered_any = true;
7728 next_load_op = wgpu::LoadOp::Load;
7729 }
7730 SegmentBatchPlan::Text { start, end } => {
7731 let viewport = ViewportUniformParams {
7732 width,
7733 height,
7734 offset: [0.0, 0.0],
7735 };
7736 let text_draws =
7737 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7738 if let Some(prepared_glyphs) = self.prepare_text_glyph_draw_cmds(
7739 text_draws,
7740 viewport,
7741 root_scale,
7742 &mut staged_uploads,
7743 )? {
7744 if prepared_glyphs.is_empty() {
7745 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
7746 continue;
7747 }
7748 let upload_offset = frame_encoder
7749 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7750 self.flush_staged_uploads_at(
7751 frame_encoder.encoder(),
7752 &staged_uploads,
7753 upload_offset,
7754 );
7755 {
7756 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7757 &wgpu::RenderPassDescriptor {
7758 label: Some("Segment Text Glyph Atlas Pass"),
7759 color_attachments: &[Some(
7760 wgpu::RenderPassColorAttachment {
7761 view: target_view,
7762 resolve_target: None,
7763 depth_slice: None,
7764 ops: wgpu::Operations {
7765 load: next_load_op,
7766 store: wgpu::StoreOp::Store,
7767 },
7768 },
7769 )],
7770 depth_stencil_attachment: None,
7771 timestamp_writes: None,
7772 occlusion_query_set: None,
7773 multiview_mask: None,
7774 },
7775 );
7776 self.draw_prepared_glyphs(&mut render_pass, &prepared_glyphs)?;
7777 }
7778 pass_count = pass_count.saturating_add(1);
7779 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
7780 rendered_any = true;
7781 next_load_op = wgpu::LoadOp::Load;
7782 } else {
7783 let text_draws =
7784 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7785 let prepared_images = self.prepare_text_image_draw_cmds(
7786 text_draws,
7787 viewport,
7788 root_scale,
7789 &mut staged_uploads,
7790 )?;
7791 if prepared_images.is_empty() {
7792 self.scratch_image_cmds = prepared_images.into_cmds();
7793 continue;
7794 }
7795 let upload_offset = frame_encoder
7796 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7797 self.flush_staged_uploads_at(
7798 frame_encoder.encoder(),
7799 &staged_uploads,
7800 upload_offset,
7801 );
7802 {
7803 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7804 &wgpu::RenderPassDescriptor {
7805 label: Some("Segment Text Pass"),
7806 color_attachments: &[Some(
7807 wgpu::RenderPassColorAttachment {
7808 view: target_view,
7809 resolve_target: None,
7810 depth_slice: None,
7811 ops: wgpu::Operations {
7812 load: next_load_op,
7813 store: wgpu::StoreOp::Store,
7814 },
7815 },
7816 )],
7817 depth_stencil_attachment: None,
7818 timestamp_writes: None,
7819 occlusion_query_set: None,
7820 multiview_mask: None,
7821 },
7822 );
7823 self.draw_prepared_images(
7824 &mut render_pass,
7825 &prepared_images,
7826 BlendMode::SrcOver,
7827 )?;
7828 }
7829 self.frame_stats.bump_text();
7830 pass_count = pass_count.saturating_add(1);
7831 self.scratch_image_cmds = prepared_images.into_cmds();
7832 rendered_any = true;
7833 next_load_op = wgpu::LoadOp::Load;
7834 }
7835 }
7836 SegmentBatchPlan::Composite { start, end } => {
7837 let batch_items: Vec<_> = ordered_items[start..end]
7838 .iter()
7839 .map(|(_, item)| match item {
7840 SegmentDrawItem::Composite(composite_index) => composites
7841 .get(*composite_index)
7842 .map(|(_, composite)| *composite)
7843 .ok_or_else(|| {
7844 "composite item index is outside the composite buffer"
7845 .to_string()
7846 }),
7847 other => Err(format!(
7848 "composite batch contains non-composite draw item: {other:?}"
7849 )),
7850 })
7851 .collect::<Result<_, _>>()?;
7852 let device = self.device.clone();
7853 self.effect_renderer.encode_composite_batch_to_view_pass(
7854 frame_encoder,
7855 &device,
7856 target_view,
7857 (width, height),
7858 next_load_op,
7859 &batch_items,
7860 );
7861 self.effect_renderer.record_composite_pass();
7862 pass_count = pass_count.saturating_add(1);
7863 rendered_any = true;
7864 next_load_op = wgpu::LoadOp::Load;
7865 }
7866 SegmentBatchPlan::ShaderComposite { start, end } => {
7867 let batch_items: Vec<_> = ordered_items[start..end]
7868 .iter()
7869 .map(|(_, item)| match item {
7870 SegmentDrawItem::ShaderComposite(composite_index) => {
7871 shader_composites
7872 .get(*composite_index)
7873 .map(|(_, composite)| *composite)
7874 .ok_or_else(|| {
7875 "shader composite item index is outside the shader composite buffer"
7876 .to_string()
7877 })
7878 }
7879 other => Err(format!(
7880 "shader composite batch contains non-shader-composite draw item: {other:?}"
7881 )),
7882 })
7883 .collect::<Result<Vec<_>, _>>()?;
7884 let device = self.device.clone();
7885 let encoded = self.effect_renderer.encode_shader_batch_src_over_to_view(
7886 frame_encoder,
7887 &device,
7888 target_view,
7889 (width, height),
7890 next_load_op,
7891 &batch_items,
7892 );
7893 if !encoded {
7894 return Err("shader composite batch failed to encode".to_string());
7895 }
7896 self.effect_renderer.record_composite_pass();
7897 self.effect_renderer.debug_effects.set(
7898 self.effect_renderer.debug_effects.get() + batch_items.len() as u32,
7899 );
7900 pass_count = pass_count.saturating_add(1);
7901 rendered_any = true;
7902 next_load_op = wgpu::LoadOp::Load;
7903 }
7904 SegmentBatchPlan::Retained { start, end } => {
7905 #[cfg(target_arch = "wasm32")]
7913 {
7914 let _ = (start, end);
7915 return Err("retained shape batches are native-only".to_string());
7916 }
7917 #[cfg(not(target_arch = "wasm32"))]
7918 {
7919 self.stage_replay_patches(&mut staged_uploads);
7920 for (_, item) in &ordered_items[start..end] {
7921 let SegmentDrawItem::Retained(index) = item else {
7922 return Err(format!(
7923 "retained batch contains non-retained draw item: {item:?}"
7924 ));
7925 };
7926 let retained = retained_draws.get(*index).ok_or_else(|| {
7927 format!("retained draw index {index} out of bounds")
7928 })?;
7929 if (*index as u32) < MAX_REPLAY_SLOTS
7930 && self.replay_slots.slots.contains_key(&retained.slot)
7931 {
7932 let transform = retained.transform.with_retained_paint();
7933 staged_uploads.stage_at(
7934 UploadTarget::ReplayTransform,
7935 *index as u64 * REPLAY_TRANSFORM_STRIDE,
7936 bytemuck::bytes_of(&transform),
7937 );
7938 }
7939 }
7940 let upload_offset = frame_encoder
7941 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7942 self.flush_staged_uploads_at(
7943 frame_encoder.encoder(),
7944 &staged_uploads,
7945 upload_offset,
7946 );
7947 {
7948 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7949 &wgpu::RenderPassDescriptor {
7950 label: Some("Segment Retained Pass"),
7951 color_attachments: &[Some(
7952 wgpu::RenderPassColorAttachment {
7953 view: target_view,
7954 resolve_target: None,
7955 depth_slice: None,
7956 ops: wgpu::Operations {
7957 load: next_load_op,
7958 store: wgpu::StoreOp::Store,
7959 },
7960 },
7961 )],
7962 depth_stencil_attachment: None,
7963 timestamp_writes: None,
7964 occlusion_query_set: None,
7965 multiview_mask: None,
7966 },
7967 );
7968 for (_, item) in &ordered_items[start..end] {
7969 if let SegmentDrawItem::Retained(index) = item {
7970 if let Some(retained) = retained_draws.get(*index) {
7971 self.draw_retained_batch(
7972 &mut render_pass,
7973 retained,
7974 *index,
7975 width,
7976 height,
7977 );
7978 }
7979 }
7980 }
7981 }
7982 pass_count = pass_count.saturating_add(1);
7983 rendered_any = true;
7984 next_load_op = wgpu::LoadOp::Load;
7985 }
7986 }
7987 }
7988 }
7989 Ok(SegmentRenderOutcome {
7990 rendered_any,
7991 pass_count,
7992 })
7993 })();
7994 self.restore_staged_uploads(staged_uploads);
7995 result
7996 }
7997
7998 fn viewport_uniforms(params: ViewportUniformParams) -> Uniforms {
7999 Uniforms {
8000 viewport: [params.width as f32, params.height as f32],
8001 viewport_offset: params.offset,
8002 }
8003 }
8004
8005 #[cfg(not(target_arch = "wasm32"))]
8006 fn stage_viewport_uniforms(
8007 &self,
8008 staged_uploads: &mut StagedBufferUploads,
8009 params: ViewportUniformParams,
8010 ) {
8011 let uniforms = Self::viewport_uniforms(params);
8012 staged_uploads.stage(UploadTarget::Uniform, bytemuck::bytes_of(&uniforms));
8013 }
8014
8015 #[cfg(not(target_arch = "wasm32"))]
8016 fn stage_retained_glyph_viewport_uniforms(
8017 &mut self,
8018 staged_uploads: &mut StagedBufferUploads,
8019 params: ViewportUniformParams,
8020 ) -> usize {
8021 let slot = self.claim_retained_glyph_uniform_slot();
8022 let uniforms = Self::viewport_uniforms(params);
8023 staged_uploads.stage_at(
8024 UploadTarget::RetainedGlyphUniform,
8025 self.retained_glyph_uniform_offset(slot),
8026 bytemuck::bytes_of(&uniforms),
8027 );
8028 slot
8029 }
8030
8031 #[cfg(not(target_arch = "wasm32"))]
8032 fn claim_retained_glyph_uniform_slot(&mut self) -> usize {
8033 let slot = self.retained_glyph_uniform_cursor;
8034 self.retained_glyph_uniform_cursor = self.retained_glyph_uniform_cursor.saturating_add(1);
8035 self.ensure_retained_glyph_uniform_capacity(slot.saturating_add(1));
8036 slot
8037 }
8038
8039 #[cfg(not(target_arch = "wasm32"))]
8040 fn retained_glyph_uniform_offset(&self, slot: usize) -> u64 {
8041 self.retained_glyph_uniform_stride * slot as u64
8042 }
8043
8044 #[cfg(not(target_arch = "wasm32"))]
8045 fn retained_glyph_uniform_dynamic_offset(&self, slot: usize) -> Result<u32, String> {
8046 let offset = self.retained_glyph_uniform_offset(slot);
8047 u32::try_from(offset).map_err(|_| {
8048 "retained glyph uniform offset exceeded WGPU dynamic offset range".to_string()
8049 })
8050 }
8051
8052 #[cfg(not(target_arch = "wasm32"))]
8053 fn ensure_retained_glyph_uniform_capacity(&mut self, required_slots: usize) {
8054 if required_slots <= self.retained_glyph_uniform_capacity {
8055 return;
8056 }
8057 let new_capacity = required_slots
8058 .next_power_of_two()
8059 .max(INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS);
8060 self.retained_glyph_uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
8061 label: Some("Retained Glyph Uniform Buffer"),
8062 size: self.retained_glyph_uniform_stride * new_capacity as u64,
8063 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
8064 mapped_at_creation: false,
8065 });
8066 self.retained_glyph_uniform_bind_group =
8067 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
8068 label: Some("Retained Glyph Uniform Bind Group"),
8069 layout: &self.retained_glyph_uniform_bind_group_layout,
8070 entries: &[wgpu::BindGroupEntry {
8071 binding: 0,
8072 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
8073 buffer: &self.retained_glyph_uniform_buffer,
8074 offset: 0,
8075 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
8076 }),
8077 }],
8078 });
8079 self.retained_glyph_uniform_capacity = new_capacity;
8080 }
8081
8082 #[cfg(target_arch = "wasm32")]
8083 fn prepare_wasm_viewport_uniforms(&mut self, params: ViewportUniformParams) -> usize {
8084 let slot = self.claim_wasm_uniform_batch();
8085 let uniforms = Self::viewport_uniforms(params);
8086 let bytes = bytemuck::bytes_of(&uniforms);
8087 let upload_stats = self.frame_graph_executor.upload_buffer(
8088 &self.queue,
8089 &self.wasm_uniform_batches[slot].buffer,
8090 0,
8091 bytes,
8092 );
8093 self.frame_stats.record_command_stats(upload_stats);
8094 slot
8095 }
8096
8097 #[cfg(target_arch = "wasm32")]
8098 fn claim_wasm_uniform_batch(&mut self) -> usize {
8099 let slot = self.wasm_uniform_batch_cursor;
8100 self.wasm_uniform_batch_cursor += 1;
8101 while self.wasm_uniform_batches.len() <= slot {
8102 self.wasm_uniform_batches.push(UniformBatchBuffer::new(
8103 &self.device,
8104 &self.uniform_bind_group_layout,
8105 ));
8106 }
8107 slot
8108 }
8109
8110 #[cfg(target_arch = "wasm32")]
8111 fn claim_wasm_shape_batch(&mut self) -> usize {
8112 let slot = self.wasm_shape_batch_cursor;
8113 self.wasm_shape_batch_cursor += 1;
8114 while self.wasm_shape_batches.len() <= slot {
8115 self.wasm_shape_batches.push(ShapeBatchBuffers::new(
8116 &self.device,
8117 &self.shape_bind_group_layout,
8118 &self.identity_similarity_buffer,
8119 self.dummy_paint_buffer.as_ref(),
8120 self.shape_batch_limits,
8121 ));
8122 }
8123 slot
8124 }
8125
8126 #[cfg(target_arch = "wasm32")]
8127 fn claim_wasm_image_batch(&mut self) -> usize {
8128 let slot = self.wasm_image_batch_cursor;
8129 self.wasm_image_batch_cursor += 1;
8130 while self.wasm_image_batches.len() <= slot {
8131 self.wasm_image_batches
8132 .push(ImageBatchBuffers::new(&self.device));
8133 }
8134 slot
8135 }
8136
8137 #[cfg(target_arch = "wasm32")]
8138 fn write_wasm_buffer(&self, buffer: &wgpu::Buffer, bytes: &[u8]) {
8139 let upload_stats = self
8140 .frame_graph_executor
8141 .upload_buffer(&self.queue, buffer, 0, bytes);
8142 self.frame_stats.record_command_stats(upload_stats);
8143 }
8144
8145 fn take_staged_uploads(&mut self) -> StagedBufferUploads {
8146 let mut staged_uploads = std::mem::take(&mut self.staged_uploads);
8147 debug_assert!(
8148 staged_uploads.is_empty(),
8149 "renderer-owned staged uploads should be restored as empty scratch storage"
8150 );
8151 staged_uploads.clear();
8152 staged_uploads
8153 }
8154
8155 fn restore_staged_uploads(&mut self, mut staged_uploads: StagedBufferUploads) {
8156 staged_uploads.clear();
8157 self.staged_uploads = staged_uploads;
8158 }
8159
8160 #[cfg(not(target_arch = "wasm32"))]
8161 fn ensure_upload_buffer_capacity(&mut self, required_bytes: u64) {
8162 if required_bytes <= self.upload_buffer.size() {
8163 return;
8164 }
8165
8166 let new_size = required_bytes
8167 .next_power_of_two()
8168 .max(INITIAL_UPLOAD_BUFFER_BYTES);
8169 self.upload_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
8170 label: Some("Frame Upload Buffer"),
8171 size: new_size,
8172 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
8173 mapped_at_creation: false,
8174 });
8175 }
8176
8177 fn flush_staged_uploads_at(
8178 &mut self,
8179 encoder: &mut wgpu::CommandEncoder,
8180 staged_uploads: &StagedBufferUploads,
8181 upload_buffer_offset: u64,
8182 ) {
8183 if staged_uploads.is_empty() {
8184 return;
8185 }
8186 debug_assert_eq!(
8187 upload_buffer_offset % wgpu::COPY_BUFFER_ALIGNMENT,
8188 0,
8189 "upload-buffer base offset must satisfy copy alignment"
8190 );
8191
8192 #[cfg(target_arch = "wasm32")]
8193 {
8194 let _ = upload_buffer_offset;
8195 let _ = encoder;
8196 debug_assert!(
8197 staged_uploads.is_empty(),
8198 "wasm draw uploads use retained per-batch resource slots"
8199 );
8200 return;
8201 }
8202
8203 #[cfg(not(target_arch = "wasm32"))]
8204 {
8205 self.ensure_upload_buffer_capacity(
8206 upload_buffer_offset + staged_uploads.bytes.len() as u64,
8207 );
8208 let upload_stats = self.frame_graph_executor.upload_buffer(
8209 &self.queue,
8210 &self.upload_buffer,
8211 upload_buffer_offset,
8212 &staged_uploads.bytes,
8213 );
8214 self.frame_stats.record_command_stats(upload_stats);
8215
8216 for copy in &staged_uploads.copies {
8217 let target_buffer = match copy.target {
8218 UploadTarget::Uniform => &self.uniform_buffer,
8219 UploadTarget::ShapeData => &self.shape_buffers.shape_buffer,
8220 UploadTarget::ShapeGradient => &self.shape_buffers.gradient_buffer,
8221 UploadTarget::ImageVertex => &self.image_vertex_buffer,
8222 UploadTarget::ImageIndex => &self.image_index_buffer,
8223 UploadTarget::RetainedGlyphUniform => &self.retained_glyph_uniform_buffer,
8224 UploadTarget::ReplayTransform => &self.replay_slots.transform_buffer,
8225 UploadTarget::ReplayPaintData(slot) => {
8226 let Some(entry) = self.replay_slots.slots.get(&slot) else {
8229 continue;
8230 };
8231 &entry.paint_buffer
8232 }
8233 };
8234 encoder.copy_buffer_to_buffer(
8235 &self.upload_buffer,
8236 upload_buffer_offset + copy.source_offset,
8237 target_buffer,
8238 copy.target_offset,
8239 copy.size,
8240 );
8241 }
8242 }
8243 }
8244
8245 #[allow(clippy::too_many_arguments)]
8246 fn encode_shadow_draw<C: FrameCommandRecorder>(
8247 &mut self,
8248 frame_encoder: &mut C,
8249 target_view: &wgpu::TextureView,
8250 shadow: &ShadowDraw,
8251 width: u32,
8252 height: u32,
8253 root_scale: f32,
8254 ) {
8255 if shadow.shapes.is_empty() && shadow.texts.is_empty() {
8256 return;
8257 }
8258
8259 let shape_bounds_opt = shadow
8260 .shapes
8261 .iter()
8262 .map(|(shape, _)| shape.rect)
8263 .reduce(|a, b| Rect {
8264 x: a.x.min(b.x),
8265 y: a.y.min(b.y),
8266 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
8267 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
8268 });
8269
8270 let text_bounds_opt = shadow
8271 .texts
8272 .iter()
8273 .map(|text| text.rect)
8274 .reduce(|a, b| Rect {
8275 x: a.x.min(b.x),
8276 y: a.y.min(b.y),
8277 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
8278 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
8279 });
8280
8281 let combined_bounds = match (shape_bounds_opt, text_bounds_opt) {
8282 (Some(s), Some(t)) => Some(Rect {
8283 x: s.x.min(t.x),
8284 y: s.y.min(t.y),
8285 width: (s.x + s.width).max(t.x + t.width) - s.x.min(t.x),
8286 height: (s.y + s.height).max(t.y + t.height) - s.y.min(t.y),
8287 }),
8288 (Some(s), None) => Some(s),
8289 (None, Some(t)) => Some(t),
8290 (None, None) => None,
8291 };
8292
8293 let Some(shape_bounds) = combined_bounds else {
8294 return;
8295 };
8296
8297 let blur_margin = blur_extent_margin(shadow.blur_radius);
8298 let source_blur_bounds = Rect {
8299 x: shape_bounds.x - blur_margin,
8300 y: shape_bounds.y - blur_margin,
8301 width: shape_bounds.width + blur_margin * 2.0,
8302 height: shape_bounds.height + blur_margin * 2.0,
8303 };
8304 let mut visible_blur_bounds = source_blur_bounds;
8305 if let Some(clip) = shadow.clip {
8306 let clip_expanded = Rect {
8307 x: clip.x - blur_margin,
8308 y: clip.y - blur_margin,
8309 width: clip.width + blur_margin * 2.0,
8310 height: clip.height + blur_margin * 2.0,
8311 };
8312 let Some(intersection) = visible_blur_bounds.intersect(clip_expanded) else {
8313 return;
8314 };
8315 visible_blur_bounds = intersection;
8316 }
8317 let processing_scissor =
8318 scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
8319 if processing_scissor.is_none() {
8320 return;
8321 }
8322
8323 if shadow.blur_radius <= 0.0 {
8325 for (shape, blend_mode) in &shadow.shapes {
8326 self.encode_shapes_pass(
8327 frame_encoder,
8328 target_view,
8329 std::iter::once(shape),
8330 *blend_mode,
8331 width,
8332 height,
8333 root_scale,
8334 wgpu::LoadOp::Load,
8335 [0.0, 0.0],
8336 );
8337 frame_encoder.record_pass();
8338 }
8339 if !shadow.texts.is_empty() {
8340 let mut staged_uploads = self.take_staged_uploads();
8341 let viewport = ViewportUniformParams {
8342 width,
8343 height,
8344 offset: [0.0, 0.0],
8345 };
8346 match self.prepare_text_image_draw_cmds(
8347 shadow.texts.iter(),
8348 viewport,
8349 root_scale,
8350 &mut staged_uploads,
8351 ) {
8352 Ok(prepared_images) if !prepared_images.is_empty() => {
8353 let upload_offset = frame_encoder
8354 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
8355 self.flush_staged_uploads_at(
8356 frame_encoder.encoder(),
8357 &staged_uploads,
8358 upload_offset,
8359 );
8360 let draw_result = {
8361 let mut render_pass = frame_encoder.encoder().begin_render_pass(
8362 &wgpu::RenderPassDescriptor {
8363 label: Some("Zero Blur Shadow Text Image Pass"),
8364 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8365 view: target_view,
8366 resolve_target: None,
8367 depth_slice: None,
8368 ops: wgpu::Operations {
8369 load: wgpu::LoadOp::Load,
8370 store: wgpu::StoreOp::Store,
8371 },
8372 })],
8373 depth_stencil_attachment: None,
8374 timestamp_writes: None,
8375 occlusion_query_set: None,
8376 multiview_mask: None,
8377 },
8378 );
8379 self.draw_prepared_images(
8380 &mut render_pass,
8381 &prepared_images,
8382 BlendMode::SrcOver,
8383 )
8384 };
8385 self.scratch_image_cmds = prepared_images.into_cmds();
8386 if let Err(e) = draw_result {
8387 eprintln!("Failed to draw text for zero-blur shadow: {}", e);
8388 } else {
8389 self.frame_stats.bump_text();
8390 frame_encoder.record_pass();
8391 }
8392 }
8393 Ok(prepared_images) => {
8394 self.scratch_image_cmds = prepared_images.into_cmds();
8395 }
8396 Err(e) => {
8397 eprintln!("Failed to prepare text image for zero-blur shadow: {}", e);
8398 }
8399 }
8400 self.restore_staged_uploads(staged_uploads);
8401 }
8402 return;
8403 }
8404
8405 let Some(device_bounds) =
8407 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)
8408 else {
8409 return;
8410 };
8411 let bounds_x = device_bounds.x;
8412 let bounds_y = device_bounds.y;
8413 let bounds_w = device_bounds.width;
8414 let bounds_h = device_bounds.height;
8415 let pixel_radius = shadow.blur_radius * root_scale;
8416
8417 if shadow.texts.is_empty() && !shadow.shapes.is_empty() {
8418 if let Some(plan) = shape_shadow_surface_plan(
8419 &shadow.shapes,
8420 shadow.clip,
8421 shadow.blur_radius,
8422 width,
8423 height,
8424 root_scale,
8425 self.max_texture_dim(),
8426 ) {
8427 if self.encode_shape_only_blurred_shadow_draw(
8428 frame_encoder,
8429 target_view,
8430 shadow,
8431 plan.source_device_bounds,
8432 plan.pixel_radius,
8433 plan.processing_scissor,
8434 width,
8435 height,
8436 root_scale,
8437 ) {
8438 return;
8439 }
8440 }
8441 }
8442
8443 if !shadow.texts.is_empty() {
8444 self.frame_stats.record_shadow_text_blur_fallback();
8445 }
8446
8447 let device = self.device.clone();
8448 let source_descriptor =
8449 self.transient_offscreen_descriptor("Shadow Source", bounds_w, bounds_h);
8450 let source = frame_encoder.acquire_transient_offscreen(&device, source_descriptor);
8451 let viewport_offset = [bounds_x, bounds_y];
8452 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
8453 let source_outcome = self.encode_shadow_shape_source_passes(
8454 frame_encoder,
8455 &source.view,
8456 &shadow.shapes,
8457 bounds_w,
8458 bounds_h,
8459 viewport_offset,
8460 root_scale,
8461 &mut next_load_op,
8462 );
8463 frame_encoder.record_passes(source_outcome.pass_count);
8464 let mut rendered_any = source_outcome.rendered_any;
8465
8466 if !shadow.texts.is_empty() {
8467 let mut shifted_texts = shadow.texts.clone();
8468 for text in &mut shifted_texts {
8469 text.rect.x -= viewport_offset[0] / root_scale;
8470 text.rect.y -= viewport_offset[1] / root_scale;
8471 if let Some(clip) = text.clip.as_mut() {
8472 clip.x -= viewport_offset[0] / root_scale;
8473 clip.y -= viewport_offset[1] / root_scale;
8474 }
8475 }
8476
8477 let mut staged_uploads = self.take_staged_uploads();
8478 let viewport = ViewportUniformParams {
8479 width: bounds_w,
8480 height: bounds_h,
8481 offset: [0.0, 0.0],
8482 };
8483 match self.prepare_text_image_draw_cmds(
8484 shifted_texts.iter(),
8485 viewport,
8486 root_scale,
8487 &mut staged_uploads,
8488 ) {
8489 Ok(prepared_images) if !prepared_images.is_empty() => {
8490 let upload_offset = frame_encoder
8491 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
8492 self.flush_staged_uploads_at(
8493 frame_encoder.encoder(),
8494 &staged_uploads,
8495 upload_offset,
8496 );
8497 let draw_result = {
8498 let mut render_pass = frame_encoder.encoder().begin_render_pass(
8499 &wgpu::RenderPassDescriptor {
8500 label: Some("Shadow Source Text Image Pass"),
8501 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8502 view: &source.view,
8503 resolve_target: None,
8504 depth_slice: None,
8505 ops: wgpu::Operations {
8506 load: next_load_op,
8507 store: wgpu::StoreOp::Store,
8508 },
8509 })],
8510 depth_stencil_attachment: None,
8511 timestamp_writes: None,
8512 occlusion_query_set: None,
8513 multiview_mask: None,
8514 },
8515 );
8516 self.draw_prepared_images(
8517 &mut render_pass,
8518 &prepared_images,
8519 BlendMode::SrcOver,
8520 )
8521 };
8522 self.scratch_image_cmds = prepared_images.into_cmds();
8523 if let Err(e) = draw_result {
8524 eprintln!("Failed to draw text for shadow: {}", e);
8525 } else {
8526 self.frame_stats.bump_text();
8527 frame_encoder.record_pass();
8528 rendered_any = true;
8529 }
8530 }
8531 Ok(prepared_images) => {
8532 self.scratch_image_cmds = prepared_images.into_cmds();
8533 }
8534 Err(e) => {
8535 eprintln!("Failed to prepare text image for shadow: {}", e);
8536 }
8537 }
8538 self.restore_staged_uploads(staged_uploads);
8539 }
8540
8541 if !rendered_any {
8542 frame_encoder.release_transient_offscreen(source_descriptor, source);
8543 return;
8544 }
8545
8546 let scratch_descriptor =
8547 self.transient_offscreen_descriptor("Shadow Blur Scratch", bounds_w, bounds_h);
8548 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
8549 {
8550 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
8551 frame_encoder,
8552 &device,
8553 &source,
8554 &scratch,
8555 &source.view,
8556 pixel_radius,
8557 pixel_radius,
8558 TileMode::Decal,
8559 None, );
8561 }
8562 frame_encoder.record_passes(2);
8563
8564 let clip_scissor = shadow
8565 .clip
8566 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
8567 let scissor = clip_scissor.or(processing_scissor);
8568 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
8569 mask.rect[0] -= viewport_offset[0];
8572 mask.rect[1] -= viewport_offset[1];
8573 mask
8574 });
8575 let dest_viewport = Some((
8576 viewport_offset[0],
8577 viewport_offset[1],
8578 bounds_w as f32,
8579 bounds_h as f32,
8580 ));
8581 {
8582 self.effect_renderer
8583 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8584 frame_encoder,
8585 &device,
8586 &source,
8587 target_view,
8588 1.0,
8589 wgpu::LoadOp::Load,
8590 scissor,
8591 rounded_mask,
8592 BlendMode::SrcOver,
8593 dest_viewport,
8594 CompositeSampleMode::Linear,
8595 );
8596 }
8597 frame_encoder.record_pass();
8598 self.effect_renderer.record_blur_pass();
8599 self.effect_renderer.record_composite_pass();
8600 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
8601 frame_encoder.release_transient_offscreen(source_descriptor, source);
8602 }
8603
8604 #[allow(clippy::too_many_arguments)]
8605 fn encode_shadow_shape_source_passes<C: FrameCommandRecorder>(
8606 &mut self,
8607 frame_encoder: &mut C,
8608 source_view: &wgpu::TextureView,
8609 shapes: &[(DrawShape, BlendMode)],
8610 width: u32,
8611 height: u32,
8612 viewport_offset: [f32; 2],
8613 root_scale: f32,
8614 next_load_op: &mut wgpu::LoadOp<wgpu::Color>,
8615 ) -> ShadowSourceRenderOutcome {
8616 if shapes.is_empty() {
8617 return ShadowSourceRenderOutcome {
8618 rendered_any: false,
8619 pass_count: 0,
8620 };
8621 }
8622
8623 let mut staged_uploads = self.take_staged_uploads();
8624 let mut rendered_any = false;
8625 let mut pass_count = 0_u32;
8626 let mut start = 0usize;
8627 while start < shapes.len() {
8628 let blend_mode = supported_blend_mode(shapes[start].1);
8629 let mut end = start + 1;
8630 while end < shapes.len()
8631 && end - start < self.shape_batch_limits.max_shapes_per_batch
8632 && supported_blend_mode(shapes[end].1) == blend_mode
8633 {
8634 end += 1;
8635 }
8636
8637 staged_uploads.clear();
8638 let viewport = ViewportUniformParams {
8639 width,
8640 height,
8641 offset: viewport_offset,
8642 };
8643 let Some(prepared_shape) = self.prepare_shapes_batch(
8644 shapes[start..end]
8645 .iter()
8646 .map(|(shape, _blend_mode)| shape)
8647 .filter(|shape| shape_draw_is_visible_in_viewport(shape, viewport, root_scale)),
8648 root_scale,
8649 viewport,
8650 &mut staged_uploads,
8651 ) else {
8652 start = end;
8653 continue;
8654 };
8655
8656 let upload_offset =
8657 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
8658 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
8659
8660 {
8661 let mut render_pass =
8662 frame_encoder
8663 .encoder()
8664 .begin_render_pass(&wgpu::RenderPassDescriptor {
8665 label: Some("Shadow Source Shape Pass"),
8666 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8667 view: source_view,
8668 resolve_target: None,
8669 depth_slice: None,
8670 ops: wgpu::Operations {
8671 load: *next_load_op,
8672 store: wgpu::StoreOp::Store,
8673 },
8674 })],
8675 depth_stencil_attachment: None,
8676 timestamp_writes: None,
8677 occlusion_query_set: None,
8678 multiview_mask: None,
8679 });
8680 self.draw_prepared_shapes(
8681 &mut render_pass,
8682 blend_mode,
8683 prepared_shape,
8684 width,
8685 height,
8686 );
8687 }
8688
8689 pass_count = pass_count.saturating_add(1);
8690 rendered_any = true;
8691 *next_load_op = wgpu::LoadOp::Load;
8692 start = end;
8693 }
8694
8695 self.restore_staged_uploads(staged_uploads);
8696 ShadowSourceRenderOutcome {
8697 rendered_any,
8698 pass_count,
8699 }
8700 }
8701
8702 #[allow(clippy::too_many_arguments)]
8703 fn encode_shape_only_blurred_shadow_draw<C: FrameCommandRecorder>(
8704 &mut self,
8705 frame_encoder: &mut C,
8706 target_view: &wgpu::TextureView,
8707 shadow: &ShadowDraw,
8708 device_bounds: DevicePixelBounds,
8709 pixel_radius: f32,
8710 processing_scissor: Option<(u32, u32, u32, u32)>,
8711 width: u32,
8712 height: u32,
8713 root_scale: f32,
8714 ) -> bool {
8715 let bounds_w = device_bounds.width;
8716 let bounds_h = device_bounds.height;
8717 let viewport_offset = [device_bounds.x, device_bounds.y];
8718 let cache_key =
8719 shape_shadow_surface_cache_key(&shadow.shapes, device_bounds, pixel_radius, root_scale);
8720
8721 if let Some(key) = cache_key {
8722 if let Some(cached) = self.cached_shadow_surface(&key) {
8723 self.frame_stats
8724 .record_shadow_shape_cache_hit(bounds_w, bounds_h);
8725 let clip_scissor = shadow
8726 .clip
8727 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
8728 let scissor = clip_scissor.or(processing_scissor);
8729 let rounded_mask =
8730 inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
8731 mask.rect[0] -= viewport_offset[0];
8732 mask.rect[1] -= viewport_offset[1];
8733 mask
8734 });
8735 let dest_viewport = Some((
8736 viewport_offset[0],
8737 viewport_offset[1],
8738 bounds_w as f32,
8739 bounds_h as f32,
8740 ));
8741 {
8742 self.effect_renderer
8743 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8744 frame_encoder,
8745 &self.device,
8746 &cached,
8747 target_view,
8748 1.0,
8749 wgpu::LoadOp::Load,
8750 scissor,
8751 rounded_mask,
8752 BlendMode::SrcOver,
8753 dest_viewport,
8754 CompositeSampleMode::Nearest,
8755 );
8756 }
8757 frame_encoder.record_pass();
8758 self.effect_renderer.record_composite_pass();
8759 return true;
8760 }
8761 self.frame_stats
8762 .record_shadow_shape_cache_miss(bounds_w, bounds_h);
8763 self.frame_stats.maybe_print_shadow_shape_cache_miss(
8764 bounds_w,
8765 bounds_h,
8766 key.content_hash,
8767 pixel_radius,
8768 viewport_offset,
8769 shadow.shapes.len(),
8770 shadow.clip,
8771 );
8772 }
8773
8774 let device = self.device.clone();
8775 let source_descriptor =
8776 self.transient_offscreen_descriptor("Shape Shadow Source", bounds_w, bounds_h);
8777 let source_is_cacheable = cache_key.is_some();
8778 let source = if source_is_cacheable {
8779 self.acquire_retained_surface(bounds_w, bounds_h)
8780 } else {
8781 frame_encoder.acquire_transient_offscreen(&device, source_descriptor)
8782 };
8783 let scratch_descriptor =
8784 self.transient_offscreen_descriptor("Shape Shadow Blur Scratch", bounds_w, bounds_h);
8785 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
8786 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
8787 let source_outcome = self.encode_shadow_shape_source_passes(
8788 frame_encoder,
8789 &source.view,
8790 &shadow.shapes,
8791 bounds_w,
8792 bounds_h,
8793 viewport_offset,
8794 root_scale,
8795 &mut next_load_op,
8796 );
8797 frame_encoder.record_passes(source_outcome.pass_count);
8798
8799 if !source_outcome.rendered_any {
8800 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
8801 if source_is_cacheable {
8802 self.defer_offscreen_release(source);
8803 } else {
8804 frame_encoder.release_transient_offscreen(source_descriptor, source);
8805 }
8806 return true;
8807 }
8808
8809 {
8810 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
8811 frame_encoder,
8812 &device,
8813 &source,
8814 &scratch,
8815 &source.view,
8816 pixel_radius,
8817 pixel_radius,
8818 TileMode::Decal,
8819 None,
8820 );
8821 }
8822 frame_encoder.record_passes(2);
8823
8824 let clip_scissor = shadow
8825 .clip
8826 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
8827 let scissor = clip_scissor.or(processing_scissor);
8828 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
8829 mask.rect[0] -= viewport_offset[0];
8830 mask.rect[1] -= viewport_offset[1];
8831 mask
8832 });
8833 let dest_viewport = Some((
8834 viewport_offset[0],
8835 viewport_offset[1],
8836 bounds_w as f32,
8837 bounds_h as f32,
8838 ));
8839 {
8840 self.effect_renderer
8841 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8842 frame_encoder,
8843 &device,
8844 &source,
8845 target_view,
8846 1.0,
8847 wgpu::LoadOp::Load,
8848 scissor,
8849 rounded_mask,
8850 BlendMode::SrcOver,
8851 dest_viewport,
8852 CompositeSampleMode::Nearest,
8853 );
8854 }
8855 frame_encoder.record_pass();
8856
8857 self.effect_renderer.record_blur_pass();
8858 self.effect_renderer.record_composite_pass();
8859 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
8860 if let Some(key) = cache_key {
8861 self.insert_cached_shadow_surface(key, source);
8862 } else {
8863 frame_encoder.release_transient_offscreen(source_descriptor, source);
8864 }
8865 true
8866 }
8867
8868 fn prepare_shapes_batch<'a, I>(
8869 &mut self,
8870 layer_shapes: I,
8871 root_scale: f32,
8872 viewport: ViewportUniformParams,
8873 staged_uploads: &mut StagedBufferUploads,
8874 ) -> Option<PreparedShapeBatch>
8875 where
8876 I: Iterator<Item = &'a DrawShape>,
8877 {
8878 #[cfg(target_arch = "wasm32")]
8879 let _ = staged_uploads;
8880
8881 let shape_refs: Vec<&DrawShape> = layer_shapes
8886 .take(self.shape_batch_limits.max_shapes_per_batch)
8887 .collect();
8888 let shape_count = shape_refs.len();
8889 if shape_count == 0 {
8890 return None;
8891 }
8892
8893 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
8897 let mut total_gradient_stops = 0u32;
8898 gradient_offsets.push(0);
8899 for shape in &shape_refs {
8900 total_gradient_stops += shape_gradient_stop_count(shape) as u32;
8901 gradient_offsets.push(total_gradient_stops);
8902 }
8903
8904 self.scratch_shape_data.clear();
8905 self.scratch_shape_data
8906 .resize(shape_count, ShapeData::zeroed());
8907 self.scratch_gradients.clear();
8908 self.scratch_gradients
8909 .resize(total_gradient_stops as usize, GradientStop::zeroed());
8910
8911 convert_shapes_into_outputs(
8912 &shape_refs,
8913 &gradient_offsets,
8914 root_scale,
8915 &mut self.scratch_shape_data,
8916 &mut self.scratch_gradients,
8917 );
8918
8919 #[cfg(not(target_arch = "wasm32"))]
8920 {
8921 self.shape_buffers.ensure_capacity(
8922 &self.device,
8923 &self.shape_bind_group_layout,
8924 &self.identity_similarity_buffer,
8925 self.dummy_paint_buffer.as_ref(),
8926 shape_count,
8927 self.scratch_gradients.len().max(1),
8928 );
8929 self.stage_viewport_uniforms(staged_uploads, viewport);
8930 staged_uploads.stage(
8931 UploadTarget::ShapeData,
8932 bytemuck::cast_slice(&self.scratch_shape_data),
8933 );
8934 if !self.scratch_gradients.is_empty() {
8935 staged_uploads.stage(
8936 UploadTarget::ShapeGradient,
8937 bytemuck::cast_slice(&self.scratch_gradients),
8938 );
8939 }
8940 }
8941
8942 #[cfg(target_arch = "wasm32")]
8943 let shape_slot = {
8944 let slot = self.claim_wasm_shape_batch();
8945 {
8946 let buffers = &mut self.wasm_shape_batches[slot];
8947 buffers.ensure_capacity(
8948 &self.device,
8949 &self.shape_bind_group_layout,
8950 &self.identity_similarity_buffer,
8951 self.dummy_paint_buffer.as_ref(),
8952 shape_count,
8953 self.scratch_gradients.len().max(1),
8954 );
8955 }
8956 let buffers = &self.wasm_shape_batches[slot];
8957 self.write_wasm_buffer(
8958 &buffers.shape_buffer,
8959 bytemuck::cast_slice(&self.scratch_shape_data),
8960 );
8961 if !self.scratch_gradients.is_empty() {
8962 self.write_wasm_buffer(
8963 &buffers.gradient_buffer,
8964 bytemuck::cast_slice(&self.scratch_gradients),
8965 );
8966 }
8967 slot
8968 };
8969
8970 #[cfg(target_arch = "wasm32")]
8971 let uniform_slot = self.prepare_wasm_viewport_uniforms(viewport);
8972
8973 Some(PreparedShapeBatch {
8974 vertex_start: 0,
8975 vertex_count: shape_count as u32 * 6,
8976 #[cfg(target_arch = "wasm32")]
8977 shape_slot,
8978 #[cfg(target_arch = "wasm32")]
8979 uniform_slot,
8980 })
8981 }
8982
8983 #[cfg(not(target_arch = "wasm32"))]
8990 fn prepare_shapes_batch_direct<'a, I, C: FrameCommandRecorder>(
8991 &mut self,
8992 frame_encoder: &mut C,
8993 layer_shapes: I,
8994 root_scale: f32,
8995 viewport: ViewportUniformParams,
8996 staged_uploads: &mut StagedBufferUploads,
8997 ) -> Option<(PreparedShapeBatch, u64)>
8998 where
8999 I: Iterator<Item = &'a DrawShape>,
9000 {
9001 let shape_refs: Vec<&DrawShape> = layer_shapes
9002 .take(self.shape_batch_limits.max_shapes_per_batch)
9003 .collect();
9004 let shape_count = shape_refs.len();
9005 if shape_count == 0 {
9006 return None;
9007 }
9008
9009 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
9010 let mut total_gradient_stops = 0u32;
9011 gradient_offsets.push(0);
9012 for shape in &shape_refs {
9013 total_gradient_stops += shape_gradient_stop_count(shape) as u32;
9014 gradient_offsets.push(total_gradient_stops);
9015 }
9016
9017 self.shape_buffers.ensure_capacity(
9018 &self.device,
9019 &self.shape_bind_group_layout,
9020 &self.identity_similarity_buffer,
9021 self.dummy_paint_buffer.as_ref(),
9022 shape_count,
9023 (total_gradient_stops as usize).max(1),
9024 );
9025
9026 self.scratch_shape_data.clear();
9027 self.scratch_shape_data
9028 .resize(shape_count, ShapeData::zeroed());
9029 self.scratch_gradients.clear();
9030 self.scratch_gradients
9031 .resize(total_gradient_stops as usize, GradientStop::zeroed());
9032 convert_shapes_into_outputs(
9033 &shape_refs,
9034 &gradient_offsets,
9035 root_scale,
9036 &mut self.scratch_shape_data,
9037 &mut self.scratch_gradients,
9038 );
9039
9040 let uniform_len = std::mem::size_of::<Uniforms>() as u64;
9047 let shape_len = (shape_count * std::mem::size_of::<ShapeData>()) as u64;
9048 let gradient_len = total_gradient_stops as u64 * std::mem::size_of::<GradientStop>() as u64;
9049 let total_len = uniform_len + shape_len + gradient_len;
9050 let upload_base = frame_encoder.allocate_staged_upload_bytes(total_len);
9051 self.ensure_upload_buffer_capacity(upload_base + total_len);
9052
9053 let shape_off = uniform_len;
9054 let gradient_off = shape_off + shape_len;
9055
9056 let uniforms = Self::viewport_uniforms(viewport);
9057 let mut upload_stats = self.frame_graph_executor.upload_buffer(
9058 &self.queue,
9059 &self.upload_buffer,
9060 upload_base,
9061 bytemuck::bytes_of(&uniforms),
9062 );
9063 upload_stats.upload_bytes += self
9064 .frame_graph_executor
9065 .upload_buffer(
9066 &self.queue,
9067 &self.upload_buffer,
9068 upload_base + shape_off,
9069 bytemuck::cast_slice(&self.scratch_shape_data),
9070 )
9071 .upload_bytes;
9072 if !self.scratch_gradients.is_empty() {
9073 upload_stats.upload_bytes += self
9074 .frame_graph_executor
9075 .upload_buffer(
9076 &self.queue,
9077 &self.upload_buffer,
9078 upload_base + gradient_off,
9079 bytemuck::cast_slice(&self.scratch_gradients),
9080 )
9081 .upload_bytes;
9082 }
9083 self.frame_stats.record_command_stats(upload_stats);
9084
9085 staged_uploads.record_upload_copy(UploadTarget::Uniform, 0, 0, uniform_len);
9086 staged_uploads.record_upload_copy(UploadTarget::ShapeData, shape_off, 0, shape_len);
9087 staged_uploads.record_upload_copy(
9088 UploadTarget::ShapeGradient,
9089 gradient_off,
9090 0,
9091 gradient_len,
9092 );
9093
9094 Some((
9095 PreparedShapeBatch {
9096 vertex_start: 0,
9097 vertex_count: shape_count as u32 * 6,
9098 },
9099 upload_base,
9100 ))
9101 }
9102
9103 pub(crate) fn replay_supported(&self) -> bool {
9109 #[cfg(target_arch = "wasm32")]
9113 {
9114 false
9115 }
9116 #[cfg(not(target_arch = "wasm32"))]
9117 {
9118 self.shape_batch_limits.storage
9119 }
9120 }
9121
9122 pub(crate) fn restore_replay_ack_confirmations(
9128 &mut self,
9129 confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
9130 ) {
9131 #[cfg(not(target_arch = "wasm32"))]
9132 {
9133 self.replay_ack_confirmations = confirmations;
9134 }
9135 #[cfg(target_arch = "wasm32")]
9136 let _ = confirmations;
9137 }
9138
9139 #[cfg(not(target_arch = "wasm32"))]
9154 fn consume_replay_ops(
9155 &mut self,
9156 mut ops: crate::frame_packet::ReplayFrameOps,
9157 shapes: &[DrawShape],
9158 root_scale: f32,
9159 ) -> (
9160 crate::frame_packet::ReplayAck,
9161 crate::frame_packet::ReplayFrameOps,
9162 ) {
9163 if ops.generation < self.store_feed_generation {
9164 self.replay_generation_drops += 1;
9170 log::warn!(
9171 "[command-feed] dropping replay ops of generation {} against store \
9172 generation {} ({} captures, {} patches, {} releases; lifetime drops {})",
9173 ops.generation,
9174 self.store_feed_generation,
9175 ops.captures.len(),
9176 ops.color_patches.len(),
9177 ops.releases.len(),
9178 self.replay_generation_drops,
9179 );
9180 ops.captures.clear();
9181 ops.color_patches.clear();
9182 ops.releases.clear();
9183 return (
9184 crate::frame_packet::ReplayAck {
9185 generation: self.store_feed_generation,
9186 confirmations: Vec::new(),
9187 },
9188 ops,
9189 );
9190 }
9191 if ops.generation > self.store_feed_generation {
9192 self.store_feed_generation = ops.generation;
9199 }
9200 let generation = ops.generation;
9201 for slot in ops.releases.drain(..) {
9204 self.release_replay_slot(slot);
9205 }
9206 let mut confirmations = std::mem::take(&mut self.replay_ack_confirmations);
9209 debug_assert!(confirmations.is_empty());
9210 for capture in ops.captures.drain(..) {
9211 if capture.frame != ops.frame {
9212 log::warn!(
9219 "[command-feed] dropping stale capture for slot {} of {:?} \
9220 (queued frame {}, ops frame {})",
9221 capture.key.1,
9222 capture.key.0,
9223 capture.frame,
9224 ops.frame,
9225 );
9226 continue;
9227 }
9228 let end = capture.shape_start + capture.shape_count;
9229 let Some(slice) = shapes.get(capture.shape_start..end) else {
9230 continue;
9231 };
9232 let refs: Vec<&DrawShape> = slice.iter().collect();
9233 let Some(gpu_slot) = self.capture_replay_slot(&refs, root_scale) else {
9234 continue;
9235 };
9236 confirmations.push((capture.key, gpu_slot));
9237 }
9238 self.replay_color_patches.clear();
9246 std::mem::swap(&mut self.replay_color_patches, &mut ops.color_patches);
9247 (
9248 crate::frame_packet::ReplayAck {
9249 generation,
9250 confirmations,
9251 },
9252 ops,
9253 )
9254 }
9255
9256 #[cfg(not(target_arch = "wasm32"))]
9261 pub(crate) fn replay_generation_drops(&self) -> u64 {
9262 self.replay_generation_drops
9263 }
9264
9265 #[cfg(not(target_arch = "wasm32"))]
9273 pub(crate) fn replay_ops_roundtrip_for_tests(&mut self, generation_skew: u64) -> usize {
9274 let generation = self.store_feed_generation.wrapping_add(generation_skew);
9275 let ops = crate::shape_replay::SHAPE_REPLAY
9276 .with(|state| state.borrow_mut().take_frame_ops(generation));
9277 let (ack, recycled) = self.consume_replay_ops(ops, &[], 1.0);
9278 let confirmed = ack.confirmations.len();
9279 self.replay_ack_confirmations = crate::shape_replay::SHAPE_REPLAY
9280 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
9281 confirmed
9282 }
9283
9284 #[cfg(not(target_arch = "wasm32"))]
9291 fn stage_replay_patches(&mut self, staged_uploads: &mut StagedBufferUploads) {
9292 std::mem::swap(
9300 &mut self.replay_color_patches,
9301 &mut self.color_patch_scratch,
9302 );
9303 let total_patches = self.color_patch_scratch.len();
9304 if total_patches == 0 {
9305 self.replay_upload_stats.note_frame(0, 0, 0, 0, 0);
9306 return;
9307 }
9308
9309 #[derive(Clone, Copy)]
9315 struct DirtySpan {
9316 paint_min: u32,
9317 paint_max: u32,
9318 }
9319 const CLEAN: DirtySpan = DirtySpan {
9320 paint_min: u32::MAX,
9321 paint_max: 0,
9322 };
9323 let mut dirty: std::collections::HashMap<
9324 u32,
9325 DirtySpan,
9326 cranpose_ui_graphics::FxBuildHasher,
9327 > = std::collections::HashMap::default();
9328
9329 for patch in &self.color_patch_scratch {
9331 let Some(slot) = self.replay_slots.slots.get_mut(&patch.slot) else {
9332 continue;
9333 };
9334 let Some(paint) = slot.paint_mirror.get_mut(patch.shape_index as usize) else {
9335 continue;
9336 };
9337 *paint = patch.color;
9338 let span = dirty.entry(patch.slot).or_insert(CLEAN);
9339 span.paint_min = span.paint_min.min(patch.shape_index);
9340 span.paint_max = span.paint_max.max(patch.shape_index);
9341 }
9342
9343 let mut uploaded_records = 0u64;
9344 let mut uploaded_bytes = 0u64;
9345 let slots_touched = dirty.len() as u64;
9346 for (slot_id, span) in dirty {
9347 let Some(slot) = self.replay_slots.slots.get(&slot_id) else {
9348 continue;
9349 };
9350 if span.paint_min <= span.paint_max {
9351 let range = span.paint_min as usize..span.paint_max as usize + 1;
9352 uploaded_records += range.len() as u64;
9353 uploaded_bytes += (range.len() * std::mem::size_of::<[f32; 4]>()) as u64;
9354 staged_uploads.stage_at(
9355 UploadTarget::ReplayPaintData(slot_id),
9356 range.start as u64 * std::mem::size_of::<[f32; 4]>() as u64,
9357 bytemuck::cast_slice(&slot.paint_mirror[range]),
9358 );
9359 }
9360 }
9361 let ideal_bytes = total_patches as u64 * 16;
9364 self.replay_upload_stats.note_frame(
9365 total_patches as u64,
9366 slots_touched,
9367 uploaded_records,
9368 uploaded_bytes,
9369 ideal_bytes,
9370 );
9371 if cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG") {
9372 log::warn!(
9373 "[replay-upload] frame: {} patches -> {} records / {:.1} KB staged \
9374 across {} slots (color-only {:.1} KB)",
9375 total_patches,
9376 uploaded_records,
9377 uploaded_bytes as f64 / 1024.0,
9378 slots_touched,
9379 ideal_bytes as f64 / 1024.0,
9380 );
9381 }
9382 self.color_patch_scratch.clear();
9383 }
9384
9385 #[cfg(not(target_arch = "wasm32"))]
9388 pub(crate) fn capture_replay_slot(
9389 &mut self,
9390 shape_refs: &[&DrawShape],
9391 root_scale: f32,
9392 ) -> Option<u32> {
9393 if !self.shape_batch_limits.storage || shape_refs.is_empty() {
9394 return None;
9395 }
9396 let id = self.replay_slots.free_ids.pop()?;
9397 let shape_count = shape_refs.len();
9398
9399 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
9400 let mut total_gradient_stops = 0u32;
9401 gradient_offsets.push(0);
9402 for shape in shape_refs {
9403 total_gradient_stops += shape_gradient_stop_count(shape) as u32;
9404 gradient_offsets.push(total_gradient_stops);
9405 }
9406
9407 let mut shape_data = vec![ShapeData::zeroed(); shape_count];
9408 let mut gradients = vec![GradientStop::zeroed(); (total_gradient_stops as usize).max(1)];
9409 convert_shapes_into_outputs(
9410 shape_refs,
9411 &gradient_offsets,
9412 root_scale,
9413 &mut shape_data,
9414 &mut gradients,
9415 );
9416
9417 let shape_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9418 label: Some("Replay Shape Buffer"),
9419 size: (std::mem::size_of::<ShapeData>() * shape_count) as u64,
9420 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
9421 mapped_at_creation: true,
9422 });
9423 shape_buffer
9424 .slice(..)
9425 .get_mapped_range_mut()
9426 .copy_from_slice(bytemuck::cast_slice(&shape_data));
9427 shape_buffer.unmap();
9428
9429 let gradient_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9430 label: Some("Replay Gradient Buffer"),
9431 size: (std::mem::size_of::<GradientStop>() * gradients.len()) as u64,
9432 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
9433 mapped_at_creation: true,
9434 });
9435 gradient_buffer
9436 .slice(..)
9437 .get_mapped_range_mut()
9438 .copy_from_slice(bytemuck::cast_slice(&gradients));
9439 gradient_buffer.unmap();
9440
9441 let mesh = if arc_mesh_enabled() {
9442 match build_arc_mesh_vertices(&shape_data) {
9443 Some(build) => {
9444 let cut = if build.quad_area > 0.0 {
9445 (1.0 - build.mesh_area / build.quad_area) * 100.0
9446 } else {
9447 0.0
9448 };
9449 log::warn!(
9455 "[arc-mesh] slot {id}: {} arcs meshed ({} segs), {} passthrough; \
9456 {} unique verts / {} indices (quad path: {} verts); \
9457 quad_px {:.0} -> mesh_px {:.0} (-{:.1}%)",
9458 build.meshed_arcs,
9459 build.meshed_segments,
9460 build.passthrough,
9461 build.vertices.len(),
9462 build.indices.len(),
9463 shape_count * 6,
9464 build.quad_area,
9465 build.mesh_area,
9466 cut,
9467 );
9468 (build.meshed_arcs > 0).then(|| {
9471 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9472 label: Some("Replay Mesh Vertex Buffer"),
9473 size: (std::mem::size_of::<MeshVertex>() * build.vertices.len()) as u64,
9474 usage: wgpu::BufferUsages::VERTEX,
9475 mapped_at_creation: true,
9476 });
9477 vertex_buffer
9478 .slice(..)
9479 .get_mapped_range_mut()
9480 .copy_from_slice(bytemuck::cast_slice(&build.vertices));
9481 vertex_buffer.unmap();
9482 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9483 label: Some("Replay Mesh Index Buffer"),
9484 size: (std::mem::size_of::<u32>() * build.indices.len()) as u64,
9485 usage: wgpu::BufferUsages::INDEX,
9486 mapped_at_creation: true,
9487 });
9488 index_buffer
9489 .slice(..)
9490 .get_mapped_range_mut()
9491 .copy_from_slice(bytemuck::cast_slice(&build.indices));
9492 index_buffer.unmap();
9493 ReplaySlotMesh {
9494 vertex_buffer,
9495 index_buffer,
9496 index_prefix: build.index_prefix,
9497 }
9498 })
9499 }
9500 None => {
9501 log::warn!(
9502 "[arc-mesh] slot {id}: geometry byte budget overflowed for \
9503 {shape_count} shapes; whole slot falls back to quad passthrough"
9504 );
9505 None
9506 }
9507 }
9508 } else {
9509 None
9510 };
9511
9512 let paint: Vec<[f32; 4]> = shape_data.iter().map(|shape| shape.color).collect();
9515 let paint_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9516 label: Some("Replay Paint Buffer"),
9517 size: (std::mem::size_of::<[f32; 4]>() * shape_count) as u64,
9518 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
9519 mapped_at_creation: true,
9520 });
9521 paint_buffer
9522 .slice(..)
9523 .get_mapped_range_mut()
9524 .copy_from_slice(bytemuck::cast_slice(&paint));
9525 paint_buffer.unmap();
9526
9527 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
9528 label: Some("Replay Shape Bind Group"),
9529 layout: &self.shape_bind_group_layout,
9530 entries: &[
9531 wgpu::BindGroupEntry {
9532 binding: 0,
9533 resource: shape_buffer.as_entire_binding(),
9534 },
9535 wgpu::BindGroupEntry {
9536 binding: 1,
9537 resource: gradient_buffer.as_entire_binding(),
9538 },
9539 wgpu::BindGroupEntry {
9543 binding: 2,
9544 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
9545 buffer: &self.replay_slots.transform_buffer,
9546 offset: 0,
9547 size: Some(
9548 std::num::NonZeroU64::new(
9549 std::mem::size_of::<SimilarityTransform>() as u64
9550 )
9551 .expect("similarity transform is non-empty"),
9552 ),
9553 }),
9554 },
9555 wgpu::BindGroupEntry {
9556 binding: 3,
9557 resource: paint_buffer.as_entire_binding(),
9558 },
9559 ],
9560 });
9561
9562 let capture_epoch = self.replay_slots.next_capture_epoch;
9563 self.replay_slots.next_capture_epoch += 1;
9564 self.replay_slots.slots.insert(
9565 id,
9566 ReplaySlot {
9567 paint_buffer,
9568 bind_group,
9569 shape_count: shape_count as u32,
9570 paint_mirror: paint,
9571 mesh,
9572 capture_epoch,
9573 },
9574 );
9575 Some(id)
9576 }
9577
9578 #[cfg(not(target_arch = "wasm32"))]
9580 pub(crate) fn release_replay_slot(&mut self, id: u32) {
9581 if self.replay_slots.slots.remove(&id).is_some() {
9582 self.replay_slots.free_ids.push(id);
9583 self.retained_bundle_cache.clear();
9589 }
9590 }
9591
9592 #[cfg(not(target_arch = "wasm32"))]
9595 #[doc(hidden)]
9596 pub fn instanced_quads_active(&self) -> bool {
9597 self.instanced_quads.is_some()
9598 }
9599
9600 #[cfg(not(target_arch = "wasm32"))]
9603 #[doc(hidden)]
9604 pub fn replay_slot_mesh_stats(&self) -> (usize, usize) {
9605 let meshed = self
9606 .replay_slots
9607 .slots
9608 .values()
9609 .filter(|slot| slot.mesh.is_some())
9610 .count();
9611 (meshed, self.replay_slots.slots.len())
9612 }
9613
9614 #[cfg(not(target_arch = "wasm32"))]
9618 fn draw_retained_batch(
9619 &self,
9620 render_pass: &mut wgpu::RenderPass<'_>,
9621 retained: &RetainedDraw,
9622 retained_index: usize,
9623 width: u32,
9624 height: u32,
9625 ) {
9626 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
9627 return;
9628 };
9629 if retained_index as u32 >= MAX_REPLAY_SLOTS {
9630 return;
9631 }
9632 let first = retained.first_shape.min(slot.shape_count);
9633 let last = retained
9634 .first_shape
9635 .saturating_add(retained.shape_count)
9636 .min(slot.shape_count);
9637 if first >= last {
9638 return;
9639 }
9640 self.frame_stats.bump_shapes();
9641 self.frame_stats.add_draw_calls(1);
9642 render_pass.set_scissor_rect(0, 0, width, height);
9643 let mesh = slot.mesh.as_ref().map(|mesh| (mesh, self.mesh_pipeline()));
9651 match &mesh {
9652 Some((_, mesh_pipeline)) => render_pass.set_pipeline(mesh_pipeline),
9653 None => match &self.instanced_quads {
9654 Some(instanced) => {
9655 render_pass.set_pipeline(self.instanced_pipeline(instanced, BlendMode::SrcOver))
9656 }
9657 None => render_pass.set_pipeline(self.shape_pipeline(BlendMode::SrcOver)),
9658 },
9659 }
9660 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
9661 render_pass.set_bind_group(
9662 1,
9663 &slot.bind_group,
9664 &[retained_index as u32 * REPLAY_TRANSFORM_STRIDE as u32],
9665 );
9666 match mesh {
9667 Some((mesh, _)) => {
9668 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
9669 render_pass
9670 .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
9671 render_pass.draw_indexed(
9672 mesh.index_prefix[first as usize]..mesh.index_prefix[last as usize],
9673 0,
9674 0..1,
9675 );
9676 }
9677 None => match &self.instanced_quads {
9678 Some(instanced) => {
9679 render_pass.set_index_buffer(
9680 instanced.index_buffer.slice(..),
9681 wgpu::IndexFormat::Uint16,
9682 );
9683 render_pass.draw_indexed(0..6, 0, first..last);
9684 }
9685 None => render_pass.draw(first * 6..last * 6, 0..1),
9686 },
9687 }
9688 }
9689
9690 #[cfg(not(target_arch = "wasm32"))]
9697 fn retained_bundle_key(
9698 &self,
9699 ordered_items: &[(usize, SegmentDrawItem)],
9700 retained_draws: &[RetainedDraw],
9701 item_range: Range<usize>,
9702 ) -> RetainedBundleKey {
9703 let mut ops = Vec::with_capacity(item_range.len());
9704 for (_, item) in &ordered_items[item_range] {
9705 let SegmentDrawItem::Retained(index) = item else {
9706 continue;
9707 };
9708 let Some(retained) = retained_draws.get(*index) else {
9709 continue;
9710 };
9711 let slot = self.replay_slots.slots.get(&retained.slot);
9712 let (first, last) = match slot {
9713 Some(slot) => (
9714 retained.first_shape.min(slot.shape_count),
9715 retained
9716 .first_shape
9717 .saturating_add(retained.shape_count)
9718 .min(slot.shape_count),
9719 ),
9720 None => (
9721 retained.first_shape,
9722 retained.first_shape.saturating_add(retained.shape_count),
9723 ),
9724 };
9725 ops.push(RetainedBundleOpKey {
9726 slot: retained.slot,
9727 capture_epoch: slot.map(|slot| slot.capture_epoch),
9728 first,
9729 last,
9730 retained_index: *index as u32,
9731 has_mesh: slot.is_some_and(|slot| slot.mesh.is_some())
9732 && self.shape_batch_limits.storage,
9733 });
9734 }
9735 RetainedBundleKey { ops }
9736 }
9737
9738 #[cfg(not(target_arch = "wasm32"))]
9745 fn build_retained_bundle(&self, key: &RetainedBundleKey) -> wgpu::RenderBundle {
9746 let mut encoder =
9747 self.device
9748 .create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
9749 label: Some("Retained Stretch Bundle"),
9750 color_formats: &[Some(self.surface_format)],
9754 depth_stencil: None,
9755 sample_count: 1,
9756 multiview: None,
9757 });
9758 for op in &key.ops {
9759 if op.capture_epoch.is_none()
9760 || op.retained_index >= MAX_REPLAY_SLOTS
9761 || op.first >= op.last
9762 {
9763 continue;
9764 }
9765 let Some(slot) = self.replay_slots.slots.get(&op.slot) else {
9766 continue;
9767 };
9768 let mesh = slot.mesh.as_ref().map(|mesh| (mesh, self.mesh_pipeline()));
9769 match &mesh {
9770 Some((_, mesh_pipeline)) => encoder.set_pipeline(mesh_pipeline),
9771 None => match &self.instanced_quads {
9772 Some(instanced) => {
9773 encoder.set_pipeline(self.instanced_pipeline(instanced, BlendMode::SrcOver))
9774 }
9775 None => encoder.set_pipeline(self.shape_pipeline(BlendMode::SrcOver)),
9776 },
9777 }
9778 encoder.set_bind_group(0, &self.uniform_bind_group, &[]);
9779 encoder.set_bind_group(
9780 1,
9781 &slot.bind_group,
9782 &[op.retained_index * REPLAY_TRANSFORM_STRIDE as u32],
9783 );
9784 match mesh {
9785 Some((mesh, _)) => {
9786 encoder.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
9787 encoder
9788 .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
9789 encoder.draw_indexed(
9790 mesh.index_prefix[op.first as usize]..mesh.index_prefix[op.last as usize],
9791 0,
9792 0..1,
9793 );
9794 }
9795 None => match &self.instanced_quads {
9800 Some(instanced) => {
9801 encoder.set_index_buffer(
9802 instanced.index_buffer.slice(..),
9803 wgpu::IndexFormat::Uint16,
9804 );
9805 encoder.draw_indexed(0..6, 0, op.first..op.last);
9806 }
9807 None => encoder.draw(op.first * 6..op.last * 6, 0..1),
9808 },
9809 }
9810 }
9811 encoder.finish(&wgpu::RenderBundleDescriptor {
9812 label: Some("Retained Stretch Bundle"),
9813 })
9814 }
9815
9816 #[cfg(not(target_arch = "wasm32"))]
9824 fn draw_retained_stretch_bundled(
9825 &mut self,
9826 render_pass: &mut wgpu::RenderPass<'_>,
9827 ordered_items: &[(usize, SegmentDrawItem)],
9828 retained_draws: &[RetainedDraw],
9829 item_range: Range<usize>,
9830 width: u32,
9831 height: u32,
9832 ) {
9833 let key = self.retained_bundle_key(ordered_items, retained_draws, item_range);
9834 if !self.retained_bundle_cache.hit(&key) {
9835 let bundle = self.build_retained_bundle(&key);
9836 self.retained_bundle_cache.insert(key.clone(), bundle);
9837 }
9838 for op in &key.ops {
9841 if op.capture_epoch.is_some()
9842 && op.retained_index < MAX_REPLAY_SLOTS
9843 && op.first < op.last
9844 {
9845 self.frame_stats.bump_shapes();
9846 self.frame_stats.add_draw_calls(1);
9847 }
9848 }
9849 render_pass.set_scissor_rect(0, 0, width, height);
9854 if let Some(bundle) = self.retained_bundle_cache.get(&key) {
9855 render_pass.execute_bundles(std::iter::once(bundle));
9856 }
9857 }
9858
9859 #[cfg(not(target_arch = "wasm32"))]
9862 #[doc(hidden)]
9863 pub fn retained_bundle_stats(&self) -> (u64, u64) {
9864 self.retained_bundle_cache.stats()
9865 }
9866
9867 fn draw_prepared_shapes(
9868 &self,
9869 render_pass: &mut wgpu::RenderPass<'_>,
9870 blend_mode: BlendMode,
9871 batch: PreparedShapeBatch,
9872 width: u32,
9873 height: u32,
9874 ) {
9875 self.frame_stats.bump_shapes();
9876 self.frame_stats.add_draw_calls(1);
9877 render_pass.set_scissor_rect(0, 0, width, height);
9878 #[cfg(not(target_arch = "wasm32"))]
9879 let (uniform_bind_group, shape_buffers) = (&self.uniform_bind_group, &self.shape_buffers);
9880 #[cfg(target_arch = "wasm32")]
9881 let (uniform_bind_group, shape_buffers) = (
9882 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
9883 &self.wasm_shape_batches[batch.shape_slot],
9884 );
9885 #[cfg(not(target_arch = "wasm32"))]
9890 if let Some(instanced) = &self.instanced_quads {
9891 assert!(
9892 batch.vertex_start.is_multiple_of(6) && batch.vertex_count.is_multiple_of(6),
9893 "shape batches are whole shapes: vertex range {}..+{} must be \
9894 six-aligned to convert to an instance range",
9895 batch.vertex_start,
9896 batch.vertex_count,
9897 );
9898 render_pass.set_pipeline(self.instanced_pipeline(instanced, blend_mode));
9899 render_pass.set_bind_group(0, uniform_bind_group, &[]);
9900 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
9903 let first_shape = batch.vertex_start / 6;
9904 let shape_count = batch.vertex_count / 6;
9905 render_pass
9906 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
9907 render_pass.draw_indexed(0..6, 0, first_shape..first_shape + shape_count);
9908 return;
9909 }
9910 render_pass.set_pipeline(self.shape_pipeline(blend_mode));
9911 render_pass.set_bind_group(0, uniform_bind_group, &[]);
9912 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
9915 render_pass.draw(
9918 batch.vertex_start..batch.vertex_start + batch.vertex_count,
9919 0..1,
9920 );
9921 }
9922
9923 #[allow(clippy::too_many_arguments)]
9926 fn encode_shapes_pass<'a, I, C: FrameCommandRecorder>(
9927 &mut self,
9928 frame_encoder: &mut C,
9929 target_view: &wgpu::TextureView,
9930 layer_shapes: I,
9931 blend_mode: BlendMode,
9932 width: u32,
9933 height: u32,
9934 root_scale: f32,
9935 load_op: wgpu::LoadOp<wgpu::Color>,
9936 viewport_offset: [f32; 2],
9937 ) where
9938 I: Iterator<Item = &'a DrawShape>,
9939 {
9940 let mut staged_uploads = self.take_staged_uploads();
9941 let viewport = ViewportUniformParams {
9942 width,
9943 height,
9944 offset: viewport_offset,
9945 };
9946 let Some(batch) = self.prepare_shapes_batch(
9947 layer_shapes
9948 .filter(|shape| shape_draw_is_visible_in_viewport(shape, viewport, root_scale)),
9949 root_scale,
9950 viewport,
9951 &mut staged_uploads,
9952 ) else {
9953 self.restore_staged_uploads(staged_uploads);
9954 return;
9955 };
9956 let upload_offset =
9957 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
9958 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
9959 self.restore_staged_uploads(staged_uploads);
9960 let mut render_pass =
9961 frame_encoder
9962 .encoder()
9963 .begin_render_pass(&wgpu::RenderPassDescriptor {
9964 label: Some("Shape Pass"),
9965 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
9966 view: target_view,
9967 resolve_target: None,
9968 depth_slice: None,
9969 ops: wgpu::Operations {
9970 load: load_op,
9971 store: wgpu::StoreOp::Store,
9972 },
9973 })],
9974 depth_stencil_attachment: None,
9975 timestamp_writes: None,
9976 occlusion_query_set: None,
9977 multiview_mask: None,
9978 });
9979 self.draw_prepared_shapes(&mut render_pass, blend_mode, batch, width, height);
9980 }
9981
9982 fn draw_prepared_images(
9983 &mut self,
9984 render_pass: &mut wgpu::RenderPass<'_>,
9985 batch: &PreparedImageBatch,
9986 blend_mode: BlendMode,
9987 ) -> Result<(), String> {
9988 if batch.cmds.is_empty() {
9989 return Ok(());
9990 }
9991 self.frame_stats.bump_images();
9992 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
9993 render_pass.set_pipeline(self.image_pipeline(blend_mode));
9994 #[cfg(not(target_arch = "wasm32"))]
9995 let (uniform_bind_group, vertex_buffer, index_buffer) = (
9996 &self.uniform_bind_group,
9997 &self.image_vertex_buffer,
9998 &self.image_index_buffer,
9999 );
10000 #[cfg(target_arch = "wasm32")]
10001 let (uniform_bind_group, vertex_buffer, index_buffer) = (
10002 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
10003 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
10004 &self.wasm_image_batches[batch.image_slot].index_buffer,
10005 );
10006 render_pass.set_bind_group(0, uniform_bind_group, &[]);
10007 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10008 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
10009
10010 for cmd in &batch.cmds {
10011 let (sx, sy, sw, sh) = cmd.scissor;
10012 render_pass.set_scissor_rect(sx, sy, sw, sh);
10013
10014 let cached = self
10015 .image_texture_cache
10016 .get(&cmd.image_id)
10017 .ok_or_else(|| "image texture missing from cache".to_string())?;
10018 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
10019 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
10020 }
10021 Ok(())
10022 }
10023
10024 fn draw_prepared_glyphs(
10025 &mut self,
10026 render_pass: &mut wgpu::RenderPass<'_>,
10027 batch: &PreparedGlyphBatch,
10028 ) -> Result<(), String> {
10029 if batch.cmds.is_empty() {
10030 return Ok(());
10031 }
10032 #[cfg(not(target_arch = "wasm32"))]
10033 {
10034 self.draw_native_prepared_glyph_cmd_range(
10035 render_pass,
10036 &batch.cmds,
10037 0..batch.cmds.len(),
10038 )?;
10039 }
10040 #[cfg(target_arch = "wasm32")]
10041 {
10042 self.frame_stats.bump_text();
10043 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
10044 render_pass.set_pipeline(self.glyph_atlas_pipeline());
10045 let (uniform_bind_group, vertex_buffer, index_buffer) = (
10046 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
10047 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
10048 &self.wasm_image_batches[batch.image_slot].index_buffer,
10049 );
10050 render_pass.set_bind_group(0, uniform_bind_group, &[]);
10051 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
10052 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10053 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
10054
10055 for cmd in &batch.cmds {
10056 let (sx, sy, sw, sh) = cmd.scissor;
10057 render_pass.set_scissor_rect(sx, sy, sw, sh);
10058 let GlyphDrawSource::Shared {
10059 index_start,
10060 index_count,
10061 } = cmd.source;
10062 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
10063 }
10064 }
10065 Ok(())
10066 }
10067
10068 #[cfg(not(target_arch = "wasm32"))]
10069 fn draw_native_prepared_image_cmd_range(
10070 &mut self,
10071 render_pass: &mut wgpu::RenderPass<'_>,
10072 cmds: &[ImageDrawCmd],
10073 cmd_range: Range<usize>,
10074 blend_mode: BlendMode,
10075 ) -> Result<(), String> {
10076 let Some(cmds) = cmds.get(cmd_range) else {
10077 return Err("image command range is outside the prepared command buffer".to_string());
10078 };
10079 if cmds.is_empty() {
10080 return Ok(());
10081 }
10082
10083 self.frame_stats.bump_images();
10084 self.frame_stats.add_draw_calls(cmds.len() as u32);
10085 render_pass.set_pipeline(self.image_pipeline(blend_mode));
10086 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
10087 render_pass.set_index_buffer(self.image_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10088 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
10089
10090 for cmd in cmds {
10091 let (sx, sy, sw, sh) = cmd.scissor;
10092 render_pass.set_scissor_rect(sx, sy, sw, sh);
10093
10094 let cached = self
10095 .image_texture_cache
10096 .get(&cmd.image_id)
10097 .ok_or_else(|| "image texture missing from cache".to_string())?;
10098 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
10099 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
10100 }
10101 Ok(())
10102 }
10103
10104 #[cfg(not(target_arch = "wasm32"))]
10105 fn draw_native_prepared_glyph_cmd_range(
10106 &mut self,
10107 render_pass: &mut wgpu::RenderPass<'_>,
10108 cmds: &[GlyphDrawCmd],
10109 cmd_range: Range<usize>,
10110 ) -> Result<(), String> {
10111 let Some(cmds) = cmds.get(cmd_range) else {
10112 return Err("glyph command range is outside the prepared command buffer".to_string());
10113 };
10114 if cmds.is_empty() {
10115 return Ok(());
10116 }
10117
10118 self.frame_stats.bump_text();
10119 self.frame_stats.add_draw_calls(cmds.len() as u32);
10120
10121 let mut shared_buffers_bound = false;
10122 let mut retained_pipeline_bound = false;
10123 for cmd in cmds {
10124 let (sx, sy, sw, sh) = cmd.scissor;
10125 render_pass.set_scissor_rect(sx, sy, sw, sh);
10126 match cmd.source {
10127 GlyphDrawSource::Shared {
10128 index_start,
10129 index_count,
10130 } => {
10131 if retained_pipeline_bound || !shared_buffers_bound {
10132 render_pass.set_pipeline(self.glyph_atlas_pipeline());
10133 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
10134 retained_pipeline_bound = false;
10135 }
10136 if !shared_buffers_bound {
10137 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
10138 render_pass.set_index_buffer(
10139 self.image_index_buffer.slice(..),
10140 wgpu::IndexFormat::Uint32,
10141 );
10142 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
10143 shared_buffers_bound = true;
10144 }
10145 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
10146 }
10147 GlyphDrawSource::Retained {
10148 cache_key,
10149 uniform_slot,
10150 } => {
10151 shared_buffers_bound = false;
10152 if !retained_pipeline_bound {
10153 render_pass.set_pipeline(self.retained_glyph_atlas_pipeline());
10154 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
10155 retained_pipeline_bound = true;
10156 }
10157 let cached = self
10158 .text_glyph_gpu_run_cache
10159 .peek(&cache_key)
10160 .ok_or_else(|| "retained glyph buffer missing from cache".to_string())?;
10161 let dynamic_offset =
10162 self.retained_glyph_uniform_dynamic_offset(uniform_slot)?;
10163 render_pass.set_bind_group(
10164 0,
10165 &self.retained_glyph_uniform_bind_group,
10166 &[dynamic_offset],
10167 );
10168 render_pass
10169 .set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10170 render_pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
10171 render_pass.draw_indexed(0..cached.index_count, 0, 0..1);
10172 }
10173 }
10174 }
10175 Ok(())
10176 }
10177
10178 fn append_image_draw_cmd(
10179 &mut self,
10180 image_draw: &ImageDraw,
10181 viewport: ViewportUniformParams,
10182 root_scale: f32,
10183 image_vertices: &mut Vec<Vertex>,
10184 image_indices: &mut Vec<u32>,
10185 image_cmds: &mut Vec<ImageDrawCmd>,
10186 ) -> Result<(), String> {
10187 let snap_delta = image_draw
10188 .snap_anchor
10189 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
10190 .unwrap_or_default();
10191 let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
10192 if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
10193 return Ok(());
10194 }
10195
10196 let (tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
10197 if tint[3] <= 0.0 {
10198 return Ok(());
10199 }
10200
10201 let prepared_image = if let Some(filter) = cpu_filter {
10202 apply_filter_to_bitmap(&image_draw.image, filter)?
10203 } else {
10204 image_draw.image.clone()
10205 };
10206 self.ensure_image_cached(&prepared_image)?;
10207
10208 let mut adjusted_image = ImageDraw {
10209 rect,
10210 local_rect: image_draw.local_rect.translate(snap_delta.x, snap_delta.y),
10211 quad: translate_quad(image_draw.quad, snap_delta),
10212 snap_anchor: image_draw.snap_anchor,
10213 image: image_draw.image.clone(),
10214 alpha: image_draw.alpha,
10215 color_filter: image_draw.color_filter,
10216 sampling: image_draw.sampling,
10217 z_index: image_draw.z_index,
10218 clip: image_draw.clip,
10219 blend_mode: image_draw.blend_mode,
10220 src_rect: image_draw.src_rect,
10221 motion_context_animated: image_draw.motion_context_animated,
10222 };
10223 snap_nearest_image_to_device_pixels(&mut adjusted_image, root_scale);
10224 let Some(scissor) =
10225 scissor_rect_for_image(&adjusted_image, root_scale, viewport.width, viewport.height)
10226 else {
10227 return Ok(());
10228 };
10229
10230 let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
10231 return Ok(());
10232 };
10233 let device_quad =
10234 nearest_image_device_quad(&adjusted_image, root_scale).unwrap_or_else(|| {
10235 if adjusted_image.snap_anchor.is_some() {
10236 canonicalized_scaled_quad(adjusted_image.quad, root_scale)
10237 } else {
10238 scaled_quad(adjusted_image.quad, root_scale)
10239 }
10240 });
10241
10242 let base_vertex = image_vertices.len() as u32;
10243 let index_start = image_indices.len() as u32;
10244 image_indices.extend_from_slice(&[
10245 base_vertex,
10246 base_vertex + 1,
10247 base_vertex + 2,
10248 base_vertex + 2,
10249 base_vertex + 1,
10250 base_vertex + 3,
10251 ]);
10252 image_vertices.extend_from_slice(&[
10253 Vertex {
10254 position: device_quad[0],
10255 color: tint,
10256 uv: [uv_rect.min[0], uv_rect.min[1]],
10257 uv_bounds: uv_rect.sample_bounds,
10258 },
10259 Vertex {
10260 position: device_quad[1],
10261 color: tint,
10262 uv: [uv_rect.max[0], uv_rect.min[1]],
10263 uv_bounds: uv_rect.sample_bounds,
10264 },
10265 Vertex {
10266 position: device_quad[2],
10267 color: tint,
10268 uv: [uv_rect.min[0], uv_rect.max[1]],
10269 uv_bounds: uv_rect.sample_bounds,
10270 },
10271 Vertex {
10272 position: device_quad[3],
10273 color: tint,
10274 uv: [uv_rect.max[0], uv_rect.max[1]],
10275 uv_bounds: uv_rect.sample_bounds,
10276 },
10277 ]);
10278
10279 image_cmds.push(ImageDrawCmd {
10280 index_start,
10281 scissor,
10282 image_id: prepared_image.id(),
10283 sampling: image_draw.sampling,
10284 });
10285 Ok(())
10286 }
10287
10288 #[cfg(not(target_arch = "wasm32"))]
10289 fn stage_native_image_buffers(
10290 &mut self,
10291 staged_uploads: &mut StagedBufferUploads,
10292 viewport: ViewportUniformParams,
10293 image_vertices: &[Vertex],
10294 image_indices: &[u32],
10295 ) {
10296 if image_indices.is_empty() {
10297 return;
10298 }
10299
10300 self.stage_viewport_uniforms(staged_uploads, viewport);
10301 let needed_bytes = std::mem::size_of_val(image_vertices) as u64;
10306 if needed_bytes > self.image_vertex_buffer.size() {
10307 self.image_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10308 label: Some("Image Vertex Buffer"),
10309 size: needed_bytes.next_power_of_two(),
10310 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
10311 mapped_at_creation: false,
10312 });
10313 }
10314 let needed_index_bytes = std::mem::size_of_val(image_indices) as u64;
10315 if needed_index_bytes > self.image_index_buffer.size() {
10316 self.image_index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10317 label: Some("Image Index Buffer"),
10318 size: needed_index_bytes.next_power_of_two(),
10319 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
10320 mapped_at_creation: false,
10321 });
10322 }
10323
10324 staged_uploads.stage(
10325 UploadTarget::ImageVertex,
10326 bytemuck::cast_slice(image_vertices),
10327 );
10328 staged_uploads.stage(
10329 UploadTarget::ImageIndex,
10330 bytemuck::cast_slice(image_indices),
10331 );
10332 }
10333
10334 fn prepare_image_draw_cmds<'a, I>(
10337 &mut self,
10338 layer_images: I,
10339 viewport: ViewportUniformParams,
10340 root_scale: f32,
10341 staged_uploads: &mut StagedBufferUploads,
10342 ) -> Result<PreparedImageBatch, String>
10343 where
10344 I: Iterator<Item = &'a ImageDraw>,
10345 {
10346 #[cfg(target_arch = "wasm32")]
10347 let _ = staged_uploads;
10348
10349 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
10350 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
10351 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
10352 image_vertices.clear();
10353 image_indices.clear();
10354 image_cmds.clear();
10355
10356 for image_draw in layer_images {
10357 self.append_image_draw_cmd(
10358 image_draw,
10359 viewport,
10360 root_scale,
10361 &mut image_vertices,
10362 &mut image_indices,
10363 &mut image_cmds,
10364 )?;
10365 }
10366
10367 #[cfg(not(target_arch = "wasm32"))]
10368 if !image_cmds.is_empty() {
10369 self.stage_native_image_buffers(
10370 staged_uploads,
10371 viewport,
10372 &image_vertices,
10373 &image_indices,
10374 );
10375 }
10376
10377 #[cfg(target_arch = "wasm32")]
10378 let image_slot = if image_cmds.is_empty() {
10379 0
10380 } else {
10381 let slot = self.claim_wasm_image_batch();
10382 {
10383 let buffers = &mut self.wasm_image_batches[slot];
10384 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
10385 }
10386 let buffers = &self.wasm_image_batches[slot];
10387 self.write_wasm_buffer(
10388 &buffers.vertex_buffer,
10389 bytemuck::cast_slice(&image_vertices),
10390 );
10391 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
10392 slot
10393 };
10394
10395 #[cfg(target_arch = "wasm32")]
10396 let uniform_slot = if image_cmds.is_empty() {
10397 0
10398 } else {
10399 self.prepare_wasm_viewport_uniforms(viewport)
10400 };
10401
10402 self.scratch_image_vertices = image_vertices;
10403 self.scratch_image_indices = image_indices;
10404 Ok(PreparedImageBatch {
10405 cmds: image_cmds,
10406 #[cfg(target_arch = "wasm32")]
10407 image_slot,
10408 #[cfg(target_arch = "wasm32")]
10409 uniform_slot,
10410 })
10411 }
10412
10413 fn glyph_atlas_entry_for(
10414 &mut self,
10415 glyph: &SoftwareGlyphAtlasGlyph,
10416 ) -> Result<GlyphAtlasEntry, String> {
10417 if let Some(entry) = self.text_glyph_atlas.upload_glyph(
10418 glyph.key,
10419 glyph,
10420 &self.queue,
10421 &mut self.frame_graph_executor,
10422 &mut self.frame_stats,
10423 ) {
10424 return Ok(entry);
10425 }
10426
10427 self.text_glyph_atlas.reset(
10428 &self.device,
10429 &self.image_bind_group_layout,
10430 &self.image_nearest_sampler,
10431 );
10432 Err("text glyph atlas filled and was reset".to_string())
10433 }
10434
10435 fn glyph_atlas_entry_for_cached(
10436 &mut self,
10437 glyph: &SoftwareGlyphAtlasPlacement,
10438 ) -> Option<GlyphAtlasEntry> {
10439 let entry = self.text_glyph_atlas.entry(&glyph.key)?;
10440 self.frame_stats.record_text_glyph_atlas_hit();
10441 Some(entry)
10442 }
10443
10444 fn glyph_atlas_entry_for_placement(
10445 &mut self,
10446 glyph: &SoftwareGlyphAtlasPlacement,
10447 ) -> Result<GlyphAtlasEntry, String> {
10448 if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
10449 return Ok(entry);
10450 }
10451
10452 let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
10453 return Err("text glyph placement has no retained raster mask".to_string());
10454 };
10455 self.glyph_atlas_entry_for(&upload_glyph)
10456 }
10457
10458 fn prepare_text_glyph_quads(
10459 &mut self,
10460 run_key: TextGlyphRunCacheKey,
10461 atlas_generation: u64,
10462 cached_glyph_run: Option<&[SoftwareGlyphAtlasPlacement]>,
10463 collected_run: &[SoftwareGlyphAtlasRunGlyph],
10464 generated_quads: &mut Vec<CachedTextGlyphQuad>,
10465 ) -> Result<Rc<[CachedTextGlyphQuad]>, String> {
10466 generated_quads.clear();
10467 if let Some(glyph_run) = cached_glyph_run {
10468 for glyph in glyph_run {
10469 if glyph.width == 0 || glyph.height == 0 || glyph.color.3 <= 0.0 {
10470 continue;
10471 }
10472 let entry = self.glyph_atlas_entry_for_placement(glyph)?;
10473 generated_quads.push(cached_text_glyph_quad(
10478 glyph,
10479 entry,
10480 self.text_glyph_atlas.size(),
10481 ));
10482 }
10483 } else {
10484 for run_glyph in collected_run {
10485 let placement = run_glyph.placement();
10486 if placement.width == 0 || placement.height == 0 || placement.color.3 <= 0.0 {
10487 continue;
10488 }
10489 let entry = match run_glyph {
10490 SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
10491 self.glyph_atlas_entry_for_placement(placement)?
10492 }
10493 SoftwareGlyphAtlasRunGlyph::New(glyph) => self.glyph_atlas_entry_for(glyph)?,
10494 };
10495 generated_quads.push(cached_text_glyph_quad(
10496 &placement,
10497 entry,
10498 self.text_glyph_atlas.size(),
10499 ));
10500 }
10501 }
10502
10503 let quads: Rc<[CachedTextGlyphQuad]> = Rc::from(generated_quads.clone().into_boxed_slice());
10504 if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
10505 cached.quads = Some(Rc::clone(&quads));
10506 cached.atlas_generation = atlas_generation;
10507 }
10508 Ok(quads)
10509 }
10510
10511 #[allow(clippy::too_many_arguments)]
10512 fn append_text_glyph_quad_run(
10513 &mut self,
10514 source_raster_rect: Rect,
10515 quads: &[CachedTextGlyphQuad],
10516 clip: Option<Rect>,
10517 viewport: ViewportUniformParams,
10518 root_scale: f32,
10519 image_vertices: &mut Vec<Vertex>,
10520 image_indices: &mut Vec<u32>,
10521 record_cached_hits: bool,
10522 ) -> usize {
10523 let mut appended = 0usize;
10524 for quad in quads {
10525 if !cached_text_glyph_quad_is_visible_in_viewport(
10526 source_raster_rect,
10527 quad,
10528 clip,
10529 viewport,
10530 root_scale,
10531 ) {
10532 continue;
10533 }
10534 if append_cached_text_glyph_quad(
10535 source_raster_rect,
10536 quad,
10537 image_vertices,
10538 image_indices,
10539 ) {
10540 if record_cached_hits {
10541 self.frame_stats.record_text_glyph_atlas_hit();
10542 }
10543 appended = appended.saturating_add(1);
10544 }
10545 }
10546 appended
10547 }
10548
10549 #[cfg(not(target_arch = "wasm32"))]
10550 fn retained_glyph_viewport(
10551 viewport: ViewportUniformParams,
10552 source_raster_rect: Rect,
10553 ) -> ViewportUniformParams {
10554 ViewportUniformParams {
10555 width: viewport.width,
10556 height: viewport.height,
10557 offset: [
10558 viewport.offset[0] - source_raster_rect.x,
10559 viewport.offset[1] - source_raster_rect.y,
10560 ],
10561 }
10562 }
10563
10564 #[cfg(not(target_arch = "wasm32"))]
10565 fn retained_text_glyph_run_ready(&mut self, cache_key: TextGlyphRunCacheKey) -> bool {
10566 let atlas_generation = self.text_glyph_atlas.generation();
10567 self.text_glyph_gpu_run_cache
10568 .peek(&cache_key)
10569 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
10570 }
10571
10572 #[cfg(not(target_arch = "wasm32"))]
10573 #[allow(clippy::too_many_arguments)]
10574 fn emit_retained_text_glyph_run_if_ready(
10575 &mut self,
10576 cache_key: TextGlyphRunCacheKey,
10577 quads: &[CachedTextGlyphQuad],
10578 clip: Option<Rect>,
10579 viewport: ViewportUniformParams,
10580 source_raster_rect: Rect,
10581 scissor: (u32, u32, u32, u32),
10582 staged_uploads: &mut StagedBufferUploads,
10583 glyph_cmds: &mut Vec<GlyphDrawCmd>,
10584 ) -> bool {
10585 if !should_use_retained_text_glyph_run(quads.len(), clip) {
10586 return false;
10587 }
10588 if !self.retained_text_glyph_run_ready(cache_key)
10589 && !self.ensure_retained_text_glyph_run(cache_key, quads)
10590 {
10591 return false;
10592 }
10593
10594 let uniform_slot = self.stage_retained_glyph_viewport_uniforms(
10595 staged_uploads,
10596 Self::retained_glyph_viewport(viewport, source_raster_rect),
10597 );
10598 glyph_cmds.push(GlyphDrawCmd::retained(cache_key, uniform_slot, scissor));
10599 true
10600 }
10601
10602 #[cfg(not(target_arch = "wasm32"))]
10603 fn ensure_retained_text_glyph_run(
10604 &mut self,
10605 cache_key: TextGlyphRunCacheKey,
10606 quads: &[CachedTextGlyphQuad],
10607 ) -> bool {
10608 let atlas_generation = self.text_glyph_atlas.generation();
10609 if self
10610 .text_glyph_gpu_run_cache
10611 .peek(&cache_key)
10612 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
10613 {
10614 return true;
10615 }
10616
10617 let mut vertices = Vec::with_capacity(quads.len().saturating_mul(4));
10618 let mut indices = Vec::with_capacity(quads.len().saturating_mul(6));
10619 let origin = Rect {
10620 x: 0.0,
10621 y: 0.0,
10622 width: 0.0,
10623 height: 0.0,
10624 };
10625 for quad in quads {
10626 append_cached_text_glyph_quad(origin, quad, &mut vertices, &mut indices);
10627 }
10628 if indices.is_empty() {
10629 return false;
10630 }
10631
10632 let vertex_bytes = bytemuck::cast_slice(&vertices);
10633 let index_bytes = bytemuck::cast_slice(&indices);
10634 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10635 label: Some("Retained Text Glyph Vertex Buffer"),
10636 size: vertex_bytes.len() as u64,
10637 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
10638 mapped_at_creation: false,
10639 });
10640 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10641 label: Some("Retained Text Glyph Index Buffer"),
10642 size: index_bytes.len() as u64,
10643 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
10644 mapped_at_creation: false,
10645 });
10646 let vertex_upload =
10647 self.frame_graph_executor
10648 .upload_buffer(&self.queue, &vertex_buffer, 0, vertex_bytes);
10649 self.frame_stats.record_command_stats(vertex_upload);
10650 let index_upload =
10651 self.frame_graph_executor
10652 .upload_buffer(&self.queue, &index_buffer, 0, index_bytes);
10653 self.frame_stats.record_command_stats(index_upload);
10654
10655 self.text_glyph_gpu_run_cache.put(
10656 cache_key,
10657 CachedGpuTextGlyphRun {
10658 vertex_buffer,
10659 index_buffer,
10660 index_count: indices.len() as u32,
10661 atlas_generation,
10662 },
10663 );
10664 true
10665 }
10666
10667 #[allow(clippy::too_many_arguments)]
10668 fn append_text_glyph_draws<'a, I>(
10669 &mut self,
10670 layer_texts: I,
10671 viewport: ViewportUniformParams,
10672 root_scale: f32,
10673 allow_offscreen_prewarm: bool,
10674 staged_uploads: &mut StagedBufferUploads,
10675 image_vertices: &mut Vec<Vertex>,
10676 image_indices: &mut Vec<u32>,
10677 glyph_cmds: &mut Vec<GlyphDrawCmd>,
10678 ) -> Result<bool, String>
10679 where
10680 I: IntoIterator<Item = &'a TextDraw>,
10681 {
10682 let append_start = Instant::now();
10683 let initial_vertex_len = image_vertices.len();
10684 let initial_index_len = image_indices.len();
10685 let initial_cmd_len = glyph_cmds.len();
10686 let initial_staged_bytes_len = staged_uploads.bytes.len();
10687 let initial_staged_copies_len = staged_uploads.copies.len();
10688 let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
10689 let mut collected_placements = std::mem::take(&mut self.scratch_text_glyph_placements);
10690 let mut generated_quads = std::mem::take(&mut self.scratch_text_glyph_quads);
10691 generated_quads.clear();
10692 let mut visited = 0usize;
10693 let mut emitted_glyphs = 0usize;
10694 let mut prewarmed_glyphs = 0usize;
10695 let mut run_hits = 0usize;
10696 let mut run_misses = 0usize;
10697
10698 for text_draw in layer_texts {
10699 visited = visited.saturating_add(1);
10700 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
10701 self.text_raster_geometry(text_draw, root_scale)
10702 else {
10703 continue;
10704 };
10705 if !static_text_motion {
10706 image_vertices.truncate(initial_vertex_len);
10707 image_indices.truncate(initial_index_len);
10708 glyph_cmds.truncate(initial_cmd_len);
10709 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
10710 self.scratch_text_glyph_run = collected_run;
10711 self.scratch_text_glyph_placements = collected_placements;
10712 self.scratch_text_glyph_quads = generated_quads;
10713 return Ok(false);
10714 }
10715 let is_visible =
10716 text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale);
10717 let draw_action = text_glyph_draw_action(
10718 is_visible,
10719 text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale),
10720 allow_offscreen_prewarm,
10721 );
10722 if draw_action == TextGlyphDrawAction::Skip {
10723 continue;
10724 }
10725
10726 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
10727 let source_draw = raster_source.draw.as_ref();
10728 let source_raster_rect = raster_source.raster_rect;
10729
10730 let run_key = Self::text_glyph_run_cache_key(
10731 source_draw,
10732 source_raster_rect,
10733 text_scale,
10734 static_text_motion,
10735 );
10736 let atlas_generation = self.text_glyph_atlas.generation();
10737 let mut cached_quad_run = None;
10738 let mut miss_collect_ms = None;
10739 let mut miss_cached_glyphs = 0usize;
10740 let mut miss_new_glyphs = 0usize;
10741 let cached_glyph_run = if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
10742 run_hits = run_hits.saturating_add(1);
10743 if cached.atlas_generation == atlas_generation {
10744 cached_quad_run = cached.quads.as_ref().map(Rc::clone);
10745 }
10746 Some(Rc::clone(&cached.glyphs))
10747 } else {
10748 run_misses = run_misses.saturating_add(1);
10749 collected_run.clear();
10750 let collect_start = Instant::now();
10751 let collect_result = collect_solid_text_atlas_run(
10752 source_draw.text.as_ref(),
10753 source_raster_rect,
10754 &source_draw.text_style,
10755 source_draw.color,
10756 source_draw.font_size,
10757 text_scale,
10758 &self.text_fonts,
10759 &mut self.text_glyph_mask_cache,
10760 &mut collected_run,
10761 );
10762 miss_collect_ms = Some(instant_ms(collect_start, Instant::now()));
10763 if collect_result.is_none() {
10764 if text_atlas_fallback_diag_enabled() {
10765 let preview: String = source_draw.text.text.chars().take(96).collect();
10766 log::warn!(
10767 "[text-atlas-fallback] node={:?} visible={} prewarm={} spans={} links={} text_len={} preview={:?} span_style={:?} paragraph_style={:?}",
10768 source_draw.node_id,
10769 is_visible,
10770 draw_action == TextGlyphDrawAction::PrewarmOffscreen,
10771 source_draw.text.span_styles.len(),
10772 source_draw.text.links.len(),
10773 source_draw.text.text.len(),
10774 preview,
10775 source_draw.text_style.span_style,
10776 source_draw.text_style.paragraph_style,
10777 );
10778 }
10779 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
10780 continue;
10781 }
10782 image_vertices.truncate(initial_vertex_len);
10783 image_indices.truncate(initial_index_len);
10784 glyph_cmds.truncate(initial_cmd_len);
10785 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
10786 self.scratch_text_glyph_run = collected_run;
10787 self.scratch_text_glyph_placements = collected_placements;
10788 self.scratch_text_glyph_quads = generated_quads;
10789 return Ok(false);
10790 }
10791 if text_glyph_run_diag_enabled() {
10792 miss_cached_glyphs = collected_run
10793 .iter()
10794 .filter(|glyph| matches!(glyph, SoftwareGlyphAtlasRunGlyph::Cached(_)))
10795 .count();
10796 miss_new_glyphs = collected_run.len().saturating_sub(miss_cached_glyphs);
10797 }
10798 collected_placements.clear();
10799 collected_placements.extend(
10800 collected_run
10801 .iter()
10802 .map(SoftwareGlyphAtlasRunGlyph::placement),
10803 );
10804 let glyphs: Rc<[SoftwareGlyphAtlasPlacement]> =
10805 Rc::from(collected_placements.clone().into_boxed_slice());
10806 self.text_glyph_run_cache.put(
10807 run_key,
10808 CachedTextGlyphRun {
10809 glyphs,
10810 quads: None,
10811 atlas_generation: 0,
10812 },
10813 );
10814 None
10815 };
10816
10817 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
10818 let prewarm_quads = if let Some(quad_run) = cached_quad_run {
10819 quad_run
10820 } else {
10821 let prepare_start = Instant::now();
10822 match self.prepare_text_glyph_quads(
10823 run_key,
10824 atlas_generation,
10825 cached_glyph_run.as_deref(),
10826 &collected_run,
10827 &mut generated_quads,
10828 ) {
10829 Ok(quads) => {
10830 if let Some(collect_ms) = miss_collect_ms {
10831 if text_glyph_run_diag_enabled() {
10832 log::warn!(
10833 "[text-glyph-run-diag] visible=false glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
10834 quads.len(),
10835 miss_cached_glyphs,
10836 miss_new_glyphs,
10837 collect_ms,
10838 instant_ms(prepare_start, Instant::now()),
10839 );
10840 }
10841 }
10842 quads
10843 }
10844 Err(_) => continue,
10845 }
10846 };
10847 #[cfg(not(target_arch = "wasm32"))]
10848 if should_use_retained_text_glyph_run(prewarm_quads.len(), source_draw.clip) {
10849 self.ensure_retained_text_glyph_run(run_key, prewarm_quads.as_ref());
10850 }
10851 prewarmed_glyphs = prewarmed_glyphs.saturating_add(prewarm_quads.len());
10852 continue;
10853 }
10854
10855 let draw_rect = Rect {
10856 x: source_raster_rect.x / root_scale,
10857 y: source_raster_rect.y / root_scale,
10858 width: source_raster_rect.width / root_scale,
10859 height: source_raster_rect.height / root_scale,
10860 };
10861 let Some(scissor) = scissor_rect_for_layer(
10862 draw_rect,
10863 source_draw.clip,
10864 root_scale,
10865 viewport.width,
10866 viewport.height,
10867 ) else {
10868 continue;
10869 };
10870
10871 #[cfg(not(target_arch = "wasm32"))]
10872 if let Some(quad_run) = cached_quad_run.as_ref() {
10873 if should_use_retained_text_glyph_run(quad_run.len(), source_draw.clip)
10874 && self.emit_retained_text_glyph_run_if_ready(
10875 run_key,
10876 quad_run.as_ref(),
10877 source_draw.clip,
10878 viewport,
10879 source_raster_rect,
10880 scissor,
10881 staged_uploads,
10882 glyph_cmds,
10883 )
10884 {
10885 emitted_glyphs = emitted_glyphs.saturating_add(quad_run.len());
10886 continue;
10887 }
10888 }
10889
10890 let index_start = image_indices.len() as u32;
10891 if let Some(quad_run) = cached_quad_run {
10892 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
10893 source_raster_rect,
10894 quad_run.as_ref(),
10895 source_draw.clip,
10896 viewport,
10897 root_scale,
10898 image_vertices,
10899 image_indices,
10900 true,
10901 ));
10902 } else {
10903 let prepare_start = Instant::now();
10904 let Ok(quad_run) = self.prepare_text_glyph_quads(
10905 run_key,
10906 atlas_generation,
10907 cached_glyph_run.as_deref(),
10908 &collected_run,
10909 &mut generated_quads,
10910 ) else {
10911 image_vertices.truncate(initial_vertex_len);
10912 image_indices.truncate(initial_index_len);
10913 glyph_cmds.truncate(initial_cmd_len);
10914 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
10915 self.scratch_text_glyph_run = collected_run;
10916 self.scratch_text_glyph_placements = collected_placements;
10917 self.scratch_text_glyph_quads = generated_quads;
10918 return Ok(false);
10919 };
10920 if let Some(collect_ms) = miss_collect_ms {
10921 if text_glyph_run_diag_enabled() {
10922 log::warn!(
10923 "[text-glyph-run-diag] visible=true glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
10924 quad_run.len(),
10925 miss_cached_glyphs,
10926 miss_new_glyphs,
10927 collect_ms,
10928 instant_ms(prepare_start, Instant::now()),
10929 );
10930 }
10931 }
10932 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
10933 source_raster_rect,
10934 quad_run.as_ref(),
10935 source_draw.clip,
10936 viewport,
10937 root_scale,
10938 image_vertices,
10939 image_indices,
10940 false,
10941 ));
10942 }
10943 let index_count = image_indices.len() as u32 - index_start;
10944 if index_count > 0 {
10945 glyph_cmds.push(GlyphDrawCmd::shared(index_start, index_count, scissor));
10946 }
10947 }
10948
10949 self.scratch_text_glyph_run = collected_run;
10950 self.scratch_text_glyph_placements = collected_placements;
10951 self.scratch_text_glyph_quads = generated_quads;
10952 let append_end = Instant::now();
10953 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
10954 log::warn!(
10955 "[wgpu-render-stage:text-glyph-atlas] total_ms={total_ms:.2} visited={} cmds={} glyphs={} prewarmed={} run_hits={} run_misses={}",
10956 visited,
10957 glyph_cmds.len().saturating_sub(initial_cmd_len),
10958 emitted_glyphs,
10959 prewarmed_glyphs,
10960 run_hits,
10961 run_misses,
10962 );
10963 }
10964 Ok(true)
10965 }
10966
10967 #[cfg(not(target_arch = "wasm32"))]
10968 fn text_glyph_prewarm_decision(
10969 &self,
10970 text_draw: &TextDraw,
10971 viewport: ViewportUniformParams,
10972 root_scale: f32,
10973 ) -> TextGlyphPrewarmDecision {
10974 let Some((logical_rect, _, clip, _, static_text_motion)) =
10975 self.text_raster_geometry(text_draw, root_scale)
10976 else {
10977 return TextGlyphPrewarmDecision::MissingGeometry;
10978 };
10979 if !static_text_motion {
10980 return TextGlyphPrewarmDecision::DynamicMotion;
10981 }
10982 if text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
10983 return TextGlyphPrewarmDecision::Visible;
10984 }
10985 if text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale) {
10986 TextGlyphPrewarmDecision::Candidate
10987 } else {
10988 TextGlyphPrewarmDecision::OutsidePrewarmWindow
10989 }
10990 }
10991
10992 #[cfg(not(target_arch = "wasm32"))]
10993 #[allow(clippy::too_many_arguments)]
10994 fn prewarm_offscreen_text_glyph_draws_in_chunk(
10995 &mut self,
10996 ordered_items: &[(usize, SegmentDrawItem)],
10997 texts: &[TextDraw],
10998 chunk: &SegmentDrawChunkPlan,
10999 viewport: ViewportUniformParams,
11000 root_scale: f32,
11001 staged_uploads: &mut StagedBufferUploads,
11002 image_vertices: &mut Vec<Vertex>,
11003 image_indices: &mut Vec<u32>,
11004 glyph_cmds: &mut Vec<GlyphDrawCmd>,
11005 ) -> Result<(), String> {
11006 let prewarm_start = Instant::now();
11007 let diag_enabled = cranpose_core::env_flag!("CRANPOSE_TEXT_PREWARM_DIAG");
11008 let mut text_items = 0usize;
11009 let mut candidates = 0usize;
11010 let mut missing_geometry = 0usize;
11011 let mut dynamic_motion = 0usize;
11012 let mut visible = 0usize;
11013 let mut outside = 0usize;
11014 let mut already_prepared = 0usize;
11015 let mut admitted_candidates = 0usize;
11016 let mut skipped_unbounded = 0usize;
11017 let mut skipped_budget = 0usize;
11018 let initial_vertex_len = image_vertices.len();
11019 let initial_index_len = image_indices.len();
11020 let initial_cmd_len = glyph_cmds.len();
11021 let initial_staged_bytes_len = staged_uploads.bytes.len();
11022 let initial_staged_copies_len = staged_uploads.copies.len();
11023 'batches: for batch in chunk.iter() {
11024 let SegmentBatchPlan::Text { start, end } = batch else {
11025 continue;
11026 };
11027 for (_, item) in &ordered_items[start..end] {
11028 if offscreen_text_glyph_prewarm_budget_exhausted(prewarm_start, admitted_candidates)
11029 {
11030 skipped_budget = skipped_budget.saturating_add(1);
11031 break 'batches;
11032 }
11033 let SegmentDrawItem::Text(text_index) = item else {
11034 return Err(format!(
11035 "text prewarm batch contains non-text draw item: {item:?}"
11036 ));
11037 };
11038 let Some(text_draw) = texts.get(*text_index) else {
11039 continue;
11040 };
11041 text_items = text_items.saturating_add(1);
11042 match self.text_glyph_prewarm_decision(text_draw, viewport, root_scale) {
11043 TextGlyphPrewarmDecision::Candidate => {}
11044 TextGlyphPrewarmDecision::MissingGeometry => {
11045 missing_geometry = missing_geometry.saturating_add(1);
11046 continue;
11047 }
11048 TextGlyphPrewarmDecision::DynamicMotion => {
11049 dynamic_motion = dynamic_motion.saturating_add(1);
11050 continue;
11051 }
11052 TextGlyphPrewarmDecision::Visible => {
11053 visible = visible.saturating_add(1);
11054 continue;
11055 }
11056 TextGlyphPrewarmDecision::OutsidePrewarmWindow => {
11057 outside = outside.saturating_add(1);
11058 continue;
11059 }
11060 }
11061
11062 candidates = candidates.saturating_add(1);
11063 let Some((_, raster_rect, _, text_scale, static_text_motion)) =
11064 self.text_raster_geometry(text_draw, root_scale)
11065 else {
11066 missing_geometry = missing_geometry.saturating_add(1);
11067 continue;
11068 };
11069 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
11070 let source_draw = raster_source.draw.as_ref();
11071 let run_key = Self::text_glyph_run_cache_key(
11072 source_draw,
11073 raster_source.raster_rect,
11074 text_scale,
11075 static_text_motion,
11076 );
11077 let atlas_generation = self.text_glyph_atlas.generation();
11078 let cached_glyphs = if let Some(cached) = self.text_glyph_run_cache.peek(&run_key) {
11079 if cached.atlas_generation == atlas_generation && cached.quads.is_some() {
11080 already_prepared = already_prepared.saturating_add(1);
11081 continue;
11082 }
11083 Some(cached.glyphs.len())
11084 } else {
11085 None
11086 };
11087 if !offscreen_text_glyph_prewarm_work_is_bounded(
11088 cached_glyphs,
11089 source_draw.text.text.len(),
11090 ) {
11091 skipped_unbounded = skipped_unbounded.saturating_add(1);
11092 continue;
11093 }
11094 admitted_candidates = admitted_candidates.saturating_add(1);
11095 self.append_text_glyph_draws(
11096 std::iter::once(text_draw),
11097 viewport,
11098 root_scale,
11099 true,
11100 staged_uploads,
11101 image_vertices,
11102 image_indices,
11103 glyph_cmds,
11104 )?;
11105 image_vertices.truncate(initial_vertex_len);
11106 image_indices.truncate(initial_index_len);
11107 glyph_cmds.truncate(initial_cmd_len);
11108 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
11109 }
11110 }
11111
11112 if diag_enabled && text_items > 0 {
11113 log::warn!(
11114 "[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}"
11115 );
11116 }
11117 if admitted_candidates > 0 {
11118 if let Some(total_ms) = should_log_wgpu_render_stage(prewarm_start, Instant::now()) {
11119 log::warn!(
11120 "[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}"
11121 );
11122 }
11123 }
11124 Ok(())
11125 }
11126
11127 fn prepare_text_glyph_draw_cmds<'a, I>(
11128 &mut self,
11129 layer_texts: I,
11130 viewport: ViewportUniformParams,
11131 root_scale: f32,
11132 staged_uploads: &mut StagedBufferUploads,
11133 ) -> Result<Option<PreparedGlyphBatch>, String>
11134 where
11135 I: IntoIterator<Item = &'a TextDraw>,
11136 {
11137 #[cfg(target_arch = "wasm32")]
11138 let _ = staged_uploads;
11139
11140 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
11141 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
11142 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
11143 image_vertices.clear();
11144 image_indices.clear();
11145 glyph_cmds.clear();
11146
11147 if !self.append_text_glyph_draws(
11148 layer_texts,
11149 viewport,
11150 root_scale,
11151 false,
11152 staged_uploads,
11153 &mut image_vertices,
11154 &mut image_indices,
11155 &mut glyph_cmds,
11156 )? {
11157 self.scratch_image_vertices = image_vertices;
11158 self.scratch_image_indices = image_indices;
11159 self.scratch_glyph_cmds = glyph_cmds;
11160 return Ok(None);
11161 }
11162
11163 #[cfg(not(target_arch = "wasm32"))]
11164 if !image_indices.is_empty() {
11165 self.stage_native_image_buffers(
11166 staged_uploads,
11167 viewport,
11168 &image_vertices,
11169 &image_indices,
11170 );
11171 }
11172
11173 #[cfg(target_arch = "wasm32")]
11174 let image_slot = if glyph_cmds.is_empty() {
11175 0
11176 } else {
11177 let slot = self.claim_wasm_image_batch();
11178 {
11179 let buffers = &mut self.wasm_image_batches[slot];
11180 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
11181 }
11182 let buffers = &self.wasm_image_batches[slot];
11183 self.write_wasm_buffer(
11184 &buffers.vertex_buffer,
11185 bytemuck::cast_slice(&image_vertices),
11186 );
11187 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
11188 slot
11189 };
11190
11191 #[cfg(target_arch = "wasm32")]
11192 let uniform_slot = if glyph_cmds.is_empty() {
11193 0
11194 } else {
11195 self.prepare_wasm_viewport_uniforms(viewport)
11196 };
11197
11198 self.scratch_image_vertices = image_vertices;
11199 self.scratch_image_indices = image_indices;
11200 Ok(Some(PreparedGlyphBatch {
11201 cmds: glyph_cmds,
11202 #[cfg(target_arch = "wasm32")]
11203 image_slot,
11204 #[cfg(target_arch = "wasm32")]
11205 uniform_slot,
11206 }))
11207 }
11208
11209 #[allow(clippy::too_many_arguments)]
11210 fn append_image_bitmap_draw_cmd(
11211 &mut self,
11212 image: &ImageBitmap,
11213 rect: Rect,
11214 clip: Option<Rect>,
11215 sampling: ImageSampling,
11216 viewport: ViewportUniformParams,
11217 root_scale: f32,
11218 image_vertices: &mut Vec<Vertex>,
11219 image_indices: &mut Vec<u32>,
11220 image_cmds: &mut Vec<ImageDrawCmd>,
11221 ) -> Result<(), String> {
11222 if rect.width <= 0.0 || rect.height <= 0.0 {
11223 return Ok(());
11224 }
11225
11226 self.ensure_image_cached(image)?;
11227
11228 let (device_quad, scissor_rect) =
11229 if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
11230 let left_px = (rect.x * root_scale).round();
11231 let top_px = (rect.y * root_scale).round();
11232 let width_px = (rect.width * root_scale).round().max(1.0);
11233 let height_px = (rect.height * root_scale).round().max(1.0);
11234 let snapped_rect = Rect {
11235 x: left_px / root_scale,
11236 y: top_px / root_scale,
11237 width: width_px / root_scale,
11238 height: height_px / root_scale,
11239 };
11240 let right_px = left_px + width_px;
11241 let bottom_px = top_px + height_px;
11242 (
11243 [
11244 [left_px, top_px],
11245 [right_px, top_px],
11246 [left_px, bottom_px],
11247 [right_px, bottom_px],
11248 ],
11249 snapped_rect,
11250 )
11251 } else {
11252 (
11253 rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
11254 rect,
11255 )
11256 };
11257
11258 let Some(scissor) = scissor_rect_for_layer(
11259 scissor_rect,
11260 clip,
11261 root_scale,
11262 viewport.width,
11263 viewport.height,
11264 ) else {
11265 return Ok(());
11266 };
11267 let Some(uv_rect) = image_uv_rect(image, None) else {
11268 return Ok(());
11269 };
11270
11271 let base_vertex = image_vertices.len() as u32;
11272 let index_start = image_indices.len() as u32;
11273 image_indices.extend_from_slice(&[
11274 base_vertex,
11275 base_vertex + 1,
11276 base_vertex + 2,
11277 base_vertex + 2,
11278 base_vertex + 1,
11279 base_vertex + 3,
11280 ]);
11281 let color = [1.0, 1.0, 1.0, 1.0];
11282 image_vertices.extend_from_slice(&[
11283 Vertex {
11284 position: device_quad[0],
11285 color,
11286 uv: [uv_rect.min[0], uv_rect.min[1]],
11287 uv_bounds: uv_rect.sample_bounds,
11288 },
11289 Vertex {
11290 position: device_quad[1],
11291 color,
11292 uv: [uv_rect.max[0], uv_rect.min[1]],
11293 uv_bounds: uv_rect.sample_bounds,
11294 },
11295 Vertex {
11296 position: device_quad[2],
11297 color,
11298 uv: [uv_rect.min[0], uv_rect.max[1]],
11299 uv_bounds: uv_rect.sample_bounds,
11300 },
11301 Vertex {
11302 position: device_quad[3],
11303 color,
11304 uv: [uv_rect.max[0], uv_rect.max[1]],
11305 uv_bounds: uv_rect.sample_bounds,
11306 },
11307 ]);
11308 image_cmds.push(ImageDrawCmd {
11309 index_start,
11310 scissor,
11311 image_id: image.id(),
11312 sampling,
11313 });
11314 Ok(())
11315 }
11316
11317 #[allow(clippy::too_many_arguments)]
11318 fn append_text_image_draw_cmds<'a, I>(
11319 &mut self,
11320 layer_texts: I,
11321 viewport: ViewportUniformParams,
11322 root_scale: f32,
11323 image_vertices: &mut Vec<Vertex>,
11324 image_indices: &mut Vec<u32>,
11325 image_cmds: &mut Vec<ImageDrawCmd>,
11326 ) -> Result<(), String>
11327 where
11328 I: Iterator<Item = &'a TextDraw>,
11329 {
11330 let append_start = Instant::now();
11331 let initial_len = image_cmds.len();
11332 let mut visited = 0usize;
11333 let mut hit_count = 0usize;
11334 let mut miss_count = 0usize;
11335 for text_draw in layer_texts {
11336 visited = visited.saturating_add(1);
11337 let _ = text_draw.node_id;
11338 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
11339 self.text_raster_geometry(text_draw, root_scale)
11340 else {
11341 continue;
11342 };
11343 if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
11344 continue;
11345 }
11346
11347 let raster_source = self.text_image_raster_source(
11348 text_draw,
11349 logical_rect,
11350 raster_rect,
11351 clip,
11352 root_scale,
11353 static_text_motion,
11354 );
11355 let source_draw = raster_source.draw.as_ref();
11356 let source_raster_rect = raster_source.raster_rect;
11357
11358 let cache_key = Self::text_image_cache_key(
11359 source_draw,
11360 source_raster_rect,
11361 text_scale,
11362 static_text_motion,
11363 );
11364 let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
11365 self.frame_stats
11366 .record_text_image_cache_hit(cached.image.width(), cached.image.height());
11367 hit_count = hit_count.saturating_add(1);
11368 cached.image.clone()
11369 } else {
11370 let Some(image) =
11371 self.rasterize_text_draw_to_image(source_draw, source_raster_rect, text_scale)
11372 else {
11373 continue;
11374 };
11375 self.frame_stats
11376 .record_text_image_cache_miss(image.width(), image.height());
11377 miss_count = miss_count.saturating_add(1);
11378 self.text_image_cache.put(
11379 cache_key,
11380 CachedTextImage {
11381 image: image.clone(),
11382 },
11383 );
11384 image
11385 };
11386
11387 let draw_origin = if static_text_motion {
11388 Point::new(
11389 source_raster_rect.x / root_scale,
11390 source_raster_rect.y / root_scale,
11391 )
11392 } else {
11393 Point::new(logical_rect.x, logical_rect.y)
11394 };
11395 let draw_rect = Rect {
11396 x: draw_origin.x,
11397 y: draw_origin.y,
11398 width: image.width() as f32 / root_scale,
11399 height: image.height() as f32 / root_scale,
11400 };
11401 self.append_image_bitmap_draw_cmd(
11402 &image,
11403 draw_rect,
11404 clip,
11405 ImageSampling::Nearest,
11406 viewport,
11407 root_scale,
11408 image_vertices,
11409 image_indices,
11410 image_cmds,
11411 )?;
11412 }
11413 let append_end = Instant::now();
11414 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
11415 log::warn!(
11416 "[wgpu-render-stage:text-images] total_ms={total_ms:.2} visited={} emitted={} hits={} misses={}",
11417 visited,
11418 image_cmds.len().saturating_sub(initial_len),
11419 hit_count,
11420 miss_count,
11421 );
11422 }
11423 Ok(())
11424 }
11425
11426 fn text_image_raster_source<'a>(
11427 &mut self,
11428 text_draw: &'a TextDraw,
11429 logical_rect: Rect,
11430 raster_rect: Rect,
11431 clip: Option<Rect>,
11432 root_scale: f32,
11433 static_text_motion: bool,
11434 ) -> TextRasterSource<'a> {
11435 let Some(clip) = clip else {
11436 return TextRasterSource {
11437 draw: Cow::Borrowed(text_draw),
11438 raster_rect,
11439 };
11440 };
11441 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
11442 return TextRasterSource {
11443 draw: Cow::Borrowed(text_draw),
11444 raster_rect,
11445 };
11446 }
11447
11448 let line_starts = self.text_line_index_cache.line_starts(&text_draw.text);
11449 clipped_text_raster_source_with_line_starts(
11450 text_draw,
11451 logical_rect,
11452 raster_rect,
11453 clip,
11454 root_scale,
11455 line_starts.as_ref(),
11456 )
11457 }
11458
11459 fn prepare_text_image_draw_cmds<'a, I>(
11460 &mut self,
11461 layer_texts: I,
11462 viewport: ViewportUniformParams,
11463 root_scale: f32,
11464 staged_uploads: &mut StagedBufferUploads,
11465 ) -> Result<PreparedImageBatch, String>
11466 where
11467 I: Iterator<Item = &'a TextDraw>,
11468 {
11469 #[cfg(target_arch = "wasm32")]
11470 let _ = staged_uploads;
11471
11472 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
11473 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
11474 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
11475 image_vertices.clear();
11476 image_indices.clear();
11477 image_cmds.clear();
11478
11479 self.append_text_image_draw_cmds(
11480 layer_texts,
11481 viewport,
11482 root_scale,
11483 &mut image_vertices,
11484 &mut image_indices,
11485 &mut image_cmds,
11486 )?;
11487
11488 #[cfg(not(target_arch = "wasm32"))]
11489 if !image_cmds.is_empty() {
11490 self.stage_native_image_buffers(
11491 staged_uploads,
11492 viewport,
11493 &image_vertices,
11494 &image_indices,
11495 );
11496 }
11497
11498 #[cfg(target_arch = "wasm32")]
11499 let image_slot = if image_cmds.is_empty() {
11500 0
11501 } else {
11502 let slot = self.claim_wasm_image_batch();
11503 {
11504 let buffers = &mut self.wasm_image_batches[slot];
11505 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
11506 }
11507 let buffers = &self.wasm_image_batches[slot];
11508 self.write_wasm_buffer(
11509 &buffers.vertex_buffer,
11510 bytemuck::cast_slice(&image_vertices),
11511 );
11512 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
11513 slot
11514 };
11515
11516 #[cfg(target_arch = "wasm32")]
11517 let uniform_slot = if image_cmds.is_empty() {
11518 0
11519 } else {
11520 self.prepare_wasm_viewport_uniforms(viewport)
11521 };
11522
11523 self.scratch_image_vertices = image_vertices;
11524 self.scratch_image_indices = image_indices;
11525 Ok(PreparedImageBatch {
11526 cmds: image_cmds,
11527 #[cfg(target_arch = "wasm32")]
11528 image_slot,
11529 #[cfg(target_arch = "wasm32")]
11530 uniform_slot,
11531 })
11532 }
11533
11534 fn text_raster_geometry(
11535 &self,
11536 text_draw: &TextDraw,
11537 root_scale: f32,
11538 ) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
11539 text_raster_geometry_for_draw(text_draw, root_scale)
11540 }
11541
11542 fn text_image_cache_key(
11543 text_draw: &TextDraw,
11544 raster_rect: Rect,
11545 text_scale: f32,
11546 static_text_motion: bool,
11547 ) -> TextImageCacheKey {
11548 let mut state = default_hash::new();
11549 text_draw.text.render_hash().hash(&mut state);
11550 text_draw.text_style.render_hash().hash(&mut state);
11551 text_draw.color.render_hash().hash(&mut state);
11552 hash_text_raster_geometry_for_cache(raster_rect, static_text_motion, &mut state);
11553 text_draw.font_size.to_bits().hash(&mut state);
11554 text_scale.to_bits().hash(&mut state);
11555 text_draw.layout_options.hash(&mut state);
11556 TextImageCacheKey(state.finish())
11557 }
11558
11559 fn text_glyph_run_cache_key(
11560 text_draw: &TextDraw,
11561 raster_rect: Rect,
11562 text_scale: f32,
11563 static_text_motion: bool,
11564 ) -> TextGlyphRunCacheKey {
11565 TextGlyphRunCacheKey(
11566 Self::text_image_cache_key(text_draw, raster_rect, text_scale, static_text_motion).0,
11567 )
11568 }
11569
11570 fn rasterize_text_draw_to_image(
11571 &mut self,
11572 text_draw: &TextDraw,
11573 raster_rect: Rect,
11574 text_scale: f32,
11575 ) -> Option<ImageBitmap> {
11576 if text_draw.text.span_styles.is_empty() {
11577 let font = self.text_fonts.resolve(&text_draw.text_style)?;
11578 return rasterize_text_to_image_with_glyph_cache(
11579 text_draw.text.text.as_str(),
11580 raster_rect,
11581 &text_draw.text_style,
11582 text_draw.color,
11583 text_draw.font_size,
11584 text_scale,
11585 font,
11586 &mut self.text_glyph_mask_cache,
11587 );
11588 }
11589
11590 if let Some(image) = rasterize_annotated_text_to_image_with_glyph_cache(
11591 text_draw.text.as_ref(),
11592 raster_rect,
11593 &text_draw.text_style,
11594 text_draw.color,
11595 text_draw.font_size,
11596 text_scale,
11597 &self.text_fonts,
11598 &mut self.text_glyph_mask_cache,
11599 ) {
11600 return Some(image);
11601 }
11602
11603 rasterize_spanned_text_to_image(
11604 text_draw,
11605 raster_rect,
11606 text_scale,
11607 &self.text_fonts,
11608 &mut self.text_glyph_mask_cache,
11609 )
11610 }
11611}
11612
11613fn rasterize_spanned_text_to_image(
11614 text_draw: &TextDraw,
11615 raster_rect: Rect,
11616 text_scale: f32,
11617 fonts: &SoftwareTextFontSet,
11618 glyph_cache: &mut SoftwareGlyphRasterCache,
11619) -> Option<ImageBitmap> {
11620 let width = raster_rect.width.ceil().max(1.0) as u32;
11621 let height = raster_rect.height.ceil().max(1.0) as u32;
11622 let mut canvas = vec![0_u8; (width as usize) * (height as usize) * 4];
11623 let boundaries = text_draw.text.span_boundaries();
11624 let base_line_height = text_draw
11625 .text_style
11626 .resolve_line_height(14.0, text_draw.font_size)
11627 .max(1.0);
11628 let mut current_line_height = base_line_height;
11629 let mut cursor_x = raster_rect.x;
11630 let mut cursor_y = raster_rect.y;
11631
11632 for window in boundaries.windows(2) {
11633 let start = window[0];
11634 let end = window[1];
11635 if start == end {
11636 continue;
11637 }
11638
11639 let chunk = &text_draw.text.text[start..end];
11640 let mut merged_span = text_draw.text_style.span_style.clone();
11641 for span in &text_draw.text.span_styles {
11642 if span.range.start <= start && span.range.end >= end {
11643 merged_span = merged_span.merge(&span.item);
11644 }
11645 }
11646
11647 let mut chunk_style = text_draw.text_style.clone();
11648 chunk_style.span_style = merged_span;
11649
11650 for part in chunk.split_inclusive('\n') {
11651 let has_newline = part.ends_with('\n');
11652 let content = if has_newline {
11653 &part[..part.len().saturating_sub(1)]
11654 } else {
11655 part
11656 };
11657
11658 if !content.is_empty() {
11659 let chunk_font_size = chunk_style.resolve_font_size(text_draw.font_size);
11660 let Some(font) = fonts.resolve(&chunk_style) else {
11661 continue;
11662 };
11663 let metrics = measure_text_with_font(content, &chunk_style, chunk_font_size, font);
11664 let segment_rect = Rect {
11665 x: cursor_x,
11666 y: cursor_y,
11667 width: (metrics.width * text_scale).ceil().max(1.0),
11668 height: (metrics.height * text_scale).ceil().max(1.0),
11669 };
11670 if let Some(segment_image) = rasterize_text_to_image_with_glyph_cache(
11671 content,
11672 segment_rect,
11673 &chunk_style,
11674 chunk_style.resolve_text_color(text_draw.color),
11675 chunk_font_size,
11676 text_scale,
11677 font,
11678 glyph_cache,
11679 ) {
11680 composite_text_segment(
11681 &mut canvas,
11682 width,
11683 height,
11684 raster_rect,
11685 segment_rect,
11686 &segment_image,
11687 );
11688 }
11689 cursor_x += metrics.width * text_scale;
11690 current_line_height = current_line_height.max(metrics.line_height.max(1.0));
11691 }
11692
11693 if has_newline {
11694 cursor_x = raster_rect.x;
11695 cursor_y += current_line_height * text_scale;
11696 current_line_height = base_line_height;
11697 }
11698 }
11699 }
11700
11701 ImageBitmap::from_rgba8(width, height, canvas).ok()
11702}
11703
11704struct TextRasterSource<'a> {
11705 draw: Cow<'a, TextDraw>,
11706 raster_rect: Rect,
11707}
11708
11709fn text_glyph_raster_source(text_draw: &TextDraw, raster_rect: Rect) -> TextRasterSource<'_> {
11710 TextRasterSource {
11711 draw: Cow::Borrowed(text_draw),
11712 raster_rect,
11713 }
11714}
11715
11716#[cfg(test)]
11717fn clipped_text_raster_source<'a>(
11718 text_draw: &'a TextDraw,
11719 logical_rect: Rect,
11720 raster_rect: Rect,
11721 clip: Option<Rect>,
11722 root_scale: f32,
11723 static_text_motion: bool,
11724) -> TextRasterSource<'a> {
11725 let Some(clip) = clip else {
11726 return TextRasterSource {
11727 draw: Cow::Borrowed(text_draw),
11728 raster_rect,
11729 };
11730 };
11731 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
11732 return TextRasterSource {
11733 draw: Cow::Borrowed(text_draw),
11734 raster_rect,
11735 };
11736 }
11737 let line_starts = line_start_offsets(text_draw.text.text.as_str());
11738 clipped_text_raster_source_with_line_starts(
11739 text_draw,
11740 logical_rect,
11741 raster_rect,
11742 clip,
11743 root_scale,
11744 &line_starts,
11745 )
11746}
11747
11748fn clipped_text_raster_source_with_line_starts<'a>(
11749 text_draw: &'a TextDraw,
11750 logical_rect: Rect,
11751 raster_rect: Rect,
11752 clip: Rect,
11753 root_scale: f32,
11754 line_starts: &[usize],
11755) -> TextRasterSource<'a> {
11756 if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
11757 return TextRasterSource {
11758 draw: Cow::Borrowed(text_draw),
11759 raster_rect,
11760 };
11761 }
11762
11763 let Some(visible_rect) = logical_rect.intersect(clip) else {
11764 return TextRasterSource {
11765 draw: Cow::Borrowed(text_draw),
11766 raster_rect,
11767 };
11768 };
11769
11770 let line_count = line_starts.len().max(1);
11771 let line_height = logical_rect.height / line_count as f32;
11772 if !line_height.is_finite() || line_height <= 0.0 {
11773 return TextRasterSource {
11774 draw: Cow::Borrowed(text_draw),
11775 raster_rect,
11776 };
11777 }
11778
11779 let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
11780 let visible_bottom =
11781 ((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
11782 let start_line = visible_top.saturating_sub(1).max(0) as usize;
11783 let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
11784 let end_line = end_line.min(line_count);
11785 if start_line == 0 && end_line >= line_count {
11786 return TextRasterSource {
11787 draw: Cow::Borrowed(text_draw),
11788 raster_rect,
11789 };
11790 }
11791
11792 let byte_start = line_starts[start_line];
11793 let byte_end = line_end_offset(text_draw.text.text.as_str(), line_starts, end_line - 1);
11794 if byte_start >= byte_end {
11795 return TextRasterSource {
11796 draw: Cow::Borrowed(text_draw),
11797 raster_rect,
11798 };
11799 }
11800
11801 let slice_y = logical_rect.y + start_line as f32 * line_height;
11802 let slice_height = (end_line - start_line) as f32 * line_height;
11803 let mut slice_raster_rect = Rect {
11804 x: logical_rect.x * root_scale,
11805 y: slice_y * root_scale,
11806 width: logical_rect.width * root_scale,
11807 height: slice_height * root_scale,
11808 };
11809 slice_raster_rect.x = slice_raster_rect.x.round();
11810 slice_raster_rect.y = slice_raster_rect.y.round();
11811 slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
11812 slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
11813
11814 let mut sliced_draw = text_draw.clone();
11815 sliced_draw.rect = Rect {
11816 x: logical_rect.x,
11817 y: slice_y,
11818 width: logical_rect.width,
11819 height: slice_height,
11820 };
11821 sliced_draw.text = Arc::new(text_draw.text.subsequence(byte_start..byte_end));
11822
11823 TextRasterSource {
11824 draw: Cow::Owned(sliced_draw),
11825 raster_rect: slice_raster_rect,
11826 }
11827}
11828
11829fn line_start_offsets(text: &str) -> Vec<usize> {
11830 let mut starts =
11831 Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
11832 starts.push(0);
11833 starts.extend(
11834 text.char_indices()
11835 .filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
11836 );
11837 starts
11838}
11839
11840fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
11841 line_starts.get(line + 1).copied().unwrap_or(text.len())
11842}
11843
11844fn composite_text_segment(
11845 canvas: &mut [u8],
11846 canvas_width: u32,
11847 canvas_height: u32,
11848 canvas_rect: Rect,
11849 segment_rect: Rect,
11850 segment_image: &ImageBitmap,
11851) {
11852 let offset_x = (segment_rect.x - canvas_rect.x).round() as i32;
11853 let offset_y = (segment_rect.y - canvas_rect.y).round() as i32;
11854 let src = segment_image.pixels();
11855 for sy in 0..segment_image.height() as i32 {
11856 let dy = offset_y + sy;
11857 if dy < 0 || dy >= canvas_height as i32 {
11858 continue;
11859 }
11860 for sx in 0..segment_image.width() as i32 {
11861 let dx = offset_x + sx;
11862 if dx < 0 || dx >= canvas_width as i32 {
11863 continue;
11864 }
11865 let src_index = ((sy as u32 * segment_image.width() + sx as u32) * 4) as usize;
11866 let dst_index = ((dy as u32 * canvas_width + dx as u32) * 4) as usize;
11867 blend_rgba_pixel(
11868 &mut canvas[dst_index..dst_index + 4],
11869 &src[src_index..src_index + 4],
11870 );
11871 }
11872 }
11873}
11874
11875fn blend_rgba_pixel(dst: &mut [u8], src: &[u8]) {
11876 let src_alpha = src[3] as f32 / 255.0;
11877 if src_alpha <= 0.0 {
11878 return;
11879 }
11880 let dst_alpha = dst[3] as f32 / 255.0;
11881 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
11882 if out_alpha <= f32::EPSILON {
11883 dst.copy_from_slice(&[0, 0, 0, 0]);
11884 return;
11885 }
11886
11887 for channel in 0..3 {
11888 let src_channel = src[channel] as f32 / 255.0;
11889 let dst_channel = dst[channel] as f32 / 255.0;
11890 let src_premult = src_channel * src_alpha;
11891 let dst_premult = dst_channel * dst_alpha;
11892 dst[channel] =
11893 (((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0) * 255.0)
11894 .round() as u8;
11895 }
11896 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
11897}
11898
11899fn align_to(value: u32, alignment: u32) -> u32 {
11900 debug_assert!(alignment > 0);
11901 value.div_ceil(alignment) * alignment
11902}
11903
11904#[cfg(not(target_arch = "wasm32"))]
11905fn align_usize_to(value: usize, alignment: usize) -> usize {
11906 debug_assert!(alignment > 0);
11907 value.div_ceil(alignment) * alignment
11908}
11909
11910impl GpuRenderer {
11911 fn convert_surface_pixels_to_rgba(&self, pixels: &mut [u8]) -> Result<(), String> {
11912 match self.surface_format {
11913 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => Ok(()),
11914 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
11915 for pixel in pixels.chunks_exact_mut(4) {
11916 pixel.swap(0, 2);
11917 }
11918 Ok(())
11919 }
11920 format => Err(format!(
11921 "Screenshot readback unsupported for texture format: {format:?}"
11922 )),
11923 }
11924 }
11925}
11926
11927fn is_in_effect_range(z_index: usize, effect_z_ranges: &[Range<usize>]) -> bool {
11928 effect_z_ranges.iter().any(|range| range.contains(&z_index))
11929}
11930
11931#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11932enum SegmentDrawItem {
11933 Shape(usize),
11934 Image(usize),
11935 Text(usize),
11936 Shadow(usize),
11937 Composite(usize),
11938 ShaderComposite(usize),
11939 Retained(usize),
11940}
11941
11942#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11943enum SegmentBatchPlan {
11944 Shape {
11945 start: usize,
11946 end: usize,
11947 blend_mode: BlendMode,
11948 },
11949 Image {
11950 start: usize,
11951 end: usize,
11952 blend_mode: BlendMode,
11953 },
11954 Text {
11955 start: usize,
11956 end: usize,
11957 },
11958 Composite {
11959 start: usize,
11960 end: usize,
11961 },
11962 ShaderComposite {
11963 start: usize,
11964 end: usize,
11965 },
11966 Retained {
11969 start: usize,
11970 end: usize,
11971 },
11972}
11973
11974#[derive(Clone, Debug, Default, PartialEq, Eq)]
11975struct SegmentDrawChunkPlan {
11976 batches: Vec<SegmentBatchPlan>,
11977}
11978
11979struct SegmentRenderOutcome {
11980 rendered_any: bool,
11981 pass_count: u32,
11982}
11983
11984struct SegmentCommandEncodeOutcome {
11985 first_batch: bool,
11986}
11987
11988#[cfg(not(target_arch = "wasm32"))]
11989#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11990enum TextGlyphPrewarmDecision {
11991 Candidate,
11992 MissingGeometry,
11993 DynamicMotion,
11994 Visible,
11995 OutsidePrewarmWindow,
11996}
11997
11998#[cfg(not(target_arch = "wasm32"))]
11999#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12000struct NativeSegmentFusionBudget {
12001 shape_count: usize,
12002 gradient_stop_count: usize,
12003}
12004
12005#[cfg(not(target_arch = "wasm32"))]
12006#[derive(Clone, Debug, PartialEq, Eq)]
12007struct NativeSegmentFusionPartition {
12008 chunk: SegmentDrawChunkPlan,
12009 budget: NativeSegmentFusionBudget,
12010}
12011
12012#[cfg(not(target_arch = "wasm32"))]
12013#[derive(Clone, Debug, PartialEq, Eq)]
12014enum FusedSegmentBatch {
12015 Shape {
12016 batch: PreparedShapeBatch,
12017 blend_mode: BlendMode,
12018 },
12019 Image {
12020 cmd_range: Range<usize>,
12021 blend_mode: BlendMode,
12022 },
12023 Text {
12024 image_cmd_range: Range<usize>,
12025 glyph_cmd_range: Range<usize>,
12026 },
12027 Composite {
12028 draw_range: Range<usize>,
12029 },
12030 ShaderComposite {
12031 draw_range: Range<usize>,
12032 },
12033 Retained {
12034 item_range: Range<usize>,
12035 },
12036}
12037
12038struct ShadowSourceRenderOutcome {
12039 rendered_any: bool,
12040 pass_count: u32,
12041}
12042
12043impl SegmentDrawChunkPlan {
12044 fn is_empty(&self) -> bool {
12045 self.batches.is_empty()
12046 }
12047
12048 fn push(&mut self, batch: SegmentBatchPlan) {
12049 self.batches.push(batch);
12050 }
12051
12052 fn iter(&self) -> impl Iterator<Item = SegmentBatchPlan> + '_ {
12053 self.batches.iter().copied()
12054 }
12055}
12056
12057#[derive(Clone, Debug, PartialEq, Eq)]
12058enum SegmentRenderCommand {
12059 DrawChunk(SegmentDrawChunkPlan),
12060 Shadow(usize),
12061}
12062
12063struct SegmentCommandIter<'a> {
12064 ordered_items: &'a [(usize, SegmentDrawItem)],
12065 shapes: &'a [DrawShape],
12066 images: &'a [ImageDraw],
12067 cursor: usize,
12068 batch_limits: ShapeBatchLimits,
12069}
12070
12071impl<'a> SegmentCommandIter<'a> {
12072 fn new(
12073 ordered_items: &'a [(usize, SegmentDrawItem)],
12074 shapes: &'a [DrawShape],
12075 images: &'a [ImageDraw],
12076 batch_limits: ShapeBatchLimits,
12077 ) -> Self {
12078 Self {
12079 ordered_items,
12080 shapes,
12081 images,
12082 cursor: 0,
12083 batch_limits,
12084 }
12085 }
12086}
12087
12088impl Iterator for SegmentCommandIter<'_> {
12089 type Item = SegmentRenderCommand;
12090
12091 fn next(&mut self) -> Option<Self::Item> {
12092 if self.cursor >= self.ordered_items.len() {
12093 return None;
12094 }
12095
12096 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
12097 self.cursor += 1;
12098 return Some(SegmentRenderCommand::Shadow(index));
12099 }
12100
12101 let mut chunk = SegmentDrawChunkPlan::default();
12102 while self.cursor < self.ordered_items.len() {
12103 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
12104 if chunk.is_empty() {
12105 self.cursor += 1;
12106 return Some(SegmentRenderCommand::Shadow(index));
12107 }
12108 break;
12109 }
12110
12111 let Some((batch, next_cursor)) = segment_batch_plan_at_cursor(
12112 self.ordered_items,
12113 self.shapes,
12114 self.images,
12115 self.cursor,
12116 self.batch_limits,
12117 ) else {
12118 break;
12119 };
12120 chunk.push(batch);
12121 self.cursor = next_cursor;
12122 }
12123
12124 Some(SegmentRenderCommand::DrawChunk(chunk))
12125 }
12126}
12127
12128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12129struct PreparedShapeBatch {
12130 vertex_start: u32,
12133 vertex_count: u32,
12134 #[cfg(target_arch = "wasm32")]
12135 shape_slot: usize,
12136 #[cfg(target_arch = "wasm32")]
12137 uniform_slot: usize,
12138}
12139
12140struct PreparedImageBatch {
12141 cmds: Vec<ImageDrawCmd>,
12142 #[cfg(target_arch = "wasm32")]
12143 image_slot: usize,
12144 #[cfg(target_arch = "wasm32")]
12145 uniform_slot: usize,
12146}
12147
12148impl PreparedImageBatch {
12149 fn is_empty(&self) -> bool {
12150 self.cmds.is_empty()
12151 }
12152
12153 fn into_cmds(self) -> Vec<ImageDrawCmd> {
12154 self.cmds
12155 }
12156}
12157
12158struct PreparedGlyphBatch {
12159 cmds: Vec<GlyphDrawCmd>,
12160 #[cfg(target_arch = "wasm32")]
12161 image_slot: usize,
12162 #[cfg(target_arch = "wasm32")]
12163 uniform_slot: usize,
12164}
12165
12166impl PreparedGlyphBatch {
12167 fn is_empty(&self) -> bool {
12168 self.cmds.is_empty()
12169 }
12170
12171 fn into_cmds(self) -> Vec<GlyphDrawCmd> {
12172 self.cmds
12173 }
12174}
12175
12176#[cfg(not(target_arch = "wasm32"))]
12177fn gradient_stop_count_for_shape(shape: &DrawShape) -> usize {
12178 match &shape.brush {
12179 Brush::Solid(_) => 0,
12180 Brush::LinearGradient { colors, .. }
12181 | Brush::RadialGradient { colors, .. }
12182 | Brush::SweepGradient { colors, .. } => colors.len(),
12183 }
12184}
12185
12186#[cfg(not(target_arch = "wasm32"))]
12187fn native_segment_fusion_budget(
12188 ordered_items: &[(usize, SegmentDrawItem)],
12189 shapes: &[DrawShape],
12190 chunk: &SegmentDrawChunkPlan,
12191 batch_limits: ShapeBatchLimits,
12192) -> Result<Option<NativeSegmentFusionBudget>, String> {
12193 let mut shape_count = 0usize;
12194 let mut gradient_stop_count = 0usize;
12195
12196 for batch in chunk.iter() {
12197 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
12198 continue;
12199 };
12200 for (_, item) in &ordered_items[start..end] {
12201 let SegmentDrawItem::Shape(shape_index) = item else {
12202 return Err(format!(
12203 "shape batch contains non-shape draw item: {item:?}"
12204 ));
12205 };
12206 let shape = &shapes[*shape_index];
12207 shape_count = shape_count.saturating_add(1);
12208 gradient_stop_count =
12209 gradient_stop_count.saturating_add(gradient_stop_count_for_shape(shape));
12210 }
12211 }
12212
12213 if shape_count > batch_limits.max_shapes_per_batch
12214 || gradient_stop_count > batch_limits.max_gradient_stops
12215 {
12216 return Ok(None);
12217 }
12218
12219 Ok(Some(NativeSegmentFusionBudget {
12220 shape_count,
12221 gradient_stop_count,
12222 }))
12223}
12224
12225#[cfg(not(target_arch = "wasm32"))]
12226fn push_native_segment_fusion_partition(
12227 partitions: &mut Vec<NativeSegmentFusionPartition>,
12228 current: &mut SegmentDrawChunkPlan,
12229 current_budget: &mut NativeSegmentFusionBudget,
12230) {
12231 if current.is_empty() {
12232 return;
12233 }
12234
12235 partitions.push(NativeSegmentFusionPartition {
12236 chunk: std::mem::take(current),
12237 budget: *current_budget,
12238 });
12239 *current_budget = NativeSegmentFusionBudget {
12240 shape_count: 0,
12241 gradient_stop_count: 0,
12242 };
12243}
12244
12245#[cfg(not(target_arch = "wasm32"))]
12246fn native_segment_fusion_partitions(
12247 ordered_items: &[(usize, SegmentDrawItem)],
12248 shapes: &[DrawShape],
12249 chunk: &SegmentDrawChunkPlan,
12250 batch_limits: ShapeBatchLimits,
12251) -> Result<Option<Vec<NativeSegmentFusionPartition>>, String> {
12252 if let Some(budget) = native_segment_fusion_budget(ordered_items, shapes, chunk, batch_limits)?
12253 {
12254 return Ok(Some(vec![NativeSegmentFusionPartition {
12255 chunk: chunk.clone(),
12256 budget,
12257 }]));
12258 }
12259
12260 let mut partitions = Vec::new();
12261 let mut current = SegmentDrawChunkPlan::default();
12262 let mut current_budget = NativeSegmentFusionBudget {
12263 shape_count: 0,
12264 gradient_stop_count: 0,
12265 };
12266
12267 for batch in chunk.iter() {
12268 let SegmentBatchPlan::Shape {
12269 start,
12270 end,
12271 blend_mode,
12272 } = batch
12273 else {
12274 current.push(batch);
12275 continue;
12276 };
12277
12278 let mut run_start = start;
12279 for (item_cursor, (_, item)) in ordered_items.iter().enumerate().take(end).skip(start) {
12280 let SegmentDrawItem::Shape(shape_index) = *item else {
12281 return Err(format!(
12282 "shape batch contains non-shape draw item: {:?}",
12283 item
12284 ));
12285 };
12286 let gradient_stop_count = gradient_stop_count_for_shape(&shapes[shape_index]);
12287 if gradient_stop_count > batch_limits.max_gradient_stops {
12288 return Ok(None);
12289 }
12290
12291 let fits_shape_count =
12292 current_budget.shape_count.saturating_add(1) <= batch_limits.max_shapes_per_batch;
12293 let fits_gradient_count = current_budget
12294 .gradient_stop_count
12295 .saturating_add(gradient_stop_count)
12296 <= batch_limits.max_gradient_stops;
12297 if !fits_shape_count || !fits_gradient_count {
12298 if run_start < item_cursor {
12299 current.push(SegmentBatchPlan::Shape {
12300 start: run_start,
12301 end: item_cursor,
12302 blend_mode,
12303 });
12304 }
12305 push_native_segment_fusion_partition(
12306 &mut partitions,
12307 &mut current,
12308 &mut current_budget,
12309 );
12310 run_start = item_cursor;
12311 }
12312
12313 current_budget.shape_count = current_budget.shape_count.saturating_add(1);
12314 current_budget.gradient_stop_count = current_budget
12315 .gradient_stop_count
12316 .saturating_add(gradient_stop_count);
12317 }
12318
12319 if run_start < end {
12320 current.push(SegmentBatchPlan::Shape {
12321 start: run_start,
12322 end,
12323 blend_mode,
12324 });
12325 }
12326 }
12327
12328 push_native_segment_fusion_partition(&mut partitions, &mut current, &mut current_budget);
12329 Ok(Some(partitions))
12330}
12331
12332fn segment_batch_plan_at_cursor(
12333 ordered_items: &[(usize, SegmentDrawItem)],
12334 shapes: &[DrawShape],
12335 images: &[ImageDraw],
12336 start: usize,
12337 batch_limits: ShapeBatchLimits,
12338) -> Option<(SegmentBatchPlan, usize)> {
12339 match ordered_items[start].1 {
12340 SegmentDrawItem::Shape(index) => {
12341 let blend_mode = supported_blend_mode(shapes[index].blend_mode);
12342 let mut end = start + 1;
12343 let shape_limit = (start + batch_limits.max_shapes_per_batch).min(ordered_items.len());
12344 while end < shape_limit {
12345 match ordered_items[end].1 {
12346 SegmentDrawItem::Shape(next_index)
12347 if supported_blend_mode(shapes[next_index].blend_mode) == blend_mode =>
12348 {
12349 end += 1;
12350 }
12351 _ => break,
12352 }
12353 }
12354 Some((
12355 SegmentBatchPlan::Shape {
12356 start,
12357 end,
12358 blend_mode,
12359 },
12360 end,
12361 ))
12362 }
12363 SegmentDrawItem::Image(index) => {
12364 let blend_mode = supported_blend_mode(images[index].blend_mode);
12365 let mut end = start + 1;
12366 while end < ordered_items.len() {
12367 match ordered_items[end].1 {
12368 SegmentDrawItem::Image(next_index)
12369 if supported_blend_mode(images[next_index].blend_mode) == blend_mode =>
12370 {
12371 end += 1;
12372 }
12373 _ => break,
12374 }
12375 }
12376 Some((
12377 SegmentBatchPlan::Image {
12378 start,
12379 end,
12380 blend_mode,
12381 },
12382 end,
12383 ))
12384 }
12385 SegmentDrawItem::Text(_) => {
12386 let mut end = start + 1;
12387 while end < ordered_items.len() {
12388 if matches!(ordered_items[end].1, SegmentDrawItem::Text(_)) {
12389 end += 1;
12390 } else {
12391 break;
12392 }
12393 }
12394 Some((SegmentBatchPlan::Text { start, end }, end))
12395 }
12396 SegmentDrawItem::Composite(_) => {
12397 let mut end = start + 1;
12398 while end < ordered_items.len() {
12399 if matches!(ordered_items[end].1, SegmentDrawItem::Composite(_)) {
12400 end += 1;
12401 } else {
12402 break;
12403 }
12404 }
12405 Some((SegmentBatchPlan::Composite { start, end }, end))
12406 }
12407 SegmentDrawItem::ShaderComposite(_) => {
12408 let mut end = start + 1;
12409 while end < ordered_items.len() {
12410 if matches!(ordered_items[end].1, SegmentDrawItem::ShaderComposite(_)) {
12411 end += 1;
12412 } else {
12413 break;
12414 }
12415 }
12416 Some((SegmentBatchPlan::ShaderComposite { start, end }, end))
12417 }
12418 SegmentDrawItem::Retained(_) => {
12419 let mut end = start + 1;
12420 while end < ordered_items.len() {
12421 if matches!(ordered_items[end].1, SegmentDrawItem::Retained(_)) {
12422 end += 1;
12423 } else {
12424 break;
12425 }
12426 }
12427 Some((SegmentBatchPlan::Retained { start, end }, end))
12428 }
12429 SegmentDrawItem::Shadow(_) => None,
12430 }
12431}
12432
12433#[allow(clippy::too_many_arguments)]
12434fn collect_non_effect_segment_items(
12435 shapes: &[DrawShape],
12436 _images: &[ImageDraw],
12437 _texts: &[TextDraw],
12438 _shadow_draws: &[ShadowDraw],
12439 draw_ops: &[DrawOp],
12440 z_start: usize,
12441 z_end: usize,
12442 effect_z_ranges: &[Range<usize>],
12443 width: u32,
12444 height: u32,
12445 root_scale: f32,
12446 scratch: &mut Vec<(usize, SegmentDrawItem)>,
12447) {
12448 scratch.clear();
12449 let viewport = ViewportUniformParams {
12450 width,
12451 height,
12452 offset: [0.0, 0.0],
12453 };
12454
12455 scratch.extend(draw_ops.iter().filter_map(|op| {
12456 if op.z_index < z_start
12457 || op.z_index >= z_end
12458 || is_in_effect_range(op.z_index, effect_z_ranges)
12459 {
12460 return None;
12461 }
12462 let item = match op.kind {
12463 DrawOpKind::Shape(index) => {
12464 let shape = shapes.get(index)?;
12465 if !shape_draw_is_visible_in_viewport(shape, viewport, root_scale) {
12466 return None;
12467 }
12468 SegmentDrawItem::Shape(index)
12469 }
12470 DrawOpKind::Image(index) => SegmentDrawItem::Image(index),
12471 DrawOpKind::Text(index) => SegmentDrawItem::Text(index),
12472 DrawOpKind::Shadow(index) => SegmentDrawItem::Shadow(index),
12473 DrawOpKind::Retained(index) => SegmentDrawItem::Retained(index),
12474 };
12475 Some((op.z_index, item))
12476 }));
12477}
12478
12479fn retain_renderable_shadow_items(
12480 ordered_items: &mut Vec<(usize, SegmentDrawItem)>,
12481 shadow_draws: &[ShadowDraw],
12482 width: u32,
12483 height: u32,
12484 root_scale: f32,
12485 max_texture_dim: u32,
12486) -> usize {
12487 let original_len = ordered_items.len();
12488 ordered_items.retain(|(_, item)| match item {
12489 SegmentDrawItem::Shadow(index) => shadow_draws.get(*index).is_some_and(|shadow| {
12490 shadow_draw_may_render(shadow, width, height, root_scale, max_texture_dim)
12491 }),
12492 _ => true,
12493 });
12494 original_len.saturating_sub(ordered_items.len())
12495}
12496
12497#[cfg(not(target_arch = "wasm32"))]
12498#[derive(Clone, Copy)]
12499struct SegmentDiagCounts {
12500 raw_shadow_items: usize,
12501 culled_shadow_items: usize,
12502 cached_shadow_composites: usize,
12503 composite_items: usize,
12504 shader_composite_items: usize,
12505}
12506
12507#[cfg(not(target_arch = "wasm32"))]
12508fn maybe_print_segment_diag(
12509 z_range: Range<usize>,
12510 ordered_items: &[(usize, SegmentDrawItem)],
12511 shapes: &[DrawShape],
12512 images: &[ImageDraw],
12513 counts: SegmentDiagCounts,
12514 batch_limits: ShapeBatchLimits,
12515) {
12516 if !cranpose_core::env_flag!("CRANPOSE_SEGMENT_DIAG") {
12517 return;
12518 }
12519 let line = SEGMENT_DIAG_LINES.fetch_add(1, Ordering::Relaxed);
12520 if line >= 64 {
12521 return;
12522 }
12523
12524 let remaining_shadow_items = ordered_items
12525 .iter()
12526 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
12527 .count();
12528 let commands: Vec<_> =
12529 SegmentCommandIter::new(ordered_items, shapes, images, batch_limits).collect();
12530 let draw_chunks = commands
12531 .iter()
12532 .filter(|command| matches!(command, SegmentRenderCommand::DrawChunk(_)))
12533 .count();
12534 let shadow_commands = commands
12535 .iter()
12536 .filter(|command| matches!(command, SegmentRenderCommand::Shadow(_)))
12537 .count();
12538 let mut native_partitions = 0usize;
12539 let mut native_unfused_chunks = 0usize;
12540 for command in &commands {
12541 let SegmentRenderCommand::DrawChunk(chunk) = command else {
12542 continue;
12543 };
12544 match native_segment_fusion_partitions(ordered_items, shapes, chunk, batch_limits) {
12545 Ok(Some(partitions)) => native_partitions += partitions.len(),
12546 Ok(None) | Err(_) => native_unfused_chunks += 1,
12547 }
12548 }
12549
12550 eprintln!(
12551 "[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={}",
12552 z_range.start,
12553 z_range.end,
12554 ordered_items.len(),
12555 counts.raw_shadow_items,
12556 counts.culled_shadow_items,
12557 counts.cached_shadow_composites,
12558 remaining_shadow_items,
12559 counts.composite_items,
12560 counts.shader_composite_items,
12561 draw_chunks,
12562 shadow_commands,
12563 native_partitions,
12564 native_unfused_chunks,
12565 );
12566}
12567
12568pub(crate) fn has_backdrop_layer_in_range(
12569 backdrop_layers: &[BackdropLayer],
12570 z_start: usize,
12571 z_end: usize,
12572) -> bool {
12573 backdrop_layers
12574 .iter()
12575 .any(|layer| layer.z_index >= z_start && layer.z_index < z_end)
12576}
12577
12578pub(crate) fn scissor_rect_for_rect(
12579 rect: Rect,
12580 root_scale: f32,
12581 width: u32,
12582 height: u32,
12583) -> Option<(u32, u32, u32, u32)> {
12584 let mut left = canonicalize_device_coordinate(rect.x * root_scale);
12585 let mut top = canonicalize_device_coordinate(rect.y * root_scale);
12586 let mut right = canonicalize_device_coordinate((rect.x + rect.width) * root_scale);
12587 let mut bottom = canonicalize_device_coordinate((rect.y + rect.height) * root_scale);
12588
12589 left = left.max(0.0).min(width as f32).floor();
12590 top = top.max(0.0).min(height as f32).floor();
12591 right = right.max(0.0).min(width as f32).ceil();
12592 bottom = bottom.max(0.0).min(height as f32).ceil();
12593
12594 if right <= left || bottom <= top {
12595 return None;
12596 }
12597
12598 Some((
12599 left as u32,
12600 top as u32,
12601 (right - left) as u32,
12602 (bottom - top) as u32,
12603 ))
12604}
12605
12606fn scissor_rect_for_layer(
12607 rect: Rect,
12608 clip: Option<Rect>,
12609 root_scale: f32,
12610 width: u32,
12611 height: u32,
12612) -> Option<(u32, u32, u32, u32)> {
12613 let clipped_rect = match clip {
12614 Some(clip_rect) => rect.intersect(clip_rect)?,
12615 None => rect,
12616 };
12617
12618 scissor_rect_for_rect(clipped_rect, root_scale, width, height)
12619}
12620
12621fn tint_for_image(
12622 color_filter: Option<ColorFilter>,
12623 alpha: f32,
12624) -> ([f32; 4], Option<ColorFilter>) {
12625 let alpha = alpha.clamp(0.0, 1.0);
12626 match color_filter {
12627 Some(filter) if filter.supports_gpu_vertex_modulation() => {
12628 let Some(tint) = filter.gpu_vertex_tint() else {
12629 return ([1.0, 1.0, 1.0, alpha], Some(filter));
12630 };
12631 (
12632 [
12633 tint[0].clamp(0.0, 1.0),
12634 tint[1].clamp(0.0, 1.0),
12635 tint[2].clamp(0.0, 1.0),
12636 (tint[3] * alpha).clamp(0.0, 1.0),
12637 ],
12638 None,
12639 )
12640 }
12641 Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
12642 None => ([1.0, 1.0, 1.0, alpha], None),
12643 }
12644}
12645
12646fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
12647 let Some(src) = src_rect else {
12648 return Some(ImageUvRect {
12649 min: [0.0, 0.0],
12650 max: [1.0, 1.0],
12651 sample_bounds: [0.0, 0.0, 1.0, 1.0],
12652 });
12653 };
12654
12655 let (u_min, u_max, u_bound_min, u_bound_max) =
12656 source_axis_uv(src.x, src.width, image.width() as f32)?;
12657 let (v_min, v_max, v_bound_min, v_bound_max) =
12658 source_axis_uv(src.y, src.height, image.height() as f32)?;
12659
12660 Some(ImageUvRect {
12661 min: [u_min, v_min],
12662 max: [u_max, v_max],
12663 sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
12664 })
12665}
12666
12667fn glyph_atlas_uv_rect(entry: GlyphAtlasEntry, atlas_size: u32) -> ImageUvRect {
12672 let atlas_width = atlas_size as f32;
12673 let atlas_height = atlas_size as f32;
12674 let min = [entry.x as f32 / atlas_width, entry.y as f32 / atlas_height];
12675 let max = [
12676 (entry.x + entry.width) as f32 / atlas_width,
12677 (entry.y + entry.height) as f32 / atlas_height,
12678 ];
12679 let center_min = [
12680 (entry.x as f32 + 0.5) / atlas_width,
12681 (entry.y as f32 + 0.5) / atlas_height,
12682 ];
12683 let center_max = [
12684 (entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) / atlas_width,
12685 (entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) / atlas_height,
12686 ];
12687 ImageUvRect {
12688 min,
12689 max,
12690 sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
12691 }
12692}
12693
12694fn snap_nearest_image_to_device_pixels(image: &mut ImageDraw, root_scale: f32) {
12695 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
12696 return;
12697 }
12698
12699 let Some(rect) = axis_aligned_quad_rect(image.quad) else {
12700 return;
12701 };
12702
12703 let left_px = (rect.x * root_scale).round();
12704 let top_px = (rect.y * root_scale).round();
12705 let width_px = (rect.width * root_scale).round().max(1.0);
12706 let height_px = (rect.height * root_scale).round().max(1.0);
12707 let snapped = Rect {
12708 x: left_px / root_scale,
12709 y: top_px / root_scale,
12710 width: width_px / root_scale,
12711 height: height_px / root_scale,
12712 };
12713
12714 image.rect = snapped;
12715 image.local_rect = Rect {
12716 x: image.local_rect.x + snapped.x - rect.x,
12717 y: image.local_rect.y + snapped.y - rect.y,
12718 width: snapped.width,
12719 height: snapped.height,
12720 };
12721 image.quad = crate::rect_to_quad(snapped);
12722}
12723
12724fn nearest_image_device_quad(image: &ImageDraw, root_scale: f32) -> Option<[[f32; 2]; 4]> {
12725 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
12726 return None;
12727 }
12728
12729 let rect = axis_aligned_quad_rect(image.quad)?;
12730 let left_px = (rect.x * root_scale).round();
12731 let top_px = (rect.y * root_scale).round();
12732 let width_px = (rect.width * root_scale).round().max(1.0);
12733 let height_px = (rect.height * root_scale).round().max(1.0);
12734 let right_px = left_px + width_px;
12735 let bottom_px = top_px + height_px;
12736 Some([
12737 [left_px, top_px],
12738 [right_px, top_px],
12739 [left_px, bottom_px],
12740 [right_px, bottom_px],
12741 ])
12742}
12743
12744fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
12745 if !start.is_finite()
12746 || !extent.is_finite()
12747 || !image_extent.is_finite()
12748 || extent == 0.0
12749 || image_extent <= 0.0
12750 {
12751 return None;
12752 }
12753
12754 let end = start + extent;
12755 let edge_min = start.min(end).clamp(0.0, image_extent);
12756 let edge_max = start.max(end).clamp(0.0, image_extent);
12757 if edge_max <= edge_min {
12758 return None;
12759 }
12760
12761 let center_min = edge_min + 0.5;
12762 let center_max = edge_max - 0.5;
12763 let (bound_min, bound_max) = if center_min <= center_max {
12764 (center_min, center_max)
12765 } else {
12766 let center = (edge_min + edge_max) * 0.5;
12767 (center, center)
12768 };
12769
12770 Some((
12771 edge_min / image_extent,
12772 edge_max / image_extent,
12773 bound_min / image_extent,
12774 bound_max / image_extent,
12775 ))
12776}
12777
12778fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
12779 let mut filtered = Vec::with_capacity(image.pixels().len());
12780 for pixel in image.pixels().chunks_exact(4) {
12781 let rgba = [
12782 pixel[0] as f32 / 255.0,
12783 pixel[1] as f32 / 255.0,
12784 pixel[2] as f32 / 255.0,
12785 pixel[3] as f32 / 255.0,
12786 ];
12787 let out = filter.apply_rgba(rgba);
12788 filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
12789 filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
12790 filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
12791 filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
12792 }
12793 ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
12794 .map_err(|error| format!("failed to build filtered bitmap: {error}"))
12795}
12796
12797fn scissor_rect_for_image(
12798 image: &ImageDraw,
12799 root_scale: f32,
12800 width: u32,
12801 height: u32,
12802) -> Option<(u32, u32, u32, u32)> {
12803 scissor_rect_for_layer(image.rect, image.clip, root_scale, width, height)
12804}
12805
12806fn inner_shadow_composite_mask(
12807 shadow: &ShadowDraw,
12808 root_scale: f32,
12809) -> Option<RoundedCompositeMask> {
12810 if !shadow
12811 .shapes
12812 .iter()
12813 .any(|(_, mode)| *mode == BlendMode::DstOut)
12814 {
12815 return None;
12816 }
12817 let (fill, _) = shadow.shapes.first()?;
12818 let rect = fill.local_rect;
12819 if rect.width <= 0.0 || rect.height <= 0.0 {
12820 return None;
12821 }
12822
12823 let radii = fill.shape.map_or([0.0; 4], |rounded| {
12824 let resolved = rounded.resolve(rect.width, rect.height);
12825 [
12826 resolved.top_left * root_scale,
12827 resolved.top_right * root_scale,
12828 resolved.bottom_left * root_scale,
12829 resolved.bottom_right * root_scale,
12830 ]
12831 });
12832
12833 Some(RoundedCompositeMask {
12834 rect: [
12835 rect.x * root_scale,
12836 rect.y * root_scale,
12837 rect.width * root_scale,
12838 rect.height * root_scale,
12839 ],
12840 radii,
12841 })
12842}
12843
12844#[cfg(test)]
12845mod tests {
12846 use super::*;
12847 use crate::normalized_scene::visible_draw_rect;
12848 use cranpose_foundation::lazy::{remember_lazy_list_state, LazyListScope, LazyListState};
12849 use cranpose_render_common::graph::{DrawPrimitiveNode, IsolationReasons, TextPrimitiveNode};
12850 use cranpose_render_common::raster_cache::LayerRasterCacheHashes;
12851 use cranpose_render_common::scene_builder::build_graph_from_applier;
12852 use cranpose_ui::text::{
12853 AnnotatedString, BaselineShift, RangeStyle, Shadow, SpanStyle, TextDecoration,
12854 TextDrawStyle, TextGeometricTransform, TextMotion, TextUnit,
12855 };
12856 use cranpose_ui::{
12857 LayoutEngine, LazyColumn, LazyColumnSpec, Modifier, Size, Text, TextLayoutOptions,
12858 TextStyle,
12859 };
12860 use cranpose_ui_graphics::{
12861 Brush, Color, CornerRadii, DrawPrimitive, Rect, RenderEffect, RoundedCornerShape,
12862 RuntimeShader,
12863 };
12864
12865 fn chunk(batches: &[SegmentBatchPlan]) -> SegmentDrawChunkPlan {
12866 let mut chunk = SegmentDrawChunkPlan::default();
12867 for batch in batches {
12868 chunk.push(*batch);
12869 }
12870 chunk
12871 }
12872
12873 fn with_test_app_context<R>(block: impl FnOnce() -> R) -> R {
12874 let app_context = cranpose_ui::AppContext::new();
12875 app_context.enter(block)
12876 }
12877
12878 fn assert_snap_anchor_close(actual: Option<SnapAnchor>, expected_origin: Point, message: &str) {
12879 let Some(actual) = actual else {
12880 panic!("{message}: missing snap anchor");
12881 };
12882 let expected = SnapAnchor::rigid(expected_origin);
12883 assert_eq!(
12884 actual.device_pixel_step, expected.device_pixel_step,
12885 "{message}: device pixel step changed"
12886 );
12887 assert!(
12888 (actual.origin.x - expected.origin.x).abs() <= 1e-4
12889 && (actual.origin.y - expected.origin.y).abs() <= 1e-4,
12890 "{message}: expected origin {:?}, got {:?}",
12891 expected.origin,
12892 actual.origin
12893 );
12894 }
12895
12896 fn effect_layer(z_start: usize, z_end: usize) -> EffectLayer {
12897 EffectLayer {
12898 rect: Rect {
12899 x: 0.0,
12900 y: 0.0,
12901 width: 10.0,
12902 height: 10.0,
12903 },
12904 clip: None,
12905 snap_anchor: None,
12906 effect: Some(RenderEffect::blur(4.0)),
12907 blend_mode: BlendMode::SrcOver,
12908 composite_alpha: 1.0,
12909 z_start,
12910 z_end,
12911 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
12912 }
12913 }
12914
12915 #[test]
12916 fn direct_shader_composite_accepts_box4_when_viewport_preserves_source_pixels() {
12917 assert_eq!(
12918 direct_shader_composite_viewport(
12919 1.0,
12920 BlendMode::SrcOver,
12921 Some((12.0, 18.0, 64.0, 32.0)),
12922 CompositeSampleMode::Box4,
12923 (64, 32),
12924 ),
12925 Some((12.0, 18.0, 64.0, 32.0))
12926 );
12927 }
12928
12929 #[test]
12930 fn direct_shader_composite_rejects_box4_when_viewport_resamples_source() {
12931 assert_eq!(
12932 direct_shader_composite_viewport(
12933 1.0,
12934 BlendMode::SrcOver,
12935 Some((12.0, 18.0, 64.5, 32.0)),
12936 CompositeSampleMode::Box4,
12937 (64, 32),
12938 ),
12939 None
12940 );
12941 assert_eq!(
12942 direct_shader_composite_viewport(
12943 1.0,
12944 BlendMode::SrcOver,
12945 Some((12.25, 18.0, 64.0, 32.0)),
12946 CompositeSampleMode::Box4,
12947 (64, 32),
12948 ),
12949 None
12950 );
12951 }
12952
12953 fn test_text_draw(rect: Rect, text_motion: TextMotion) -> TextDraw {
12954 let mut text_style = TextStyle::default();
12955 text_style.paragraph_style.text_motion = Some(text_motion);
12956 TextDraw {
12957 node_id: 42,
12958 rect,
12959 snap_anchor: None,
12960 translated_content_context: false,
12961 text: Arc::new(AnnotatedString::new("stable markdown row".to_string()).render_string()),
12962 color: Color::WHITE,
12963 text_style,
12964 font_size: 14.0,
12965 scale: 1.0,
12966 layout_options: TextLayoutOptions::default(),
12967 z_index: 0,
12968 clip: None,
12969 }
12970 }
12971
12972 #[test]
12973 fn static_text_image_cache_key_ignores_absolute_scroll_position() {
12974 let base = test_text_draw(
12975 Rect {
12976 x: 12.25,
12977 y: 40.75,
12978 width: 220.0,
12979 height: 24.0,
12980 },
12981 TextMotion::Static,
12982 );
12983 let scrolled = test_text_draw(
12984 Rect {
12985 x: 12.75,
12986 y: -318.5,
12987 width: 220.0,
12988 height: 24.0,
12989 },
12990 TextMotion::Static,
12991 );
12992
12993 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, true);
12994 let scrolled_key = GpuRenderer::text_image_cache_key(&scrolled, scrolled.rect, 1.0, true);
12995
12996 assert_eq!(
12997 base_key, scrolled_key,
12998 "scrolling static text must reuse the same raster cache entry"
12999 );
13000 }
13001
13002 #[test]
13003 fn static_text_glyph_run_cache_key_ignores_absolute_scroll_position() {
13004 let base = test_text_draw(
13005 Rect {
13006 x: 12.25,
13007 y: 40.75,
13008 width: 220.0,
13009 height: 24.0,
13010 },
13011 TextMotion::Static,
13012 );
13013 let scrolled = test_text_draw(
13014 Rect {
13015 x: 12.75,
13016 y: -318.5,
13017 width: 220.0,
13018 height: 24.0,
13019 },
13020 TextMotion::Static,
13021 );
13022
13023 let base_key = GpuRenderer::text_glyph_run_cache_key(&base, base.rect, 1.0, true);
13024 let scrolled_key =
13025 GpuRenderer::text_glyph_run_cache_key(&scrolled, scrolled.rect, 1.0, true);
13026
13027 assert_eq!(
13028 base_key, scrolled_key,
13029 "scrolling static text must reuse the same retained glyph run"
13030 );
13031 }
13032
13033 #[test]
13034 fn static_multiline_text_glyph_source_keeps_full_text_when_image_source_slices() {
13035 let rect = Rect {
13036 x: 8.0,
13037 y: 100.0,
13038 width: 240.0,
13039 height: 1_000.0,
13040 };
13041 let mut draw = test_text_draw(rect, TextMotion::Static);
13042 let lines = (0..100)
13043 .map(|line| format!("line-{line:03}"))
13044 .collect::<Vec<_>>()
13045 .join("\n");
13046 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
13047
13048 let raster_rect = Rect {
13049 x: 16.0,
13050 y: 200.0,
13051 width: 480.0,
13052 height: 2_000.0,
13053 };
13054 let clipped = clipped_text_raster_source(
13055 &draw,
13056 rect,
13057 raster_rect,
13058 Some(Rect {
13059 x: 0.0,
13060 y: 610.0,
13061 width: 800.0,
13062 height: 40.0,
13063 }),
13064 2.0,
13065 true,
13066 );
13067 let glyph = text_glyph_raster_source(&draw, raster_rect);
13068
13069 assert!(
13070 matches!(clipped.draw, Cow::Owned(_)),
13071 "the image source should still slice large clipped multiline text"
13072 );
13073 assert!(
13074 matches!(glyph.draw, Cow::Borrowed(_)),
13075 "the glyph source must keep a stable full-text run key while scrolling"
13076 );
13077
13078 let clipped_key = GpuRenderer::text_glyph_run_cache_key(
13079 clipped.draw.as_ref(),
13080 clipped.raster_rect,
13081 2.0,
13082 true,
13083 );
13084 let glyph_key = GpuRenderer::text_glyph_run_cache_key(
13085 glyph.draw.as_ref(),
13086 glyph.raster_rect,
13087 2.0,
13088 true,
13089 );
13090
13091 assert_ne!(
13092 clipped_key, glyph_key,
13093 "image slicing must not force glyph rendering onto per-scroll line-window cache keys"
13094 );
13095 }
13096
13097 #[cfg(not(target_arch = "wasm32"))]
13098 #[test]
13099 fn retained_glyph_viewport_offsets_relative_vertices_by_source_origin() {
13100 let viewport = ViewportUniformParams {
13101 width: 800,
13102 height: 600,
13103 offset: [10.0, 20.0],
13104 };
13105 let source = Rect {
13106 x: 40.0,
13107 y: 90.0,
13108 width: 120.0,
13109 height: 48.0,
13110 };
13111
13112 let retained = GpuRenderer::retained_glyph_viewport(viewport, source);
13113
13114 assert_eq!(retained.width, viewport.width);
13115 assert_eq!(retained.height, viewport.height);
13116 assert_eq!(retained.offset, [-30.0, -70.0]);
13117 }
13118
13119 #[cfg(not(target_arch = "wasm32"))]
13120 #[test]
13121 fn tiny_text_glyph_runs_stay_in_shared_uploads() {
13122 assert!(
13123 !should_use_retained_text_glyph_run(8, None),
13124 "tiny labels must stay in the shared fused batch"
13125 );
13126 }
13127
13128 #[cfg(not(target_arch = "wasm32"))]
13129 #[test]
13130 fn line_sized_text_glyph_runs_stay_in_shared_uploads() {
13131 assert!(
13132 !should_use_retained_text_glyph_run(64, None),
13133 "Markdown scroll frames contain many line-sized text runs; retaining each one creates per-run buffer binds instead of one shared glyph batch"
13134 );
13135 }
13136
13137 #[cfg(not(target_arch = "wasm32"))]
13138 #[test]
13139 fn large_clipped_text_glyph_runs_stay_in_shared_uploads() {
13140 assert!(
13141 !should_use_retained_text_glyph_run(
13142 MIN_RETAINED_TEXT_GLYPH_QUADS.saturating_mul(2),
13143 Some(Rect {
13144 x: 0.0,
13145 y: 0.0,
13146 width: 200.0,
13147 height: 100.0,
13148 }),
13149 ),
13150 "clipped lazy-list text must not draw a full retained run outside the viewport"
13151 );
13152 }
13153
13154 #[test]
13155 fn normal_text_glyph_draw_skips_offscreen_prewarm_candidates() {
13156 assert_eq!(
13157 text_glyph_draw_action(false, true, false),
13158 TextGlyphDrawAction::Skip,
13159 "normal draw traversal must not prepare offscreen text"
13160 );
13161 }
13162
13163 #[test]
13164 fn bounded_text_glyph_prewarm_admits_offscreen_candidates() {
13165 assert_eq!(
13166 text_glyph_draw_action(false, true, true),
13167 TextGlyphDrawAction::PrewarmOffscreen,
13168 "only the bounded prewarm path may prepare offscreen text"
13169 );
13170 }
13171
13172 #[test]
13173 fn visible_text_glyph_draws_are_always_admitted() {
13174 assert_eq!(
13175 text_glyph_draw_action(true, false, false),
13176 TextGlyphDrawAction::DrawVisible
13177 );
13178 assert_eq!(
13179 text_glyph_draw_action(true, true, true),
13180 TextGlyphDrawAction::DrawVisible
13181 );
13182 }
13183
13184 #[cfg(not(target_arch = "wasm32"))]
13185 #[test]
13186 fn offscreen_text_prewarm_skips_large_uncached_text_runs() {
13187 assert!(
13188 !offscreen_text_glyph_prewarm_work_is_bounded(
13189 None,
13190 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS + 1,
13191 ),
13192 "offscreen prewarm must not collect large uncached text runs in an input frame"
13193 );
13194 }
13195
13196 #[cfg(not(target_arch = "wasm32"))]
13197 #[test]
13198 fn offscreen_text_prewarm_admits_small_uncached_text_runs() {
13199 assert!(
13200 offscreen_text_glyph_prewarm_work_is_bounded(
13201 None,
13202 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
13203 ),
13204 "small labels can be warmed without risking a frame-budget spike"
13205 );
13206 }
13207
13208 #[cfg(not(target_arch = "wasm32"))]
13209 #[test]
13210 fn offscreen_text_prewarm_skips_large_cached_runs_without_quads() {
13211 assert!(
13212 !offscreen_text_glyph_prewarm_work_is_bounded(
13213 Some(MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS + 1),
13214 0,
13215 ),
13216 "cached glyph placements can still be too large to prepare during input frames"
13217 );
13218 }
13219
13220 #[cfg(not(target_arch = "wasm32"))]
13221 #[test]
13222 fn offscreen_text_prewarm_stops_after_candidate_budget() {
13223 assert!(
13224 offscreen_text_glyph_prewarm_budget_exhausted(
13225 Instant::now(),
13226 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES,
13227 ),
13228 "prewarm must be bounded by candidate count even when each candidate is cheap"
13229 );
13230 }
13231
13232 #[test]
13233 fn clipped_cached_glyph_quads_are_filtered_to_viewport() {
13234 fn quad(y: i32) -> CachedTextGlyphQuad {
13235 CachedTextGlyphQuad {
13236 x: 8,
13237 y,
13238 width: 20,
13239 height: 10,
13240 color: (1.0, 1.0, 1.0, 1.0),
13241 uv: ImageUvRect {
13242 min: [0.0, 0.0],
13243 max: [1.0, 1.0],
13244 sample_bounds: [0.0, 0.0, 1.0, 1.0],
13245 },
13246 }
13247 }
13248
13249 let source = Rect {
13250 x: 0.0,
13251 y: 0.0,
13252 width: 320.0,
13253 height: 400.0,
13254 };
13255 let clip = Some(Rect {
13256 x: 0.0,
13257 y: 0.0,
13258 width: 320.0,
13259 height: 80.0,
13260 });
13261 let viewport = ViewportUniformParams {
13262 width: 320,
13263 height: 80,
13264 offset: [0.0, 0.0],
13265 };
13266
13267 assert!(cached_text_glyph_quad_is_visible_in_viewport(
13268 source,
13269 &quad(40),
13270 clip,
13271 viewport,
13272 1.0,
13273 ));
13274 assert!(
13275 !cached_text_glyph_quad_is_visible_in_viewport(source, &quad(140), clip, viewport, 1.0,),
13276 "glyphs outside the effective clip should not enter the frame command stream"
13277 );
13278 }
13279
13280 #[test]
13281 fn small_scene_range_cache_miss_observes_first_render() {
13282 let key = LayerRasterCacheKey::scene_range(
13283 0xCACE,
13284 Rect {
13285 x: 0.0,
13286 y: 0.0,
13287 width: 120.0,
13288 height: 80.0,
13289 },
13290 (120, 80),
13291 ScaleBucket::from_scale(1.0),
13292 );
13293
13294 assert!(
13295 !first_cache_miss_admission(&key),
13296 "a small scene-range miss should render directly first instead of materializing a tiny one-frame retained target"
13297 );
13298 assert!(
13299 repeated_cache_miss_admission(&key),
13300 "a repeated small scene-range miss is stable enough to materialize into the retained cache"
13301 );
13302 }
13303
13304 #[test]
13305 fn large_scene_range_cache_miss_requires_repeated_stable_key() {
13306 let key = LayerRasterCacheKey::scene_range(
13307 0xCACE,
13308 Rect {
13309 x: 0.0,
13310 y: 0.0,
13311 width: 1200.0,
13312 height: 900.0,
13313 },
13314 (1200, 900),
13315 ScaleBucket::from_scale(1.0),
13316 );
13317
13318 assert!(
13319 !first_cache_miss_admission(&key),
13320 "a large first scene-range miss should render directly instead of materializing a multi-MB one-frame cache entry"
13321 );
13322 assert!(
13323 repeated_cache_miss_admission(&key),
13324 "a repeated scene-range miss is stable enough to materialize into the retained cache"
13325 );
13326 }
13327
13328 #[test]
13329 fn renderer_warmup_frame_is_requested_for_cache_miss_stats_only() {
13330 let stats = gpu_stats::FrameStats::default();
13331 let mut snapshot = stats.snapshot();
13332 assert!(
13333 !frame_stats_need_warmup_frame(&snapshot),
13334 "a clean frame must not keep a static scene redrawing"
13335 );
13336
13337 snapshot.layer_cache_misses = 1;
13338 assert!(frame_stats_need_warmup_frame(&snapshot));
13339 snapshot.layer_cache_misses = 0;
13340
13341 snapshot.shadow_shape_cache_misses = 1;
13342 assert!(frame_stats_need_warmup_frame(&snapshot));
13343 snapshot.shadow_shape_cache_misses = 0;
13344
13345 snapshot.text_image_cache_misses = 1;
13346 assert!(frame_stats_need_warmup_frame(&snapshot));
13347 snapshot.text_image_cache_misses = 0;
13348
13349 snapshot.text_glyph_atlas_misses = 1;
13350 assert!(frame_stats_need_warmup_frame(&snapshot));
13351 }
13352
13353 #[test]
13354 fn renderer_warmup_budget_is_consumed_by_a_repeated_cache_miss() {
13355 let stats = gpu_stats::FrameStats::default();
13356 let mut snapshot = stats.snapshot();
13357 snapshot.layer_cache_misses = 1;
13358 let mut pending_frames = 0;
13359
13360 update_frame_warmup_budget(&mut pending_frames, &snapshot);
13361 assert_eq!(pending_frames, CACHE_MISS_WARMUP_FRAMES);
13362
13363 update_frame_warmup_budget(&mut pending_frames, &snapshot);
13364 assert_eq!(
13365 pending_frames, 0,
13366 "a cache miss during the warmup frame must not replenish its budget"
13367 );
13368 }
13369
13370 #[test]
13371 fn non_scene_layer_surface_cache_miss_admits_first_render() {
13372 let key = LayerRasterCacheKey::new(
13373 Some(77),
13374 0xC0FFEE,
13375 0,
13376 Rect {
13377 x: 0.0,
13378 y: 0.0,
13379 width: 120.0,
13380 height: 80.0,
13381 },
13382 (120, 80),
13383 ScaleBucket::from_scale(1.0),
13384 );
13385
13386 assert!(
13387 first_cache_miss_admission(&key),
13388 "ordinary retained layer surfaces should still cache on first miss"
13389 );
13390 }
13391
13392 #[test]
13393 fn text_image_cache_key_is_content_addressed_not_node_addressed() {
13394 let first = test_text_draw(
13395 Rect {
13396 x: 12.25,
13397 y: 40.75,
13398 width: 220.0,
13399 height: 24.0,
13400 },
13401 TextMotion::Static,
13402 );
13403 let mut second = first.clone();
13404 second.node_id = first.node_id + 1;
13405
13406 let first_key = GpuRenderer::text_image_cache_key(&first, first.rect, 1.0, true);
13407 let second_key = GpuRenderer::text_image_cache_key(&second, second.rect, 1.0, true);
13408
13409 assert_eq!(
13410 first_key, second_key,
13411 "text raster cache keys must be based on rendered pixels, not node identity"
13412 );
13413 }
13414
13415 #[test]
13416 fn animated_text_image_cache_key_keeps_fractional_phase_only() {
13417 let base = test_text_draw(
13418 Rect {
13419 x: 12.25,
13420 y: 40.75,
13421 width: 220.0,
13422 height: 24.0,
13423 },
13424 TextMotion::Animated,
13425 );
13426 let integer_translated = test_text_draw(
13427 Rect {
13428 x: 44.25,
13429 y: 88.75,
13430 width: 220.0,
13431 height: 24.0,
13432 },
13433 TextMotion::Animated,
13434 );
13435 let phase_shifted = test_text_draw(
13436 Rect {
13437 x: 44.5,
13438 y: 88.75,
13439 width: 220.0,
13440 height: 24.0,
13441 },
13442 TextMotion::Animated,
13443 );
13444
13445 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, false);
13446 let translated_key = GpuRenderer::text_image_cache_key(
13447 &integer_translated,
13448 integer_translated.rect,
13449 1.0,
13450 false,
13451 );
13452 let phase_shifted_key =
13453 GpuRenderer::text_image_cache_key(&phase_shifted, phase_shifted.rect, 1.0, false);
13454
13455 assert_eq!(
13456 base_key, translated_key,
13457 "integer translation should not invalidate animated text raster cache entries"
13458 );
13459 assert_ne!(
13460 base_key, phase_shifted_key,
13461 "fractional phase affects animated text rasterization and must stay in the key"
13462 );
13463 }
13464
13465 #[test]
13466 fn animated_translated_text_raster_geometry_applies_snap_anchor() {
13467 let mut base = test_text_draw(
13468 Rect {
13469 x: 14.25,
13470 y: 16.50,
13471 width: 220.0,
13472 height: 24.0,
13473 },
13474 TextMotion::Animated,
13475 );
13476 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.50)));
13477
13478 let mut scrolled = test_text_draw(
13479 Rect {
13480 x: 14.25,
13481 y: 15.80,
13482 width: 220.0,
13483 height: 24.0,
13484 },
13485 TextMotion::Animated,
13486 );
13487 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 15.80)));
13488
13489 let (base_logical, base_raster, _, _, base_static) =
13490 text_raster_geometry_for_draw(&base, 1.0).expect("base text geometry");
13491 let (scrolled_logical, scrolled_raster, _, _, scrolled_static) =
13492 text_raster_geometry_for_draw(&scrolled, 1.0).expect("scrolled text geometry");
13493
13494 assert!(!base_static);
13495 assert!(!scrolled_static);
13496 assert!((base_logical.x - 14.0).abs() < f32::EPSILON);
13497 assert!((base_logical.y - 17.0).abs() < f32::EPSILON);
13498 assert!((scrolled_logical.x - 14.0).abs() < f32::EPSILON);
13499 assert!((scrolled_logical.y - 16.0).abs() < f32::EPSILON);
13500 assert_eq!(base_raster.x.fract(), 0.0);
13501 assert_eq!(base_raster.y.fract(), 0.0);
13502 assert_eq!(scrolled_raster.x.fract(), 0.0);
13503 assert_eq!(scrolled_raster.y.fract(), 0.0);
13504
13505 let base_key = GpuRenderer::text_image_cache_key(&base, base_raster, 1.0, false);
13506 let scrolled_key =
13507 GpuRenderer::text_image_cache_key(&scrolled, scrolled_raster, 1.0, false);
13508 assert_eq!(
13509 base_key, scrolled_key,
13510 "translated animated text should keep a stable raster phase while scrolling"
13511 );
13512 }
13513
13514 #[test]
13515 fn translated_static_text_moves_one_device_pixel_at_half_pixel_phase() {
13516 let root_scale = 1.25;
13517 let mut base = test_text_draw(
13518 Rect {
13519 x: 14.0,
13520 y: 276.0,
13521 width: 220.0,
13522 height: 24.0,
13523 },
13524 TextMotion::Static,
13525 );
13526 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.600_006)));
13527
13528 let mut scrolled = test_text_draw(
13529 Rect {
13530 x: 14.0,
13531 y: 275.2,
13532 width: 220.0,
13533 height: 24.0,
13534 },
13535 TextMotion::Static,
13536 );
13537 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 126.799_99)));
13538
13539 let (_, base_raster, _, _, _) =
13540 text_raster_geometry_for_draw(&base, root_scale).expect("base text geometry");
13541 let (_, scrolled_raster, _, _, _) =
13542 text_raster_geometry_for_draw(&scrolled, root_scale).expect("scrolled text geometry");
13543
13544 assert_eq!(
13545 base_raster.y - scrolled_raster.y,
13546 1.0,
13547 "one physical pixel of rigid scrolling must move static text by one raster pixel"
13548 );
13549 }
13550
13551 #[test]
13552 fn translated_text_snap_does_not_move_its_fixed_ancestor_clip() {
13553 let root_scale = 1.25;
13554 let fixed_clip = Rect {
13555 x: 8.0,
13556 y: 20.0,
13557 width: 300.0,
13558 height: 680.0,
13559 };
13560 let mut draw = test_text_draw(
13561 Rect {
13562 x: 14.0,
13563 y: 276.0,
13564 width: 220.0,
13565 height: 24.0,
13566 },
13567 TextMotion::Static,
13568 );
13569 draw.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.4)));
13570 draw.clip = Some(fixed_clip);
13571
13572 let (_, _, clip, _, _) =
13573 text_raster_geometry_for_draw(&draw, root_scale).expect("clipped text geometry");
13574
13575 assert_eq!(
13576 clip,
13577 Some(fixed_clip),
13578 "content pixel snapping must not translate a fixed ancestor clip"
13579 );
13580 }
13581
13582 #[test]
13583 fn clipped_static_multiline_text_raster_source_limits_visible_line_window() {
13584 let rect = Rect {
13585 x: 8.0,
13586 y: 100.0,
13587 width: 240.0,
13588 height: 1_000.0,
13589 };
13590 let mut draw = test_text_draw(rect, TextMotion::Static);
13591 let lines = (0..100)
13592 .map(|line| format!("line-{line:03}"))
13593 .collect::<Vec<_>>()
13594 .join("\n");
13595 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
13596
13597 let raster_rect = Rect {
13598 x: 16.0,
13599 y: 200.0,
13600 width: 480.0,
13601 height: 2_000.0,
13602 };
13603 let source = clipped_text_raster_source(
13604 &draw,
13605 rect,
13606 raster_rect,
13607 Some(Rect {
13608 x: 0.0,
13609 y: 610.0,
13610 width: 800.0,
13611 height: 40.0,
13612 }),
13613 2.0,
13614 true,
13615 );
13616
13617 let Cow::Owned(sliced_draw) = source.draw else {
13618 panic!("clipped static multiline text should rasterize only the visible line window");
13619 };
13620 let sliced_text = sliced_draw.text.text.as_str();
13621 assert!(sliced_text.contains("line-050"));
13622 assert!(sliced_text.contains("line-055"));
13623 assert!(!sliced_text.contains("line-000"));
13624 assert!(!sliced_text.contains("line-099"));
13625 assert_eq!(source.raster_rect.x, raster_rect.x);
13626 assert!(source.raster_rect.y > raster_rect.y);
13627 assert!(source.raster_rect.height < raster_rect.height);
13628 }
13629
13630 #[test]
13631 fn clipped_static_multiline_text_raster_source_slices_short_multiline_text() {
13632 let rect = Rect {
13633 x: 8.0,
13634 y: 100.0,
13635 width: 240.0,
13636 height: 320.0,
13637 };
13638 let mut draw = test_text_draw(rect, TextMotion::Static);
13639 let lines = (0..24)
13640 .map(|line| format!("code-line-{line:02}"))
13641 .collect::<Vec<_>>()
13642 .join("\n");
13643 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
13644
13645 let raster_rect = Rect {
13646 x: 16.0,
13647 y: 200.0,
13648 width: 480.0,
13649 height: 640.0,
13650 };
13651 let source = clipped_text_raster_source(
13652 &draw,
13653 rect,
13654 raster_rect,
13655 Some(Rect {
13656 x: 0.0,
13657 y: 190.0,
13658 width: 800.0,
13659 height: 120.0,
13660 }),
13661 2.0,
13662 true,
13663 );
13664
13665 let Cow::Owned(sliced_draw) = source.draw else {
13666 panic!("clipped multiline text should rasterize only the visible line window");
13667 };
13668 assert!(sliced_draw.text.text.as_str().contains("code-line-06"));
13669 assert!(!sliced_draw.text.text.as_str().contains("code-line-00"));
13670 assert!(!sliced_draw.text.text.as_str().contains("code-line-23"));
13671 assert_eq!(source.raster_rect.x, raster_rect.x);
13672 assert!(source.raster_rect.y > raster_rect.y);
13673 assert!(source.raster_rect.height < raster_rect.height);
13674 }
13675
13676 #[test]
13677 fn text_line_index_cache_reuses_retained_index_for_same_text_instance() {
13678 let mut cache = TextLineIndexCache::new(4);
13679 let text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
13680
13681 let first = cache.line_starts(&text);
13682 let second = cache.line_starts(&text);
13683
13684 assert_eq!(first.as_ref(), &[0, 2, 4]);
13685 assert!(
13686 Rc::ptr_eq(&first, &second),
13687 "retained text should not rebuild its line index on every clipped frame"
13688 );
13689 }
13690
13691 #[test]
13692 fn text_line_index_cache_is_retained_text_instance_local() {
13693 let mut cache = TextLineIndexCache::new(4);
13694 let first_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
13695 let second_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
13696
13697 let first = cache.line_starts(&first_text);
13698 let second = cache.line_starts(&second_text);
13699
13700 assert_eq!(first.as_ref(), second.as_ref());
13701 assert!(
13702 !Rc::ptr_eq(&first, &second),
13703 "line index lookup should not hash large text contents to find unrelated retained nodes"
13704 );
13705 }
13706
13707 #[test]
13708 fn device_pixel_bounds_for_rect_snaps_origin_and_extents() {
13709 let bounds = device_pixel_bounds_for_rect(
13710 Rect {
13711 x: 10.25,
13712 y: 14.6,
13713 width: 20.1,
13714 height: 9.2,
13715 },
13716 200,
13717 120,
13718 2.0,
13719 )
13720 .expect("rect should intersect the viewport");
13721
13722 assert_eq!(
13723 bounds,
13724 DevicePixelBounds {
13725 x: 20.0,
13726 y: 29.0,
13727 width: 41,
13728 height: 19,
13729 }
13730 );
13731 }
13732
13733 #[test]
13734 fn visible_layer_rect_intersects_clip_and_viewport() {
13735 let visible = visible_layer_rect(
13736 Rect {
13737 x: -10.0,
13738 y: 5.0,
13739 width: 80.0,
13740 height: 40.0,
13741 },
13742 Some(Rect {
13743 x: 4.0,
13744 y: 8.0,
13745 width: 20.0,
13746 height: 50.0,
13747 }),
13748 2.0,
13749 60,
13750 40,
13751 )
13752 .expect("visible rect");
13753
13754 assert_eq!(
13755 visible,
13756 Rect {
13757 x: 4.0,
13758 y: 8.0,
13759 width: 20.0,
13760 height: 12.0,
13761 }
13762 );
13763 }
13764
13765 #[test]
13766 fn clamp_effect_surface_scale_caps_large_surfaces_but_keeps_base_scale() {
13767 let clamped = clamp_effect_surface_scale(
13768 Rect {
13769 x: 0.0,
13770 y: 0.0,
13771 width: 1200.0,
13772 height: 900.0,
13773 },
13774 1.0,
13775 8.0,
13776 16_384,
13777 );
13778
13779 assert!(
13780 clamped < 8.0,
13781 "large translated effect layers must be capped to avoid OOM, got {clamped}"
13782 );
13783 assert!(
13784 clamped >= 1.0,
13785 "effect surfaces must not fall below destination resolution, got {clamped}"
13786 );
13787 }
13788
13789 #[test]
13790 fn clamp_effect_surface_scale_keeps_decorated_text_capture_scale() {
13791 let clamped = clamp_effect_surface_scale(
13792 Rect {
13793 x: 0.0,
13794 y: 0.0,
13795 width: 446.0,
13796 height: 44.0,
13797 },
13798 1.0,
13799 9.0,
13800 16_384,
13801 );
13802
13803 assert_eq!(
13804 clamped, 9.0,
13805 "decorated text motion-stable captures must keep full scale"
13806 );
13807 }
13808
13809 fn backdrop_layer(z_index: usize) -> BackdropLayer {
13810 BackdropLayer {
13811 node_id: Some(700 + z_index),
13812 rect: Rect {
13813 x: 0.0,
13814 y: 0.0,
13815 width: 10.0,
13816 height: 10.0,
13817 },
13818 clip: None,
13819 snap_anchor: None,
13820 effect: RenderEffect::blur(2.0),
13821 z_index,
13822 }
13823 }
13824
13825 fn test_shape(z_index: usize, blend_mode: BlendMode) -> DrawShape {
13826 DrawShape {
13827 rect: Rect {
13828 x: 0.0,
13829 y: 0.0,
13830 width: 8.0,
13831 height: 8.0,
13832 },
13833 local_rect: Rect {
13834 x: 0.0,
13835 y: 0.0,
13836 width: 8.0,
13837 height: 8.0,
13838 },
13839 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
13840 snap_anchor: None,
13841 brush: Brush::solid(Color::BLACK),
13842 shape: None,
13843 stroke: None,
13844 arc: None,
13845 z_index,
13846 clip: None,
13847 blend_mode,
13848 motion_context_animated: false,
13849 }
13850 }
13851
13852 #[test]
13853 fn shape_shadow_content_hash_ignores_viewport_translation() {
13854 fn translate_shape(shape: &DrawShape, dx: f32, dy: f32) -> DrawShape {
13855 let mut translated = shape.clone();
13856 translated.rect.x += dx;
13857 translated.rect.y += dy;
13858 translated.local_rect.x += dx;
13859 translated.local_rect.y += dy;
13860 for point in &mut translated.quad {
13861 point[0] += dx;
13862 point[1] += dy;
13863 }
13864 translated.snap_anchor = translated.snap_anchor.map(|anchor| {
13865 SnapAnchor::rigid(Point::new(anchor.origin.x + dx, anchor.origin.y + dy))
13866 });
13867 translated.clip = translated.clip.map(|mut clip| {
13868 clip.x += dx;
13869 clip.y += dy;
13870 clip
13871 });
13872 translated
13873 }
13874
13875 let mut first = test_shape(1, BlendMode::SrcOver);
13876 first.rect = Rect {
13877 x: 10.0,
13878 y: 20.0,
13879 width: 80.0,
13880 height: 40.0,
13881 };
13882 first.local_rect = first.rect;
13883 first.quad = [[10.0, 20.0], [90.0, 20.0], [10.0, 60.0], [90.0, 60.0]];
13884 first.snap_anchor = Some(SnapAnchor::rigid(Point::new(7.0, 11.0)));
13885 first.shape = Some(RoundedCornerShape::uniform(8.0));
13886 first.clip = Some(Rect {
13887 x: 8.0,
13888 y: 18.0,
13889 width: 86.0,
13890 height: 44.0,
13891 });
13892 let mut cutout = test_shape(2, BlendMode::DstOut);
13893 cutout.rect = Rect {
13894 x: 18.0,
13895 y: 26.0,
13896 width: 62.0,
13897 height: 22.0,
13898 };
13899 cutout.local_rect = cutout.rect;
13900 cutout.quad = [[18.0, 26.0], [80.0, 26.0], [18.0, 48.0], [80.0, 48.0]];
13901 cutout.shape = Some(RoundedCornerShape::uniform(4.0));
13902
13903 let dx = 37.0;
13904 let dy = -11.5;
13905 let translated = translate_shape(&first, dx, dy);
13906 let translated_cutout = translate_shape(&cutout, dx, dy);
13907
13908 let root_scale = 1.25;
13909 let first_shapes = vec![
13910 (first.clone(), BlendMode::SrcOver),
13911 (cutout, BlendMode::DstOut),
13912 ];
13913 let translated_shapes = vec![
13914 (translated.clone(), BlendMode::SrcOver),
13915 (translated_cutout, BlendMode::DstOut),
13916 ];
13917
13918 let first_hash = shape_shadow_content_hash(&first_shapes, root_scale);
13919 let translated_hash = shape_shadow_content_hash(&translated_shapes, root_scale);
13920
13921 assert_eq!(first_hash, translated_hash);
13922
13923 let mut changed_shapes = translated_shapes;
13924 changed_shapes[0].0.rect.width += 1.0;
13925 let changed_hash = shape_shadow_content_hash(&changed_shapes, root_scale);
13926
13927 assert_ne!(first_hash, changed_hash);
13928 }
13929
13930 #[test]
13931 fn shape_shadow_content_hash_is_stable_under_fractional_scale_scroll() {
13932 fn shadow_shapes_at(y: f32) -> Vec<(DrawShape, BlendMode)> {
13938 let mut shape = test_shape(1, BlendMode::SrcOver);
13939 shape.rect = Rect {
13940 x: 24.0,
13941 y,
13942 width: 180.0,
13943 height: 90.0,
13944 };
13945 shape.local_rect = shape.rect;
13946 shape.quad = crate::rect_to_quad(shape.rect);
13947 shape.shape = Some(RoundedCornerShape::uniform(14.0));
13948 vec![(shape, BlendMode::SrcOver)]
13949 }
13950
13951 let root_scale = 130.0f32 / 96.0;
13952 let blur_radius = 18.0f32;
13953 let pixel_radius = blur_radius * root_scale;
13954
13955 let key_at = |y: f32| {
13956 let shapes = shadow_shapes_at(y);
13957 let plan =
13958 shape_shadow_surface_plan(&shapes, None, blur_radius, 1600, 1600, root_scale, 8192)
13959 .expect("surface plan");
13960 shape_shadow_surface_cache_key(
13961 &shapes,
13962 plan.source_device_bounds,
13963 pixel_radius,
13964 root_scale,
13965 )
13966 .expect("cache key")
13967 };
13968
13969 let base = key_at(640.0);
13974 for step in 1..=12 {
13975 let scrolled = key_at(640.0 - step as f32 * 4.0);
13976 assert_eq!(
13977 base, scrolled,
13978 "scrolled shadow cache key must stay stable at fractional scale (step {step})"
13979 );
13980 }
13981 }
13982
13983 #[test]
13984 fn shape_shadow_cache_key_uses_unclipped_source_bounds_for_scrolled_clip() {
13985 fn translated_card_shadow(y: f32) -> Vec<(DrawShape, BlendMode)> {
13986 let mut shape = test_shape(1, BlendMode::SrcOver);
13987 shape.rect = Rect {
13988 x: 24.0,
13989 y,
13990 width: 280.0,
13991 height: 120.0,
13992 };
13993 shape.local_rect = shape.rect;
13994 shape.quad = [[24.0, y], [304.0, y], [24.0, y + 120.0], [304.0, y + 120.0]];
13995 shape.shape = Some(RoundedCornerShape::uniform(18.0));
13996 vec![(shape, BlendMode::SrcOver)]
13997 }
13998
13999 let root_scale = 1.0;
14000 let blur_radius = 18.0;
14001 let viewport_clip = Rect {
14002 x: 0.0,
14003 y: 96.0,
14004 width: 360.0,
14005 height: 720.0,
14006 };
14007 let key_for = |y: f32| {
14008 let shapes = translated_card_shadow(y);
14009 let plan = shape_shadow_surface_plan(
14010 &shapes,
14011 Some(viewport_clip),
14012 blur_radius,
14013 360,
14014 900,
14015 root_scale,
14016 4096,
14017 )
14018 .expect("surface plan");
14019 shape_shadow_surface_cache_key(
14020 &shapes,
14021 plan.source_device_bounds,
14022 plan.pixel_radius,
14023 root_scale,
14024 )
14025 .expect("cache key")
14026 };
14027
14028 assert_eq!(key_for(740.0), key_for(756.0));
14031 }
14032
14033 #[test]
14034 fn shape_visibility_uses_nonzero_viewport_offset_for_cropped_offscreen() {
14035 let mut shape = test_shape(1, BlendMode::SrcOver);
14036 shape.rect = Rect {
14037 x: 24.0,
14038 y: 740.0,
14039 width: 280.0,
14040 height: 120.0,
14041 };
14042 shape.local_rect = shape.rect;
14043 shape.quad = [[24.0, 740.0], [304.0, 740.0], [24.0, 860.0], [304.0, 860.0]];
14044 let viewport = ViewportUniformParams {
14045 width: 316,
14046 height: 228,
14047 offset: [6.0, 686.0],
14048 };
14049
14050 assert!(shape_draw_is_visible_in_viewport(&shape, viewport, 1.0));
14051 }
14052
14053 #[test]
14054 fn text_prewarm_uses_nonzero_viewport_offset_for_cropped_offscreen() {
14055 let viewport = ViewportUniformParams {
14056 width: 316,
14057 height: 228,
14058 offset: [6.0, 686.0],
14059 };
14060 let text_rect = Rect {
14061 x: 24.0,
14062 y: 740.0,
14063 width: 280.0,
14064 height: 40.0,
14065 };
14066
14067 assert!(text_draw_is_visible_in_viewport(
14068 text_rect, None, viewport, 1.0
14069 ));
14070 assert!(text_draw_should_prewarm_in_viewport(
14071 text_rect, None, viewport, 1.0
14072 ));
14073 }
14074
14075 fn test_shadow_draw(shapes: Vec<(DrawShape, BlendMode)>) -> ShadowDraw {
14076 ShadowDraw {
14077 shapes,
14078 texts: vec![],
14079 blur_radius: 8.0,
14080 clip: None,
14081 z_index: 0,
14082 }
14083 }
14084
14085 fn test_image(z_index: usize, blend_mode: BlendMode) -> ImageDraw {
14086 ImageDraw {
14087 rect: Rect {
14088 x: 0.0,
14089 y: 0.0,
14090 width: 8.0,
14091 height: 8.0,
14092 },
14093 local_rect: Rect {
14094 x: 0.0,
14095 y: 0.0,
14096 width: 8.0,
14097 height: 8.0,
14098 },
14099 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
14100 snap_anchor: None,
14101 image: ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image"),
14102 alpha: 1.0,
14103 color_filter: None,
14104 sampling: ImageSampling::Nearest,
14105 z_index,
14106 clip: None,
14107 blend_mode,
14108 src_rect: None,
14109 motion_context_animated: false,
14110 }
14111 }
14112
14113 #[test]
14114 fn image_sampler_descriptors_match_requested_sampling() {
14115 let nearest = image_sampler_descriptor(ImageSampling::Nearest);
14116 assert_eq!(nearest.mag_filter, wgpu::FilterMode::Nearest);
14117 assert_eq!(nearest.min_filter, wgpu::FilterMode::Nearest);
14118
14119 let linear = image_sampler_descriptor(ImageSampling::Linear);
14120 assert_eq!(linear.mag_filter, wgpu::FilterMode::Linear);
14121 assert_eq!(linear.min_filter, wgpu::FilterMode::Linear);
14122 }
14123
14124 #[test]
14125 fn image_uv_rect_clamps_source_rect_to_texel_centers() {
14126 let image = ImageBitmap::from_rgba8(24, 16, vec![0; 24 * 16 * 4]).expect("image");
14127 let uv = image_uv_rect(
14128 &image,
14129 Some(Rect {
14130 x: 0.0,
14131 y: 0.0,
14132 width: 16.0,
14133 height: 16.0,
14134 }),
14135 )
14136 .expect("uv rect");
14137
14138 assert_eq!(uv.min, [0.0, 0.0]);
14139 assert_eq!(uv.max, [16.0 / 24.0, 1.0]);
14140 assert_eq!(
14141 uv.sample_bounds,
14142 [0.5 / 24.0, 0.5 / 16.0, 15.5 / 24.0, 15.5 / 16.0]
14143 );
14144 }
14145
14146 #[test]
14147 fn image_uv_rect_keeps_full_image_unclamped() {
14148 let image = ImageBitmap::from_rgba8(2, 2, vec![0; 16]).expect("image");
14149 let uv = image_uv_rect(&image, None).expect("uv rect");
14150
14151 assert_eq!(uv.min, [0.0, 0.0]);
14152 assert_eq!(uv.max, [1.0, 1.0]);
14153 assert_eq!(uv.sample_bounds, [0.0, 0.0, 1.0, 1.0]);
14154 }
14155
14156 fn test_text(z_index: usize) -> TextDraw {
14157 TextDraw {
14158 node_id: 0,
14159 rect: Rect {
14160 x: 0.0,
14161 y: 0.0,
14162 width: 8.0,
14163 height: 8.0,
14164 },
14165 snap_anchor: None,
14166 translated_content_context: false,
14167 text: Arc::new(cranpose_ui::text::AnnotatedString::from("t").render_string()),
14168 color: Color::WHITE,
14169 text_style: cranpose_ui::TextStyle::default(),
14170 font_size: 12.0,
14171 scale: 1.0,
14172 layout_options: cranpose_ui::TextLayoutOptions::default(),
14173 z_index,
14174 clip: None,
14175 }
14176 }
14177
14178 #[test]
14179 fn text_draw_visibility_rejects_text_outside_clip_before_rasterization() {
14180 let viewport = ViewportUniformParams {
14181 width: 320,
14182 height: 240,
14183 offset: [0.0, 0.0],
14184 };
14185 let text_rect = Rect {
14186 x: 0.0,
14187 y: 260.0,
14188 width: 200.0,
14189 height: 40.0,
14190 };
14191 let clip = Some(Rect {
14192 x: 0.0,
14193 y: 0.0,
14194 width: 320.0,
14195 height: 200.0,
14196 });
14197
14198 assert!(
14199 !text_draw_is_visible_in_viewport(text_rect, clip, viewport, 1.0),
14200 "lazy-list beyond-bound text outside the clip must not be rasterized"
14201 );
14202 }
14203
14204 #[test]
14205 fn text_draw_prewarm_accepts_clipped_text_near_viewport() {
14206 let viewport = ViewportUniformParams {
14207 width: 320,
14208 height: 240,
14209 offset: [0.0, 0.0],
14210 };
14211 let text_rect = Rect {
14212 x: 0.0,
14213 y: 260.0,
14214 width: 200.0,
14215 height: 40.0,
14216 };
14217 let clip = Some(Rect {
14218 x: 0.0,
14219 y: 0.0,
14220 width: 320.0,
14221 height: 200.0,
14222 });
14223
14224 assert!(!text_draw_is_visible_in_viewport(
14225 text_rect, clip, viewport, 1.0
14226 ));
14227 assert!(text_draw_should_prewarm_in_viewport(
14228 text_rect, clip, viewport, 1.0
14229 ));
14230 }
14231
14232 #[test]
14233 fn text_draw_prewarm_rejects_far_clipped_text() {
14234 let viewport = ViewportUniformParams {
14235 width: 320,
14236 height: 240,
14237 offset: [0.0, 0.0],
14238 };
14239 let text_rect = Rect {
14240 x: 0.0,
14241 y: 1600.0,
14242 width: 200.0,
14243 height: 40.0,
14244 };
14245 let clip = Some(Rect {
14246 x: 0.0,
14247 y: 0.0,
14248 width: 320.0,
14249 height: 200.0,
14250 });
14251
14252 assert!(!text_draw_should_prewarm_in_viewport(
14253 text_rect, clip, viewport, 1.0
14254 ));
14255 }
14256
14257 #[test]
14258 fn text_draw_visibility_rejects_unclipped_text_outside_viewport() {
14259 let viewport = ViewportUniformParams {
14260 width: 320,
14261 height: 240,
14262 offset: [0.0, 0.0],
14263 };
14264 let text_rect = Rect {
14265 x: 0.0,
14266 y: 241.0,
14267 width: 200.0,
14268 height: 40.0,
14269 };
14270
14271 assert!(
14272 !text_draw_is_visible_in_viewport(text_rect, None, viewport, 1.0),
14273 "unclipped text outside the target viewport must not be rasterized"
14274 );
14275 }
14276
14277 #[test]
14278 fn text_draw_visibility_keeps_partially_visible_text() {
14279 let viewport = ViewportUniformParams {
14280 width: 320,
14281 height: 240,
14282 offset: [0.0, 0.0],
14283 };
14284 let text_rect = Rect {
14285 x: 0.0,
14286 y: 220.0,
14287 width: 200.0,
14288 height: 40.0,
14289 };
14290
14291 assert!(text_draw_is_visible_in_viewport(
14292 text_rect, None, viewport, 1.0
14293 ));
14294 }
14295
14296 fn test_draw_ops(
14297 shapes: &[DrawShape],
14298 images: &[ImageDraw],
14299 texts: &[TextDraw],
14300 shadows: &[ShadowDraw],
14301 ) -> Vec<DrawOp> {
14302 let mut ops = Vec::new();
14303 ops.extend(shapes.iter().enumerate().map(|(index, shape)| DrawOp {
14304 z_index: shape.z_index,
14305 kind: DrawOpKind::Shape(index),
14306 }));
14307 ops.extend(images.iter().enumerate().map(|(index, image)| DrawOp {
14308 z_index: image.z_index,
14309 kind: DrawOpKind::Image(index),
14310 }));
14311 ops.extend(texts.iter().enumerate().map(|(index, text)| DrawOp {
14312 z_index: text.z_index,
14313 kind: DrawOpKind::Text(index),
14314 }));
14315 ops.extend(shadows.iter().enumerate().map(|(index, shadow)| DrawOp {
14316 z_index: shadow.z_index,
14317 kind: DrawOpKind::Shadow(index),
14318 }));
14319 ops.sort_by_key(|op| op.z_index);
14320 ops
14321 }
14322
14323 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
14324 crate::test_support::layer_node(
14325 local_bounds,
14326 ProjectiveTransform::identity(),
14327 GraphicsLayer::default(),
14328 children,
14329 )
14330 }
14331
14332 fn cacheable_layer(
14333 node_id: cranpose_core::NodeId,
14334 local_bounds: Rect,
14335 children: Vec<RenderNode>,
14336 ) -> LayerNode {
14337 let mut layer = test_layer(local_bounds, children);
14338 layer.node_id = Some(node_id);
14339 layer.cache_policy = cranpose_render_common::graph::CachePolicy::Auto;
14340 layer.recompute_raster_cache_hashes();
14341 layer
14342 }
14343
14344 fn text_layer_with_style(text: AnnotatedString, text_style: TextStyle) -> LayerNode {
14345 test_layer(
14346 Rect {
14347 x: 0.0,
14348 y: 0.0,
14349 width: 64.0,
14350 height: 32.0,
14351 },
14352 vec![RenderNode::Primitive(PrimitiveEntry {
14353 phase: PrimitivePhase::BeforeChildren,
14354 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
14355 node_id: 1,
14356 rect: Rect {
14357 x: 2.0,
14358 y: 3.0,
14359 width: 48.0,
14360 height: 18.0,
14361 },
14362 text: std::rc::Rc::new(text),
14363 text_style,
14364 font_size: 14.0,
14365 layout_options: TextLayoutOptions::default(),
14366 clip: None,
14367 })),
14368 })],
14369 )
14370 }
14371
14372 fn snapped_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
14373 LayerNode {
14374 node_id: Some(77),
14375 local_bounds: Rect {
14376 x: 0.0,
14377 y: 0.0,
14378 width: 48.0,
14379 height: 24.0,
14380 },
14381 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
14382 motion_context_animated: animated,
14383 translated_content_context,
14384 translated_content_offset: Point::default(),
14385 content_offset: Point::default(),
14386 scene_children_origin: cranpose_ui_graphics::Point::default(),
14387 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
14388 graphics_layer: GraphicsLayer::default(),
14389 clip_to_bounds: false,
14390 shadow_clip: None,
14391 hit_test: None,
14392 has_hit_targets: false,
14393 isolation: IsolationReasons::default(),
14394 cache_policy: CachePolicy::None,
14395 cache_hashes: LayerRasterCacheHashes::default(),
14396 cache_hashes_valid: false,
14397 children: vec![
14398 RenderNode::Primitive(PrimitiveEntry {
14399 phase: PrimitivePhase::BeforeChildren,
14400 node: PrimitiveNode::Draw(DrawPrimitiveNode {
14401 primitive: DrawPrimitive::RoundRect {
14402 rect: Rect {
14403 x: 0.0,
14404 y: 0.0,
14405 width: 48.0,
14406 height: 24.0,
14407 },
14408 brush: Brush::solid(Color(0.28, 0.30, 0.46, 0.88)),
14409 radii: CornerRadii::uniform(6.0),
14410 stroke: None,
14411 },
14412 clip: None,
14413 }),
14414 }),
14415 RenderNode::Primitive(PrimitiveEntry {
14416 phase: PrimitivePhase::BeforeChildren,
14417 node: PrimitiveNode::Draw(DrawPrimitiveNode {
14418 primitive: DrawPrimitive::Image {
14419 rect: Rect {
14420 x: 2.0,
14421 y: 2.0,
14422 width: 12.0,
14423 height: 12.0,
14424 },
14425 image: ImageBitmap::from_rgba8(
14426 2,
14427 2,
14428 vec![
14429 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255,
14430 255,
14431 ],
14432 )
14433 .expect("image"),
14434 alpha: 1.0,
14435 color_filter: None,
14436 sampling: ImageSampling::Linear,
14437 src_rect: None,
14438 },
14439 clip: None,
14440 }),
14441 }),
14442 RenderNode::Primitive(PrimitiveEntry {
14443 phase: PrimitivePhase::BeforeChildren,
14444 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
14445 node_id: 77,
14446 rect: Rect {
14447 x: 6.0,
14448 y: 4.0,
14449 width: 36.0,
14450 height: 16.0,
14451 },
14452 text: std::rc::Rc::new(AnnotatedString::from("48 px")),
14453 text_style: TextStyle::default(),
14454 font_size: 14.0,
14455 layout_options: TextLayoutOptions::default(),
14456 clip: None,
14457 })),
14458 }),
14459 ],
14460 }
14461 }
14462
14463 fn snapped_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
14464 let text_leaf = snapped_text_leaf(animated, translated_content_context);
14465 test_layer(
14466 Rect {
14467 x: 0.0,
14468 y: 0.0,
14469 width: 96.0,
14470 height: 64.0,
14471 },
14472 vec![RenderNode::Layer(Box::new(text_leaf))],
14473 )
14474 }
14475
14476 fn translated_content_local_surface_root() -> LayerNode {
14477 let mut effectful_text = text_layer_with_style(
14478 AnnotatedString::from("shadow"),
14479 TextStyle::from_span_style(SpanStyle {
14480 shadow: Some(Shadow {
14481 color: Color::BLACK,
14482 offset: Point::new(1.0, 2.0),
14483 blur_radius: 3.0,
14484 }),
14485 ..SpanStyle::default()
14486 }),
14487 );
14488 effectful_text.translated_content_context = true;
14489
14490 let translated_content = LayerNode {
14491 node_id: Some(78),
14492 local_bounds: Rect {
14493 x: 0.0,
14494 y: 0.0,
14495 width: 96.0,
14496 height: 64.0,
14497 },
14498 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
14499 motion_context_animated: false,
14500 translated_content_context: true,
14501 translated_content_offset: Point::default(),
14502 content_offset: Point::default(),
14503 scene_children_origin: cranpose_ui_graphics::Point::default(),
14504 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
14505 graphics_layer: GraphicsLayer::default(),
14506 clip_to_bounds: false,
14507 shadow_clip: None,
14508 hit_test: None,
14509 has_hit_targets: false,
14510 isolation: IsolationReasons::default(),
14511 cache_policy: CachePolicy::None,
14512 cache_hashes: LayerRasterCacheHashes::default(),
14513 cache_hashes_valid: false,
14514 children: vec![RenderNode::Layer(Box::new(effectful_text))],
14515 };
14516
14517 test_layer(
14518 Rect {
14519 x: 0.0,
14520 y: 0.0,
14521 width: 160.0,
14522 height: 120.0,
14523 },
14524 vec![RenderNode::Layer(Box::new(translated_content))],
14525 )
14526 }
14527
14528 #[test]
14529 fn scissor_rect_for_layer_intersects_with_clip() {
14530 let rect = Rect {
14531 x: 10.0,
14532 y: 10.0,
14533 width: 30.0,
14534 height: 20.0,
14535 };
14536 let clip = Rect {
14537 x: 20.0,
14538 y: 15.0,
14539 width: 100.0,
14540 height: 100.0,
14541 };
14542
14543 let scissor = scissor_rect_for_layer(rect, Some(clip), 1.0, 200, 200);
14544 assert_eq!(scissor, Some((20, 15, 20, 15)));
14545 }
14546
14547 #[test]
14548 fn visible_draw_rect_no_clip_returns_original() {
14549 let rect = Rect {
14550 x: 100.0,
14551 y: 200.0,
14552 width: 300.0,
14553 height: 400.0,
14554 };
14555 assert_eq!(visible_draw_rect(rect, None), Some(rect));
14556 }
14557
14558 #[test]
14559 fn visible_draw_rect_with_clip_intersects() {
14560 let rect = Rect {
14561 x: 0.0,
14562 y: 0.0,
14563 width: 2000.0,
14564 height: 5000.0,
14565 };
14566 let clip = Rect {
14567 x: 0.0,
14568 y: 0.0,
14569 width: 800.0,
14570 height: 600.0,
14571 };
14572 let visible = visible_draw_rect(rect, Some(clip)).expect("should have visible area");
14573 assert_eq!(visible.width, 800.0);
14574 assert_eq!(visible.height, 600.0);
14575 }
14576
14577 #[test]
14578 fn visible_draw_rect_fully_clipped_returns_none() {
14579 let rect = Rect {
14580 x: 1000.0,
14581 y: 1000.0,
14582 width: 200.0,
14583 height: 200.0,
14584 };
14585 let clip = Rect {
14586 x: 0.0,
14587 y: 0.0,
14588 width: 800.0,
14589 height: 600.0,
14590 };
14591 assert!(visible_draw_rect(rect, Some(clip)).is_none());
14592 }
14593
14594 #[test]
14595 fn scene_bounds_respects_clip_on_shapes() {
14596 let mut scene = CompositorScene::new();
14597 scene.shapes.push(DrawShape {
14599 rect: Rect {
14600 x: 10.0,
14601 y: 10.0,
14602 width: 100.0,
14603 height: 50.0,
14604 },
14605 clip: Some(Rect {
14606 x: 0.0,
14607 y: 0.0,
14608 width: 800.0,
14609 height: 600.0,
14610 }),
14611 ..test_shape(0, BlendMode::SrcOver)
14612 });
14613 scene.shapes.push(DrawShape {
14615 rect: Rect {
14616 x: 0.0,
14617 y: 3000.0,
14618 width: 100.0,
14619 height: 50.0,
14620 },
14621 clip: Some(Rect {
14622 x: 0.0,
14623 y: 0.0,
14624 width: 800.0,
14625 height: 600.0,
14626 }),
14627 ..test_shape(1, BlendMode::SrcOver)
14628 });
14629 let bounds = scene_bounds(&scene).expect("should have bounds");
14630 assert!(bounds.y + bounds.height <= 600.0);
14633 }
14634
14635 #[test]
14636 fn scene_bounds_scroll_content_clipped_to_viewport() {
14637 let mut scene = CompositorScene::new();
14640 let viewport_clip = Rect {
14641 x: 0.0,
14642 y: 0.0,
14643 width: 800.0,
14644 height: 600.0,
14645 };
14646 for i in 0..20 {
14647 scene.shapes.push(DrawShape {
14648 rect: Rect {
14649 x: 0.0,
14650 y: i as f32 * 300.0,
14651 width: 800.0,
14652 height: 200.0,
14653 },
14654 clip: Some(viewport_clip),
14655 ..test_shape(i, BlendMode::SrcOver)
14656 });
14657 }
14658 let bounds = scene_bounds(&scene).expect("should have bounds");
14659 assert_eq!(bounds.x, 0.0);
14662 assert_eq!(bounds.y, 0.0);
14663 assert!(bounds.width <= 800.0);
14664 assert!(bounds.height <= 600.0);
14665 }
14666
14667 #[test]
14668 fn scene_bounds_stable_across_scroll_offsets() {
14669 let viewport_clip = Rect {
14672 x: 0.0,
14673 y: 0.0,
14674 width: 400.0,
14675 height: 50.0,
14676 };
14677 let compute_bounds_at_offset = |scroll_x: f32| {
14678 let mut scene = CompositorScene::new();
14679 for i in 0..10 {
14680 scene.shapes.push(DrawShape {
14681 rect: Rect {
14682 x: i as f32 * 100.0 - scroll_x,
14683 y: 0.0,
14684 width: 80.0,
14685 height: 40.0,
14686 },
14687 clip: Some(viewport_clip),
14688 ..test_shape(i, BlendMode::SrcOver)
14689 });
14690 }
14691 scene_bounds(&scene).expect("bounds")
14692 };
14693 let bounds_at_0 = compute_bounds_at_offset(0.0);
14694 let bounds_at_300 = compute_bounds_at_offset(300.0);
14695 let bounds_at_600 = compute_bounds_at_offset(600.0);
14696 assert!(
14698 (bounds_at_0.width - bounds_at_300.width).abs() < 1.0,
14699 "bounds width changed with scroll: {} vs {}",
14700 bounds_at_0.width,
14701 bounds_at_300.width
14702 );
14703 assert!(
14704 (bounds_at_0.width - bounds_at_600.width).abs() < 1.0,
14705 "bounds width changed with scroll: {} vs {}",
14706 bounds_at_0.width,
14707 bounds_at_600.width
14708 );
14709 }
14710
14711 #[test]
14712 fn collect_effect_ranges_respects_excluded_effect() {
14713 let layers = vec![effect_layer(10, 40), effect_layer(20, 30)];
14714 let mut ranges = Vec::new();
14715 collect_effect_ranges(&layers, 10, 40, Some(0), &mut ranges);
14716 assert_eq!(ranges.len(), 1);
14717 assert_eq!(ranges[0], 20..30);
14718 }
14719
14720 #[test]
14721 fn collect_layer_events_includes_nested_when_parent_excluded() {
14722 let effects = vec![effect_layer(10, 40), effect_layer(20, 30)];
14723 let backdrops = vec![backdrop_layer(25)];
14724 let mut events = Vec::new();
14725 collect_layer_events(&effects, &backdrops, 10, 40, Some(0), &mut events);
14726 assert_eq!(events.len(), 2);
14727
14728 match events[0].kind {
14729 LayerEventKind::Effect(index) => assert_eq!(index, 1),
14730 LayerEventKind::Backdrop(_) => panic!("expected nested effect as first event"),
14731 }
14732 match events[1].kind {
14733 LayerEventKind::Backdrop(index) => assert_eq!(index, 0),
14734 LayerEventKind::Effect(_) => panic!("expected backdrop as second event"),
14735 }
14736 }
14737
14738 fn pure_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
14739 LayerNode {
14740 node_id: Some(177),
14741 local_bounds: Rect {
14742 x: 0.0,
14743 y: 0.0,
14744 width: 96.0,
14745 height: 32.0,
14746 },
14747 transform_to_parent: ProjectiveTransform::translation(11.4, 23.6),
14748 motion_context_animated: animated,
14749 translated_content_context,
14750 translated_content_offset: Point::default(),
14751 content_offset: Point::default(),
14752 scene_children_origin: cranpose_ui_graphics::Point::default(),
14753 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
14754 graphics_layer: GraphicsLayer::default(),
14755 clip_to_bounds: false,
14756 shadow_clip: None,
14757 hit_test: None,
14758 has_hit_targets: false,
14759 isolation: IsolationReasons::default(),
14760 cache_policy: CachePolicy::None,
14761 cache_hashes: LayerRasterCacheHashes::default(),
14762 cache_hashes_valid: false,
14763 children: vec![RenderNode::Primitive(PrimitiveEntry {
14764 phase: PrimitivePhase::BeforeChildren,
14765 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
14766 node_id: 177,
14767 rect: Rect {
14768 x: 0.0,
14769 y: 0.0,
14770 width: 96.0,
14771 height: 24.0,
14772 },
14773 clip: None,
14774 text: std::rc::Rc::new(AnnotatedString::from("Pure text")),
14775 text_style: TextStyle::default(),
14776 font_size: 14.0,
14777 layout_options: TextLayoutOptions::default(),
14778 })),
14779 })],
14780 }
14781 }
14782
14783 fn pure_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
14784 let text_leaf = pure_text_leaf(animated, translated_content_context);
14785 test_layer(
14786 Rect {
14787 x: 0.0,
14788 y: 0.0,
14789 width: 160.0,
14790 height: 96.0,
14791 },
14792 vec![RenderNode::Layer(Box::new(text_leaf))],
14793 )
14794 }
14795
14796 #[test]
14797 fn collect_layer_events_sorts_backdrop_before_effect_at_same_z() {
14798 let effects = vec![effect_layer(10, 20)];
14799 let backdrops = vec![backdrop_layer(10)];
14800 let mut events = Vec::new();
14801 collect_layer_events(&effects, &backdrops, 0, 30, None, &mut events);
14802 assert_eq!(events.len(), 2);
14803
14804 match events[0].kind {
14805 LayerEventKind::Backdrop(_) => {}
14806 LayerEventKind::Effect(_) => panic!("expected backdrop to run before effect"),
14807 }
14808 match events[1].kind {
14809 LayerEventKind::Effect(_) => {}
14810 LayerEventKind::Backdrop(_) => panic!("expected effect as second event"),
14811 }
14812 }
14813
14814 #[test]
14815 fn collect_layer_events_prefers_outer_effect_when_same_start_z() {
14816 let effects = vec![effect_layer(10, 20), effect_layer(10, 40)];
14819 let mut events = Vec::new();
14820 collect_layer_events(&effects, &[], 0, 50, None, &mut events);
14821
14822 assert_eq!(events.len(), 2);
14823 match events[0].kind {
14824 LayerEventKind::Effect(index) => assert_eq!(index, 1),
14825 LayerEventKind::Backdrop(_) => panic!("expected outer effect first"),
14826 }
14827 match events[1].kind {
14828 LayerEventKind::Effect(index) => assert_eq!(index, 0),
14829 LayerEventKind::Backdrop(_) => panic!("expected child effect second"),
14830 }
14831 }
14832
14833 #[test]
14834 fn collect_layer_events_prefers_later_effect_when_ranges_match() {
14835 let effects = vec![effect_layer(10, 20), effect_layer(10, 20)];
14836 let mut events = Vec::new();
14837 collect_layer_events(&effects, &[], 0, 30, None, &mut events);
14838
14839 assert_eq!(events.len(), 2);
14840 match events[0].kind {
14841 LayerEventKind::Effect(index) => assert_eq!(index, 1),
14842 LayerEventKind::Backdrop(_) => panic!("expected later effect first"),
14843 }
14844 match events[1].kind {
14845 LayerEventKind::Effect(index) => assert_eq!(index, 0),
14846 LayerEventKind::Backdrop(_) => panic!("expected earlier effect second"),
14847 }
14848 }
14849
14850 #[test]
14851 fn has_backdrop_layer_in_range_detects_nested_layers() {
14852 let backdrops = vec![backdrop_layer(5), backdrop_layer(15), backdrop_layer(25)];
14853 assert!(has_backdrop_layer_in_range(&backdrops, 10, 20));
14854 assert!(has_backdrop_layer_in_range(&backdrops, 0, 6));
14855 assert!(!has_backdrop_layer_in_range(&backdrops, 20, 25));
14856 }
14857
14858 #[test]
14859 fn layer_contains_descendant_backdrop_ignores_self_backdrop() {
14860 let mut self_backdrop = test_layer(
14861 Rect {
14862 x: 0.0,
14863 y: 0.0,
14864 width: 10.0,
14865 height: 10.0,
14866 },
14867 vec![],
14868 );
14869 self_backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
14870 assert!(!layer_contains_descendant_backdrop(&self_backdrop));
14871
14872 let mut child = test_layer(
14873 Rect {
14874 x: 0.0,
14875 y: 0.0,
14876 width: 8.0,
14877 height: 8.0,
14878 },
14879 vec![],
14880 );
14881 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
14882
14883 let parent = test_layer(
14884 Rect {
14885 x: 0.0,
14886 y: 0.0,
14887 width: 20.0,
14888 height: 20.0,
14889 },
14890 vec![RenderNode::Layer(Box::new(child))],
14891 );
14892 assert!(layer_contains_descendant_backdrop(&parent));
14893 }
14894
14895 fn child_layer_composite(
14896 layer: &LayerNode,
14897 z_index: usize,
14898 rect: Rect,
14899 needs_nested_underlay: bool,
14900 ) -> crate::normalized_scene::ChildLayerComposite {
14901 let mut requirements_cache = cranpose_core::collections::map::HashMap::new();
14902 let surface_requirements =
14903 crate::surface_plan::layer_surface_requirements_cached(layer, &mut requirements_cache);
14904 crate::normalized_scene::ChildLayerComposite {
14905 z_index,
14906 logical_rect: Rect {
14907 x: 0.0,
14908 y: 0.0,
14909 width: rect.width,
14910 height: rect.height,
14911 },
14912 dest_quad: rect_to_quad(rect),
14913 snap_anchor: None,
14914 composite_snap_origin: None,
14915 backdrop_rect: rect,
14916 visual_clip: None,
14917 surface_clip: None,
14918 shadow_draws: Vec::new(),
14919 needs_nested_underlay,
14920 node_id: layer.node_id,
14921 backdrop: layer.backdrop().cloned(),
14922 has_effect: layer.effect().is_some(),
14923 effect_contains_runtime_shader: layer
14924 .effect()
14925 .is_some_and(|effect| effect.contains_runtime_shader()),
14926 target_content_hash: layer.target_content_hash(),
14927 effect_hash: layer.effect_hash(),
14928 motion_source_content_hash: Some(layer.motion_source_content_hash()),
14929 contains_descendant_backdrop: layer_contains_descendant_backdrop(layer),
14930 cache_policy: layer.cache_policy,
14931 surface_requirements,
14932 rounded_clip: crate::surface_executor::backend::LayerSurfaceRoundedClip::from_layer(
14933 layer,
14934 ),
14935 isolation: cranpose_render_common::layer_composition::effective_layer_isolation(
14936 &layer.graphics_layer,
14937 ),
14938 translated_content_context: layer.translated_content_context,
14939 own_translated_content_axes: crate::surface_plan::translated_content_axes_for_layer(
14940 layer,
14941 ),
14942 clip_rect: layer.clip_rect(),
14943 local_bounds: layer.local_bounds,
14944 surface_scale: crate::surface_plan::layer_surface_scale(layer),
14945 source: crate::normalized_scene::LoweredChildSource::default(),
14946 }
14947 }
14948
14949 #[test]
14950 fn root_direct_preflight_allows_first_translated_child_underlay() {
14951 let child = test_layer(
14952 Rect {
14953 x: 0.0,
14954 y: 0.0,
14955 width: 400.0,
14956 height: 280.0,
14957 },
14958 vec![],
14959 );
14960 let collected = CollectedLayer {
14961 scene: CompositorScene::new(),
14962 child_layers: vec![child_layer_composite(
14963 &child,
14964 3,
14965 Rect {
14966 x: 48.0,
14967 y: 96.0,
14968 width: 400.0,
14969 height: 280.0,
14970 },
14971 true,
14972 )],
14973 };
14974
14975 assert!(direct_root_child_underlays_are_supported(&collected));
14976 }
14977
14978 #[test]
14979 fn root_direct_preflight_allows_axis_aligned_prior_child_underlay() {
14980 let first = test_layer(
14981 Rect {
14982 x: 0.0,
14983 y: 0.0,
14984 width: 80.0,
14985 height: 40.0,
14986 },
14987 vec![],
14988 );
14989 let backdrop_child = test_layer(
14990 Rect {
14991 x: 0.0,
14992 y: 0.0,
14993 width: 400.0,
14994 height: 280.0,
14995 },
14996 vec![],
14997 );
14998 let collected = CollectedLayer {
14999 scene: CompositorScene::new(),
15000 child_layers: vec![
15001 child_layer_composite(
15002 &first,
15003 1,
15004 Rect {
15005 x: 8.0,
15006 y: 16.0,
15007 width: 80.0,
15008 height: 40.0,
15009 },
15010 false,
15011 ),
15012 child_layer_composite(
15013 &backdrop_child,
15014 4,
15015 Rect {
15016 x: 48.0,
15017 y: 96.0,
15018 width: 400.0,
15019 height: 280.0,
15020 },
15021 true,
15022 ),
15023 ],
15024 };
15025
15026 assert!(direct_root_child_underlays_are_supported(&collected));
15027 }
15028
15029 #[test]
15030 fn root_direct_preflight_rejects_effectful_prior_child_underlay() {
15031 let mut first = test_layer(
15032 Rect {
15033 x: 0.0,
15034 y: 0.0,
15035 width: 80.0,
15036 height: 40.0,
15037 },
15038 vec![],
15039 );
15040 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
15041 let backdrop_child = test_layer(
15042 Rect {
15043 x: 0.0,
15044 y: 0.0,
15045 width: 400.0,
15046 height: 280.0,
15047 },
15048 vec![],
15049 );
15050 let collected = CollectedLayer {
15051 scene: CompositorScene::new(),
15052 child_layers: vec![
15053 child_layer_composite(
15054 &first,
15055 1,
15056 Rect {
15057 x: 64.0,
15058 y: 112.0,
15059 width: 80.0,
15060 height: 40.0,
15061 },
15062 false,
15063 ),
15064 child_layer_composite(
15065 &backdrop_child,
15066 4,
15067 Rect {
15068 x: 48.0,
15069 y: 96.0,
15070 width: 400.0,
15071 height: 280.0,
15072 },
15073 true,
15074 ),
15075 ],
15076 };
15077
15078 assert!(!direct_root_child_underlays_are_supported(&collected));
15079 }
15080
15081 #[test]
15082 fn root_direct_preflight_ignores_non_overlapping_effectful_prior_child_underlay() {
15083 let mut first = test_layer(
15084 Rect {
15085 x: 0.0,
15086 y: 0.0,
15087 width: 80.0,
15088 height: 40.0,
15089 },
15090 vec![],
15091 );
15092 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
15093 let backdrop_child = test_layer(
15094 Rect {
15095 x: 0.0,
15096 y: 0.0,
15097 width: 400.0,
15098 height: 280.0,
15099 },
15100 vec![],
15101 );
15102 let collected = CollectedLayer {
15103 scene: CompositorScene::new(),
15104 child_layers: vec![
15105 child_layer_composite(
15106 &first,
15107 1,
15108 Rect {
15109 x: 8.0,
15110 y: 16.0,
15111 width: 80.0,
15112 height: 40.0,
15113 },
15114 false,
15115 ),
15116 child_layer_composite(
15117 &backdrop_child,
15118 4,
15119 Rect {
15120 x: 48.0,
15121 y: 96.0,
15122 width: 400.0,
15123 height: 280.0,
15124 },
15125 true,
15126 ),
15127 ],
15128 };
15129
15130 assert!(direct_root_child_underlays_are_supported(&collected));
15131 }
15132
15133 #[test]
15134 fn root_direct_preflight_rejects_underlay_that_would_replay_prior_scene_effects() {
15135 let backdrop_child = test_layer(
15136 Rect {
15137 x: 0.0,
15138 y: 0.0,
15139 width: 400.0,
15140 height: 280.0,
15141 },
15142 vec![],
15143 );
15144 let mut scene = CompositorScene::new();
15145 scene.next_z = 1;
15146 scene.push_effect_layer(
15147 Rect {
15148 x: 0.0,
15149 y: 0.0,
15150 width: 120.0,
15151 height: 120.0,
15152 },
15153 None,
15154 Some(RenderEffect::blur(2.0)),
15155 BlendMode::SrcOver,
15156 1.0,
15157 0,
15158 1,
15159 );
15160 let collected = CollectedLayer {
15161 scene,
15162 child_layers: vec![child_layer_composite(
15163 &backdrop_child,
15164 4,
15165 Rect {
15166 x: 48.0,
15167 y: 96.0,
15168 width: 400.0,
15169 height: 280.0,
15170 },
15171 true,
15172 )],
15173 };
15174
15175 assert!(!direct_root_child_underlays_are_supported(&collected));
15176 }
15177
15178 #[test]
15179 fn root_direct_eligibility_does_not_reject_descendant_backdrop() {
15180 let mut backdrop = test_layer(
15181 Rect {
15182 x: 0.0,
15183 y: 0.0,
15184 width: 40.0,
15185 height: 40.0,
15186 },
15187 vec![],
15188 );
15189 backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
15190 let child = test_layer(
15191 Rect {
15192 x: 0.0,
15193 y: 0.0,
15194 width: 120.0,
15195 height: 96.0,
15196 },
15197 vec![RenderNode::Layer(Box::new(backdrop))],
15198 );
15199 let root = test_layer(
15200 Rect {
15201 x: 0.0,
15202 y: 0.0,
15203 width: 240.0,
15204 height: 160.0,
15205 },
15206 vec![RenderNode::Layer(Box::new(child))],
15207 );
15208 let mut cache = HashMap::new();
15209
15210 assert!(root_can_render_directly_cached(&root, &mut cache));
15211 }
15212
15213 #[test]
15214 fn root_direct_scene_events_allow_root_local_effects() {
15215 let mut scene = CompositorScene::new();
15216 scene.effect_layers.push(EffectLayer {
15217 rect: Rect {
15218 x: 20.0,
15219 y: 30.0,
15220 width: 120.0,
15221 height: 80.0,
15222 },
15223 clip: None,
15224 snap_anchor: None,
15225 effect: Some(RenderEffect::blur(6.0)),
15226 blend_mode: BlendMode::SrcOver,
15227 composite_alpha: 1.0,
15228 z_start: 0,
15229 z_end: 1,
15230 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
15231 });
15232
15233 assert!(root_direct_scene_events_are_supported(&scene));
15234 }
15235
15236 #[test]
15237 fn root_direct_scene_events_reject_root_local_backdrops() {
15238 let mut scene = CompositorScene::new();
15239 scene.backdrop_layers.push(BackdropLayer {
15240 node_id: Some(99),
15241 rect: Rect {
15242 x: 20.0,
15243 y: 30.0,
15244 width: 120.0,
15245 height: 80.0,
15246 },
15247 clip: None,
15248 snap_anchor: None,
15249 effect: RenderEffect::blur(6.0),
15250 z_index: 1,
15251 });
15252
15253 assert!(!root_direct_scene_events_are_supported(&scene));
15254 }
15255
15256 #[test]
15257 fn estimate_layer_surface_rect_includes_transformed_child_bounds() {
15258 let mut child = test_layer(
15259 Rect {
15260 x: 0.0,
15261 y: 0.0,
15262 width: 10.0,
15263 height: 6.0,
15264 },
15265 vec![RenderNode::Primitive(PrimitiveEntry {
15266 phase: PrimitivePhase::BeforeChildren,
15267 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15268 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15269 rect: Rect {
15270 x: 0.0,
15271 y: 0.0,
15272 width: 10.0,
15273 height: 6.0,
15274 },
15275 brush: Brush::solid(Color::WHITE),
15276 stroke: None,
15277 },
15278 clip: None,
15279 }),
15280 })],
15281 );
15282 child.transform_to_parent = ProjectiveTransform::translation(18.0, 7.0);
15283
15284 let parent = test_layer(
15285 Rect {
15286 x: 0.0,
15287 y: 0.0,
15288 width: 4.0,
15289 height: 4.0,
15290 },
15291 vec![RenderNode::Layer(Box::new(child))],
15292 );
15293
15294 assert_eq!(
15295 estimate_layer_surface_rect(&parent),
15296 Rect {
15297 x: 18.0,
15298 y: 7.0,
15299 width: 10.0,
15300 height: 6.0,
15301 }
15302 );
15303 }
15304
15305 #[test]
15306 fn estimate_layer_surface_rect_clips_translated_clip_layers_without_hidden_leading_content() {
15307 let mut layer = test_layer(
15308 Rect {
15309 x: 0.0,
15310 y: 0.0,
15311 width: 120.0,
15312 height: 72.0,
15313 },
15314 vec![RenderNode::Primitive(PrimitiveEntry {
15315 phase: PrimitivePhase::BeforeChildren,
15316 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15317 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15318 rect: Rect {
15319 x: 24.0,
15320 y: 0.0,
15321 width: 200.0,
15322 height: 480.0,
15323 },
15324 brush: Brush::solid(Color::WHITE),
15325 stroke: None,
15326 },
15327 clip: None,
15328 }),
15329 })],
15330 );
15331 layer.translated_content_context = true;
15332 layer.motion_context_animated = true;
15333 layer.clip_to_bounds = true;
15334
15335 assert_eq!(
15336 estimate_layer_surface_rect(&layer),
15337 Rect {
15338 x: 24.0,
15339 y: 0.0,
15340 width: 96.0,
15341 height: 72.0,
15342 }
15343 );
15344 }
15345
15346 #[test]
15347 fn estimate_layer_surface_rect_clips_active_horizontal_scroll_content() {
15348 let mut layer = test_layer(
15349 Rect {
15350 x: 0.0,
15351 y: 0.0,
15352 width: 120.0,
15353 height: 72.0,
15354 },
15355 vec![RenderNode::Primitive(PrimitiveEntry {
15356 phase: PrimitivePhase::BeforeChildren,
15357 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15358 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15359 rect: Rect {
15360 x: -24.0,
15361 y: 0.0,
15362 width: 200.0,
15363 height: 480.0,
15364 },
15365 brush: Brush::solid(Color::WHITE),
15366 stroke: None,
15367 },
15368 clip: None,
15369 }),
15370 })],
15371 );
15372 layer.translated_content_context = true;
15373 layer.motion_context_animated = true;
15374 layer.clip_to_bounds = true;
15375
15376 assert_eq!(
15377 estimate_layer_surface_rect(&layer),
15378 Rect {
15379 x: 0.0,
15380 y: 0.0,
15381 width: 120.0,
15382 height: 72.0,
15383 }
15384 );
15385 }
15386
15387 #[test]
15388 fn estimate_layer_surface_rect_clips_active_vertical_scroll_content() {
15389 let mut layer = test_layer(
15390 Rect {
15391 x: 0.0,
15392 y: 0.0,
15393 width: 120.0,
15394 height: 72.0,
15395 },
15396 vec![RenderNode::Primitive(PrimitiveEntry {
15397 phase: PrimitivePhase::BeforeChildren,
15398 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15399 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15400 rect: Rect {
15401 x: 0.0,
15402 y: -24.0,
15403 width: 120.0,
15404 height: 200.0,
15405 },
15406 brush: Brush::solid(Color::WHITE),
15407 stroke: None,
15408 },
15409 clip: None,
15410 }),
15411 })],
15412 );
15413 layer.translated_content_context = true;
15414 layer.motion_context_animated = true;
15415 layer.clip_to_bounds = true;
15416
15417 assert_eq!(
15418 estimate_layer_surface_rect(&layer),
15419 Rect {
15420 x: 0.0,
15421 y: 0.0,
15422 width: 120.0,
15423 height: 72.0,
15424 }
15425 );
15426 }
15427
15428 #[test]
15429 fn estimate_layer_surface_rect_keeps_shallow_scroll_capture_origin_stable() {
15430 fn shallow_scroll_surface_rect(content_y: f32) -> Rect {
15431 let mut layer = test_layer(
15432 Rect {
15433 x: 0.0,
15434 y: 0.0,
15435 width: 120.0,
15436 height: 72.0,
15437 },
15438 vec![RenderNode::Primitive(PrimitiveEntry {
15439 phase: PrimitivePhase::BeforeChildren,
15440 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15441 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15442 rect: Rect {
15443 x: 0.0,
15444 y: content_y,
15445 width: 120.0,
15446 height: 200.0,
15447 },
15448 brush: Brush::solid(Color::WHITE),
15449 stroke: None,
15450 },
15451 clip: None,
15452 }),
15453 })],
15454 );
15455 layer.translated_content_context = true;
15456 layer.motion_context_animated = true;
15457 layer.clip_to_bounds = true;
15458 estimate_layer_surface_rect(&layer)
15459 }
15460
15461 assert_eq!(
15462 shallow_scroll_surface_rect(-24.0),
15463 shallow_scroll_surface_rect(-25.0),
15464 "shallow scroll capture bounds must not move the offscreen surface origin on adjacent scroll positions"
15465 );
15466 }
15467
15468 #[test]
15469 fn estimate_layer_surface_rect_clips_active_xy_scroll_content() {
15470 let mut layer = test_layer(
15471 Rect {
15472 x: 0.0,
15473 y: 0.0,
15474 width: 120.0,
15475 height: 72.0,
15476 },
15477 vec![RenderNode::Primitive(PrimitiveEntry {
15478 phase: PrimitivePhase::BeforeChildren,
15479 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15480 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15481 rect: Rect {
15482 x: -16.0,
15483 y: -24.0,
15484 width: 180.0,
15485 height: 240.0,
15486 },
15487 brush: Brush::solid(Color::WHITE),
15488 stroke: None,
15489 },
15490 clip: None,
15491 }),
15492 })],
15493 );
15494 layer.translated_content_context = true;
15495 layer.motion_context_animated = true;
15496 layer.clip_to_bounds = true;
15497
15498 assert_eq!(
15499 estimate_layer_surface_rect(&layer),
15500 Rect {
15501 x: 0.0,
15502 y: 0.0,
15503 width: 120.0,
15504 height: 72.0,
15505 }
15506 );
15507 }
15508
15509 #[test]
15510 fn estimate_layer_surface_rect_clips_deep_hidden_active_scroll_content() {
15511 let mut layer = test_layer(
15512 Rect {
15513 x: 0.0,
15514 y: 0.0,
15515 width: 120.0,
15516 height: 72.0,
15517 },
15518 vec![RenderNode::Primitive(PrimitiveEntry {
15519 phase: PrimitivePhase::BeforeChildren,
15520 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15521 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15522 rect: Rect {
15523 x: 0.0,
15524 y: -1200.0,
15525 width: 120.0,
15526 height: 1400.0,
15527 },
15528 brush: Brush::solid(Color::WHITE),
15529 stroke: None,
15530 },
15531 clip: None,
15532 }),
15533 })],
15534 );
15535 layer.translated_content_context = true;
15536 layer.motion_context_animated = true;
15537 layer.clip_to_bounds = true;
15538
15539 assert_eq!(
15540 estimate_layer_surface_rect(&layer),
15541 Rect {
15542 x: 0.0,
15543 y: 0.0,
15544 width: 120.0,
15545 height: 72.0,
15546 }
15547 );
15548 }
15549
15550 #[test]
15551 fn estimate_layer_surface_rect_keeps_deep_scroll_capture_origin_stable() {
15552 fn deep_scroll_surface_rect(content_y: f32) -> Rect {
15553 let mut layer = test_layer(
15554 Rect {
15555 x: 0.0,
15556 y: 0.0,
15557 width: 120.0,
15558 height: 72.0,
15559 },
15560 vec![RenderNode::Primitive(PrimitiveEntry {
15561 phase: PrimitivePhase::BeforeChildren,
15562 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15563 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15564 rect: Rect {
15565 x: 0.0,
15566 y: content_y,
15567 width: 120.0,
15568 height: 1400.0,
15569 },
15570 brush: Brush::solid(Color::WHITE),
15571 stroke: None,
15572 },
15573 clip: None,
15574 }),
15575 })],
15576 );
15577 layer.translated_content_context = true;
15578 layer.motion_context_animated = true;
15579 layer.clip_to_bounds = true;
15580 estimate_layer_surface_rect(&layer)
15581 }
15582
15583 assert_eq!(
15584 deep_scroll_surface_rect(-1200.0),
15585 deep_scroll_surface_rect(-1201.0),
15586 "deep scroll capture bounds must not re-phase the offscreen surface origin on adjacent scroll positions"
15587 );
15588 }
15589
15590 #[test]
15591 fn motion_stable_capture_bounds_bounds_shadows_for_clipped_effect_layer() {
15592 let mut layer = test_layer(
15593 Rect {
15594 x: 0.0,
15595 y: 0.0,
15596 width: 120.0,
15597 height: 72.0,
15598 },
15599 vec![],
15600 );
15601 layer.clip_to_bounds = true;
15602 layer.graphics_layer.clip = true;
15603 layer.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
15604
15605 let mut shadow_shape = test_shape(0, BlendMode::SrcOver);
15606 shadow_shape.rect = Rect {
15607 x: -24.0,
15608 y: -1200.0,
15609 width: 180.0,
15610 height: 1400.0,
15611 };
15612 let mut scene = CompositorScene::new();
15613 scene
15614 .shadow_draws
15615 .push(test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]));
15616
15617 let requirements = SurfaceRequirementSet::default()
15618 .with(SurfaceRequirement::RenderEffect)
15619 .with(SurfaceRequirement::MotionStableCapture);
15620
15621 assert_eq!(
15622 motion_stable_capture_bounds(
15623 &layer,
15624 &scene,
15625 &[],
15626 requirements,
15627 TranslatedContentAxes::default(),
15628 None,
15629 ),
15630 Some(Rect {
15631 x: -360.0,
15632 y: -216.0,
15633 width: 480.0,
15634 height: 288.0,
15635 })
15636 );
15637 }
15638
15639 #[test]
15640 fn vertical_motion_stable_capture_uses_viewport_cross_axis_bounds() {
15641 let mut layer = test_layer(
15642 Rect {
15643 x: 0.0,
15644 y: 0.0,
15645 width: 200.0,
15646 height: 100.0,
15647 },
15648 vec![],
15649 );
15650 layer.clip_to_bounds = true;
15651 layer.graphics_layer.clip = true;
15652
15653 let mut shape = test_shape(0, BlendMode::SrcOver);
15654 shape.rect = Rect {
15655 x: 60.0,
15656 y: -80.0,
15657 width: 80.0,
15658 height: 220.0,
15659 };
15660 let mut scene = CompositorScene::new();
15661 scene.shapes.push(shape);
15662
15663 let requirements =
15664 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
15665
15666 assert_eq!(
15667 motion_stable_capture_bounds(
15668 &layer,
15669 &scene,
15670 &[],
15671 requirements,
15672 TranslatedContentAxes { x: false, y: true },
15673 None,
15674 ),
15675 Some(Rect {
15676 x: -96.0,
15677 y: -64.0,
15678 width: 296.0,
15679 height: 164.0,
15680 })
15681 );
15682 }
15683
15684 #[test]
15685 fn vertical_motion_stable_capture_uses_external_surface_clip() {
15686 let layer = test_layer(
15687 Rect {
15688 x: 0.0,
15689 y: 0.0,
15690 width: 200.0,
15691 height: 100.0,
15692 },
15693 vec![],
15694 );
15695
15696 let mut shape = test_shape(0, BlendMode::SrcOver);
15697 shape.rect = Rect {
15698 x: 60.0,
15699 y: -80.0,
15700 width: 80.0,
15701 height: 220.0,
15702 };
15703 let mut scene = CompositorScene::new();
15704 scene.shapes.push(shape);
15705
15706 let requirements =
15707 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
15708
15709 assert_eq!(
15710 motion_stable_capture_bounds(
15711 &layer,
15712 &scene,
15713 &[],
15714 requirements,
15715 TranslatedContentAxes { x: false, y: true },
15716 Some(Rect {
15717 x: 0.0,
15718 y: 0.0,
15719 width: 200.0,
15720 height: 100.0,
15721 }),
15722 ),
15723 Some(Rect {
15724 x: -96.0,
15725 y: -64.0,
15726 width: 296.0,
15727 height: 164.0,
15728 })
15729 );
15730 }
15731
15732 #[test]
15733 fn estimate_layer_surface_rect_expands_for_child_layer_shadow() {
15734 let mut child = test_layer(
15735 Rect {
15736 x: 0.0,
15737 y: 0.0,
15738 width: 12.0,
15739 height: 8.0,
15740 },
15741 vec![],
15742 );
15743 child.transform_to_parent = ProjectiveTransform::translation(20.0, 9.0);
15744 child.graphics_layer.shadow_elevation = 6.0;
15745
15746 let parent = test_layer(
15747 Rect {
15748 x: 0.0,
15749 y: 0.0,
15750 width: 4.0,
15751 height: 4.0,
15752 },
15753 vec![RenderNode::Layer(Box::new(child))],
15754 );
15755
15756 let rect = estimate_layer_surface_rect(&parent);
15757 assert!(rect.x < 20.0);
15758 assert!(rect.y < 9.0);
15759 assert!(rect.width > 12.0);
15760 assert!(rect.height > 8.0);
15761 }
15762
15763 #[test]
15764 fn estimate_layer_surface_rect_respects_local_bounds_for_effect_layers() {
15765 let mut layer = test_layer(
15766 Rect {
15767 x: 0.0,
15768 y: 0.0,
15769 width: 28.0,
15770 height: 28.0,
15771 },
15772 vec![RenderNode::Primitive(PrimitiveEntry {
15773 phase: PrimitivePhase::BeforeChildren,
15774 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15775 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15776 rect: Rect {
15777 x: 10.0,
15778 y: 10.0,
15779 width: 10.0,
15780 height: 10.0,
15781 },
15782 brush: Brush::solid(Color::WHITE),
15783 stroke: None,
15784 },
15785 clip: None,
15786 }),
15787 })],
15788 );
15789 layer.graphics_layer.render_effect = Some(RenderEffect::blur(12.0));
15790
15791 assert_eq!(
15792 estimate_layer_surface_rect(&layer),
15793 Rect {
15794 x: 0.0,
15795 y: 0.0,
15796 width: 28.0,
15797 height: 28.0,
15798 }
15799 );
15800 }
15801
15802 #[test]
15803 fn layer_raster_cache_candidate_ignores_parent_transform() {
15804 let primitive = PrimitiveEntry {
15805 phase: PrimitivePhase::BeforeChildren,
15806 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15807 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15808 rect: Rect {
15809 x: 2.0,
15810 y: 3.0,
15811 width: 6.0,
15812 height: 4.0,
15813 },
15814 brush: Brush::solid(Color::BLACK),
15815 stroke: None,
15816 },
15817 clip: None,
15818 }),
15819 };
15820 let base = cacheable_layer(
15821 41,
15822 Rect {
15823 x: 0.0,
15824 y: 0.0,
15825 width: 20.0,
15826 height: 20.0,
15827 },
15828 vec![RenderNode::Primitive(primitive.clone())],
15829 );
15830 let mut moved = base.clone();
15831 moved.transform_to_parent = ProjectiveTransform::translation(32.0, 18.0);
15832
15833 assert_eq!(
15834 layer_raster_cache_candidate(&base, 1.25, false, false),
15835 layer_raster_cache_candidate(&moved, 1.25, false, false)
15836 );
15837 }
15838
15839 #[test]
15840 fn layer_raster_cache_candidate_changes_for_translated_content_offset() {
15841 let primitive = PrimitiveEntry {
15842 phase: PrimitivePhase::BeforeChildren,
15843 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15844 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15845 rect: Rect {
15846 x: 2.0,
15847 y: 3.0,
15848 width: 6.0,
15849 height: 4.0,
15850 },
15851 brush: Brush::solid(Color::BLACK),
15852 stroke: None,
15853 },
15854 clip: None,
15855 }),
15856 };
15857 let mut base = cacheable_layer(
15858 42,
15859 Rect {
15860 x: 0.0,
15861 y: 0.0,
15862 width: 20.0,
15863 height: 20.0,
15864 },
15865 vec![RenderNode::Primitive(primitive)],
15866 );
15867 base.translated_content_context = true;
15868 base.translated_content_offset = Point::new(0.0, -8.0);
15869 base.recompute_raster_cache_hashes();
15870
15871 let mut moved = base.clone();
15872 moved.translated_content_offset = Point::new(0.0, -16.0);
15873 moved.recompute_raster_cache_hashes();
15874
15875 assert_ne!(
15876 layer_raster_cache_candidate(&base, 1.25, false, false),
15877 layer_raster_cache_candidate(&moved, 1.25, false, false),
15878 "full-surface layer cache candidates must not alias different scroll offsets"
15879 );
15880 }
15881
15882 #[test]
15883 fn layer_raster_cache_candidate_changes_for_child_transform() {
15884 let mut child = cacheable_layer(
15885 8,
15886 Rect {
15887 x: 0.0,
15888 y: 0.0,
15889 width: 12.0,
15890 height: 10.0,
15891 },
15892 vec![],
15893 );
15894 child.transform_to_parent = ProjectiveTransform::translation(4.0, 6.0);
15895 let base = cacheable_layer(
15896 7,
15897 Rect {
15898 x: 0.0,
15899 y: 0.0,
15900 width: 20.0,
15901 height: 20.0,
15902 },
15903 vec![RenderNode::Layer(Box::new(child.clone()))],
15904 );
15905 let mut moved_child = child;
15906 moved_child.transform_to_parent = ProjectiveTransform::translation(9.0, 6.0);
15907 let moved = cacheable_layer(
15908 7,
15909 Rect {
15910 x: 0.0,
15911 y: 0.0,
15912 width: 20.0,
15913 height: 20.0,
15914 },
15915 vec![RenderNode::Layer(Box::new(moved_child))],
15916 );
15917
15918 assert_ne!(
15919 layer_raster_cache_candidate(&base, 1.0, false, false),
15920 layer_raster_cache_candidate(&moved, 1.0, false, false)
15921 );
15922 }
15923
15924 #[test]
15925 fn layer_raster_cache_candidate_rejects_external_backdrop_dependency() {
15926 let mut child = cacheable_layer(
15927 12,
15928 Rect {
15929 x: 0.0,
15930 y: 0.0,
15931 width: 8.0,
15932 height: 8.0,
15933 },
15934 vec![],
15935 );
15936 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
15937 let parent = cacheable_layer(
15938 11,
15939 Rect {
15940 x: 0.0,
15941 y: 0.0,
15942 width: 16.0,
15943 height: 16.0,
15944 },
15945 vec![RenderNode::Layer(Box::new(child))],
15946 );
15947
15948 assert!(layer_raster_cache_candidate(&parent, 1.0, false, false).is_some());
15949 assert!(layer_raster_cache_candidate(&parent, 1.0, true, false).is_none());
15950 }
15951
15952 #[test]
15953 fn layer_raster_cache_candidate_does_not_force_translation_only_text_surfaces() {
15954 let text = RenderNode::Primitive(PrimitiveEntry {
15955 phase: PrimitivePhase::BeforeChildren,
15956 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
15957 node_id: 77,
15958 rect: Rect {
15959 x: 2.0,
15960 y: 3.0,
15961 width: 48.0,
15962 height: 18.0,
15963 },
15964 text: std::rc::Rc::new(AnnotatedString::from("runtime cache")),
15965 text_style: TextStyle::default(),
15966 font_size: 14.0,
15967 layout_options: TextLayoutOptions::default(),
15968 clip: None,
15969 })),
15970 });
15971 let mut layer = test_layer(
15972 Rect {
15973 x: 0.0,
15974 y: 0.0,
15975 width: 64.0,
15976 height: 32.0,
15977 },
15978 vec![text],
15979 );
15980 layer.node_id = Some(77);
15981 layer.recompute_raster_cache_hashes();
15982
15983 assert!(
15984 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
15985 "root path should not isolate plain translation-only text layers"
15986 );
15987 assert!(
15988 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
15989 "child path should also render plain translation-only text layers directly"
15990 );
15991 }
15992
15993 #[test]
15994 fn layer_raster_cache_candidate_allows_stable_runtime_child_effect_surfaces() {
15995 let mut layer = test_layer(
15996 Rect {
15997 x: 0.0,
15998 y: 0.0,
15999 width: 64.0,
16000 height: 32.0,
16001 },
16002 vec![RenderNode::Primitive(PrimitiveEntry {
16003 phase: PrimitivePhase::BeforeChildren,
16004 node: PrimitiveNode::Draw(DrawPrimitiveNode {
16005 primitive: DrawPrimitive::Rect {
16006 rect: Rect {
16007 x: 0.0,
16008 y: 0.0,
16009 width: 64.0,
16010 height: 32.0,
16011 },
16012 brush: Brush::solid(Color::WHITE),
16013 stroke: None,
16014 },
16015 clip: None,
16016 }),
16017 })],
16018 );
16019 layer.node_id = Some(78);
16020 layer.graphics_layer.render_effect = Some(RenderEffect::blur(4.0));
16021 layer.recompute_raster_cache_hashes();
16022
16023 assert!(
16024 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
16025 "root direct path should not force-cache ordinary stable effects"
16026 );
16027 assert!(
16028 layer_raster_cache_candidate(&layer, 1.0, false, true).is_some(),
16029 "child surface rendering should retain stable non-runtime effects"
16030 );
16031 }
16032
16033 #[test]
16034 fn layer_raster_cache_candidate_rejects_runtime_shader_child_effect_surfaces() {
16035 let mut layer = test_layer(
16036 Rect {
16037 x: 0.0,
16038 y: 0.0,
16039 width: 64.0,
16040 height: 32.0,
16041 },
16042 vec![],
16043 );
16044 layer.node_id = Some(79);
16045 layer.graphics_layer.render_effect = Some(RenderEffect::runtime_shader(
16046 RuntimeShader::new("runtime shader"),
16047 ));
16048 layer.recompute_raster_cache_hashes();
16049
16050 assert!(
16051 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
16052 "runtime shaders must not fill the retained layer cache with per-frame uniform variants"
16053 );
16054 }
16055
16056 #[test]
16057 fn layer_surface_requirements_keep_plain_text_on_direct_path() {
16058 let layer = text_layer_with_style(AnnotatedString::from("plain"), TextStyle::default());
16059
16060 let requirements = layer_surface_requirements(&layer);
16061
16062 assert_eq!(requirements.direct_translation, Some(Point::default()));
16063 assert!(requirements
16064 .surface_requirements
16065 .contains(SurfaceRequirement::PixelStableComposite));
16066 assert!(!requirements
16067 .surface_requirements
16068 .has_isolating_requirement());
16069 }
16070
16071 #[test]
16072 fn layer_surface_requirements_keep_translated_plain_text_leaf_on_direct_path() {
16073 let layer = pure_text_leaf(false, true);
16074
16075 let requirements = layer_surface_requirements(&layer);
16076
16077 assert_eq!(
16078 requirements.direct_translation,
16079 Some(Point::new(11.4, 23.6))
16080 );
16081 assert!(
16082 requirements
16083 .surface_requirements
16084 .contains(SurfaceRequirement::PixelStableComposite)
16085 && !requirements
16086 .surface_requirements
16087 .has_isolating_requirement(),
16088 "translated plain text should stay on the direct path and isolate only the glyph draw"
16089 );
16090 }
16091
16092 #[test]
16093 fn layer_surface_requirements_keep_translated_text_leaf_with_background_on_direct_path() {
16094 let layer = snapped_text_leaf(false, true);
16095
16096 let requirements = layer_surface_requirements(&layer);
16097
16098 assert_eq!(
16099 requirements.direct_translation,
16100 Some(Point::new(14.25, 16.5))
16101 );
16102 assert!(
16103 requirements
16104 .surface_requirements
16105 .contains(SurfaceRequirement::PixelStableComposite)
16106 && !requirements
16107 .surface_requirements
16108 .has_isolating_requirement(),
16109 "translated text with direct sibling decoration/background should keep the layer direct"
16110 );
16111 }
16112
16113 #[test]
16114 fn translated_plain_text_uses_bounded_snap_surface() {
16115 let root = pure_text_leaf_root(true, true);
16116 let mut rect_cache = HashMap::new();
16117 let mut requirements_cache = HashMap::new();
16118 let collected =
16119 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16120
16121 assert_eq!(collected.child_layers.len(), 1);
16122 assert!(collected.scene.texts.is_empty());
16123 assert!(collected.scene.effect_layers.is_empty());
16124 assert_snap_anchor_close(
16125 collected.child_layers[0].snap_anchor,
16126 Point::new(11.4, 23.6),
16127 "translated plain text's bounded local surface should composite at the content-origin snap phase",
16128 );
16129 }
16130
16131 #[test]
16135 #[ignore]
16136 fn shape_run_collect_timing_harness() {
16137 use cranpose_render_common::graph::DrawPrimitiveNode;
16138 use cranpose_render_common::layer_composition::local_content_layer_for;
16139 use cranpose_ui_graphics::Stroke;
16140
16141 let bounds = Rect {
16142 x: 0.0,
16143 y: 0.0,
16144 width: 1080.0,
16145 height: 2244.0,
16146 };
16147 let graphics_layer = GraphicsLayer::default();
16148
16149 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
16152 for i in 0..3000u32 {
16153 let f = i as f32;
16154 let brush = if i % 8 == 0 {
16155 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
16156 } else {
16157 Brush::Solid(Color(0.5, 0.2, 0.8, 1.0))
16158 };
16159 let center = Point::new(540.0 + (f % 400.0), 1122.0 + (f % 350.0));
16160 let radius = 8.0 + (i % 23) as f32;
16161 let half = radius + 4.0;
16162 nodes.push(DrawPrimitiveNode {
16163 primitive: DrawPrimitive::Arc {
16164 rect: Rect {
16165 x: center.x - half,
16166 y: center.y - half,
16167 width: half * 2.0,
16168 height: half * 2.0,
16169 },
16170 brush,
16171 center,
16172 radius,
16173 start_angle: f * 0.07,
16174 sweep_angle: 0.5 + (i % 5) as f32,
16175 stroke: (i % 3 != 0).then(|| Stroke::new(4.0)),
16176 inner_radius: if i % 3 == 0 { radius * 0.6 } else { 0.0 },
16177 },
16178 clip: None,
16179 });
16180 }
16181
16182 let children: Vec<RenderNode> = nodes
16183 .iter()
16184 .map(|node| {
16185 RenderNode::Primitive(PrimitiveEntry {
16186 phase: PrimitivePhase::BeforeChildren,
16187 node: PrimitiveNode::Draw(node.clone()),
16188 })
16189 })
16190 .collect();
16191 let layer = crate::test_support::layer_node(
16192 bounds,
16193 ProjectiveTransform::identity(),
16194 graphics_layer,
16195 children,
16196 );
16197
16198 const ITERS: usize = 300;
16199
16200 let local_layer = local_content_layer_for(&layer.graphics_layer);
16202 let start = Instant::now();
16203 let mut sink_shapes = 0usize;
16204 for _ in 0..ITERS {
16205 let mut scene = CompositorScene::new();
16206 for node in &nodes {
16207 crate::pipeline::push_draw_primitive(
16208 &node.primitive,
16209 bounds,
16210 &local_layer,
16211 None,
16212 &mut scene,
16213 None,
16214 false,
16215 );
16216 }
16217 sink_shapes = scene.shapes.len();
16218 }
16219 let serial = start.elapsed();
16220
16221 let mut rect_cache = HashMap::new();
16222 let mut requirements_cache = HashMap::new();
16223 let start = Instant::now();
16224 let mut run_shapes = 0usize;
16225 for _ in 0..ITERS {
16226 let collected = collect_layer_contents(
16227 &layer,
16228 None,
16229 None,
16230 &mut rect_cache,
16231 &mut requirements_cache,
16232 );
16233 run_shapes = collected.scene.shapes.len();
16234 }
16235 let run = start.elapsed();
16236
16237 println!(
16238 "per-primitive: {:?}/iter ({sink_shapes} shapes) shape-run: {:?}/iter ({run_shapes} shapes)",
16239 serial / ITERS as u32,
16240 run / ITERS as u32,
16241 );
16242 }
16243
16244 fn assert_shape_run_collect_matches_per_primitive_emission() {
16246 use cranpose_render_common::graph::DrawPrimitiveNode;
16247 use cranpose_render_common::layer_composition::local_content_layer_for;
16248 use cranpose_render_common::primitive_emit::{resolve_primitive_clip, PrimitiveClipSpace};
16249 use cranpose_ui_graphics::{CornerRadii, Stroke};
16250
16251 let bounds = Rect {
16252 x: 0.0,
16253 y: 0.0,
16254 width: 800.0,
16255 height: 800.0,
16256 };
16257 let graphics_layer = GraphicsLayer {
16260 scale: 1.25,
16261 translation_x: 3.5,
16262 translation_y: -2.0,
16263 alpha: 0.9,
16264 rotation_z: 0.35,
16265 ..GraphicsLayer::default()
16266 };
16267
16268 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
16269 for i in 0..600u32 {
16270 let f = i as f32;
16271 let brush = if i % 11 == 0 {
16272 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
16273 } else {
16274 Brush::Solid(Color(0.1 + (i % 7) as f32 * 0.1, 0.5, 0.9, 1.0))
16275 };
16276 let stroke = (i % 5 == 0).then(|| Stroke::new(1.0 + (i % 3) as f32));
16277 let primitive = match i % 3 {
16278 0 => DrawPrimitive::Rect {
16279 rect: Rect {
16280 x: f % 37.0,
16281 y: f % 53.0,
16282 width: 8.0 + f % 9.0,
16283 height: 6.0 + f % 5.0,
16284 },
16285 brush,
16286 stroke,
16287 },
16288 1 => DrawPrimitive::RoundRect {
16289 rect: Rect {
16290 x: f % 41.0,
16291 y: f % 43.0,
16292 width: 12.0,
16293 height: 10.0,
16294 },
16295 brush,
16296 radii: CornerRadii::uniform(2.0 + (i % 4) as f32),
16297 stroke,
16298 },
16299 _ => {
16300 let center = Point::new(60.0 + f % 71.0, 60.0 + f % 67.0);
16301 let radius = 5.0 + (i % 13) as f32;
16302 let sweep_angle = if i == 302 { 0.0 } else { 0.4 + (i % 6) as f32 };
16304 let half = radius + 4.0;
16305 DrawPrimitive::Arc {
16306 rect: Rect {
16307 x: center.x - half,
16308 y: center.y - half,
16309 width: half * 2.0,
16310 height: half * 2.0,
16311 },
16312 brush,
16313 center,
16314 radius,
16315 start_angle: f * 0.11,
16316 sweep_angle,
16317 stroke: (i % 2 == 0).then(|| Stroke::new(3.0)),
16318 inner_radius: if i % 4 == 2 { radius * 0.5 } else { 0.0 },
16319 }
16320 }
16321 };
16322 let primitive = if i == 300 {
16323 DrawPrimitive::Blend {
16327 primitive: Box::new(DrawPrimitive::Blend {
16328 primitive: Box::new(primitive),
16329 blend_mode: BlendMode::SrcOver,
16330 }),
16331 blend_mode: BlendMode::DstOut,
16332 }
16333 } else if i % 7 == 3 {
16334 DrawPrimitive::Blend {
16335 primitive: Box::new(primitive),
16336 blend_mode: BlendMode::DstOut,
16337 }
16338 } else {
16339 primitive
16340 };
16341 let clip = (i % 31 == 7).then_some(Rect {
16342 x: 0.0,
16343 y: 0.0,
16344 width: 30.0,
16345 height: 30.0,
16346 });
16347 nodes.push(DrawPrimitiveNode { primitive, clip });
16348 }
16349
16350 let children: Vec<RenderNode> = nodes
16351 .iter()
16352 .map(|node| {
16353 RenderNode::Primitive(PrimitiveEntry {
16354 phase: PrimitivePhase::BeforeChildren,
16355 node: PrimitiveNode::Draw(node.clone()),
16356 })
16357 })
16358 .collect();
16359 let layer = crate::test_support::layer_node(
16360 bounds,
16361 ProjectiveTransform::identity(),
16362 graphics_layer,
16363 children,
16364 );
16365
16366 let mut rect_cache = HashMap::new();
16367 let mut requirements_cache = HashMap::new();
16368 let collected =
16369 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
16370
16371 let local_layer = local_content_layer_for(&layer.graphics_layer);
16374 let mut expected = CompositorScene::new();
16375 for node in &nodes {
16376 let clip = resolve_primitive_clip(
16377 node.clip,
16378 bounds,
16379 &local_layer,
16380 None,
16381 PrimitiveClipSpace::Local,
16382 );
16383 if node.clip.is_some() && clip.is_none() {
16384 continue;
16385 }
16386 crate::pipeline::push_draw_primitive(
16387 &node.primitive,
16388 bounds,
16389 &local_layer,
16390 clip,
16391 &mut expected,
16392 None,
16393 false,
16394 );
16395 }
16396
16397 assert!(
16398 collected.scene.shapes.len() >= 590,
16399 "the runs should engage the parallel branch: got {} shapes",
16400 collected.scene.shapes.len()
16401 );
16402 assert_eq!(collected.scene.shapes.len(), expected.shapes.len());
16403 assert_eq!(collected.scene.draw_ops, expected.draw_ops);
16404 assert_eq!(collected.scene.next_z, expected.next_z);
16405 assert!(
16406 collected
16407 .scene
16408 .shapes
16409 .iter()
16410 .all(|s| s.snap_anchor.is_none()),
16411 "a rotated layer must not rigid-snap; the reference scene assumes it"
16412 );
16413 for (index, (got, want)) in collected
16414 .scene
16415 .shapes
16416 .iter()
16417 .zip(&expected.shapes)
16418 .enumerate()
16419 {
16420 assert_eq!(got.rect, want.rect, "shape {index} rect");
16421 assert_eq!(got.local_rect, want.local_rect, "shape {index} local_rect");
16422 assert_eq!(got.quad, want.quad, "shape {index} quad");
16423 assert_eq!(got.snap_anchor, want.snap_anchor, "shape {index} snap");
16424 assert_eq!(got.brush, want.brush, "shape {index} brush");
16425 assert_eq!(got.shape, want.shape, "shape {index} shape");
16426 assert_eq!(got.stroke, want.stroke, "shape {index} stroke");
16427 assert_eq!(got.arc, want.arc, "shape {index} arc");
16428 assert_eq!(got.z_index, want.z_index, "shape {index} z");
16429 assert_eq!(got.clip, want.clip, "shape {index} clip");
16430 assert_eq!(got.blend_mode, want.blend_mode, "shape {index} blend");
16431 assert_eq!(
16432 got.motion_context_animated, want.motion_context_animated,
16433 "shape {index} motion flag"
16434 );
16435 }
16436 }
16437
16438 #[test]
16443 fn shape_run_collect_matches_per_primitive_emission_exactly() {
16444 assert_shape_run_collect_matches_per_primitive_emission();
16445 crate::normalized_scene::force_shape_run_parallel_for_tests(true);
16446 let outcome =
16447 std::panic::catch_unwind(assert_shape_run_collect_matches_per_primitive_emission);
16448 crate::normalized_scene::force_shape_run_parallel_for_tests(false);
16449 if let Err(payload) = outcome {
16450 std::panic::resume_unwind(payload);
16451 }
16452 }
16453
16454 #[test]
16455 fn non_translated_text_local_surface_keeps_linear_composite_resolve() {
16456 let layer = text_layer_with_style(
16457 AnnotatedString::from("gradient"),
16458 TextStyle::from_span_style(SpanStyle {
16459 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16460 ..SpanStyle::default()
16461 }),
16462 );
16463 let requirements = layer_surface_requirements(&layer);
16464
16465 assert!(requirements
16466 .surface_requirements
16467 .contains(SurfaceRequirement::TextMaterialMask));
16468 assert_eq!(
16469 composite_sample_mode_for_requirements(false, false, requirements),
16470 CompositeSampleMode::Linear
16471 );
16472 }
16473
16474 #[test]
16475 fn inherited_translated_text_local_surface_uses_box4_layer_surface() {
16476 let layer = text_layer_with_style(
16477 AnnotatedString::from("shadow"),
16478 TextStyle::from_span_style(SpanStyle {
16479 shadow: Some(Shadow {
16480 color: Color::BLACK,
16481 offset: Point::new(1.0, 2.0),
16482 blur_radius: 3.0,
16483 }),
16484 ..SpanStyle::default()
16485 }),
16486 );
16487 let requirements = layer_surface_requirements(&layer);
16488
16489 assert!(requirements
16490 .surface_requirements
16491 .contains(SurfaceRequirement::TextMaterialMask));
16492 assert_eq!(
16493 composite_sample_mode_for_requirements(true, false, requirements),
16494 CompositeSampleMode::Box4
16495 );
16496 assert_eq!(
16497 layer_surface_target_scale(
16498 true,
16499 false,
16500 requirements,
16501 1.25,
16502 layer_surface_scale(&layer)
16503 ),
16504 SurfaceRequirementSet::default()
16505 .with(SurfaceRequirement::TextMaterialMask)
16506 .with(SurfaceRequirement::MotionStableCapture)
16507 .target_scale(1.25, 1.0)
16508 );
16509 }
16510
16511 #[test]
16512 fn translated_text_local_surface_inside_capture_keeps_parent_scale() {
16513 let layer = text_layer_with_style(
16514 AnnotatedString::from("shadow"),
16515 TextStyle::from_span_style(SpanStyle {
16516 shadow: Some(Shadow {
16517 color: Color::BLACK,
16518 offset: Point::new(1.0, 2.0),
16519 blur_radius: 3.0,
16520 }),
16521 ..SpanStyle::default()
16522 }),
16523 );
16524 let requirements = layer_surface_requirements(&layer);
16525
16526 assert_eq!(
16527 composite_sample_mode_for_requirements(true, true, requirements),
16528 CompositeSampleMode::Linear
16529 );
16530 assert_eq!(
16531 layer_surface_target_scale(true, true, requirements, 10.0, layer_surface_scale(&layer)),
16532 SurfaceRequirementSet::default()
16533 .with(SurfaceRequirement::TextMaterialMask)
16534 .target_scale(10.0, 1.0)
16535 );
16536 }
16537
16538 #[test]
16539 fn layer_surface_requirements_use_local_surface_for_gradient_and_stroke_text() {
16540 let cases = [
16541 (
16542 "draw_style",
16543 AnnotatedString::from("draw_style"),
16544 TextStyle::from_span_style(SpanStyle {
16545 draw_style: Some(TextDrawStyle::Stroke { width: 2.0 }),
16546 ..SpanStyle::default()
16547 }),
16548 ),
16549 (
16550 "gradient_brush",
16551 AnnotatedString::from("gradient"),
16552 TextStyle::from_span_style(SpanStyle {
16553 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16554 ..SpanStyle::default()
16555 }),
16556 ),
16557 ];
16558
16559 for (label, text, text_style) in cases {
16560 let layer = text_layer_with_style(text, text_style);
16561 let requirements = layer_surface_requirements(&layer);
16562 assert!(
16563 requirements
16564 .surface_requirements
16565 .contains(SurfaceRequirement::TextMaterialMask),
16566 "{label} text should use a bounded local surface: {requirements:?}"
16567 );
16568 }
16569 }
16570
16571 #[test]
16572 fn layer_surface_requirements_use_local_surface_for_complex_text_effects() {
16573 let cases = [
16574 (
16575 "shadow",
16576 AnnotatedString::from("shadow"),
16577 TextStyle::from_span_style(SpanStyle {
16578 shadow: Some(Shadow {
16579 color: Color::BLACK,
16580 offset: Point::new(1.0, 2.0),
16581 blur_radius: 3.0,
16582 }),
16583 ..SpanStyle::default()
16584 }),
16585 ),
16586 (
16587 "background",
16588 AnnotatedString::from("background"),
16589 TextStyle::from_span_style(SpanStyle {
16590 background: Some(Color::BLACK),
16591 ..SpanStyle::default()
16592 }),
16593 ),
16594 (
16595 "baseline_shift",
16596 AnnotatedString::from("baseline_shift"),
16597 TextStyle::from_span_style(SpanStyle {
16598 baseline_shift: Some(BaselineShift::SUPERSCRIPT),
16599 ..SpanStyle::default()
16600 }),
16601 ),
16602 (
16603 "geometric_transform",
16604 AnnotatedString::from("geometric_transform"),
16605 TextStyle::from_span_style(SpanStyle {
16606 text_geometric_transform: Some(TextGeometricTransform {
16607 scale_x: 1.2,
16608 skew_x: 0.15,
16609 }),
16610 ..SpanStyle::default()
16611 }),
16612 ),
16613 (
16614 "letter_spacing",
16615 AnnotatedString::from("letter_spacing"),
16616 TextStyle::from_span_style(SpanStyle {
16617 letter_spacing: TextUnit::Em(0.2),
16618 ..SpanStyle::default()
16619 }),
16620 ),
16621 ];
16622
16623 for (label, text, text_style) in cases {
16624 let layer = text_layer_with_style(text, text_style);
16625 let requirements = layer_surface_requirements(&layer);
16626 assert!(
16627 requirements
16628 .surface_requirements
16629 .contains(SurfaceRequirement::TextMaterialMask),
16630 "{label} text should use a bounded local surface: {requirements:?}"
16631 );
16632 assert_eq!(
16633 requirements.direct_translation,
16634 Some(Point::default()),
16635 "{label} text should still classify as a direct translation"
16636 );
16637 }
16638 }
16639
16640 #[test]
16641 fn layer_surface_requirements_color_only_span_styles_use_direct_path() {
16642 let layer = text_layer_with_style(
16643 AnnotatedString {
16644 text: "styled".to_string(),
16645 span_styles: vec![RangeStyle {
16646 item: SpanStyle {
16647 color: Some(Color::BLACK),
16648 ..SpanStyle::default()
16649 },
16650 range: 0..3,
16651 }],
16652 ..AnnotatedString::default()
16653 },
16654 TextStyle::default(),
16655 );
16656 let requirements = layer_surface_requirements(&layer);
16657 assert!(
16658 !requirements
16659 .surface_requirements
16660 .contains(SurfaceRequirement::TextMaterialMask),
16661 "color-only span styles should render directly via software text raster colors"
16662 );
16663 }
16664
16665 #[test]
16666 fn layer_surface_requirements_keep_decoration_only_text_on_direct_path() {
16667 let layer = text_layer_with_style(
16668 AnnotatedString::from("decoration"),
16669 TextStyle::from_span_style(SpanStyle {
16670 text_decoration: Some(TextDecoration::UNDERLINE),
16671 ..SpanStyle::default()
16672 }),
16673 );
16674
16675 let requirements = layer_surface_requirements(&layer);
16676
16677 assert_eq!(requirements.direct_translation, Some(Point::default()));
16678 assert!(
16679 requirements
16680 .surface_requirements
16681 .contains(SurfaceRequirement::PixelStableComposite)
16682 && !requirements
16683 .surface_requirements
16684 .has_isolating_requirement(),
16685 "decoration-only text should not force an isolating layer surface: {requirements:?}"
16686 );
16687 }
16688
16689 #[test]
16690 fn direct_text_leaf_snaps_modifier_background_and_text_with_one_anchor() {
16691 let root = snapped_text_leaf_root(false, false);
16692 let mut rect_cache = HashMap::new();
16693 let mut requirements_cache = HashMap::new();
16694
16695 let collected =
16696 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16697
16698 assert_eq!(collected.scene.shapes.len(), 1);
16699 assert_eq!(collected.scene.images.len(), 1);
16700 assert_eq!(collected.scene.texts.len(), 1);
16701 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
16702 assert_eq!(collected.scene.shapes[0].snap_anchor, expected_anchor);
16703 assert_eq!(collected.scene.images[0].snap_anchor, expected_anchor);
16704 assert_eq!(collected.scene.texts[0].snap_anchor, expected_anchor);
16705 }
16706
16707 #[test]
16708 fn animated_translated_content_text_leaf_uses_bounded_content_snap() {
16709 let root = snapped_text_leaf_root(true, true);
16710 let mut rect_cache = HashMap::new();
16711 let mut requirements_cache = HashMap::new();
16712
16713 let collected =
16714 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16715
16716 assert_eq!(collected.child_layers.len(), 1);
16717 assert!(collected.scene.shapes.is_empty());
16718 assert!(collected.scene.images.is_empty());
16719 assert!(collected.scene.texts.is_empty());
16720 assert!(collected.scene.effect_layers.is_empty());
16721 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
16722 assert_eq!(
16723 collected.child_layers[0].snap_anchor, expected_anchor,
16724 "active translated leaf surface should keep the content-origin snap phase"
16725 );
16726 }
16727
16728 #[test]
16729 fn translated_content_assigns_motion_anchor_to_rotated_child_surface() {
16730 let mut child = snapped_text_leaf(false, false);
16731 child.graphics_layer.rotation_z = 5.0;
16732 child.transform_to_parent =
16733 cranpose_render_common::layer_transform::layer_transform_to_parent(
16734 child.local_bounds,
16735 Point::new(108.0, 3.0),
16736 &child.graphics_layer,
16737 );
16738 child.recompute_raster_cache_hashes();
16739 let mut root = test_layer(
16740 Rect {
16741 x: 0.0,
16742 y: 0.0,
16743 width: 320.0,
16744 height: 180.0,
16745 },
16746 vec![RenderNode::Layer(Box::new(child))],
16747 );
16748 root.translated_content_context = true;
16749 root.translated_content_offset = Point::new(0.0, -80.8);
16750 root.recompute_raster_cache_hashes();
16751 let mut rect_cache = HashMap::new();
16752 let mut requirements_cache = HashMap::new();
16753
16754 let collected =
16755 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16756
16757 assert_eq!(collected.child_layers.len(), 1);
16758 assert!(
16759 collected.child_layers[0].snap_anchor.is_some(),
16760 "a projective child still translates rigidly with its scrolling parent"
16761 );
16762 }
16763
16764 #[test]
16765 fn rested_translated_content_context_text_leaf_snaps_for_crisp_scroll_rest() {
16766 let root = snapped_text_leaf_root(false, true);
16767 let mut rect_cache = HashMap::new();
16768 let mut requirements_cache = HashMap::new();
16769
16770 let collected =
16771 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16772
16773 assert_eq!(collected.child_layers.len(), 0);
16774 assert_eq!(collected.scene.shapes.len(), 1);
16775 assert_eq!(collected.scene.images.len(), 1);
16776 assert_eq!(collected.scene.texts.len(), 1);
16777 assert_eq!(collected.scene.effect_layers.len(), 0);
16778 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
16779 assert_eq!(
16780 collected.scene.shapes[0].snap_anchor, expected_anchor,
16781 "rested scroll content should snap back to device pixels"
16782 );
16783 assert_eq!(
16784 collected.scene.images[0].snap_anchor, expected_anchor,
16785 "rested scroll images should snap back to device pixels"
16786 );
16787 assert_eq!(
16788 collected.scene.texts[0].snap_anchor, expected_anchor,
16789 "rested scroll text should snap back to device pixels"
16790 );
16791 }
16792
16793 #[test]
16794 fn complex_text_uses_local_surface() {
16795 let root = translated_content_local_surface_root();
16796 let mut rect_cache = HashMap::new();
16797 let mut requirements_cache = HashMap::new();
16798
16799 let collected =
16800 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16801
16802 assert!(
16803 !collected.child_layers.is_empty(),
16804 "translated-content effectful text should render through a bounded local surface"
16805 );
16806 assert!(collected.scene.texts.is_empty());
16807 assert!(collected.scene.shadow_draws.is_empty());
16808 }
16809
16810 #[test]
16811 fn translated_content_surface_composite_uses_scroll_content_snap_anchor() {
16812 let mut root = translated_content_local_surface_root();
16813 let scroll_offset = Point::new(0.0, -18.5);
16814 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
16815 panic!("expected translated content layer");
16816 };
16817 translated_content.translated_content_offset = scroll_offset;
16818 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
16819 panic!("expected effectful text layer");
16820 };
16821 effectful_text.transform_to_parent =
16822 effectful_text
16823 .transform_to_parent
16824 .then(ProjectiveTransform::translation(
16825 scroll_offset.x,
16826 scroll_offset.y,
16827 ));
16828
16829 let mut rect_cache = HashMap::new();
16830 let mut requirements_cache = HashMap::new();
16831 let collected =
16832 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16833
16834 assert_eq!(collected.child_layers.len(), 1);
16835 assert_eq!(
16836 collected.child_layers[0].snap_anchor,
16837 Some(SnapAnchor::rigid(Point::new(14.25, -2.0))),
16838 "isolated scrolled descendants must composite with the same content-origin snap phase"
16839 );
16840 }
16841
16842 #[test]
16843 fn animated_translated_content_surface_composite_uses_scroll_content_snap_anchor() {
16844 let mut root = translated_content_local_surface_root();
16845 let scroll_offset = Point::new(0.0, -18.5);
16846 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
16847 panic!("expected translated content layer");
16848 };
16849 translated_content.motion_context_animated = true;
16850 translated_content.translated_content_offset = scroll_offset;
16851 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
16852 panic!("expected effectful text layer");
16853 };
16854 effectful_text.transform_to_parent =
16855 effectful_text
16856 .transform_to_parent
16857 .then(ProjectiveTransform::translation(
16858 scroll_offset.x,
16859 scroll_offset.y,
16860 ));
16861
16862 let mut rect_cache = HashMap::new();
16863 let mut requirements_cache = HashMap::new();
16864 let collected =
16865 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16866
16867 assert_eq!(collected.child_layers.len(), 1);
16868 assert_eq!(
16869 collected.child_layers[0].snap_anchor,
16870 Some(SnapAnchor::rigid(Point::new(14.25, 16.5))),
16871 "animated translated content should composite the stable local surface at the viewport-origin snap phase"
16872 );
16873 }
16874
16875 #[test]
16876 fn translated_text_material_effect_layer_uses_scroll_content_snap_anchor() {
16877 let mut layer = text_layer_with_style(
16878 AnnotatedString::from("gradient"),
16879 TextStyle::from_span_style(SpanStyle {
16880 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16881 ..SpanStyle::default()
16882 }),
16883 );
16884 layer.translated_content_context = true;
16885 layer.translated_content_offset = Point::new(0.0, -18.5);
16886 let mut rect_cache = HashMap::new();
16887 let mut requirements_cache = HashMap::new();
16888
16889 let collected =
16890 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
16891
16892 assert_eq!(collected.scene.effect_layers.len(), 1);
16893 assert_eq!(
16894 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
16895 CompositeSampleMode::Box4
16896 );
16897 assert_eq!(
16898 collected.scene.effect_layers[0].snap_anchor,
16899 Some(SnapAnchor::rigid(Point::new(0.0, -18.5))),
16900 "text material surfaces must composite with the scroll content-origin snap phase"
16901 );
16902 }
16903
16904 #[test]
16905 fn translated_layer_surface_capture_does_not_restart_local_picture_for_shadow_text() {
16906 let mut layer = text_layer_with_style(
16907 AnnotatedString::from("shadow"),
16908 TextStyle::from_span_style(SpanStyle {
16909 shadow: Some(Shadow {
16910 color: Color::BLACK,
16911 offset: Point::new(1.0, 2.0),
16912 blur_radius: 3.0,
16913 }),
16914 ..SpanStyle::default()
16915 }),
16916 );
16917 layer.translated_content_context = true;
16918 let mut rect_cache = HashMap::new();
16919 let mut requirements_cache = HashMap::new();
16920
16921 let collected = collect_layer_contents_with_translation_context(
16922 &layer,
16923 None,
16924 None,
16925 TranslationRenderContext {
16926 inherited_content_translation: false,
16927 surface_capture_active: true,
16928 local_picture_capture_active: true,
16929 ..TranslationRenderContext::default()
16930 },
16931 &mut rect_cache,
16932 &mut requirements_cache,
16933 );
16934
16935 assert!(
16936 collected.scene.effect_layers.is_empty(),
16937 "a translated layer surface already provides the stable local capture"
16938 );
16939 assert_eq!(collected.scene.shadow_draws.len(), 1);
16940 assert_eq!(collected.scene.texts.len(), 1);
16941 assert!(
16942 !collected.scene.texts[0].translated_content_context,
16943 "text inside an active motion-stable capture must raster in capture-local coordinates"
16944 );
16945 }
16946
16947 #[test]
16948 fn translated_layer_surface_capture_keeps_only_material_effect_layers() {
16949 let mut layer = text_layer_with_style(
16950 AnnotatedString::from("gradient"),
16951 TextStyle::from_span_style(SpanStyle {
16952 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16953 ..SpanStyle::default()
16954 }),
16955 );
16956 layer.translated_content_context = true;
16957 let mut rect_cache = HashMap::new();
16958 let mut requirements_cache = HashMap::new();
16959
16960 let collected = collect_layer_contents_with_translation_context(
16961 &layer,
16962 None,
16963 None,
16964 TranslationRenderContext {
16965 inherited_content_translation: false,
16966 surface_capture_active: true,
16967 local_picture_capture_active: true,
16968 ..TranslationRenderContext::default()
16969 },
16970 &mut rect_cache,
16971 &mut requirements_cache,
16972 );
16973
16974 assert_eq!(collected.scene.effect_layers.len(), 1);
16975 assert!(
16976 collected.scene.effect_layers[0]
16977 .requirements
16978 .contains(SurfaceRequirement::MotionStableCapture),
16979 "translated text materials still need motion-stable resolve semantics inside a stable capture"
16980 );
16981 assert_eq!(
16982 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
16983 CompositeSampleMode::Box4
16984 );
16985 assert_eq!(
16986 effect_layer_target_scale(&collected.scene.effect_layers[0], 10.0),
16987 10.0
16988 );
16989 assert!(collected.scene.effect_layers[0].effect.is_some());
16990 }
16991
16992 #[test]
16993 fn translated_viewport_surface_does_not_add_plain_local_picture_capture() {
16994 let mut layer = text_layer_with_style(
16995 AnnotatedString::from("shadow"),
16996 TextStyle::from_span_style(SpanStyle {
16997 shadow: Some(Shadow {
16998 color: Color::BLACK,
16999 offset: Point::new(1.0, 2.0),
17000 blur_radius: 3.0,
17001 }),
17002 ..SpanStyle::default()
17003 }),
17004 );
17005 layer.translated_content_context = true;
17006 layer.motion_context_animated = true;
17007 let mut rect_cache = HashMap::new();
17008 let mut requirements_cache = HashMap::new();
17009
17010 let collected = collect_layer_contents_with_translation_context(
17011 &layer,
17012 None,
17013 None,
17014 TranslationRenderContext {
17015 surface_capture_active: true,
17016 ..TranslationRenderContext::default()
17017 },
17018 &mut rect_cache,
17019 &mut requirements_cache,
17020 );
17021
17022 assert_eq!(
17023 collected.scene.effect_layers.len(),
17024 0,
17025 "plain translated content inside a viewport surface should not be captured again"
17026 );
17027 assert_eq!(collected.scene.shadow_draws.len(), 1);
17028 assert_eq!(collected.scene.texts.len(), 1);
17029 }
17030
17031 #[test]
17032 fn static_pure_text_leaf_snaps_without_sibling_draw_primitives() {
17033 let root = pure_text_leaf_root(false, false);
17034 let mut rect_cache = HashMap::new();
17035 let mut requirements_cache = HashMap::new();
17036
17037 let collected =
17038 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17039
17040 assert_eq!(collected.scene.texts.len(), 1);
17041 assert!(
17042 collected.scene.texts[0].snap_anchor.is_some(),
17043 "idle pure text leaves should participate in rigid snap anchoring"
17044 );
17045 }
17046
17047 #[test]
17048 fn animated_pure_text_leaf_stays_unsnapped() {
17049 let root = pure_text_leaf_root(true, false);
17050 let mut rect_cache = HashMap::new();
17051 let mut requirements_cache = HashMap::new();
17052
17053 let collected =
17054 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17055
17056 assert_eq!(collected.scene.texts.len(), 1);
17057 assert_eq!(collected.scene.texts[0].snap_anchor, None);
17058 }
17059
17060 #[test]
17061 fn animated_translated_pure_text_uses_bounded_content_snap() {
17062 let root = pure_text_leaf_root(true, true);
17063 let mut rect_cache = HashMap::new();
17064 let mut requirements_cache = HashMap::new();
17065
17066 let collected =
17067 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17068
17069 assert_eq!(collected.child_layers.len(), 1);
17070 assert!(collected.scene.texts.is_empty());
17071 assert!(collected.scene.effect_layers.is_empty());
17072 assert_snap_anchor_close(
17073 collected.child_layers[0].snap_anchor,
17074 Point::new(11.4, 23.6),
17075 "animated translated pure text should use the bounded content snap phase",
17076 );
17077 }
17078
17079 #[test]
17080 fn rested_translated_pure_text_leaf_snaps_for_crisp_scroll_rest() {
17081 let root = pure_text_leaf_root(false, true);
17082 let mut rect_cache = HashMap::new();
17083 let mut requirements_cache = HashMap::new();
17084
17085 let collected =
17086 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17087
17088 assert_eq!(collected.child_layers.len(), 0);
17089 assert_eq!(collected.scene.texts.len(), 1);
17090 assert_eq!(collected.scene.effect_layers.len(), 0);
17091 assert_snap_anchor_close(
17092 collected.scene.texts[0].snap_anchor,
17093 Point::new(11.4, 23.6),
17094 "rested translated text should snap to device pixels",
17095 );
17096 }
17097
17098 #[test]
17099 fn static_gpu_effect_text_leaf_stays_unsnapped() {
17100 let root = text_layer_with_style(
17101 AnnotatedString::from("Gradient"),
17102 TextStyle::from_span_style(SpanStyle {
17103 brush: Some(Brush::linear_gradient(vec![
17104 Color(0.2, 0.8, 1.0, 1.0),
17105 Color(1.0, 0.7, 0.4, 1.0),
17106 ])),
17107 draw_style: Some(TextDrawStyle::Stroke { width: 2.5 }),
17108 ..SpanStyle::default()
17109 }),
17110 );
17111 let mut rect_cache = HashMap::new();
17112 let mut requirements_cache = HashMap::new();
17113
17114 let collected =
17115 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17116
17117 assert_eq!(collected.scene.texts.len(), 1);
17118 assert_eq!(
17119 collected.scene.texts[0].snap_anchor, None,
17120 "gpu text-effect leaves must not take the rigid text snap path"
17121 );
17122 assert_eq!(
17123 collected.scene.effect_layers.len(),
17124 1,
17125 "gradient stroke text should still emit a runtime shader effect layer"
17126 );
17127 }
17128
17129 #[test]
17130 fn layer_surface_requirements_keep_shape_plus_direct_child_on_direct_path() {
17131 let mut child = test_layer(
17132 Rect {
17133 x: 0.0,
17134 y: 0.0,
17135 width: 40.0,
17136 height: 20.0,
17137 },
17138 vec![RenderNode::Primitive(PrimitiveEntry {
17139 phase: PrimitivePhase::BeforeChildren,
17140 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17141 primitive: DrawPrimitive::Rect {
17142 rect: Rect {
17143 x: 0.0,
17144 y: 0.0,
17145 width: 40.0,
17146 height: 20.0,
17147 },
17148 brush: Brush::solid(Color::WHITE),
17149 stroke: None,
17150 },
17151 clip: None,
17152 }),
17153 })],
17154 );
17155 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
17156
17157 let layer = test_layer(
17158 Rect {
17159 x: 0.0,
17160 y: 0.0,
17161 width: 64.0,
17162 height: 32.0,
17163 },
17164 vec![
17165 RenderNode::Primitive(PrimitiveEntry {
17166 phase: PrimitivePhase::BeforeChildren,
17167 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17168 primitive: DrawPrimitive::Rect {
17169 rect: Rect {
17170 x: 0.0,
17171 y: 0.0,
17172 width: 64.0,
17173 height: 32.0,
17174 },
17175 brush: Brush::solid(Color::BLACK),
17176 stroke: None,
17177 },
17178 clip: None,
17179 }),
17180 }),
17181 RenderNode::Layer(Box::new(child)),
17182 ],
17183 );
17184
17185 let requirements = layer_surface_requirements(&layer);
17186
17187 assert_eq!(requirements.direct_translation, Some(Point::default()));
17188 assert!(!requirements
17189 .surface_requirements
17190 .contains(SurfaceRequirement::MixedDirectContent));
17191 assert!(!requirements
17192 .surface_requirements
17193 .has_isolating_requirement());
17194 }
17195
17196 #[test]
17197 fn collect_layer_contents_translates_direct_text_rects_into_parent_space() {
17198 let mut child = text_layer_with_style(
17199 AnnotatedString::from("direct"),
17200 TextStyle::from_span_style(SpanStyle {
17201 text_decoration: Some(TextDecoration::UNDERLINE),
17202 ..SpanStyle::default()
17203 }),
17204 );
17205 child.transform_to_parent = ProjectiveTransform::translation(9.0, 7.0);
17206
17207 let parent = test_layer(
17208 Rect {
17209 x: 0.0,
17210 y: 0.0,
17211 width: 64.0,
17212 height: 32.0,
17213 },
17214 vec![RenderNode::Layer(Box::new(child))],
17215 );
17216
17217 let mut rect_cache = HashMap::new();
17218 let mut requirements_cache = HashMap::new();
17219 let collected = with_test_app_context(|| {
17220 collect_layer_contents(
17221 &parent,
17222 None,
17223 None,
17224 &mut rect_cache,
17225 &mut requirements_cache,
17226 )
17227 });
17228
17229 assert!(
17230 collected.child_layers.is_empty(),
17231 "decoration-only text child should collapse directly into the parent scene"
17232 );
17233 assert_eq!(collected.scene.texts.len(), 1, "expected one text draw");
17234 let text = &collected.scene.texts[0];
17235 assert!(
17236 text.rect.x >= 9.0 && text.rect.y >= 7.0,
17237 "collapsed text rect should be translated into parent space, got {:?}",
17238 text.rect
17239 );
17240 assert!(
17241 collected
17242 .scene
17243 .shapes
17244 .iter()
17245 .any(|shape| shape.rect.y >= 7.0),
17246 "collapsed underline geometry should also be translated into parent space"
17247 );
17248 }
17249
17250 #[test]
17251 fn normalized_scene_keeps_lazy_after_bound_text_for_prewarm() {
17252 use std::cell::RefCell;
17253
17254 fn collect_graph_text_labels(layer: &LayerNode, labels: &mut Vec<String>) {
17255 for child in &layer.children {
17256 match child {
17257 RenderNode::Primitive(PrimitiveEntry {
17258 node: PrimitiveNode::Text(text),
17259 ..
17260 }) => labels.push(text.text.text.clone()),
17261 RenderNode::Layer(child_layer) => {
17262 collect_graph_text_labels(child_layer, labels)
17263 }
17264 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
17265 }
17266 }
17267 }
17268
17269 let state_holder: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
17270 let state_holder_for_comp = state_holder.clone();
17271 let mut composition = cranpose_ui::run_test_composition(move || {
17272 let list_state = remember_lazy_list_state();
17273 *state_holder_for_comp.borrow_mut() = Some(list_state);
17274 let mut spec = LazyColumnSpec::new()
17275 .vertical_arrangement(cranpose_ui::LinearArrangement::SpacedBy(6.0));
17276 spec.beyond_bounds_item_count = 0;
17277 LazyColumn(Modifier::empty().height(96.0), list_state, spec, |scope| {
17278 scope.items(
17279 12,
17280 None::<fn(usize) -> u64>,
17281 None::<fn(usize) -> u64>,
17282 |index| {
17283 Text(
17284 format!("WarmRow {index}"),
17285 Modifier::empty().height(32.0),
17286 TextStyle::default(),
17287 );
17288 },
17289 );
17290 });
17291 });
17292
17293 let list_state = (*state_holder.borrow()).expect("lazy list state should be captured");
17294 list_state.scroll_to_item(4, 0.0);
17295
17296 let root = composition.root().expect("lazy column root");
17297 let handle = composition.runtime_handle();
17298 let mut applier = composition.applier_mut();
17299 applier.set_runtime_handle(handle);
17300 let _ = applier
17301 .compute_layout(
17302 root,
17303 Size {
17304 width: 240.0,
17305 height: 240.0,
17306 },
17307 )
17308 .expect("lazy column layout");
17309 let graph = build_graph_from_applier(&mut applier, root, 1.0).expect("lazy column graph");
17310 applier.clear_runtime_handle();
17311 let mut graph_labels = Vec::new();
17312 collect_graph_text_labels(&graph.root, &mut graph_labels);
17313
17314 let visible_indices: Vec<_> = list_state
17315 .layout_info()
17316 .visible_items_info
17317 .iter()
17318 .map(|item| item.index)
17319 .collect();
17320 assert_eq!(
17321 visible_indices,
17322 vec![4, 5, 6],
17323 "test setup expects exactly three viewport-visible rows"
17324 );
17325
17326 let mut rect_cache = HashMap::new();
17327 let mut requirements_cache = HashMap::new();
17328 let collected = with_test_app_context(|| {
17329 collect_layer_contents(
17330 &graph.root,
17331 None,
17332 None,
17333 &mut rect_cache,
17334 &mut requirements_cache,
17335 )
17336 });
17337 let root_text_labels: Vec<_> = collected
17338 .scene
17339 .texts
17340 .iter()
17341 .map(|text| text.text.text.clone())
17342 .collect();
17343 let child_layer_count = collected.child_layers.len();
17344 let warm_text = collected
17345 .scene
17346 .texts
17347 .iter()
17348 .find(|text| text.text.text == "WarmRow 7")
17349 .unwrap_or_else(|| {
17350 panic!(
17351 "after-bound lazy text should reach WGPU scene collection; graph_texts={graph_labels:?} root_texts={root_text_labels:?} child_layers={child_layer_count}"
17352 )
17353 });
17354
17355 assert!(
17356 warm_text.rect.y >= 96.0,
17357 "after-bound text should be below the viewport, got {:?}",
17358 warm_text.rect
17359 );
17360 assert_eq!(
17361 visible_draw_rect(warm_text.rect, warm_text.clip),
17362 None,
17363 "after-bound text should remain clipped away for drawing while staying available for glyph prewarm"
17364 );
17365 assert!(
17366 text_draw_should_prewarm_in_viewport(
17367 warm_text.rect,
17368 warm_text.clip,
17369 ViewportUniformParams {
17370 width: 240,
17371 height: 96,
17372 offset: [0.0, 0.0],
17373 },
17374 1.0,
17375 ),
17376 "after-bound text inside the warm window must be selected by WGPU prewarm"
17377 );
17378 }
17379
17380 #[test]
17381 fn direct_translation_accepts_nearly_identity_axis_scale_noise() {
17382 let local_bounds = Rect {
17383 x: 0.0,
17384 y: 0.0,
17385 width: 393.3,
17386 height: 16.8,
17387 };
17388 let quad = [
17389 [10.0, 78.399_994],
17390 [403.3, 78.399_994],
17391 [10.0, 95.2],
17392 [403.3, 95.2],
17393 ];
17394 let transform = ProjectiveTransform::from_rect_to_quad(local_bounds, quad);
17395
17396 assert_eq!(
17397 direct_translation(transform),
17398 Some(Point::new(10.0, 78.399_994)),
17399 );
17400 }
17401
17402 #[test]
17403 fn layer_surface_requirements_keep_shape_plus_isolating_child_as_mixed_content() {
17404 let mut child = test_layer(
17405 Rect {
17406 x: 0.0,
17407 y: 0.0,
17408 width: 24.0,
17409 height: 18.0,
17410 },
17411 vec![RenderNode::Primitive(PrimitiveEntry {
17412 phase: PrimitivePhase::BeforeChildren,
17413 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17414 primitive: DrawPrimitive::Rect {
17415 rect: Rect {
17416 x: 0.0,
17417 y: 0.0,
17418 width: 24.0,
17419 height: 18.0,
17420 },
17421 brush: Brush::solid(Color::WHITE),
17422 stroke: None,
17423 },
17424 clip: None,
17425 }),
17426 })],
17427 );
17428 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
17429 child.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
17430
17431 let layer = test_layer(
17432 Rect {
17433 x: 0.0,
17434 y: 0.0,
17435 width: 64.0,
17436 height: 32.0,
17437 },
17438 vec![
17439 RenderNode::Primitive(PrimitiveEntry {
17440 phase: PrimitivePhase::BeforeChildren,
17441 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17442 primitive: DrawPrimitive::Rect {
17443 rect: Rect {
17444 x: 0.0,
17445 y: 0.0,
17446 width: 64.0,
17447 height: 32.0,
17448 },
17449 brush: Brush::solid(Color::BLACK),
17450 stroke: None,
17451 },
17452 clip: None,
17453 }),
17454 }),
17455 RenderNode::Layer(Box::new(child)),
17456 ],
17457 );
17458
17459 let requirements = layer_surface_requirements(&layer);
17460
17461 assert!(requirements
17462 .surface_requirements
17463 .contains(SurfaceRequirement::MixedDirectContent));
17464 assert!(!requirements
17465 .surface_requirements
17466 .has_isolating_requirement());
17467 }
17468
17469 #[test]
17470 fn build_scene_window_filters_and_translates_items() {
17471 let mut shape = test_shape(6, BlendMode::SrcOver);
17472 shape.rect.x = 12.0;
17473 shape.rect.y = 25.0;
17474 shape.local_rect.x = 12.0;
17475 shape.local_rect.y = 25.0;
17476 shape.quad = [[12.0, 25.0], [20.0, 25.0], [12.0, 33.0], [20.0, 33.0]];
17477 shape.clip = Some(Rect {
17478 x: 11.0,
17479 y: 24.0,
17480 width: 10.0,
17481 height: 10.0,
17482 });
17483
17484 let mut image = test_image(8, BlendMode::SrcOver);
17485 image.rect.x = 18.0;
17486 image.rect.y = 27.0;
17487 image.local_rect.x = 18.0;
17488 image.local_rect.y = 27.0;
17489 image.quad = [[18.0, 27.0], [26.0, 27.0], [18.0, 35.0], [26.0, 35.0]];
17490
17491 let mut text = test_text(9);
17492 text.rect.x = 16.0;
17493 text.rect.y = 29.0;
17494 text.clip = Some(Rect {
17495 x: 15.0,
17496 y: 28.0,
17497 width: 9.0,
17498 height: 6.0,
17499 });
17500
17501 let mut shadow_shape = test_shape(7, BlendMode::SrcOver);
17502 shadow_shape.rect.x = 14.0;
17503 shadow_shape.rect.y = 26.0;
17504 shadow_shape.local_rect.x = 14.0;
17505 shadow_shape.local_rect.y = 26.0;
17506 shadow_shape.quad = [[14.0, 26.0], [22.0, 26.0], [14.0, 34.0], [22.0, 34.0]];
17507 let mut shadow = test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]);
17508 shadow.z_index = 7;
17509
17510 let mut nested_effect = effect_layer(6, 10);
17511 nested_effect.rect.x = 13.0;
17512 nested_effect.rect.y = 24.0;
17513 nested_effect.clip = Some(Rect {
17514 x: 15.0,
17515 y: 25.0,
17516 width: 4.0,
17517 height: 5.0,
17518 });
17519
17520 let mut nested_backdrop = backdrop_layer(8);
17521 nested_backdrop.rect.x = 17.0;
17522 nested_backdrop.rect.y = 26.0;
17523 nested_backdrop.clip = Some(Rect {
17524 x: 18.0,
17525 y: 27.0,
17526 width: 3.0,
17527 height: 4.0,
17528 });
17529
17530 let window = build_scene_window(
17531 SceneWindowSource {
17532 shapes: &[test_shape(4, BlendMode::SrcOver), shape],
17533 images: &[image],
17534 texts: &[text],
17535 shadow_draws: &[shadow],
17536 draw_ops: &[],
17537 effect_layers: &[effect_layer(2, 4), nested_effect.clone()],
17538 backdrop_layers: &[backdrop_layer(4), nested_backdrop.clone()],
17539 },
17540 5,
17541 10,
17542 Rect {
17543 x: 10.0,
17544 y: 20.0,
17545 width: 20.0,
17546 height: 20.0,
17547 },
17548 );
17549
17550 assert_eq!(window.shapes.len(), 1);
17551 assert_eq!(
17552 window.shapes[0].rect,
17553 Rect {
17554 x: 2.0,
17555 y: 5.0,
17556 width: 8.0,
17557 height: 8.0,
17558 }
17559 );
17560 assert_eq!(
17561 window.shapes[0].clip,
17562 Some(Rect {
17563 x: 1.0,
17564 y: 4.0,
17565 width: 10.0,
17566 height: 10.0,
17567 })
17568 );
17569 assert_eq!(window.images.len(), 1);
17570 assert_eq!(window.images[0].rect.x, 8.0);
17571 assert_eq!(window.images[0].rect.y, 7.0);
17572 assert_eq!(window.texts.len(), 1);
17573 assert_eq!(window.texts[0].rect.x, 6.0);
17574 assert_eq!(window.texts[0].rect.y, 9.0);
17575 assert_eq!(
17576 window.texts[0].clip,
17577 Some(Rect {
17578 x: 5.0,
17579 y: 8.0,
17580 width: 9.0,
17581 height: 6.0,
17582 })
17583 );
17584 assert_eq!(window.shadow_draws.len(), 1);
17585 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.x, 4.0);
17586 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.y, 6.0);
17587 assert_eq!(window.effect_layers.len(), 1);
17588 assert_eq!(
17589 window.effect_layers[0].rect,
17590 Rect {
17591 x: 3.0,
17592 y: 4.0,
17593 width: 10.0,
17594 height: 10.0,
17595 }
17596 );
17597 assert_eq!(
17598 window.effect_layers[0].clip,
17599 Some(Rect {
17600 x: 5.0,
17601 y: 5.0,
17602 width: 4.0,
17603 height: 5.0,
17604 })
17605 );
17606 assert_eq!(window.backdrop_layers.len(), 1);
17607 assert_eq!(
17608 window.backdrop_layers[0].rect,
17609 Rect {
17610 x: 7.0,
17611 y: 6.0,
17612 width: 10.0,
17613 height: 10.0,
17614 }
17615 );
17616 assert_eq!(
17617 window.backdrop_layers[0].clip,
17618 Some(Rect {
17619 x: 8.0,
17620 y: 7.0,
17621 width: 3.0,
17622 height: 4.0,
17623 })
17624 );
17625 }
17626
17627 #[test]
17628 fn filtered_effect_layer_index_counts_only_window_members() {
17629 let effects = vec![
17630 effect_layer(0, 2),
17631 effect_layer(5, 12),
17632 effect_layer(6, 10),
17633 effect_layer(14, 20),
17634 ];
17635
17636 assert_eq!(filtered_effect_layer_index(&effects, 1, 5, 12), Some(0));
17637 assert_eq!(filtered_effect_layer_index(&effects, 2, 5, 12), Some(1));
17638 assert_eq!(filtered_effect_layer_index(&effects, 3, 5, 12), None);
17639 }
17640
17641 #[test]
17642 fn blend_mode_support_matrix_is_explicit() {
17643 assert!(is_blend_mode_supported(BlendMode::SrcOver));
17644 assert!(is_blend_mode_supported(BlendMode::DstOut));
17645 assert!(!is_blend_mode_supported(BlendMode::Clear));
17646 assert!(!is_blend_mode_supported(BlendMode::Multiply));
17647 }
17648
17649 #[test]
17650 fn collect_non_effect_segment_items_preserves_global_z_order() {
17651 let shapes = vec![
17652 test_shape(3, BlendMode::SrcOver),
17653 test_shape(1, BlendMode::DstOut),
17654 ];
17655 let images = vec![test_image(2, BlendMode::SrcOver)];
17656 let texts = vec![test_text(0)];
17657 let shadows: Vec<ShadowDraw> = Vec::new();
17658 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
17659
17660 let mut scratch = Vec::new();
17661 collect_non_effect_segment_items(
17662 &shapes,
17663 &images,
17664 &texts,
17665 &shadows,
17666 &draw_ops,
17667 0,
17668 4,
17669 &[],
17670 100,
17671 100,
17672 1.0,
17673 &mut scratch,
17674 );
17675 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
17676 assert_eq!(
17677 items,
17678 vec![
17679 SegmentDrawItem::Text(0),
17680 SegmentDrawItem::Shape(1),
17681 SegmentDrawItem::Image(0),
17682 SegmentDrawItem::Shape(0),
17683 ]
17684 );
17685 }
17686
17687 #[test]
17688 fn collect_non_effect_segment_items_filters_effect_ranges() {
17689 let shapes = vec![
17690 test_shape(1, BlendMode::SrcOver),
17691 test_shape(3, BlendMode::DstOut),
17692 ];
17693 let images = vec![test_image(2, BlendMode::SrcOver)];
17694 let texts = vec![test_text(4)];
17695 let shadows: Vec<ShadowDraw> = Vec::new();
17696 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
17697 let effect_ranges = [std::ops::Range { start: 2, end: 4 }];
17698
17699 let mut scratch = Vec::new();
17700 collect_non_effect_segment_items(
17701 &shapes,
17702 &images,
17703 &texts,
17704 &shadows,
17705 &draw_ops,
17706 0,
17707 5,
17708 &effect_ranges,
17709 100,
17710 100,
17711 1.0,
17712 &mut scratch,
17713 );
17714 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
17715 assert_eq!(
17716 items,
17717 vec![SegmentDrawItem::Shape(0), SegmentDrawItem::Text(0)]
17718 );
17719 }
17720
17721 #[test]
17722 fn collect_non_effect_segment_items_culls_offscreen_shapes_but_keeps_text_prewarm() {
17723 let mut shape = test_shape(0, BlendMode::SrcOver);
17724 shape.rect.y = 160.0;
17725 shape.local_rect.y = 160.0;
17726 shape.quad = [[0.0, 160.0], [8.0, 160.0], [0.0, 168.0], [8.0, 168.0]];
17727
17728 let shapes = vec![shape];
17729 let images = Vec::new();
17730 let mut text = test_text(1);
17731 text.rect.y = 160.0;
17732 let texts = vec![text];
17733 let shadows: Vec<ShadowDraw> = Vec::new();
17734 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
17735
17736 let mut scratch = Vec::new();
17737 collect_non_effect_segment_items(
17738 &shapes,
17739 &images,
17740 &texts,
17741 &shadows,
17742 &draw_ops,
17743 0,
17744 2,
17745 &[],
17746 100,
17747 100,
17748 1.0,
17749 &mut scratch,
17750 );
17751
17752 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
17753 assert_eq!(items, vec![SegmentDrawItem::Text(0)]);
17754 }
17755
17756 #[test]
17757 fn segment_command_iter_merges_non_conflicting_batches_into_one_chunk() {
17758 let ordered_items = vec![
17759 (0, SegmentDrawItem::Shape(0)),
17760 (1, SegmentDrawItem::Image(0)),
17761 (2, SegmentDrawItem::Text(0)),
17762 ];
17763 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
17764 let images = vec![test_image(1, BlendMode::DstOut)];
17765
17766 let commands: Vec<_> = SegmentCommandIter::new(
17767 &ordered_items,
17768 &shapes,
17769 &images,
17770 ShapeBatchLimits::desktop(),
17771 )
17772 .collect();
17773
17774 assert_eq!(
17775 commands,
17776 vec![SegmentRenderCommand::DrawChunk(chunk(&[
17777 SegmentBatchPlan::Shape {
17778 start: 0,
17779 end: 1,
17780 blend_mode: BlendMode::SrcOver,
17781 },
17782 SegmentBatchPlan::Image {
17783 start: 1,
17784 end: 2,
17785 blend_mode: BlendMode::DstOut,
17786 },
17787 SegmentBatchPlan::Text { start: 2, end: 3 },
17788 ]))]
17789 );
17790 }
17791
17792 #[test]
17793 fn segment_command_iter_keeps_layer_composites_in_ordered_draw_chunk() {
17794 let ordered_items = vec![
17795 (0, SegmentDrawItem::Shape(0)),
17796 (1, SegmentDrawItem::Composite(0)),
17797 (2, SegmentDrawItem::Image(0)),
17798 (3, SegmentDrawItem::Composite(1)),
17799 (4, SegmentDrawItem::Text(0)),
17800 ];
17801 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
17802 let images = vec![test_image(2, BlendMode::SrcOver)];
17803
17804 let commands: Vec<_> = SegmentCommandIter::new(
17805 &ordered_items,
17806 &shapes,
17807 &images,
17808 ShapeBatchLimits::desktop(),
17809 )
17810 .collect();
17811
17812 assert_eq!(
17813 commands,
17814 vec![SegmentRenderCommand::DrawChunk(chunk(&[
17815 SegmentBatchPlan::Shape {
17816 start: 0,
17817 end: 1,
17818 blend_mode: BlendMode::SrcOver,
17819 },
17820 SegmentBatchPlan::Composite { start: 1, end: 2 },
17821 SegmentBatchPlan::Image {
17822 start: 2,
17823 end: 3,
17824 blend_mode: BlendMode::SrcOver,
17825 },
17826 SegmentBatchPlan::Composite { start: 3, end: 4 },
17827 SegmentBatchPlan::Text { start: 4, end: 5 },
17828 ]))]
17829 );
17830 }
17831
17832 #[test]
17833 fn retain_renderable_shadow_items_culls_invisible_shadow_boundaries() {
17834 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
17835 let images = vec![test_image(2, BlendMode::SrcOver)];
17836 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
17837 shadow_shape.rect = Rect {
17838 x: 500.0,
17839 y: 500.0,
17840 width: 12.0,
17841 height: 12.0,
17842 };
17843 let shadow_draws = vec![ShadowDraw {
17844 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
17845 texts: Vec::new(),
17846 blur_radius: 8.0,
17847 clip: None,
17848 z_index: 1,
17849 }];
17850 let mut ordered_items = vec![
17851 (0, SegmentDrawItem::Shape(0)),
17852 (1, SegmentDrawItem::Shadow(0)),
17853 (2, SegmentDrawItem::Image(0)),
17854 ];
17855
17856 let culled =
17857 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
17858 let commands: Vec<_> = SegmentCommandIter::new(
17859 &ordered_items,
17860 &shapes,
17861 &images,
17862 ShapeBatchLimits::desktop(),
17863 )
17864 .collect();
17865
17866 assert_eq!(culled, 1);
17867 assert_eq!(
17868 commands,
17869 vec![SegmentRenderCommand::DrawChunk(chunk(&[
17870 SegmentBatchPlan::Shape {
17871 start: 0,
17872 end: 1,
17873 blend_mode: BlendMode::SrcOver,
17874 },
17875 SegmentBatchPlan::Image {
17876 start: 1,
17877 end: 2,
17878 blend_mode: BlendMode::SrcOver,
17879 },
17880 ]))]
17881 );
17882 }
17883
17884 #[test]
17885 fn retain_renderable_shadow_items_keeps_visible_shadow_boundaries() {
17886 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
17887 shadow_shape.rect = Rect {
17888 x: 20.0,
17889 y: 20.0,
17890 width: 12.0,
17891 height: 12.0,
17892 };
17893 let shadow_draws = vec![ShadowDraw {
17894 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
17895 texts: Vec::new(),
17896 blur_radius: 8.0,
17897 clip: None,
17898 z_index: 1,
17899 }];
17900 let mut ordered_items = vec![(1, SegmentDrawItem::Shadow(0))];
17901
17902 let culled =
17903 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
17904
17905 assert_eq!(culled, 0);
17906 assert_eq!(ordered_items, vec![(1, SegmentDrawItem::Shadow(0))]);
17907 }
17908
17909 #[test]
17910 fn shape_data_layout_matches_the_wgsl_mirror() {
17911 assert_eq!(std::mem::size_of::<ShapeData>(), 160);
17915 assert_eq!(std::mem::size_of::<ShapeData>() % 16, 0);
17916 assert_eq!(std::mem::size_of::<GradientStop>(), 32);
17917 }
17918
17919 #[test]
17920 fn shape_flags_pack_kind_cap_and_join_without_collision() {
17921 assert_eq!(
17922 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
17923 0.0
17924 );
17925 assert_eq!(
17926 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
17927 1.0
17928 );
17929 assert_eq!(
17930 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter),
17931 2.0
17932 );
17933 assert_eq!(
17935 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Round, StrokeJoin::Miter),
17936 2.0 + 4.0
17937 );
17938 assert_eq!(
17939 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Square, StrokeJoin::Miter),
17940 2.0 + 8.0
17941 );
17942 assert_eq!(
17943 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Round),
17944 1.0 + 16.0
17945 );
17946 assert_eq!(
17947 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Bevel),
17948 1.0 + 32.0
17949 );
17950 for kind in [SHAPE_KIND_FILL, SHAPE_KIND_STROKE, SHAPE_KIND_ARC] {
17952 for cap in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square] {
17953 for join in [StrokeJoin::Miter, StrokeJoin::Round, StrokeJoin::Bevel] {
17954 let packed = pack_shape_flags(kind, cap, join);
17955 let bits = packed as u32;
17956 assert_eq!(bits & 3, kind);
17957 assert_eq!((bits >> 2) & 3, stroke_cap_code(cap));
17958 assert_eq!((bits >> 4) & 3, stroke_join_code(join));
17959 assert_eq!(packed, bits as f32, "flags must be exact in f32");
17960 }
17961 }
17962 }
17963 }
17964
17965 #[cfg(not(target_arch = "wasm32"))]
17966 #[test]
17967 fn mesh_vertex_layout_matches_the_wgsl_input() {
17968 assert_eq!(std::mem::size_of::<MeshVertex>(), 20);
17971 }
17972
17973 #[cfg(not(target_arch = "wasm32"))]
17977 #[allow(clippy::too_many_arguments)]
17978 fn sdf_arc_band_reference(
17979 p: [f32; 2],
17980 center: [f32; 2],
17981 inner: f32,
17982 outer: f32,
17983 mid_sin_cos: [f32; 2],
17984 half_sin_cos: [f32; 2],
17985 cap: u32,
17986 ) -> f32 {
17987 let ra = (outer + inner) * 0.5;
17988 let rb = ((outer - inner) * 0.5).max(0.0);
17989 let sm = mid_sin_cos[0];
17990 let cm = mid_sin_cos[1];
17991 let d = [p[0] - center[0], p[1] - center[1]];
17992 let mut q = [-sm * d[0] + cm * d[1], cm * d[0] + sm * d[1]];
17993 q[0] = q[0].abs();
17994 let sc = half_sin_cos;
17995 let mut dist = if sc[1] * q[0] > sc[0] * q[1] {
17996 let dx = q[0] - sc[0] * ra;
17997 let dy = q[1] - sc[1] * ra;
17998 (dx * dx + dy * dy).sqrt() - rb
17999 } else {
18000 ((q[0] * q[0] + q[1] * q[1]).sqrt() - ra).abs() - rb
18001 };
18002 let plane = sc[1] * q[0] - sc[0] * q[1];
18003 if cap == 0 {
18005 dist = dist.max(plane);
18006 } else if cap == 2 {
18007 dist = dist.max(plane - rb);
18008 }
18009 dist
18010 }
18011
18012 #[cfg(not(target_arch = "wasm32"))]
18013 fn point_in_triangle(p: [f64; 2], tri: &[[f64; 2]; 3]) -> bool {
18014 let side = |a: [f64; 2], b: [f64; 2]| {
18015 (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
18016 };
18017 let d0 = side(tri[0], tri[1]);
18018 let d1 = side(tri[1], tri[2]);
18019 let d2 = side(tri[2], tri[0]);
18020 let has_neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
18021 let has_pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
18022 !(has_neg && has_pos)
18023 }
18024
18025 #[cfg(not(target_arch = "wasm32"))]
18026 fn converted_arc_shape(arc: cranpose_ui_graphics::ArcGeometry, root_scale: f32) -> ShapeData {
18027 let bounds = arc.bounds();
18028 let mut shape = test_shape(0, BlendMode::SrcOver);
18029 shape.rect = bounds;
18030 shape.local_rect = bounds;
18031 shape.quad = [
18032 [bounds.x, bounds.y],
18033 [bounds.x + bounds.width, bounds.y],
18034 [bounds.x, bounds.y + bounds.height],
18035 [bounds.x + bounds.width, bounds.y + bounds.height],
18036 ];
18037 shape.arc = Some(arc);
18038 let mut converted = ShapeData::zeroed();
18039 convert_shape_into_slots(&shape, root_scale, 0, &mut converted, &mut []);
18040 converted
18041 }
18042
18043 #[cfg(not(target_arch = "wasm32"))]
18048 #[test]
18049 fn arc_mesh_contains_every_band_pixel() {
18050 use cranpose_ui_graphics::ArcGeometry;
18051 let tau = cranpose_ui_graphics::TAU;
18052 let center = Point::new(250.0, 250.0);
18053 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
18054 (90.0, 100.0, 0.0, tau, StrokeCap::Round),
18056 (80.0, 100.0, 1.0, 10.0, StrokeCap::Butt),
18058 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
18060 (30.0, 80.0, 0.7, 2.5, StrokeCap::Butt),
18062 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
18063 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
18064 (99.0, 101.0, 3.0, 4.0, StrokeCap::Round),
18066 (0.6, 2.0, 0.3, 1.2, StrokeCap::Butt),
18068 (1900.0, 1904.0, 0.1, 0.35, StrokeCap::Square),
18070 (40.0, 60.0, 5.0, 1e-3, StrokeCap::Round),
18072 (40.0, 60.0, 0.2, tau - 1e-3, StrokeCap::Butt),
18074 (0.0, 3.0, 1.0, 2.0, StrokeCap::Round),
18076 (20.0, 60.0, 4.5, 1.9, StrokeCap::Butt),
18078 ];
18079 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
18080 for root_scale in [1.0f32, 2.0, 2.75] {
18085 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
18086 assert!(!arc.is_degenerate(), "case {case} must be drawable");
18087 let converted = converted_arc_shape(arc, root_scale);
18088 let band = arc_mesh_band(&converted)
18089 .unwrap_or_else(|| panic!("case {case} must qualify for meshing"));
18090 let mut vertices = Vec::new();
18091 let mut indices = Vec::new();
18092 let segments =
18093 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
18094 .unwrap_or_else(|| panic!("case {case} must produce a mesh"));
18095 assert!(segments >= ARC_MESH_MIN_SEGMENTS);
18096 let position = |index: u32| {
18099 let p = vertices[index as usize].position;
18100 [p[0] as f64, p[1] as f64]
18101 };
18102 let triangles: Vec<[[f64; 2]; 3]> = indices
18103 .chunks_exact(3)
18104 .map(|tri| [position(tri[0]), position(tri[1]), position(tri[2])])
18105 .collect();
18106
18107 let [qx, qy, ..] = converted.quad01;
18111 let [_, _, qr, qb] = converted.quad23;
18112 let (rw, rh) = (qr - qx, qb - qy);
18113 let cap_bits = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
18114 let step = (rw.max(rh) / 400.0).clamp(0.25, 2.0);
18115 let mut band_points = 0usize;
18116 let mut y = qy;
18117 while y <= qb {
18118 let mut x = qx;
18119 while x <= qr {
18120 let dist = sdf_arc_band_reference(
18121 [x, y],
18122 [converted.arc_params[0], converted.arc_params[1]],
18123 converted.stroke_params[3],
18124 converted.stroke_params[2],
18125 [converted.radii[0], converted.radii[1]],
18126 [converted.radii[2], converted.radii[3]],
18127 cap_bits,
18128 );
18129 if dist <= 0.5 {
18130 band_points += 1;
18131 let p = [x as f64, y as f64];
18132 assert!(
18133 triangles.iter().any(|tri| point_in_triangle(p, tri)),
18134 "case {case} scale {root_scale}: band point ({x}, {y}) \
18135 dist {dist} escapes the mesh"
18136 );
18137 }
18138 x += step;
18139 }
18140 y += step;
18141 }
18142 assert!(
18143 band_points > 0,
18144 "case {case} scale {root_scale}: the sampling grid never hit the band"
18145 );
18146 }
18147 }
18148 }
18149
18150 #[cfg(not(target_arch = "wasm32"))]
18151 #[test]
18152 fn arc_mesh_passthrough_replicates_the_quad_expansion() {
18153 let shape = test_shape(0, BlendMode::SrcOver);
18154 let mut converted = ShapeData::zeroed();
18155 convert_shape_into_slots(&shape, 1.0, 0, &mut converted, &mut []);
18156 let build =
18157 build_arc_mesh_vertices(std::slice::from_ref(&converted)).expect("within budget");
18158 assert_eq!(build.meshed_arcs, 0);
18159 assert_eq!(build.passthrough, 1);
18160 assert_eq!(build.vertices.len(), 4);
18162 assert_eq!(build.index_prefix, vec![0, 6]);
18163 assert_eq!(build.indices, vec![0, 1, 2, 2, 1, 3]);
18164 let corners = [
18165 ([converted.quad01[0], converted.quad01[1]], [0.0f32, 0.0]),
18166 ([converted.quad01[2], converted.quad01[3]], [1.0, 0.0]),
18167 ([converted.quad23[0], converted.quad23[1]], [0.0, 1.0]),
18168 ([converted.quad23[2], converted.quad23[3]], [1.0, 1.0]),
18169 ];
18170 for (vertex, corner) in build.vertices.iter().zip(corners) {
18171 assert_eq!(vertex.position, corner.0);
18172 assert_eq!(vertex.uv, corner.1);
18173 assert_eq!(vertex.shape_idx, 0);
18174 }
18175 for (index, corner) in build.indices.iter().zip([0usize, 1, 2, 2, 1, 3]) {
18178 assert_eq!(build.vertices[*index as usize].position, corners[corner].0);
18179 assert_eq!(build.vertices[*index as usize].uv, corners[corner].1);
18180 }
18181 }
18182
18183 #[cfg(not(target_arch = "wasm32"))]
18189 #[test]
18190 fn arc_mesh_indices_share_boundary_vertices_and_wrap_closed_rings() {
18191 use cranpose_ui_graphics::ArcGeometry;
18192 let tau = cranpose_ui_graphics::TAU;
18193 for (sweep, closed) in [(tau, true), (1.9f32, false)] {
18196 let arc = ArcGeometry::new(
18197 Point::new(250.0, 250.0),
18198 80.0,
18199 100.0,
18200 0.7,
18201 sweep,
18202 StrokeCap::Round,
18203 );
18204 let mut converted = converted_arc_shape(arc, 1.0);
18205 converted.rect = [0.0, 0.0, 500.0, 500.0];
18209 converted.quad01 = [0.0, 0.0, 500.0, 0.0];
18210 converted.quad23 = [0.0, 500.0, 500.0, 500.0];
18211 let band = arc_mesh_band(&converted).expect("arc must qualify");
18212 let mut vertices = Vec::new();
18213 let mut indices = Vec::new();
18214 let segments = emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
18215 .expect("arc must mesh");
18216 let boundary_count = if closed { segments } else { segments + 1 };
18217 assert_eq!(
18218 vertices.len(),
18219 2 * boundary_count,
18220 "closed={closed}: every boundary owns exactly one (inner, outer) pair"
18221 );
18222 assert_eq!(indices.len(), 6 * segments);
18223 for j in 0..segments {
18227 let jb = (j + 1) % boundary_count;
18228 let (in_a, out_a) = (2 * j as u32, 2 * j as u32 + 1);
18229 let (in_b, out_b) = (2 * jb as u32, 2 * jb as u32 + 1);
18230 assert_eq!(
18231 indices[6 * j..6 * j + 6],
18232 [in_a, out_a, out_b, in_a, out_b, in_b],
18233 "closed={closed}: segment {j} must share its boundary pairs"
18234 );
18235 }
18236 if closed {
18237 assert_eq!(indices[6 * segments - 1], 0);
18240 }
18241 for pair in vertices.chunks_exact(2) {
18245 let radius = |v: &MeshVertex| {
18246 let dx = v.position[0] - 250.0;
18247 let dy = v.position[1] - 250.0;
18248 (dx * dx + dy * dy).sqrt()
18249 };
18250 assert!(radius(&pair[0]) < radius(&pair[1]));
18251 }
18252 }
18253 }
18254
18255 #[cfg(not(target_arch = "wasm32"))]
18261 #[test]
18262 fn arc_mesh_clipped_segments_fan_over_private_vertices() {
18263 use cranpose_ui_graphics::ArcGeometry;
18264 let arc = ArcGeometry::new(
18265 Point::new(250.0, 250.0),
18266 80.0,
18267 100.0,
18268 0.0,
18269 cranpose_ui_graphics::TAU,
18270 StrokeCap::Round,
18271 );
18272 let converted = converted_arc_shape(arc, 1.0);
18273 let band = arc_mesh_band(&converted).expect("ring must qualify");
18274 let mut vertices = Vec::new();
18275 let mut indices = Vec::new();
18276 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
18277 .expect("ring must mesh");
18278 let mut uses = vec![0usize; vertices.len()];
18281 for &index in &indices {
18282 uses[index as usize] += 1;
18283 }
18284 assert!(
18285 uses.iter().any(|&count| count >= 3),
18286 "some boundary vertices must be shared across trapezoids"
18287 );
18288 let [left, top, ..] = converted.quad01;
18294 let [.., right, bottom] = converted.quad23;
18295 let clipped: Vec<&MeshVertex> = vertices
18296 .iter()
18297 .filter(|vertex| {
18298 let [x, y] = vertex.position;
18299 x == left || x == right || y == top || y == bottom
18300 })
18301 .collect();
18302 assert!(
18303 !clipped.is_empty(),
18304 "the tight box must clip the pushed-out chord vertices"
18305 );
18306 assert!(
18309 vertices.len() < indices.len(),
18310 "{} unique vertices should undercut {} triangle corners",
18311 vertices.len(),
18312 indices.len()
18313 );
18314 }
18315
18316 #[cfg(not(target_arch = "wasm32"))]
18317 #[test]
18318 fn arc_mesh_budget_overflow_falls_back_to_whole_slot_passthrough() {
18319 use cranpose_ui_graphics::ArcGeometry;
18320 let arc = ArcGeometry::new(
18325 Point::new(2000.0, 2000.0),
18326 1690.0,
18327 1710.0,
18328 0.0,
18329 cranpose_ui_graphics::TAU,
18330 StrokeCap::Round,
18331 );
18332 let converted = converted_arc_shape(arc, 1.0);
18333 let shapes = vec![converted; 100];
18334 assert!(build_arc_mesh_vertices(&shapes).is_none());
18335 }
18336
18337 #[cfg(not(target_arch = "wasm32"))]
18338 #[test]
18339 fn shape_batch_limits_follow_uniform_binding_size() {
18340 let desktop_shapes = 65536 / std::mem::size_of::<ShapeData>();
18343 assert_eq!(desktop_shapes, 409);
18344 assert_eq!(
18345 ShapeBatchLimits::desktop(),
18346 ShapeBatchLimits {
18347 max_shapes_per_batch: desktop_shapes.min(MAX_SHAPES_PER_BATCH),
18348 max_gradient_stops: MAX_GRADIENT_STOPS,
18349 storage: false,
18350 }
18351 );
18352
18353 let downlevel = ShapeBatchLimits::for_uniform_binding_size(16384);
18356 assert_eq!(downlevel.max_shapes_per_batch, 16384 / 160);
18357 assert_eq!(downlevel.max_shapes_per_batch, 102);
18358 assert_eq!(downlevel.max_gradient_stops, 512.min(MAX_GRADIENT_STOPS));
18359 assert!(downlevel.max_shapes_per_batch * std::mem::size_of::<ShapeData>() <= 16384);
18360 assert!(downlevel.max_gradient_stops * std::mem::size_of::<GradientStop>() <= 16384);
18361
18362 let tiny = ShapeBatchLimits::for_uniform_binding_size(1);
18364 assert_eq!(tiny.max_shapes_per_batch, 1);
18365 assert_eq!(tiny.max_gradient_stops, 1);
18366 }
18367
18368 #[test]
18369 fn storage_shape_batch_limits_uncap_the_batch_and_start_small() {
18370 let storage = ShapeBatchLimits::for_storage_binding_size(128 << 20);
18373 assert!(storage.storage);
18374 assert_eq!(storage.max_shapes_per_batch, MAX_SHAPES_PER_STORAGE_BATCH);
18375 assert_eq!(
18376 storage.max_gradient_stops,
18377 MAX_GRADIENT_STOPS_PER_STORAGE_BATCH
18378 );
18379
18380 assert_eq!(
18383 storage.initial_shape_capacity(),
18384 INITIAL_STORAGE_BATCH_CAPACITY
18385 );
18386 assert_eq!(
18387 storage.initial_gradient_capacity(),
18388 INITIAL_STORAGE_BATCH_CAPACITY
18389 );
18390 assert_eq!(
18391 storage.data_binding_type(),
18392 wgpu::BufferBindingType::Storage { read_only: true }
18393 );
18394 assert!(storage
18395 .data_buffer_usage()
18396 .contains(wgpu::BufferUsages::STORAGE));
18397
18398 let uniform = ShapeBatchLimits::desktop();
18401 assert_eq!(
18402 uniform.initial_shape_capacity(),
18403 uniform.max_shapes_per_batch
18404 );
18405 assert_eq!(
18406 uniform.initial_gradient_capacity(),
18407 uniform.max_gradient_stops
18408 );
18409 assert_eq!(
18410 uniform.data_binding_type(),
18411 wgpu::BufferBindingType::Uniform
18412 );
18413 assert!(uniform
18414 .data_buffer_usage()
18415 .contains(wgpu::BufferUsages::UNIFORM));
18416 }
18417
18418 #[test]
18419 fn storage_shape_shader_swaps_the_arrays_to_runtime_sized_storage() {
18420 let source = shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20));
18421 assert!(
18422 source.contains("var<storage, read> shape_data: array<ShapeData>;"),
18423 "storage-mode shader must declare a runtime-sized shape array"
18424 );
18425 assert!(
18426 source.contains("var<storage, read> gradient_stops: array<GradientStop>;"),
18427 "storage-mode shader must declare a runtime-sized gradient array"
18428 );
18429 assert!(
18430 !source.contains("var<uniform> shape_data"),
18431 "the uniform shape declaration must be fully replaced"
18432 );
18433 assert!(
18434 !source.contains("var<uniform> gradient_stops"),
18435 "the uniform gradient declaration must be fully replaced"
18436 );
18437 assert!(
18438 source.contains("var<storage, read> paint: array<vec4<f32>>;"),
18439 "storage-mode shader must declare the retained paint array"
18440 );
18441 assert!(
18442 source.contains("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)"),
18443 "storage-mode shader must read paint under the paint_select flag"
18444 );
18445 assert!(
18446 source.contains("fn vs_mesh("),
18447 "the storage rewrite must leave the retained-mesh vertex entry intact"
18448 );
18449 assert!(
18450 source.contains("fn vs_shape_instanced("),
18451 "the storage rewrite must leave the instanced-quad vertex entry intact"
18452 );
18453 assert_eq!(
18454 source
18455 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
18456 .count(),
18457 3,
18458 "vs_main, vs_shape_instanced and vs_mesh must all read paint under \
18459 the paint_select flag (meshless retained draws ride the instanced \
18460 entry when the selection is latched on)"
18461 );
18462
18463 let module = naga::front::wgsl::parse_str(&source)
18466 .expect("storage-mode shape shader must parse as WGSL");
18467 naga::valid::Validator::new(
18468 naga::valid::ValidationFlags::all(),
18469 naga::valid::Capabilities::all(),
18470 )
18471 .validate(&module)
18472 .expect("storage-mode shape shader must validate for WebGPU");
18473 }
18474
18475 #[test]
18476 fn uniform_shape_shader_keeps_the_in_record_color_and_no_paint_binding() {
18477 for source in [
18481 Cow::Borrowed(shaders::SHADER),
18482 shape_shader_source(ShapeBatchLimits::desktop()),
18483 ] {
18484 assert!(
18485 !source.contains("paint: array"),
18486 "the uniform variant must not declare a paint array"
18487 );
18488 assert!(
18489 source.contains("output.color = shape.color;"),
18490 "the uniform variant must read the color from ShapeData \
18491 (this literal is also what `shape_shader_source` rewrites)"
18492 );
18493 assert!(
18494 source.contains("paint_select: f32"),
18495 "SimilarityTransform must name the flag field in both \
18496 variants; the Rust mirror is Pod and uploads raw bytes"
18497 );
18498 }
18499 }
18500
18501 #[test]
18502 fn shipped_shape_shader_array_length_fits_the_downlevel_uniform_floor() {
18503 assert!(
18507 shaders::SHADER.contains("array<ShapeData, 102>"),
18508 "shape.wgsl array length must stay in sync with \
18509 `shape_shader_source`'s replace string and MAX_SHAPES_PER_BATCH"
18510 );
18511 assert!(102 * std::mem::size_of::<ShapeData>() <= 16384);
18512 assert!(103 * std::mem::size_of::<ShapeData>() > 16384);
18513 }
18514
18515 #[test]
18516 fn glyph_atlas_doubles_on_overflow_and_stops_at_the_device_ceiling() {
18517 assert_eq!(
18520 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MIN_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
18521 1024
18522 );
18523 assert_eq!(
18524 next_glyph_atlas_size(2048, TEXT_GLYPH_ATLAS_MAX_SIZE),
18525 TEXT_GLYPH_ATLAS_MAX_SIZE
18526 );
18527 assert_eq!(
18528 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MAX_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
18529 TEXT_GLYPH_ATLAS_MAX_SIZE
18530 );
18531
18532 assert_eq!(next_glyph_atlas_size(1024, 2048), 2048);
18535 assert_eq!(next_glyph_atlas_size(2048, 2048), 2048);
18536
18537 assert_eq!(next_glyph_atlas_size(u32::MAX, 4096), 4096);
18539 assert_eq!(next_glyph_atlas_size(0, 0), 1);
18540 }
18541
18542 #[test]
18543 fn glyph_atlas_uv_rect_normalizes_against_the_atlas_it_was_placed_in() {
18544 let entry = GlyphAtlasEntry {
18548 x: 128,
18549 y: 256,
18550 width: 16,
18551 height: 32,
18552 };
18553
18554 let small = glyph_atlas_uv_rect(entry, 512);
18555 let large = glyph_atlas_uv_rect(entry, 4096);
18556
18557 assert_eq!(small.min, [128.0 / 512.0, 256.0 / 512.0]);
18558 assert_eq!(large.min, [128.0 / 4096.0, 256.0 / 4096.0]);
18559 assert_eq!(small.max, [144.0 / 512.0, 288.0 / 512.0]);
18560 assert_eq!(large.max, [144.0 / 4096.0, 288.0 / 4096.0]);
18561 }
18562
18563 #[test]
18564 fn native_shape_shader_source_uses_native_batch_limits() {
18565 let limits = ShapeBatchLimits::desktop();
18566 let source = shape_shader_source(limits);
18567
18568 assert!(source.contains(&format!(
18569 "array<ShapeData, {}>",
18570 limits.max_shapes_per_batch
18571 )));
18572 assert!(source.contains(&format!(
18573 "array<GradientStop, {}>",
18574 limits.max_gradient_stops
18575 )));
18576 assert!(!source.contains("array<ShapeData, 146>"));
18579 }
18580
18581 #[test]
18582 fn stroked_and_arc_shapes_batch_together_with_fills() {
18583 let fill = test_shape(0, BlendMode::SrcOver);
18589 let mut stroked = test_shape(1, BlendMode::SrcOver);
18590 stroked.stroke = Some(
18591 cranpose_ui_graphics::Stroke::new(3.0)
18592 .with_cap(StrokeCap::Round)
18593 .with_join(StrokeJoin::Bevel),
18594 );
18595 let mut arc = test_shape(2, BlendMode::SrcOver);
18596 arc.arc = Some(cranpose_ui_graphics::ArcGeometry::new(
18597 Point::new(4.0, 4.0),
18598 2.0,
18599 4.0,
18600 0.0,
18601 1.0,
18602 StrokeCap::Round,
18603 ));
18604 let trailing_fill = test_shape(3, BlendMode::SrcOver);
18605
18606 assert!(!fill.has_stroke_or_arc());
18607 assert!(stroked.has_stroke_or_arc());
18608 assert!(arc.has_stroke_or_arc());
18609 assert!(!trailing_fill.has_stroke_or_arc());
18610
18611 let shapes = vec![fill, stroked, arc, trailing_fill];
18612 let ordered_items: Vec<_> = (0..shapes.len())
18613 .map(|index| (index, SegmentDrawItem::Shape(index)))
18614 .collect();
18615 let images = Vec::new();
18616
18617 let commands: Vec<_> = SegmentCommandIter::new(
18618 &ordered_items,
18619 &shapes,
18620 &images,
18621 ShapeBatchLimits::desktop(),
18622 )
18623 .collect();
18624
18625 assert_eq!(
18626 commands,
18627 vec![SegmentRenderCommand::DrawChunk(chunk(&[
18628 SegmentBatchPlan::Shape {
18629 start: 0,
18630 end: 4,
18631 blend_mode: BlendMode::SrcOver,
18632 }
18633 ]))],
18634 "mixed fill/stroke/arc runs must stay one batch"
18635 );
18636 }
18637
18638 #[cfg(not(target_arch = "wasm32"))]
18639 #[test]
18640 fn native_segment_fusion_budget_allows_small_interleaved_chunks() {
18641 let ordered_items = vec![
18642 (0, SegmentDrawItem::Shape(0)),
18643 (1, SegmentDrawItem::Image(0)),
18644 (2, SegmentDrawItem::Text(0)),
18645 (3, SegmentDrawItem::Shape(1)),
18646 ];
18647 let shapes = vec![
18648 test_shape(0, BlendMode::SrcOver),
18649 test_shape(3, BlendMode::DstOut),
18650 ];
18651 let segment = chunk(&[
18652 SegmentBatchPlan::Shape {
18653 start: 0,
18654 end: 1,
18655 blend_mode: BlendMode::SrcOver,
18656 },
18657 SegmentBatchPlan::Image {
18658 start: 1,
18659 end: 2,
18660 blend_mode: BlendMode::SrcOver,
18661 },
18662 SegmentBatchPlan::Text { start: 2, end: 3 },
18663 SegmentBatchPlan::Shape {
18664 start: 3,
18665 end: 4,
18666 blend_mode: BlendMode::DstOut,
18667 },
18668 ]);
18669
18670 let budget = native_segment_fusion_budget(
18671 &ordered_items,
18672 &shapes,
18673 &segment,
18674 ShapeBatchLimits::desktop(),
18675 )
18676 .expect("budget should be valid")
18677 .expect("chunk should fit native fusion budget");
18678
18679 assert_eq!(
18680 budget,
18681 NativeSegmentFusionBudget {
18682 shape_count: 2,
18683 gradient_stop_count: 0,
18684 }
18685 );
18686 }
18687
18688 #[cfg(not(target_arch = "wasm32"))]
18689 #[test]
18690 fn native_segment_fusion_budget_rejects_shape_uniform_overflow() {
18691 let ordered_items: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
18692 .map(|index| (index, SegmentDrawItem::Shape(index)))
18693 .collect();
18694 let shapes: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
18695 .map(|index| test_shape(index, BlendMode::SrcOver))
18696 .collect();
18697 let segment = chunk(&[
18698 SegmentBatchPlan::Shape {
18699 start: 0,
18700 end: MAX_SHAPES_PER_BATCH,
18701 blend_mode: BlendMode::SrcOver,
18702 },
18703 SegmentBatchPlan::Shape {
18704 start: MAX_SHAPES_PER_BATCH,
18705 end: MAX_SHAPES_PER_BATCH + 1,
18706 blend_mode: BlendMode::SrcOver,
18707 },
18708 ]);
18709
18710 let budget = native_segment_fusion_budget(
18711 &ordered_items,
18712 &shapes,
18713 &segment,
18714 ShapeBatchLimits::desktop(),
18715 )
18716 .expect("valid plan");
18717
18718 assert_eq!(budget, None);
18719 }
18720
18721 #[cfg(not(target_arch = "wasm32"))]
18722 #[test]
18723 fn native_segment_fusion_budget_rejects_gradient_uniform_overflow() {
18724 let ordered_items = vec![(0, SegmentDrawItem::Shape(0))];
18725 let mut shape = test_shape(0, BlendMode::SrcOver);
18726 shape.brush = Brush::linear_gradient(vec![Color::BLACK; MAX_GRADIENT_STOPS + 1]);
18727 let shapes = vec![shape];
18728 let segment = chunk(&[SegmentBatchPlan::Shape {
18729 start: 0,
18730 end: 1,
18731 blend_mode: BlendMode::SrcOver,
18732 }]);
18733
18734 let budget = native_segment_fusion_budget(
18735 &ordered_items,
18736 &shapes,
18737 &segment,
18738 ShapeBatchLimits::desktop(),
18739 )
18740 .expect("valid plan");
18741
18742 assert_eq!(budget, None);
18743 }
18744
18745 #[cfg(not(target_arch = "wasm32"))]
18746 #[test]
18747 fn native_segment_fusion_partitions_shape_uniform_overflow() {
18748 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
18751 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
18752 .map(|index| (index, SegmentDrawItem::Shape(index)))
18753 .collect();
18754 let shapes: Vec<_> = (0..=desktop_batch_cap)
18755 .map(|index| test_shape(index, BlendMode::SrcOver))
18756 .collect();
18757 let segment = chunk(&[
18758 SegmentBatchPlan::Shape {
18759 start: 0,
18760 end: desktop_batch_cap,
18761 blend_mode: BlendMode::SrcOver,
18762 },
18763 SegmentBatchPlan::Shape {
18764 start: desktop_batch_cap,
18765 end: desktop_batch_cap + 1,
18766 blend_mode: BlendMode::SrcOver,
18767 },
18768 ]);
18769
18770 let partitions = native_segment_fusion_partitions(
18771 &ordered_items,
18772 &shapes,
18773 &segment,
18774 ShapeBatchLimits::desktop(),
18775 )
18776 .expect("valid plan")
18777 .expect("overflowing segment should be partitionable");
18778
18779 assert_eq!(partitions.len(), 2);
18780 assert_eq!(
18781 partitions[0],
18782 NativeSegmentFusionPartition {
18783 chunk: chunk(&[SegmentBatchPlan::Shape {
18784 start: 0,
18785 end: desktop_batch_cap,
18786 blend_mode: BlendMode::SrcOver,
18787 }]),
18788 budget: NativeSegmentFusionBudget {
18789 shape_count: desktop_batch_cap,
18790 gradient_stop_count: 0,
18791 },
18792 }
18793 );
18794 assert_eq!(
18795 partitions[1],
18796 NativeSegmentFusionPartition {
18797 chunk: chunk(&[SegmentBatchPlan::Shape {
18798 start: desktop_batch_cap,
18799 end: desktop_batch_cap + 1,
18800 blend_mode: BlendMode::SrcOver,
18801 }]),
18802 budget: NativeSegmentFusionBudget {
18803 shape_count: 1,
18804 gradient_stop_count: 0,
18805 },
18806 }
18807 );
18808 }
18809
18810 #[cfg(not(target_arch = "wasm32"))]
18811 #[test]
18812 fn native_segment_fusion_partitions_gradient_uniform_overflow() {
18813 const STOPS_PER_SHAPE: usize = MAX_GRADIENT_STOPS / 2;
18814 let ordered_items = vec![
18815 (0, SegmentDrawItem::Shape(0)),
18816 (1, SegmentDrawItem::Shape(1)),
18817 (2, SegmentDrawItem::Shape(2)),
18818 ];
18819 let mut shapes = Vec::new();
18820 for index in 0..3 {
18821 let mut shape = test_shape(index, BlendMode::SrcOver);
18822 shape.brush = Brush::linear_gradient(vec![Color::BLACK; STOPS_PER_SHAPE]);
18823 shapes.push(shape);
18824 }
18825 let segment = chunk(&[SegmentBatchPlan::Shape {
18826 start: 0,
18827 end: 3,
18828 blend_mode: BlendMode::SrcOver,
18829 }]);
18830
18831 let partitions = native_segment_fusion_partitions(
18832 &ordered_items,
18833 &shapes,
18834 &segment,
18835 ShapeBatchLimits::desktop(),
18836 )
18837 .expect("valid plan")
18838 .expect("overflowing gradient segment should be partitionable");
18839
18840 assert_eq!(partitions.len(), 2);
18841 assert_eq!(
18842 partitions[0],
18843 NativeSegmentFusionPartition {
18844 chunk: chunk(&[SegmentBatchPlan::Shape {
18845 start: 0,
18846 end: 2,
18847 blend_mode: BlendMode::SrcOver,
18848 }]),
18849 budget: NativeSegmentFusionBudget {
18850 shape_count: 2,
18851 gradient_stop_count: MAX_GRADIENT_STOPS,
18852 },
18853 }
18854 );
18855 assert_eq!(
18856 partitions[1],
18857 NativeSegmentFusionPartition {
18858 chunk: chunk(&[SegmentBatchPlan::Shape {
18859 start: 2,
18860 end: 3,
18861 blend_mode: BlendMode::SrcOver,
18862 }]),
18863 budget: NativeSegmentFusionBudget {
18864 shape_count: 1,
18865 gradient_stop_count: STOPS_PER_SHAPE,
18866 },
18867 }
18868 );
18869 }
18870
18871 #[cfg(not(target_arch = "wasm32"))]
18872 #[test]
18873 fn native_segment_fusion_accepts_layer_composite_chunks() {
18874 let ordered_items = vec![
18875 (0, SegmentDrawItem::Shape(0)),
18876 (1, SegmentDrawItem::Composite(0)),
18877 (2, SegmentDrawItem::ShaderComposite(0)),
18878 (3, SegmentDrawItem::Shape(1)),
18879 ];
18880 let shapes = vec![
18881 test_shape(0, BlendMode::SrcOver),
18882 test_shape(1, BlendMode::SrcOver),
18883 ];
18884 let segment = chunk(&[
18885 SegmentBatchPlan::Shape {
18886 start: 0,
18887 end: 1,
18888 blend_mode: BlendMode::SrcOver,
18889 },
18890 SegmentBatchPlan::Composite { start: 1, end: 2 },
18891 SegmentBatchPlan::ShaderComposite { start: 2, end: 3 },
18892 SegmentBatchPlan::Shape {
18893 start: 3,
18894 end: 4,
18895 blend_mode: BlendMode::SrcOver,
18896 },
18897 ]);
18898
18899 let partitions = native_segment_fusion_partitions(
18900 &ordered_items,
18901 &shapes,
18902 &segment,
18903 ShapeBatchLimits::desktop(),
18904 )
18905 .expect("valid plan")
18906 .expect("composites are drawable inside the native fused pass");
18907
18908 assert_eq!(
18909 partitions,
18910 vec![NativeSegmentFusionPartition {
18911 chunk: segment,
18912 budget: NativeSegmentFusionBudget {
18913 shape_count: 2,
18914 gradient_stop_count: 0,
18915 },
18916 }],
18917 "layer composites and shader composites must preserve order without forcing separate render passes"
18918 );
18919 }
18920
18921 #[cfg(not(target_arch = "wasm32"))]
18922 #[test]
18923 fn native_segment_fusion_partitions_preserve_non_shape_order_at_budget_boundary() {
18924 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
18927 let ordered_items: Vec<_> = (0..desktop_batch_cap)
18928 .map(|index| (index, SegmentDrawItem::Shape(index)))
18929 .chain([
18930 (desktop_batch_cap, SegmentDrawItem::Image(0)),
18931 (
18932 desktop_batch_cap + 1,
18933 SegmentDrawItem::Shape(desktop_batch_cap),
18934 ),
18935 ])
18936 .collect();
18937 let shapes: Vec<_> = (0..=desktop_batch_cap)
18938 .map(|index| test_shape(index, BlendMode::SrcOver))
18939 .collect();
18940 let segment = chunk(&[
18941 SegmentBatchPlan::Shape {
18942 start: 0,
18943 end: desktop_batch_cap,
18944 blend_mode: BlendMode::SrcOver,
18945 },
18946 SegmentBatchPlan::Image {
18947 start: desktop_batch_cap,
18948 end: desktop_batch_cap + 1,
18949 blend_mode: BlendMode::SrcOver,
18950 },
18951 SegmentBatchPlan::Shape {
18952 start: desktop_batch_cap + 1,
18953 end: desktop_batch_cap + 2,
18954 blend_mode: BlendMode::SrcOver,
18955 },
18956 ]);
18957
18958 let partitions = native_segment_fusion_partitions(
18959 &ordered_items,
18960 &shapes,
18961 &segment,
18962 ShapeBatchLimits::desktop(),
18963 )
18964 .expect("valid plan")
18965 .expect("overflowing segment should be partitionable");
18966
18967 assert_eq!(partitions.len(), 2);
18968 assert_eq!(
18969 partitions[0].chunk,
18970 chunk(&[
18971 SegmentBatchPlan::Shape {
18972 start: 0,
18973 end: desktop_batch_cap,
18974 blend_mode: BlendMode::SrcOver,
18975 },
18976 SegmentBatchPlan::Image {
18977 start: desktop_batch_cap,
18978 end: desktop_batch_cap + 1,
18979 blend_mode: BlendMode::SrcOver,
18980 },
18981 ])
18982 );
18983 assert_eq!(
18984 partitions[1].chunk,
18985 chunk(&[SegmentBatchPlan::Shape {
18986 start: desktop_batch_cap + 1,
18987 end: desktop_batch_cap + 2,
18988 blend_mode: BlendMode::SrcOver,
18989 }])
18990 );
18991 }
18992
18993 #[test]
18994 fn segment_command_iter_keeps_repeated_batch_kinds_in_one_chunk() {
18995 let ordered_items = vec![
18996 (0, SegmentDrawItem::Shape(0)),
18997 (1, SegmentDrawItem::Image(0)),
18998 (2, SegmentDrawItem::Shape(1)),
18999 ];
19000 let shapes = vec![
19001 test_shape(0, BlendMode::SrcOver),
19002 test_shape(2, BlendMode::DstOut),
19003 ];
19004 let images = vec![test_image(1, BlendMode::SrcOver)];
19005
19006 let commands: Vec<_> = SegmentCommandIter::new(
19007 &ordered_items,
19008 &shapes,
19009 &images,
19010 ShapeBatchLimits::desktop(),
19011 )
19012 .collect();
19013
19014 assert_eq!(
19015 commands,
19016 vec![SegmentRenderCommand::DrawChunk(chunk(&[
19017 SegmentBatchPlan::Shape {
19018 start: 0,
19019 end: 1,
19020 blend_mode: BlendMode::SrcOver,
19021 },
19022 SegmentBatchPlan::Image {
19023 start: 1,
19024 end: 2,
19025 blend_mode: BlendMode::SrcOver,
19026 },
19027 SegmentBatchPlan::Shape {
19028 start: 2,
19029 end: 3,
19030 blend_mode: BlendMode::DstOut,
19031 },
19032 ]))]
19033 );
19034 }
19035
19036 #[test]
19037 fn segment_command_iter_splits_contiguous_shape_runs_at_uniform_batch_limit() {
19038 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
19041 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
19042 .map(|index| (index, SegmentDrawItem::Shape(index)))
19043 .collect();
19044 let shapes: Vec<_> = (0..=desktop_batch_cap)
19045 .map(|index| test_shape(index, BlendMode::SrcOver))
19046 .collect();
19047 let images = Vec::new();
19048
19049 let commands: Vec<_> = SegmentCommandIter::new(
19050 &ordered_items,
19051 &shapes,
19052 &images,
19053 ShapeBatchLimits::desktop(),
19054 )
19055 .collect();
19056
19057 assert_eq!(
19058 commands,
19059 vec![SegmentRenderCommand::DrawChunk(chunk(&[
19060 SegmentBatchPlan::Shape {
19061 start: 0,
19062 end: desktop_batch_cap,
19063 blend_mode: BlendMode::SrcOver,
19064 },
19065 SegmentBatchPlan::Shape {
19066 start: desktop_batch_cap,
19067 end: desktop_batch_cap + 1,
19068 blend_mode: BlendMode::SrcOver,
19069 },
19070 ]))]
19071 );
19072 }
19073
19074 #[test]
19075 fn segment_command_iter_keeps_shadows_as_explicit_boundaries() {
19076 let ordered_items = vec![
19077 (0, SegmentDrawItem::Shape(0)),
19078 (1, SegmentDrawItem::Shadow(0)),
19079 (2, SegmentDrawItem::Image(0)),
19080 (3, SegmentDrawItem::Text(0)),
19081 ];
19082 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
19083 let images = vec![test_image(2, BlendMode::SrcOver)];
19084
19085 let commands: Vec<_> = SegmentCommandIter::new(
19086 &ordered_items,
19087 &shapes,
19088 &images,
19089 ShapeBatchLimits::desktop(),
19090 )
19091 .collect();
19092
19093 assert_eq!(
19094 commands,
19095 vec![
19096 SegmentRenderCommand::DrawChunk(chunk(&[SegmentBatchPlan::Shape {
19097 start: 0,
19098 end: 1,
19099 blend_mode: BlendMode::SrcOver,
19100 }])),
19101 SegmentRenderCommand::Shadow(0),
19102 SegmentRenderCommand::DrawChunk(chunk(&[
19103 SegmentBatchPlan::Image {
19104 start: 2,
19105 end: 3,
19106 blend_mode: BlendMode::SrcOver,
19107 },
19108 SegmentBatchPlan::Text { start: 3, end: 4 },
19109 ])),
19110 ]
19111 );
19112 }
19113
19114 #[test]
19115 fn staged_buffer_uploads_align_new_copies_to_copy_buffer_alignment() {
19116 let mut uploads = StagedBufferUploads::default();
19117 uploads.bytes.extend_from_slice(&[1, 2]);
19118
19119 uploads.stage(UploadTarget::ImageIndex, &[3, 4, 5, 6]);
19120
19121 assert_eq!(uploads.bytes, vec![1, 2, 0, 0, 3, 4, 5, 6]);
19122 assert_eq!(
19123 uploads.copies,
19124 vec![PendingBufferCopy {
19125 source_offset: 4,
19126 target_offset: 0,
19127 size: 4,
19128 target: UploadTarget::ImageIndex,
19129 }]
19130 );
19131 }
19132
19133 #[test]
19134 fn staged_buffer_uploads_ignore_empty_payloads() {
19135 let mut uploads = StagedBufferUploads::default();
19136
19137 uploads.stage(UploadTarget::Uniform, &[]);
19138
19139 assert!(uploads.is_empty());
19140 assert!(uploads.bytes.is_empty());
19141 }
19142
19143 #[test]
19144 fn staged_buffer_uploads_return_exact_payload_slice_for_copy() {
19145 let mut uploads = StagedBufferUploads::default();
19146 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
19147 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
19148
19149 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
19150 assert_eq!(uploads.payload_for_copy(uploads.copies[1]), &[5, 6, 7, 8]);
19151 }
19152
19153 #[test]
19154 fn staged_buffer_uploads_record_destination_offsets() {
19155 let mut uploads = StagedBufferUploads::default();
19156
19157 uploads.stage_at(UploadTarget::ImageIndex, 256, &[1, 2, 3, 4]);
19158
19159 assert_eq!(uploads.copies[0].target_offset, 256);
19160 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
19161 }
19162
19163 #[test]
19164 fn staged_buffer_uploads_truncate_restores_previous_state() {
19165 let mut uploads = StagedBufferUploads::default();
19166 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
19167 let bytes_len = uploads.bytes.len();
19168 let copies_len = uploads.copies.len();
19169 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
19170
19171 uploads.truncate(bytes_len, copies_len);
19172
19173 assert_eq!(uploads.bytes, vec![1, 2, 3, 4]);
19174 assert_eq!(uploads.copies.len(), 1);
19175 }
19176
19177 #[test]
19178 fn inner_shadow_composite_mask_uses_fill_shape_and_scale() {
19179 let mut fill = test_shape(0, BlendMode::SrcOver);
19180 fill.local_rect = Rect {
19181 x: 10.0,
19182 y: 12.0,
19183 width: 40.0,
19184 height: 20.0,
19185 };
19186 fill.shape = Some(RoundedCornerShape::uniform(6.0));
19187
19188 let cutout = test_shape(1, BlendMode::DstOut);
19189 let shadow = test_shadow_draw(vec![
19190 (fill, BlendMode::SrcOver),
19191 (cutout, BlendMode::DstOut),
19192 ]);
19193
19194 let mask = inner_shadow_composite_mask(&shadow, 1.5).expect("inner mask expected");
19195 assert_eq!(mask.rect, [15.0, 18.0, 60.0, 30.0]);
19196 assert_eq!(mask.radii, [9.0, 9.0, 9.0, 9.0]);
19197 }
19198
19199 #[test]
19200 fn inner_shadow_composite_mask_is_none_without_dst_out() {
19201 let fill = test_shape(0, BlendMode::SrcOver);
19202 let shadow = test_shadow_draw(vec![(fill, BlendMode::SrcOver)]);
19203 assert!(inner_shadow_composite_mask(&shadow, 1.0).is_none());
19204 }
19205
19206 #[test]
19207 fn render_effect_support_matrix_covers_all_variants() {
19208 let blur = RenderEffect::blur(4.0);
19209 let offset = RenderEffect::offset(2.0, 3.0);
19210 let shader = RenderEffect::runtime_shader(cranpose_ui_graphics::RuntimeShader::new(
19211 r#"
19212 @group(0) @binding(0) var input_texture: texture_2d<f32>;
19213 @group(0) @binding(1) var input_sampler: sampler;
19214 @group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
19215 struct VertexOutput {
19216 @builtin(position) position: vec4<f32>,
19217 @location(0) uv: vec2<f32>,
19218 }
19219 @vertex
19220 fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
19221 var output: VertexOutput;
19222 let x = f32(i32(vertex_index & 1u) * 2 - 1);
19223 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
19224 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
19225 output.position = vec4<f32>(x, y, 0.0, 1.0);
19226 return output;
19227 }
19228 @fragment
19229 fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
19230 return textureSample(input_texture, input_sampler, input.uv);
19231 }
19232 "#,
19233 ));
19234 let chain = blur.clone().then(offset.clone());
19235
19236 assert!(is_render_effect_supported(&blur));
19237 assert!(is_render_effect_supported(&offset));
19238 assert!(is_render_effect_supported(&shader));
19239 assert!(is_render_effect_supported(&chain));
19240 }
19241
19242 #[test]
19243 fn clip_to_bounds_propagates_visual_clip_to_all_descendant_shapes() {
19244 let container_local_bounds = Rect {
19247 x: 0.0,
19248 y: 0.0,
19249 width: 800.0,
19250 height: 500.0,
19251 };
19252 let container_clip_in_parent = Rect {
19254 x: 0.0,
19255 y: 50.0,
19256 width: 800.0,
19257 height: 500.0,
19258 };
19259
19260 let shape_above = RenderNode::Primitive(PrimitiveEntry {
19262 phase: PrimitivePhase::BeforeChildren,
19263 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19264 primitive: DrawPrimitive::Rect {
19265 rect: Rect {
19266 x: 10.0,
19267 y: -30.0,
19268 width: 100.0,
19269 height: 40.0,
19270 },
19271 brush: Brush::solid(Color::WHITE),
19272 stroke: None,
19273 },
19274 clip: None,
19275 }),
19276 });
19277
19278 let shape_inside = RenderNode::Primitive(PrimitiveEntry {
19280 phase: PrimitivePhase::BeforeChildren,
19281 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19282 primitive: DrawPrimitive::Rect {
19283 rect: Rect {
19284 x: 10.0,
19285 y: 100.0,
19286 width: 100.0,
19287 height: 40.0,
19288 },
19289 brush: Brush::solid(Color::WHITE),
19290 stroke: None,
19291 },
19292 clip: None,
19293 }),
19294 });
19295
19296 let shape_below = RenderNode::Primitive(PrimitiveEntry {
19298 phase: PrimitivePhase::BeforeChildren,
19299 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19300 primitive: DrawPrimitive::Rect {
19301 rect: Rect {
19302 x: 10.0,
19303 y: 600.0,
19304 width: 100.0,
19305 height: 40.0,
19306 },
19307 brush: Brush::solid(Color::WHITE),
19308 stroke: None,
19309 },
19310 clip: None,
19311 }),
19312 });
19313
19314 let mut content_layer = test_layer(
19316 Rect {
19317 x: 0.0,
19318 y: 0.0,
19319 width: 800.0,
19320 height: 1000.0,
19321 },
19322 vec![shape_above, shape_inside, shape_below],
19323 );
19324 content_layer.transform_to_parent = ProjectiveTransform::translation(0.0, -30.0);
19325 content_layer.translated_content_context = true;
19326
19327 let mut clip_container = test_layer(
19329 container_local_bounds,
19330 vec![RenderNode::Layer(Box::new(content_layer))],
19331 );
19332 clip_container.clip_to_bounds = true;
19333 clip_container.transform_to_parent = ProjectiveTransform::translation(0.0, 50.0);
19334
19335 let root = test_layer(
19337 Rect {
19338 x: 0.0,
19339 y: 0.0,
19340 width: 800.0,
19341 height: 600.0,
19342 },
19343 vec![RenderNode::Layer(Box::new(clip_container))],
19344 );
19345
19346 let mut rect_cache = HashMap::new();
19347 let mut requirements_cache = HashMap::new();
19348 let collected =
19349 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19350
19351 assert_eq!(
19352 collected.scene.shapes.len(),
19353 3,
19354 "all three shapes should be flattened into the scene"
19355 );
19356
19357 for (i, shape) in collected.scene.shapes.iter().enumerate() {
19358 assert!(
19359 shape.clip.is_some(),
19360 "shape {} at rect {:?} must have a clip from clip_to_bounds container, but clip is None",
19361 i,
19362 shape.rect
19363 );
19364 let clip = shape.clip.unwrap();
19365 assert_eq!(
19366 clip, container_clip_in_parent,
19367 "shape {} clip should match the clip_to_bounds container bounds in parent space",
19368 i
19369 );
19370 }
19371 }
19372
19373 #[test]
19374 fn clip_to_bounds_culls_child_layers_outside_boundary() {
19375 let clip_container_bounds = Rect {
19383 x: 0.0,
19384 y: 0.0,
19385 width: 800.0,
19386 height: 500.0,
19387 };
19388
19389 let shape_in_card = RenderNode::Primitive(PrimitiveEntry {
19390 phase: PrimitivePhase::BeforeChildren,
19391 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19392 primitive: DrawPrimitive::Rect {
19393 rect: Rect {
19394 x: 0.0,
19395 y: 0.0,
19396 width: 300.0,
19397 height: 80.0,
19398 },
19399 brush: Brush::solid(Color::WHITE),
19400 stroke: None,
19401 },
19402 clip: None,
19403 }),
19404 });
19405
19406 let mut card_outside = crate::test_support::layer_node(
19408 Rect {
19409 x: 0.0,
19410 y: 0.0,
19411 width: 300.0,
19412 height: 80.0,
19413 },
19414 ProjectiveTransform::identity(),
19415 GraphicsLayer {
19416 clip: true,
19417 ..GraphicsLayer::default()
19418 },
19419 vec![shape_in_card.clone()],
19420 );
19421 card_outside.transform_to_parent = ProjectiveTransform::translation(10.0, 600.0);
19422
19423 let mut card_inside = crate::test_support::layer_node(
19425 Rect {
19426 x: 0.0,
19427 y: 0.0,
19428 width: 300.0,
19429 height: 80.0,
19430 },
19431 ProjectiveTransform::identity(),
19432 GraphicsLayer {
19433 clip: true,
19434 ..GraphicsLayer::default()
19435 },
19436 vec![shape_in_card],
19437 );
19438 card_inside.transform_to_parent = ProjectiveTransform::translation(10.0, 100.0);
19439
19440 let content = test_layer(
19442 Rect {
19443 x: 0.0,
19444 y: 0.0,
19445 width: 800.0,
19446 height: 1000.0,
19447 },
19448 vec![
19449 RenderNode::Layer(Box::new(card_inside)),
19450 RenderNode::Layer(Box::new(card_outside)),
19451 ],
19452 );
19453
19454 let mut clip_container = test_layer(
19456 clip_container_bounds,
19457 vec![RenderNode::Layer(Box::new(content))],
19458 );
19459 clip_container.clip_to_bounds = true;
19460
19461 let root = test_layer(
19463 Rect {
19464 x: 0.0,
19465 y: 0.0,
19466 width: 800.0,
19467 height: 600.0,
19468 },
19469 vec![RenderNode::Layer(Box::new(clip_container))],
19470 );
19471
19472 let mut rect_cache = HashMap::new();
19473 let mut requirements_cache = HashMap::new();
19474 let collected =
19475 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19476
19477 assert_eq!(
19478 collected.scene.shapes.len(),
19479 1,
19480 "only the card inside the clip boundary should produce shapes; \
19481 the card outside must be culled entirely"
19482 );
19483
19484 let shape = &collected.scene.shapes[0];
19485 assert!(
19486 shape.clip.is_some(),
19487 "the visible card's shape must have a clip from clip_to_bounds"
19488 );
19489 }
19490
19491 #[test]
19492 fn flattened_layer_shadow_z_index_is_below_content() {
19493 let shape = RenderNode::Primitive(PrimitiveEntry {
19497 phase: PrimitivePhase::BeforeChildren,
19498 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19499 primitive: DrawPrimitive::Rect {
19500 rect: Rect {
19501 x: 0.0,
19502 y: 0.0,
19503 width: 100.0,
19504 height: 100.0,
19505 },
19506 brush: Brush::solid(Color::WHITE),
19507 stroke: None,
19508 },
19509 clip: None,
19510 }),
19511 });
19512
19513 let child_bounds = Rect {
19514 x: 0.0,
19515 y: 0.0,
19516 width: 100.0,
19517 height: 100.0,
19518 };
19519
19520 let child = crate::test_support::layer_node(
19521 child_bounds,
19522 ProjectiveTransform::translation(50.0, 50.0),
19523 GraphicsLayer {
19524 shadow_elevation: 20.0,
19525 ..GraphicsLayer::default()
19526 },
19527 vec![shape],
19528 );
19529
19530 let root = test_layer(
19531 Rect {
19532 x: 0.0,
19533 y: 0.0,
19534 width: 800.0,
19535 height: 600.0,
19536 },
19537 vec![RenderNode::Layer(Box::new(child))],
19538 );
19539
19540 let mut rect_cache = HashMap::new();
19541 let mut requirements_cache = HashMap::new();
19542 let collected =
19543 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19544
19545 assert!(
19546 !collected.scene.shadow_draws.is_empty(),
19547 "shadow_elevation > 0 must produce shadow draws"
19548 );
19549 let max_shadow_z = collected
19550 .scene
19551 .shadow_draws
19552 .iter()
19553 .map(|s| s.z_index)
19554 .max()
19555 .unwrap();
19556 let min_content_z = collected
19557 .scene
19558 .shapes
19559 .iter()
19560 .map(|s| s.z_index)
19561 .min()
19562 .unwrap();
19563 assert!(
19564 max_shadow_z < min_content_z,
19565 "shadow z-index ({}) must be less than content z-index ({}); \
19566 shadows must render behind their content",
19567 max_shadow_z,
19568 min_content_z
19569 );
19570 }
19571
19572 #[cfg(not(target_arch = "wasm32"))]
19575 fn bundle_op(slot: u32, epoch: Option<u64>, first: u32, last: u32) -> RetainedBundleOpKey {
19576 RetainedBundleOpKey {
19577 slot,
19578 capture_epoch: epoch,
19579 first,
19580 last,
19581 retained_index: slot,
19582 has_mesh: false,
19583 }
19584 }
19585
19586 #[cfg(not(target_arch = "wasm32"))]
19587 fn bundle_key(ops: &[RetainedBundleOpKey]) -> RetainedBundleKey {
19588 RetainedBundleKey { ops: ops.to_vec() }
19589 }
19590
19591 #[cfg(not(target_arch = "wasm32"))]
19594 #[test]
19595 fn retained_bundle_cache_reuses_stable_keys() {
19596 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
19597 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
19598 let key = bundle_key(&ops);
19599
19600 assert!(!cache.hit(&key), "empty cache must miss");
19601 cache.insert(key.clone(), 111);
19602 assert_eq!(cache.get(&key), Some(&111));
19603 cache.end_frame();
19604
19605 for _ in 0..3 {
19606 assert!(cache.hit(&bundle_key(&ops)), "stable key must stay cached");
19607 cache.end_frame();
19608 }
19609 assert_eq!(cache.stats(), (1, 3), "one rebuild, three cached executes");
19610 }
19611
19612 #[cfg(not(target_arch = "wasm32"))]
19616 #[test]
19617 fn retained_bundle_cache_invalidates_on_any_op_change() {
19618 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
19619 let variants: [Vec<RetainedBundleOpKey>; 5] = [
19620 vec![bundle_op(3, Some(8), 0, 40), bundle_op(5, Some(9), 4, 12)],
19622 vec![bundle_op(5, Some(9), 4, 12), bundle_op(3, Some(7), 0, 40)],
19624 vec![bundle_op(3, Some(7), 0, 40)],
19626 vec![bundle_op(3, Some(7), 0, 41), bundle_op(5, Some(9), 4, 12)],
19628 vec![bundle_op(3, Some(7), 0, 40), bundle_op(5, None, 4, 12)],
19630 ];
19631 for changed in variants {
19632 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
19633 cache.insert(bundle_key(&ops), 111);
19634 cache.end_frame();
19635 assert!(
19636 !cache.hit(&RetainedBundleKey {
19637 ops: changed.clone()
19638 }),
19639 "changed key {changed:?} must not reuse the stale bundle"
19640 );
19641 }
19642 }
19643
19644 #[cfg(not(target_arch = "wasm32"))]
19648 #[test]
19649 fn retained_bundle_cache_evicts_unused_entries() {
19650 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
19651 let stale = bundle_key(&[bundle_op(1, Some(1), 0, 6)]);
19652 let live = bundle_key(&[bundle_op(2, Some(2), 0, 6)]);
19653 cache.insert(stale.clone(), 1);
19654 cache.insert(live.clone(), 2);
19655 cache.end_frame();
19656
19657 assert!(cache.hit(&live));
19658 cache.end_frame();
19659
19660 assert!(
19661 !cache.hit(&stale),
19662 "entry unused for a frame must have been evicted"
19663 );
19664 assert!(cache.hit(&live), "used entry must survive eviction");
19665
19666 cache.clear();
19667 assert!(!cache.hit(&live), "clear must drop every entry");
19668 }
19669}