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 fragment_entry: &'static str,
1349) -> wgpu::RenderPipeline {
1350 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1351 label: Some("Shape Shader"),
1352 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1353 });
1354
1355 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1356 label: Some("Render Pipeline Layout"),
1357 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1358 immediate_size: 0,
1359 });
1360
1361 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1362 label: Some("Render Pipeline"),
1363 layout: Some(&pipeline_layout),
1364 vertex: wgpu::VertexState {
1365 module: &shader,
1366 entry_point: Some("vs_main"),
1367 compilation_options: wgpu::PipelineCompilationOptions::default(),
1368 buffers: &[],
1371 },
1372 fragment: Some(wgpu::FragmentState {
1373 module: &shader,
1374 entry_point: Some(fragment_entry),
1375 compilation_options: wgpu::PipelineCompilationOptions::default(),
1376 targets: &[Some(wgpu::ColorTargetState {
1377 format: surface_format,
1378 blend: Some(blend_state_for_mode(blend_mode)),
1379 write_mask: wgpu::ColorWrites::ALL,
1380 })],
1381 }),
1382 primitive: wgpu::PrimitiveState {
1383 topology: wgpu::PrimitiveTopology::TriangleList,
1384 strip_index_format: None,
1385 front_face: wgpu::FrontFace::Ccw,
1386 cull_mode: None,
1387 unclipped_depth: false,
1388 polygon_mode: wgpu::PolygonMode::Fill,
1389 conservative: false,
1390 },
1391 depth_stencil: None,
1392 multisample: wgpu::MultisampleState::default(),
1393 multiview_mask: None,
1394 cache: None,
1395 })
1396}
1397
1398#[cfg(not(target_arch = "wasm32"))]
1405fn create_mesh_shape_pipeline(
1406 device: &wgpu::Device,
1407 surface_format: wgpu::TextureFormat,
1408 uniform_layout: &wgpu::BindGroupLayout,
1409 shape_layout: &wgpu::BindGroupLayout,
1410 batch_limits: ShapeBatchLimits,
1411) -> wgpu::RenderPipeline {
1412 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1413 label: Some("Shape Mesh Shader"),
1414 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1415 });
1416
1417 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1418 label: Some("Mesh Render Pipeline Layout"),
1419 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1420 immediate_size: 0,
1421 });
1422
1423 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1424 label: Some("Retained Mesh Pipeline"),
1425 layout: Some(&pipeline_layout),
1426 vertex: wgpu::VertexState {
1427 module: &shader,
1428 entry_point: Some("vs_mesh"),
1429 compilation_options: wgpu::PipelineCompilationOptions::default(),
1430 buffers: &[MeshVertex::desc()],
1431 },
1432 fragment: Some(wgpu::FragmentState {
1433 module: &shader,
1434 entry_point: Some("fs_main"),
1435 compilation_options: wgpu::PipelineCompilationOptions::default(),
1436 targets: &[Some(wgpu::ColorTargetState {
1437 format: surface_format,
1438 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1439 write_mask: wgpu::ColorWrites::ALL,
1440 })],
1441 }),
1442 primitive: wgpu::PrimitiveState {
1443 topology: wgpu::PrimitiveTopology::TriangleList,
1444 strip_index_format: None,
1445 front_face: wgpu::FrontFace::Ccw,
1446 cull_mode: None,
1447 unclipped_depth: false,
1448 polygon_mode: wgpu::PolygonMode::Fill,
1449 conservative: false,
1450 },
1451 depth_stencil: None,
1452 multisample: wgpu::MultisampleState::default(),
1453 multiview_mask: None,
1454 cache: None,
1455 })
1456}
1457
1458#[cfg(not(target_arch = "wasm32"))]
1466fn create_instanced_shape_pipeline(
1467 device: &wgpu::Device,
1468 surface_format: wgpu::TextureFormat,
1469 uniform_layout: &wgpu::BindGroupLayout,
1470 shape_layout: &wgpu::BindGroupLayout,
1471 blend_mode: BlendMode,
1472 batch_limits: ShapeBatchLimits,
1473 fragment_entry: &'static str,
1474) -> wgpu::RenderPipeline {
1475 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1476 label: Some("Shape Instanced Shader"),
1477 source: wgpu::ShaderSource::Wgsl(shape_shader_source(batch_limits)),
1478 });
1479
1480 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1481 label: Some("Instanced Render Pipeline Layout"),
1482 bind_group_layouts: &[Some(uniform_layout), Some(shape_layout)],
1483 immediate_size: 0,
1484 });
1485
1486 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1487 label: Some("Instanced Render Pipeline"),
1488 layout: Some(&pipeline_layout),
1489 vertex: wgpu::VertexState {
1490 module: &shader,
1491 entry_point: Some("vs_shape_instanced"),
1492 compilation_options: wgpu::PipelineCompilationOptions::default(),
1493 buffers: &[],
1497 },
1498 fragment: Some(wgpu::FragmentState {
1499 module: &shader,
1500 entry_point: Some(fragment_entry),
1501 compilation_options: wgpu::PipelineCompilationOptions::default(),
1502 targets: &[Some(wgpu::ColorTargetState {
1503 format: surface_format,
1504 blend: Some(blend_state_for_mode(blend_mode)),
1505 write_mask: wgpu::ColorWrites::ALL,
1506 })],
1507 }),
1508 primitive: wgpu::PrimitiveState {
1509 topology: wgpu::PrimitiveTopology::TriangleList,
1510 strip_index_format: None,
1511 front_face: wgpu::FrontFace::Ccw,
1512 cull_mode: None,
1513 unclipped_depth: false,
1514 polygon_mode: wgpu::PolygonMode::Fill,
1515 conservative: false,
1516 },
1517 depth_stencil: None,
1518 multisample: wgpu::MultisampleState::default(),
1519 multiview_mask: None,
1520 cache: None,
1521 })
1522}
1523
1524fn create_image_pipeline(
1525 device: &wgpu::Device,
1526 surface_format: wgpu::TextureFormat,
1527 uniform_layout: &wgpu::BindGroupLayout,
1528 image_layout: &wgpu::BindGroupLayout,
1529 blend_mode: BlendMode,
1530) -> wgpu::RenderPipeline {
1531 let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1532 label: Some("Image Shader"),
1533 source: wgpu::ShaderSource::Wgsl(shaders::IMAGE_SHADER.into()),
1534 });
1535
1536 let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1537 label: Some("Image Pipeline Layout"),
1538 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1539 immediate_size: 0,
1540 });
1541
1542 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1543 label: Some("Image Pipeline"),
1544 layout: Some(&image_pipeline_layout),
1545 vertex: wgpu::VertexState {
1546 module: &image_shader,
1547 entry_point: Some("image_vs_main"),
1548 compilation_options: wgpu::PipelineCompilationOptions::default(),
1549 buffers: &[Vertex::desc()],
1550 },
1551 fragment: Some(wgpu::FragmentState {
1552 module: &image_shader,
1553 entry_point: Some("image_fs_main"),
1554 compilation_options: wgpu::PipelineCompilationOptions::default(),
1555 targets: &[Some(wgpu::ColorTargetState {
1556 format: surface_format,
1557 blend: Some(blend_state_for_mode(blend_mode)),
1558 write_mask: wgpu::ColorWrites::ALL,
1559 })],
1560 }),
1561 primitive: wgpu::PrimitiveState {
1562 topology: wgpu::PrimitiveTopology::TriangleList,
1563 strip_index_format: None,
1564 front_face: wgpu::FrontFace::Ccw,
1565 cull_mode: None,
1566 unclipped_depth: false,
1567 polygon_mode: wgpu::PolygonMode::Fill,
1568 conservative: false,
1569 },
1570 depth_stencil: None,
1571 multisample: wgpu::MultisampleState::default(),
1572 multiview_mask: None,
1573 cache: None,
1574 })
1575}
1576
1577fn create_glyph_atlas_pipeline(
1578 device: &wgpu::Device,
1579 surface_format: wgpu::TextureFormat,
1580 uniform_layout: &wgpu::BindGroupLayout,
1581 image_layout: &wgpu::BindGroupLayout,
1582) -> wgpu::RenderPipeline {
1583 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1584 label: Some("Glyph Atlas Shader"),
1585 source: wgpu::ShaderSource::Wgsl(shaders::GLYPH_ATLAS_SHADER.into()),
1586 });
1587
1588 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1589 label: Some("Glyph Atlas Pipeline Layout"),
1590 bind_group_layouts: &[Some(uniform_layout), Some(image_layout)],
1591 immediate_size: 0,
1592 });
1593
1594 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1595 label: Some("Glyph Atlas Pipeline"),
1596 layout: Some(&pipeline_layout),
1597 vertex: wgpu::VertexState {
1598 module: &shader,
1599 entry_point: Some("glyph_atlas_vs_main"),
1600 compilation_options: wgpu::PipelineCompilationOptions::default(),
1601 buffers: &[Vertex::desc()],
1602 },
1603 fragment: Some(wgpu::FragmentState {
1604 module: &shader,
1605 entry_point: Some("glyph_atlas_fs_main"),
1606 compilation_options: wgpu::PipelineCompilationOptions::default(),
1607 targets: &[Some(wgpu::ColorTargetState {
1608 format: surface_format,
1609 blend: Some(blend_state_for_mode(BlendMode::SrcOver)),
1610 write_mask: wgpu::ColorWrites::ALL,
1611 })],
1612 }),
1613 primitive: wgpu::PrimitiveState {
1614 topology: wgpu::PrimitiveTopology::TriangleList,
1615 strip_index_format: None,
1616 front_face: wgpu::FrontFace::Ccw,
1617 cull_mode: None,
1618 unclipped_depth: false,
1619 polygon_mode: wgpu::PolygonMode::Fill,
1620 conservative: false,
1621 },
1622 depth_stencil: None,
1623 multisample: wgpu::MultisampleState::default(),
1624 multiview_mask: None,
1625 cache: None,
1626 })
1627}
1628
1629#[repr(C)]
1630#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1631struct Vertex {
1632 position: [f32; 2],
1633 color: [f32; 4],
1634 uv: [f32; 2],
1635 uv_bounds: [f32; 4],
1636}
1637
1638impl Vertex {
1639 const ATTRIBS: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
1640 0 => Float32x2,
1641 1 => Float32x4,
1642 2 => Float32x2,
1643 3 => Float32x4
1644 ];
1645
1646 fn desc() -> wgpu::VertexBufferLayout<'static> {
1647 wgpu::VertexBufferLayout {
1648 array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
1649 step_mode: wgpu::VertexStepMode::Vertex,
1650 attributes: &Self::ATTRIBS,
1651 }
1652 }
1653}
1654
1655#[repr(C)]
1656#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1657struct Uniforms {
1658 viewport: [f32; 2],
1659 viewport_offset: [f32; 2],
1660}
1661
1662#[repr(C)]
1668#[derive(Copy, Clone, Debug, Pod, Zeroable)]
1669struct ShapeData {
1670 rect: [f32; 4], radii: [f32; 4],
1675 gradient_params: [f32; 4], clip_rect: [f32; 4], stroke_params: [f32; 4],
1680 arc_params: [f32; 4],
1682 quad01: [f32; 4],
1684 quad23: [f32; 4],
1686 color: [f32; 4],
1688 brush_type: u32, gradient_start: u32, gradient_count: u32, gradient_tile_mode: u32, }
1693
1694const SHAPE_KIND_FILL: u32 = 0;
1696const SHAPE_KIND_STROKE: u32 = 1;
1697const SHAPE_KIND_ARC: u32 = 2;
1698
1699fn stroke_cap_code(cap: StrokeCap) -> u32 {
1700 match cap {
1701 StrokeCap::Butt => 0,
1702 StrokeCap::Round => 1,
1703 StrokeCap::Square => 2,
1704 }
1705}
1706
1707fn stroke_join_code(join: StrokeJoin) -> u32 {
1708 match join {
1709 StrokeJoin::Miter => 0,
1710 StrokeJoin::Round => 1,
1711 StrokeJoin::Bevel => 2,
1712 }
1713}
1714
1715fn pack_shape_flags(kind: u32, cap: StrokeCap, join: StrokeJoin) -> f32 {
1721 ((kind & 3) | (stroke_cap_code(cap) << 2) | (stroke_join_code(join) << 4)) as f32
1722}
1723
1724#[cfg(not(target_arch = "wasm32"))]
1732static SHAPE_CONVERT_TUNER: crate::cost_tuner::CostTuner =
1733 crate::cost_tuner::CostTuner::new("shape-convert", 256, 400_000);
1734
1735#[cfg(not(target_arch = "wasm32"))]
1736pub(crate) fn shape_convert_worker_count() -> usize {
1737 static WORKERS: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
1738 *WORKERS.get_or_init(|| {
1739 let cpus = std::thread::available_parallelism()
1740 .map(|count| count.get())
1741 .unwrap_or(1);
1742 let workers = cpus.clamp(1, 4);
1743 log::info!("[shape-convert] fan-out width {workers} (available parallelism {cpus})");
1747 workers
1748 })
1749}
1750
1751#[cfg(target_arch = "wasm32")]
1752pub(crate) fn shape_convert_worker_count() -> usize {
1753 1
1754}
1755
1756fn shape_gradient_stop_count(shape: &DrawShape) -> usize {
1757 match &shape.brush {
1758 Brush::Solid(_) => 0,
1759 Brush::LinearGradient { colors, .. }
1760 | Brush::RadialGradient { colors, .. }
1761 | Brush::SweepGradient { colors, .. } => colors.len(),
1762 }
1763}
1764
1765fn convert_shape_into_slots(
1770 shape: &DrawShape,
1771 root_scale: f32,
1772 gradient_start: u32,
1773 shape_out: &mut ShapeData,
1774 gradient_out: &mut [GradientStop],
1775) {
1776 let snap_delta = shape
1777 .snap_anchor
1778 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
1779 .unwrap_or_default();
1780 let local_rect = shape.local_rect.translate(snap_delta.x, snap_delta.y);
1781 let quad = translate_quad(shape.quad, snap_delta);
1782 let clip = shape.clip;
1785 let canonicalize = shape.snap_anchor.is_some();
1786 let device_local_rect = if canonicalize {
1787 canonicalized_scaled_rect(local_rect, root_scale)
1788 } else {
1789 Rect {
1790 x: local_rect.x * root_scale,
1791 y: local_rect.y * root_scale,
1792 width: local_rect.width * root_scale,
1793 height: local_rect.height * root_scale,
1794 }
1795 };
1796 let device_quad = if canonicalize {
1797 canonicalized_scaled_quad(quad, root_scale)
1798 } else {
1799 scaled_quad(quad, root_scale)
1800 };
1801 let canonicalize_brush_coordinate = |value| {
1802 if canonicalize {
1803 canonicalize_device_coordinate(value)
1804 } else {
1805 value
1806 }
1807 };
1808
1809 let clip_rect = if let Some(clip) = clip {
1811 let device_clip = if canonicalize {
1812 canonicalized_scaled_rect(clip, root_scale)
1813 } else {
1814 Rect {
1815 x: clip.x * root_scale,
1816 y: clip.y * root_scale,
1817 width: clip.width * root_scale,
1818 height: clip.height * root_scale,
1819 }
1820 };
1821 [
1822 device_clip.x,
1823 device_clip.y,
1824 device_clip.width,
1825 device_clip.height,
1826 ]
1827 } else {
1828 [0.0, 0.0, 0.0, 0.0]
1829 };
1830
1831 let mut fill_gradient_entries = |colors: &[Color], stops: Option<&[f32]>| {
1833 let count = colors.len();
1834 let explicit_stops = stops.filter(|values| values.len() == count);
1835 for (index, color) in colors.iter().enumerate() {
1836 let position = explicit_stops
1837 .map(|values| values[index])
1838 .unwrap_or_else(|| {
1839 if count <= 1 {
1840 0.0
1841 } else {
1842 index as f32 / (count - 1) as f32
1843 }
1844 });
1845 gradient_out[index] = GradientStop {
1846 color: [color.r(), color.g(), color.b(), color.a()],
1847 position: [position, 0.0, 0.0, 0.0],
1848 };
1849 }
1850 count as u32
1851 };
1852 let mut gradient_params = [0.0f32; 4];
1853 let (brush_type, gradient_count, gradient_tile_mode) = match &shape.brush {
1854 Brush::Solid(_) => (0u32, 0u32, gradient_tile_mode_value(TileMode::Clamp)),
1855 Brush::LinearGradient {
1856 colors,
1857 stops,
1858 start,
1859 end,
1860 tile_mode,
1861 } => {
1862 let count = fill_gradient_entries(colors, stops.as_deref());
1863 gradient_params = [
1864 canonicalize_brush_coordinate(resolve_gradient_point(
1865 device_local_rect.x,
1866 device_local_rect.width,
1867 start.x * root_scale,
1868 )),
1869 canonicalize_brush_coordinate(resolve_gradient_point(
1870 device_local_rect.y,
1871 device_local_rect.height,
1872 start.y * root_scale,
1873 )),
1874 canonicalize_brush_coordinate(resolve_gradient_point(
1875 device_local_rect.x,
1876 device_local_rect.width,
1877 end.x * root_scale,
1878 )),
1879 canonicalize_brush_coordinate(resolve_gradient_point(
1880 device_local_rect.y,
1881 device_local_rect.height,
1882 end.y * root_scale,
1883 )),
1884 ];
1885 (1u32, count, gradient_tile_mode_value(*tile_mode))
1886 }
1887 Brush::RadialGradient {
1888 colors,
1889 stops,
1890 center,
1891 radius,
1892 tile_mode,
1893 } => {
1894 let count = fill_gradient_entries(colors, stops.as_deref());
1895 gradient_params = [
1896 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
1897 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
1898 (radius * root_scale).max(f32::EPSILON),
1899 0.0,
1900 ];
1901 (2u32, count, gradient_tile_mode_value(*tile_mode))
1902 }
1903 Brush::SweepGradient {
1904 colors,
1905 stops,
1906 center,
1907 } => {
1908 let count = fill_gradient_entries(colors, stops.as_deref());
1909 gradient_params = [
1910 canonicalize_brush_coordinate(device_local_rect.x + center.x * root_scale),
1911 canonicalize_brush_coordinate(device_local_rect.y + center.y * root_scale),
1912 0.0,
1913 0.0,
1914 ];
1915 (3u32, count, gradient_tile_mode_value(TileMode::Clamp))
1916 }
1917 };
1918
1919 let stroke_outset = shape
1924 .stroke
1925 .map(|stroke| stroke.half_width())
1926 .unwrap_or(0.0);
1927 let geometry_width = (local_rect.width - stroke_outset * 2.0).max(0.0);
1928 let geometry_height = (local_rect.height - stroke_outset * 2.0).max(0.0);
1929
1930 let radii = if let Some(arc) = shape.arc {
1931 if arc.sweep_angle >= cranpose_ui_graphics::TAU && arc.start_angle == 0.0 {
1941 [0.0, -1.0, 0.0, -1.0]
1942 } else {
1943 let half_sweep = arc.sweep_angle.clamp(0.0, cranpose_ui_graphics::TAU) * 0.5;
1944 let (mid_sin, mid_cos) = (arc.start_angle + half_sweep).sin_cos();
1945 let (half_sin, half_cos) = half_sweep.sin_cos();
1946 [mid_sin, mid_cos, half_sin.max(0.0), half_cos]
1947 }
1948 } else if let Some(rounded) = shape.shape {
1949 let resolved = rounded.resolve(geometry_width, geometry_height);
1950 [
1951 resolved.top_left * root_scale,
1952 resolved.top_right * root_scale,
1953 resolved.bottom_left * root_scale,
1954 resolved.bottom_right * root_scale,
1955 ]
1956 } else {
1957 [0.0, 0.0, 0.0, 0.0]
1958 };
1959
1960 let device_rect = [
1961 device_local_rect.x,
1962 device_local_rect.y,
1963 device_local_rect.width,
1964 device_local_rect.height,
1965 ];
1966
1967 let (stroke_params, arc_params) = match (shape.arc, shape.stroke) {
1971 (Some(arc), _) => (
1972 [
1973 0.0,
1974 pack_shape_flags(SHAPE_KIND_ARC, arc.cap, StrokeJoin::Miter),
1975 arc.outer_radius * root_scale,
1976 arc.inner_radius * root_scale,
1977 ],
1978 [
1979 (arc.center.x + snap_delta.x) * root_scale,
1980 (arc.center.y + snap_delta.y) * root_scale,
1981 arc.start_angle,
1982 arc.sweep_angle,
1983 ],
1984 ),
1985 (None, Some(stroke)) => (
1986 [
1987 stroke.width.max(0.0) * root_scale,
1988 pack_shape_flags(SHAPE_KIND_STROKE, stroke.cap, stroke.join),
1989 0.0,
1990 0.0,
1991 ],
1992 [0.0; 4],
1993 ),
1994 (None, None) => (
1995 [
1996 0.0,
1997 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
1998 0.0,
1999 0.0,
2000 ],
2001 [0.0; 4],
2002 ),
2003 };
2004
2005 let color = match &shape.brush {
2006 Brush::Solid(c) => [c.r(), c.g(), c.b(), c.a()],
2007 Brush::LinearGradient { colors, .. } => {
2008 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2009 [first.r(), first.g(), first.b(), first.a()]
2010 }
2011 Brush::RadialGradient { colors, .. } | Brush::SweepGradient { colors, .. } => {
2012 let first = colors.first().unwrap_or(&Color(1.0, 1.0, 1.0, 1.0));
2013 [first.r(), first.g(), first.b(), first.a()]
2014 }
2015 };
2016
2017 *shape_out = ShapeData {
2018 rect: device_rect,
2019 radii,
2020 gradient_params,
2021 clip_rect,
2022 stroke_params,
2023 arc_params,
2024 quad01: [
2025 device_quad[0][0],
2026 device_quad[0][1],
2027 device_quad[1][0],
2028 device_quad[1][1],
2029 ],
2030 quad23: [
2031 device_quad[2][0],
2032 device_quad[2][1],
2033 device_quad[3][0],
2034 device_quad[3][1],
2035 ],
2036 color,
2037 brush_type,
2038 gradient_start,
2039 gradient_count,
2040 gradient_tile_mode,
2041 };
2042}
2043
2044fn quad_area_diag_enabled() -> bool {
2051 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2052 *ENABLED.get_or_init(|| std::env::var_os("CRANPOSE_QUAD_AREA_DIAG").is_some())
2053}
2054
2055fn convert_shapes_into_outputs(
2061 shape_refs: &[&DrawShape],
2062 gradient_offsets: &[u32],
2063 root_scale: f32,
2064 shape_data_out: &mut [ShapeData],
2065 gradients_out: &mut [GradientStop],
2066) {
2067 let shape_count = shape_refs.len();
2068 #[cfg(not(target_arch = "wasm32"))]
2069 let convert_started = Instant::now();
2070 #[cfg(not(target_arch = "wasm32"))]
2071 let parallel =
2072 SHAPE_CONVERT_TUNER.choose_parallel(shape_count) && shape_convert_worker_count() > 1;
2073 if quad_area_diag_enabled() {
2074 let quad_area = |q: [[f32; 2]; 4]| {
2075 let poly = [q[0], q[1], q[3], q[2]];
2077 let mut twice = 0.0f64;
2078 for i in 0..4 {
2079 let a = poly[i];
2080 let b = poly[(i + 1) % 4];
2081 twice += a[0] as f64 * b[1] as f64 - b[0] as f64 * a[1] as f64;
2082 }
2083 twice.abs() * 0.5
2084 };
2085 let mut arc_quad = 0.0f64; let mut arc_band = 0.0f64; let mut arc_count = 0usize;
2088 let mut ring_count = 0usize;
2089 let mut other_quad = 0.0f64;
2090 let mut other_count = 0usize;
2091 let mut top_other: Vec<(f64, usize)> = Vec::new();
2094 for (index, shape) in shape_refs.iter().enumerate() {
2095 let area = quad_area(shape.quad);
2096 if let Some(arc) = shape.arc {
2097 arc_quad += area;
2098 arc_count += 1;
2099 if arc.sweep_angle >= cranpose_ui_graphics::TAU {
2100 ring_count += 1;
2101 }
2102 let ra = arc.mid_radius() as f64;
2103 let rb = arc.half_thickness() as f64;
2104 arc_band +=
2105 arc.sweep_angle as f64 * ra * (2.0 * rb) + std::f64::consts::PI * rb * rb;
2106 } else {
2107 other_quad += area;
2108 other_count += 1;
2109 top_other.push((area, index));
2110 }
2111 }
2112 let scale2 = (root_scale as f64) * (root_scale as f64);
2113 eprintln!(
2114 "[quad-area] arcs={arc_count} (rings={ring_count}) arc_quad_px={:.0} arc_band_px={:.0} | other={other_count} other_px={:.0}",
2115 arc_quad * scale2,
2116 arc_band * scale2,
2117 other_quad * scale2,
2118 );
2119 top_other.sort_by(|a, b| b.0.total_cmp(&a.0));
2120 for &(area, index) in top_other.iter().take(4) {
2121 let shape = shape_refs[index];
2122 let brush = match &shape.brush {
2123 cranpose_ui_graphics::Brush::Solid(color) => format!("solid a={:.2}", color.3),
2124 cranpose_ui_graphics::Brush::LinearGradient { colors, .. } => {
2125 format!("linear n={}", colors.len())
2126 }
2127 cranpose_ui_graphics::Brush::RadialGradient { colors, .. } => {
2128 format!("radial n={}", colors.len())
2129 }
2130 cranpose_ui_graphics::Brush::SweepGradient { colors, .. } => {
2131 format!("sweep n={}", colors.len())
2132 }
2133 };
2134 eprintln!(
2135 "[quad-area] top other: {:.0}px {}x{} at ({:.0},{:.0}) {} shape={} stroke={} clip={} blend={:?} z={}",
2136 area * scale2,
2137 shape.rect.width.round(),
2138 shape.rect.height.round(),
2139 shape.rect.x,
2140 shape.rect.y,
2141 brush,
2142 shape.shape.is_some(),
2143 shape.stroke.is_some(),
2144 shape.clip.is_some(),
2145 shape.blend_mode,
2146 shape.z_index,
2147 );
2148 }
2149 }
2150 #[cfg(target_arch = "wasm32")]
2151 let parallel = false;
2152 let workers = if parallel {
2153 shape_convert_worker_count()
2154 } else {
2155 1
2156 };
2157 if workers <= 1 {
2158 for (idx, shape) in shape_refs.iter().enumerate() {
2159 let gradient_start = gradient_offsets[idx];
2160 let gradient_end = gradient_offsets[idx + 1];
2161 convert_shape_into_slots(
2162 shape,
2163 root_scale,
2164 gradient_start,
2165 &mut shape_data_out[idx],
2166 &mut gradients_out[gradient_start as usize..gradient_end as usize],
2167 );
2168 }
2169 #[cfg(not(target_arch = "wasm32"))]
2170 SHAPE_CONVERT_TUNER.record(
2171 false,
2172 shape_count,
2173 convert_started.elapsed().as_nanos() as u64,
2174 );
2175 return;
2176 }
2177
2178 let chunk_len = shape_count.div_ceil(workers);
2179 let mut shape_data_rest = shape_data_out;
2180 let mut gradients_rest = gradients_out;
2181 std::thread::scope(|scope| {
2182 let mut chunk_start = 0usize;
2183 while chunk_start < shape_count {
2184 let chunk_end = (chunk_start + chunk_len).min(shape_count);
2185 let count = chunk_end - chunk_start;
2186 let gradient_base = gradient_offsets[chunk_start];
2187 let gradient_span = (gradient_offsets[chunk_end] - gradient_base) as usize;
2188 let (shape_data_chunk, rest) = std::mem::take(&mut shape_data_rest).split_at_mut(count);
2189 shape_data_rest = rest;
2190 let (gradient_chunk, rest) =
2191 std::mem::take(&mut gradients_rest).split_at_mut(gradient_span);
2192 gradients_rest = rest;
2193 let chunk_refs = &shape_refs[chunk_start..chunk_end];
2194 let chunk_offsets = &gradient_offsets[chunk_start..=chunk_end];
2195 let mut convert_chunk = move || {
2196 for (j, shape) in chunk_refs.iter().enumerate() {
2197 let gradient_start = chunk_offsets[j];
2198 let local_start = (gradient_start - gradient_base) as usize;
2199 let local_end = (chunk_offsets[j + 1] - gradient_base) as usize;
2200 convert_shape_into_slots(
2201 shape,
2202 root_scale,
2203 gradient_start,
2204 &mut shape_data_chunk[j],
2205 &mut gradient_chunk[local_start..local_end],
2206 );
2207 }
2208 };
2209 if chunk_end == shape_count {
2210 convert_chunk();
2214 } else {
2215 scope.spawn(convert_chunk);
2216 }
2217 chunk_start = chunk_end;
2218 }
2219 });
2220 #[cfg(not(target_arch = "wasm32"))]
2221 SHAPE_CONVERT_TUNER.record(
2222 true,
2223 shape_count,
2224 convert_started.elapsed().as_nanos() as u64,
2225 );
2226}
2227
2228#[repr(C)]
2229#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2230struct GradientStop {
2231 color: [f32; 4],
2232 position: [f32; 4],
2233}
2234
2235#[cfg(not(target_arch = "wasm32"))]
2239const MAX_REPLAY_SLOTS: u32 = 128;
2240#[cfg(not(target_arch = "wasm32"))]
2241const REPLAY_TRANSFORM_STRIDE: u64 = 256;
2242
2243#[cfg(not(target_arch = "wasm32"))]
2250struct ReplaySlot {
2251 paint_buffer: wgpu::Buffer,
2256 bind_group: wgpu::BindGroup,
2257 shape_count: u32,
2258 paint_mirror: Vec<[f32; 4]>,
2264 mesh: Option<ReplaySlotMesh>,
2269 capture_epoch: u64,
2274 has_gradient: bool,
2279}
2280
2281#[cfg(not(target_arch = "wasm32"))]
2286struct ReplaySlotMesh {
2287 vertex_buffer: wgpu::Buffer,
2288 index_buffer: wgpu::Buffer,
2294 index_prefix: Vec<u32>,
2299}
2300
2301#[cfg(not(target_arch = "wasm32"))]
2305#[repr(C)]
2306#[derive(Copy, Clone, Debug, Pod, Zeroable)]
2307struct MeshVertex {
2308 position: [f32; 2],
2309 uv: [f32; 2],
2310 shape_idx: u32,
2311}
2312
2313#[cfg(not(target_arch = "wasm32"))]
2314impl MeshVertex {
2315 const ATTRIBS: [wgpu::VertexAttribute; 3] =
2316 wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2, 2 => Uint32];
2317
2318 fn desc() -> wgpu::VertexBufferLayout<'static> {
2319 wgpu::VertexBufferLayout {
2320 array_stride: std::mem::size_of::<MeshVertex>() as wgpu::BufferAddress,
2321 step_mode: wgpu::VertexStepMode::Vertex,
2322 attributes: &Self::ATTRIBS,
2323 }
2324 }
2325}
2326
2327#[cfg(not(target_arch = "wasm32"))]
2332fn arc_mesh_enabled() -> bool {
2333 matches!(std::env::var("CRANPOSE_ARC_MESH").as_deref(), Ok(v) if v != "0")
2341}
2342
2343#[cfg(not(target_arch = "wasm32"))]
2349const ARC_MESH_MARGIN: f32 = 1.0;
2350
2351#[cfg(not(target_arch = "wasm32"))]
2355const ARC_MESH_OVERSHOOT: f32 = 2.0;
2356
2357#[cfg(not(target_arch = "wasm32"))]
2358const ARC_MESH_MIN_SEGMENTS: usize = 4;
2359#[cfg(not(target_arch = "wasm32"))]
2360const ARC_MESH_MAX_SEGMENTS: usize = 64;
2361
2362#[cfg(not(target_arch = "wasm32"))]
2371const ARC_MESH_BUDGET_BYTES_PER_SHAPE: usize = 48 * std::mem::size_of::<MeshVertex>();
2372#[cfg(not(target_arch = "wasm32"))]
2373const ARC_MESH_BUDGET_FLOOR_BYTES: usize = 4096 * std::mem::size_of::<MeshVertex>();
2374
2375#[cfg(not(target_arch = "wasm32"))]
2378fn arc_mesh_bytes(vertices: usize, indices: usize) -> usize {
2379 vertices * std::mem::size_of::<MeshVertex>() + indices * std::mem::size_of::<u32>()
2380}
2381
2382#[cfg(not(target_arch = "wasm32"))]
2387struct ArcMeshBand {
2388 center: [f32; 2],
2389 inner: f32,
2390 outer: f32,
2391 start: f32,
2392 sweep: f32,
2393}
2394
2395#[cfg(not(target_arch = "wasm32"))]
2396fn arc_mesh_band(shape: &ShapeData) -> Option<ArcMeshBand> {
2397 let flags = shape.stroke_params[1].max(0.0) as u32;
2399 if flags & 3 != SHAPE_KIND_ARC {
2400 return None;
2401 }
2402 if shape.brush_type != 0 {
2406 return None;
2407 }
2408 if shape.clip_rect[2] > 0.0 && shape.clip_rect[3] > 0.0 {
2413 return None;
2414 }
2415 let [_, _, w, h] = shape.rect;
2416 if !(w > 0.0 && h > 0.0) {
2417 return None;
2418 }
2419 let [left, top, right, _] = shape.quad01;
2427 let [bl_x, bottom, br_x, br_y] = shape.quad23;
2428 let axis_aligned = shape.quad01[3] == top
2429 && bl_x == left
2430 && br_x == right
2431 && br_y == bottom
2432 && left < right
2433 && top < bottom;
2434 if !axis_aligned {
2435 return None;
2436 }
2437 let center = [shape.arc_params[0], shape.arc_params[1]];
2438 let start = shape.arc_params[2];
2439 let sweep = shape.arc_params[3];
2440 let outer = shape.stroke_params[2];
2441 let inner = shape.stroke_params[3];
2442 let finite = center[0].is_finite()
2443 && center[1].is_finite()
2444 && start.is_finite()
2445 && sweep.is_finite()
2446 && outer.is_finite()
2447 && inner.is_finite();
2448 if !finite || outer <= 0.0 || sweep <= 0.0 {
2449 return None;
2450 }
2451 Some(ArcMeshBand {
2452 center,
2453 inner,
2454 outer,
2455 start,
2456 sweep,
2457 })
2458}
2459
2460#[cfg(not(target_arch = "wasm32"))]
2466fn emit_passthrough_quad(
2467 shape: &ShapeData,
2468 shape_idx: u32,
2469 vertices: &mut Vec<MeshVertex>,
2470 indices: &mut Vec<u32>,
2471) {
2472 let base = vertices.len() as u32;
2473 let corners = [
2474 ([shape.quad01[0], shape.quad01[1]], [0.0, 0.0]),
2475 ([shape.quad01[2], shape.quad01[3]], [1.0, 0.0]),
2476 ([shape.quad23[0], shape.quad23[1]], [0.0, 1.0]),
2477 ([shape.quad23[2], shape.quad23[3]], [1.0, 1.0]),
2478 ];
2479 for (position, uv) in corners {
2480 vertices.push(MeshVertex {
2481 position,
2482 uv,
2483 shape_idx,
2484 });
2485 }
2486 indices.extend([0u32, 1, 2, 2, 1, 3].map(|corner| base + corner));
2487}
2488
2489#[cfg(not(target_arch = "wasm32"))]
2500fn clip_polygon_axis(
2501 input: &[[f32; 2]],
2502 axis: usize,
2503 bound: f32,
2504 keep_at_most: bool,
2505 output: &mut Vec<[f32; 2]>,
2506) {
2507 output.clear();
2508 let inside = |p: [f32; 2]| {
2509 if keep_at_most {
2510 p[axis] <= bound
2511 } else {
2512 p[axis] >= bound
2513 }
2514 };
2515 let intersect = |a: [f32; 2], b: [f32; 2]| {
2516 let (p, q) = if (b[0], b[1]) < (a[0], a[1]) {
2517 (b, a)
2518 } else {
2519 (a, b)
2520 };
2521 let t = (bound - p[axis]) / (q[axis] - p[axis]);
2522 let mut point = [0.0f32; 2];
2523 point[axis] = bound;
2524 point[1 - axis] = p[1 - axis] + t * (q[1 - axis] - p[1 - axis]);
2525 point
2526 };
2527 for (index, ¤t) in input.iter().enumerate() {
2528 let previous = input[(index + input.len() - 1) % input.len()];
2529 match (inside(previous), inside(current)) {
2530 (true, true) => output.push(current),
2531 (true, false) => output.push(intersect(previous, current)),
2532 (false, true) => {
2533 output.push(intersect(previous, current));
2534 output.push(current);
2535 }
2536 (false, false) => {}
2537 }
2538 }
2539}
2540
2541#[cfg(not(target_arch = "wasm32"))]
2576fn emit_arc_band_mesh(
2577 shape: &ShapeData,
2578 shape_idx: u32,
2579 band: &ArcMeshBand,
2580 vertices: &mut Vec<MeshVertex>,
2581 indices: &mut Vec<u32>,
2582) -> Option<usize> {
2583 let [cx, cy] = band.center;
2584 let ra = (band.outer + band.inner) * 0.5;
2585 let rb = ((band.outer - band.inner) * 0.5).max(0.0);
2586 let rb_m = rb + ARC_MESH_MARGIN;
2587 let ro = ra + rb_m;
2588 let ri = (ra - rb_m).max(0.0);
2589 let tau = cranpose_ui_graphics::TAU;
2590
2591 let (range_start, range) = if band.sweep >= tau {
2592 (0.0, tau)
2593 } else {
2594 let pad = if rb_m < ra {
2595 (rb_m / ra).asin() + 0.05
2596 } else {
2597 std::f32::consts::PI
2600 };
2601 let padded = band.sweep + pad + pad;
2602 if padded >= tau {
2603 (0.0, tau)
2604 } else {
2605 (band.start - pad, padded)
2606 }
2607 };
2608 let closed = range >= tau;
2609
2610 let dtheta = (2.0 * (ro / (ro + ARC_MESH_OVERSHOOT)).acos()).clamp(tau / 64.0, tau / 6.0);
2611 let segments =
2612 ((range / dtheta).ceil() as usize).clamp(ARC_MESH_MIN_SEGMENTS, ARC_MESH_MAX_SEGMENTS);
2613 let step = range / segments as f32;
2614 let rc = ro / (step * 0.5).cos();
2615
2616 let boundary_count = if closed { segments } else { segments + 1 };
2620 let mut boundaries = Vec::with_capacity(boundary_count);
2621 for j in 0..boundary_count {
2622 let (sin, cos) = (range_start + step * j as f32).sin_cos();
2623 boundaries.push((
2624 [cx + cos * ri, cy + sin * ri],
2625 [cx + cos * rc, cy + sin * rc],
2626 ));
2627 }
2628
2629 let quad_min = [shape.quad01[0], shape.quad01[1]];
2630 let quad_max = [shape.quad23[2], shape.quad23[3]];
2631
2632 enum SegmentGeometry {
2637 Shared,
2638 Fan(Vec<[f32; 2]>),
2639 Empty,
2640 }
2641
2642 let mut polygon: Vec<[f32; 2]> = Vec::with_capacity(8);
2644 let mut scratch: Vec<[f32; 2]> = Vec::with_capacity(8);
2645 let mut segment_geometry = Vec::with_capacity(segments);
2646 let mut boundary_used = vec![false; boundary_count];
2647 for j in 0..segments {
2648 let jb = (j + 1) % boundary_count;
2649 let (inner_a, outer_a) = boundaries[j];
2650 let (inner_b, outer_b) = boundaries[jb];
2651 polygon.clear();
2652 polygon.extend_from_slice(&[inner_a, outer_a, outer_b, inner_b]);
2653 clip_polygon_axis(&polygon, 0, quad_min[0], false, &mut scratch);
2654 clip_polygon_axis(&scratch, 0, quad_max[0], true, &mut polygon);
2655 clip_polygon_axis(&polygon, 1, quad_min[1], false, &mut scratch);
2656 clip_polygon_axis(&scratch, 1, quad_max[1], true, &mut polygon);
2657 scratch.clear();
2660 for &point in polygon.iter() {
2661 if scratch.last() != Some(&point) {
2662 scratch.push(point);
2663 }
2664 }
2665 while scratch.len() > 1 && scratch.first() == scratch.last() {
2666 scratch.pop();
2667 }
2668 if scratch.len() < 3 {
2669 segment_geometry.push(SegmentGeometry::Empty);
2670 } else if scratch[..] == [inner_a, outer_a, outer_b, inner_b] {
2671 boundary_used[j] = true;
2672 boundary_used[jb] = true;
2673 segment_geometry.push(SegmentGeometry::Shared);
2674 } else {
2675 segment_geometry.push(SegmentGeometry::Fan(scratch.clone()));
2676 }
2677 }
2678
2679 let push_vertex = |vertices: &mut Vec<MeshVertex>, position: [f32; 2]| -> u32 {
2680 let index = vertices.len() as u32;
2681 vertices.push(MeshVertex {
2682 position,
2683 uv: [
2684 (position[0] - shape.rect[0]) / shape.rect[2],
2685 (position[1] - shape.rect[1]) / shape.rect[3],
2686 ],
2687 shape_idx,
2688 });
2689 index
2690 };
2691
2692 let mut boundary_vertex = vec![[0u32; 2]; boundary_count];
2695 for (j, used) in boundary_used.iter().enumerate() {
2696 if *used {
2697 let (inner, outer) = boundaries[j];
2698 boundary_vertex[j] = [push_vertex(vertices, inner), push_vertex(vertices, outer)];
2699 }
2700 }
2701
2702 let start_len = indices.len();
2705 for (j, geometry) in segment_geometry.iter().enumerate() {
2706 match geometry {
2707 SegmentGeometry::Empty => {}
2708 SegmentGeometry::Shared => {
2709 let jb = (j + 1) % boundary_count;
2710 let [in_a, out_a] = boundary_vertex[j];
2711 let [in_b, out_b] = boundary_vertex[jb];
2712 indices.extend_from_slice(&[in_a, out_a, out_b, in_a, out_b, in_b]);
2716 }
2717 SegmentGeometry::Fan(points) => {
2718 let base = vertices.len() as u32;
2719 for &point in points {
2720 push_vertex(vertices, point);
2721 }
2722 for i in 1..points.len() as u32 - 1 {
2723 indices.extend_from_slice(&[base, base + i, base + i + 1]);
2724 }
2725 }
2726 }
2727 }
2728 if indices.len() == start_len {
2729 return None;
2730 }
2731 Some(segments)
2732}
2733
2734#[cfg(not(target_arch = "wasm32"))]
2737fn triangles_shoelace_area(vertices: &[MeshVertex], indices: &[u32]) -> f64 {
2738 indices
2739 .chunks_exact(3)
2740 .map(|tri| {
2741 let [a, b, c] = [
2742 vertices[tri[0] as usize].position,
2743 vertices[tri[1] as usize].position,
2744 vertices[tri[2] as usize].position,
2745 ];
2746 let cross = (b[0] as f64 - a[0] as f64) * (c[1] as f64 - a[1] as f64)
2747 - (b[1] as f64 - a[1] as f64) * (c[0] as f64 - a[0] as f64);
2748 cross.abs() * 0.5
2749 })
2750 .sum()
2751}
2752
2753#[cfg(not(target_arch = "wasm32"))]
2756fn quad_shoelace_area(shape: &ShapeData) -> f64 {
2757 let corners = [
2758 [shape.quad01[0] as f64, shape.quad01[1] as f64],
2759 [shape.quad01[2] as f64, shape.quad01[3] as f64],
2760 [shape.quad23[0] as f64, shape.quad23[1] as f64],
2761 [shape.quad23[2] as f64, shape.quad23[3] as f64],
2762 ];
2763 let tri = |a: [f64; 2], b: [f64; 2], c: [f64; 2]| {
2764 ((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])).abs() * 0.5
2765 };
2766 tri(corners[0], corners[1], corners[2]) + tri(corners[2], corners[1], corners[3])
2767}
2768
2769#[cfg(not(target_arch = "wasm32"))]
2770struct ArcMeshBuild {
2771 vertices: Vec<MeshVertex>,
2772 indices: Vec<u32>,
2774 index_prefix: Vec<u32>,
2777 meshed_arcs: usize,
2778 meshed_segments: usize,
2779 passthrough: usize,
2780 quad_area: f64,
2781 mesh_area: f64,
2782}
2783
2784#[cfg(not(target_arch = "wasm32"))]
2791fn build_arc_mesh_vertices(shape_data: &[ShapeData]) -> Option<ArcMeshBuild> {
2792 let budget_bytes =
2793 (shape_data.len() * ARC_MESH_BUDGET_BYTES_PER_SHAPE).max(ARC_MESH_BUDGET_FLOOR_BYTES);
2794 let mut build = ArcMeshBuild {
2795 vertices: Vec::new(),
2796 indices: Vec::new(),
2797 index_prefix: Vec::with_capacity(shape_data.len() + 1),
2798 meshed_arcs: 0,
2799 meshed_segments: 0,
2800 passthrough: 0,
2801 quad_area: 0.0,
2802 mesh_area: 0.0,
2803 };
2804 build.index_prefix.push(0);
2805 for (index, shape) in shape_data.iter().enumerate() {
2806 let start = build.indices.len();
2807 let meshed = arc_mesh_band(shape).and_then(|band| {
2808 emit_arc_band_mesh(
2809 shape,
2810 index as u32,
2811 &band,
2812 &mut build.vertices,
2813 &mut build.indices,
2814 )
2815 });
2816 match meshed {
2817 Some(segments) => {
2818 build.meshed_arcs += 1;
2819 build.meshed_segments += segments;
2820 }
2821 None => {
2822 emit_passthrough_quad(shape, index as u32, &mut build.vertices, &mut build.indices);
2823 build.passthrough += 1;
2824 }
2825 }
2826 if arc_mesh_bytes(build.vertices.len(), build.indices.len()) > budget_bytes {
2827 return None;
2828 }
2829 build.index_prefix.push(build.indices.len() as u32);
2830 build.quad_area += quad_shoelace_area(shape);
2831 build.mesh_area += triangles_shoelace_area(&build.vertices, &build.indices[start..]);
2832 }
2833 Some(build)
2834}
2835
2836#[cfg(not(target_arch = "wasm32"))]
2839struct ReplaySlotStore {
2840 slots: std::collections::HashMap<u32, ReplaySlot, cranpose_ui_graphics::FxBuildHasher>,
2841 transform_buffer: wgpu::Buffer,
2842 free_ids: Vec<u32>,
2843 next_capture_epoch: u64,
2847}
2848
2849#[cfg(not(target_arch = "wasm32"))]
2850impl ReplaySlotStore {
2851 fn new(device: &wgpu::Device) -> Self {
2852 let transform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2853 label: Some("Replay Transform Buffer"),
2854 size: MAX_REPLAY_SLOTS as u64 * REPLAY_TRANSFORM_STRIDE,
2855 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
2856 mapped_at_creation: false,
2857 });
2858 Self {
2859 slots: std::collections::HashMap::default(),
2860 transform_buffer,
2861 free_ids: (0..MAX_REPLAY_SLOTS).rev().collect(),
2862 next_capture_epoch: 1,
2863 }
2864 }
2865}
2866
2867#[cfg(not(target_arch = "wasm32"))]
2873fn retained_bundles_enabled() -> bool {
2874 std::env::var("CRANPOSE_RETAINED_BUNDLES").as_deref() != Ok("0")
2875}
2876
2877#[cfg(not(target_arch = "wasm32"))]
2885fn instanced_quads_enabled() -> bool {
2886 std::env::var("CRANPOSE_INSTANCED_QUADS").as_deref() != Ok("0")
2887}
2888
2889#[cfg(not(target_arch = "wasm32"))]
2893const INSTANCED_QUAD_INDICES: [u16; 6] = [0, 1, 2, 2, 1, 3];
2894
2895#[cfg(not(target_arch = "wasm32"))]
2901struct InstancedQuadPipelines {
2902 pipeline: LazyGpuResource<wgpu::RenderPipeline>,
2903 pipeline_dst_out: LazyGpuResource<wgpu::RenderPipeline>,
2904 pipeline_solid: LazyGpuResource<wgpu::RenderPipeline>,
2908 index_buffer: wgpu::Buffer,
2911}
2912
2913#[cfg(not(target_arch = "wasm32"))]
2921#[derive(Clone, Debug, PartialEq, Eq, Hash)]
2922struct RetainedBundleOpKey {
2923 slot: u32,
2924 capture_epoch: Option<u64>,
2929 first: u32,
2930 last: u32,
2931 retained_index: u32,
2932 has_mesh: bool,
2933}
2934
2935#[cfg(not(target_arch = "wasm32"))]
2939#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
2940struct RetainedBundleKey {
2941 ops: Vec<RetainedBundleOpKey>,
2942}
2943
2944#[cfg(not(target_arch = "wasm32"))]
2945struct RetainedBundleCacheEntry<B> {
2946 bundle: B,
2947 last_used_frame: u64,
2948}
2949
2950#[cfg(not(target_arch = "wasm32"))]
2959struct RetainedBundleCacheImpl<B> {
2960 entries: HashMap<RetainedBundleKey, RetainedBundleCacheEntry<B>>,
2961 frame: u64,
2962 rebuilds: u64,
2963 cached_executes: u64,
2964 window_rebuilds: u64,
2965 window_executes: u64,
2966}
2967
2968#[cfg(not(target_arch = "wasm32"))]
2969type RetainedBundleCache = RetainedBundleCacheImpl<wgpu::RenderBundle>;
2970
2971#[cfg(not(target_arch = "wasm32"))]
2972impl<B> RetainedBundleCacheImpl<B> {
2973 fn new() -> Self {
2974 Self {
2975 entries: HashMap::default(),
2976 frame: 0,
2977 rebuilds: 0,
2978 cached_executes: 0,
2979 window_rebuilds: 0,
2980 window_executes: 0,
2981 }
2982 }
2983
2984 fn hit(&mut self, key: &RetainedBundleKey) -> bool {
2987 let frame = self.frame;
2988 match self.entries.get_mut(key) {
2989 Some(entry) => {
2990 entry.last_used_frame = frame;
2991 self.cached_executes += 1;
2992 self.window_executes += 1;
2993 true
2994 }
2995 None => false,
2996 }
2997 }
2998
2999 fn insert(&mut self, key: RetainedBundleKey, bundle: B) {
3001 self.rebuilds += 1;
3002 self.window_rebuilds += 1;
3003 self.entries.insert(
3004 key,
3005 RetainedBundleCacheEntry {
3006 bundle,
3007 last_used_frame: self.frame,
3008 },
3009 );
3010 }
3011
3012 fn get(&self, key: &RetainedBundleKey) -> Option<&B> {
3013 self.entries.get(key).map(|entry| &entry.bundle)
3014 }
3015
3016 fn clear(&mut self) {
3021 self.entries.clear();
3022 }
3023
3024 fn end_frame(&mut self) {
3028 let frame = self.frame;
3029 self.entries
3030 .retain(|_, entry| entry.last_used_frame >= frame);
3031 self.frame = self.frame.wrapping_add(1);
3032 let due = self.frame.is_multiple_of(1024)
3037 || (cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG")
3038 && self.frame.is_multiple_of(120));
3039 if due && self.window_rebuilds + self.window_executes > 0 {
3040 log::warn!(
3041 "[retained-bundles] {} stretches, {} rebuilds, {} cached executes ({} live bundles)",
3042 self.window_rebuilds + self.window_executes,
3043 self.window_rebuilds,
3044 self.window_executes,
3045 self.entries.len(),
3046 );
3047 self.window_rebuilds = 0;
3048 self.window_executes = 0;
3049 }
3050 }
3051
3052 fn stats(&self) -> (u64, u64) {
3054 (self.rebuilds, self.cached_executes)
3055 }
3056}
3057
3058struct CachedImageTexture {
3059 _texture: wgpu::Texture,
3060 _view: wgpu::TextureView,
3061 nearest_bind_group: wgpu::BindGroup,
3062 linear_bind_group: wgpu::BindGroup,
3063 bytes: usize,
3070}
3071
3072impl CachedImageTexture {
3073 fn bind_group(&self, sampling: ImageSampling) -> &wgpu::BindGroup {
3074 match sampling {
3075 ImageSampling::Nearest => &self.nearest_bind_group,
3076 ImageSampling::Linear => &self.linear_bind_group,
3077 }
3078 }
3079}
3080
3081#[derive(Clone, Copy)]
3082struct GlyphAtlasEntry {
3083 x: u32,
3084 y: u32,
3085 width: u32,
3086 height: u32,
3087}
3088
3089fn next_glyph_atlas_size(current: u32, max: u32) -> u32 {
3096 current.saturating_mul(2).clamp(1, max.max(1))
3097}
3098
3099struct TextGlyphAtlas {
3100 texture: wgpu::Texture,
3101 _view: wgpu::TextureView,
3102 bind_group: wgpu::BindGroup,
3103 entries: BoundedLruCache<SoftwareGlyphAtlasKey, GlyphAtlasEntry>,
3104 generation: u64,
3105 size: u32,
3109 max_size: u32,
3115 cursor_x: u32,
3116 cursor_y: u32,
3117 row_height: u32,
3118 upload_scratch: Vec<u8>,
3119}
3120
3121impl TextGlyphAtlas {
3122 fn new(
3123 device: &wgpu::Device,
3124 image_layout: &wgpu::BindGroupLayout,
3125 sampler: &wgpu::Sampler,
3126 size: u32,
3127 ) -> Self {
3128 let max_size = TEXT_GLYPH_ATLAS_MAX_SIZE.min(device.limits().max_texture_dimension_2d);
3129 let size = size.clamp(TEXT_GLYPH_ATLAS_MIN_SIZE.min(max_size), max_size);
3130 let texture = Self::create_texture(device, size);
3131 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
3132 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3133 label: Some("Text Glyph Atlas Bind Group"),
3134 layout: image_layout,
3135 entries: &[
3136 wgpu::BindGroupEntry {
3137 binding: 0,
3138 resource: wgpu::BindingResource::TextureView(&view),
3139 },
3140 wgpu::BindGroupEntry {
3141 binding: 1,
3142 resource: wgpu::BindingResource::Sampler(sampler),
3143 },
3144 ],
3145 });
3146 Self {
3147 texture,
3148 _view: view,
3149 bind_group,
3150 entries: BoundedLruCache::with_capacity_at_least_one(MAX_TEXT_GLYPH_ATLAS_ITEMS),
3151 generation: 0,
3152 size,
3153 max_size,
3154 cursor_x: TEXT_GLYPH_ATLAS_PADDING,
3155 cursor_y: TEXT_GLYPH_ATLAS_PADDING,
3156 row_height: 0,
3157 upload_scratch: Vec::new(),
3158 }
3159 }
3160
3161 fn create_texture(device: &wgpu::Device, size: u32) -> wgpu::Texture {
3162 device.create_texture(&wgpu::TextureDescriptor {
3163 label: Some("Text Glyph Atlas Texture"),
3164 size: wgpu::Extent3d {
3165 width: size,
3166 height: size,
3167 depth_or_array_layers: 1,
3168 },
3169 mip_level_count: 1,
3170 sample_count: 1,
3171 dimension: wgpu::TextureDimension::D2,
3172 format: wgpu::TextureFormat::R8Unorm,
3173 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
3174 view_formats: &[],
3175 })
3176 }
3177
3178 fn reset(
3194 &mut self,
3195 device: &wgpu::Device,
3196 image_layout: &wgpu::BindGroupLayout,
3197 sampler: &wgpu::Sampler,
3198 ) {
3199 let generation = self.generation.wrapping_add(1);
3200 let grown = next_glyph_atlas_size(self.size, self.max_size);
3201 let mut next = Self::new(device, image_layout, sampler, grown);
3202 next.generation = generation;
3203 *self = next;
3204 }
3205
3206 fn generation(&self) -> u64 {
3207 self.generation
3208 }
3209
3210 fn size(&self) -> u32 {
3211 self.size
3212 }
3213
3214 fn entry(&mut self, key: &SoftwareGlyphAtlasKey) -> Option<GlyphAtlasEntry> {
3215 self.entries.get(key).copied()
3216 }
3217
3218 fn allocate(&mut self, width: u32, height: u32) -> Option<GlyphAtlasEntry> {
3219 if width == 0
3220 || height == 0
3221 || width + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
3222 || height + TEXT_GLYPH_ATLAS_PADDING * 2 > self.size
3223 {
3224 return None;
3225 }
3226
3227 if self.cursor_x + width + TEXT_GLYPH_ATLAS_PADDING > self.size {
3228 self.cursor_x = TEXT_GLYPH_ATLAS_PADDING;
3229 self.cursor_y = self
3230 .cursor_y
3231 .saturating_add(self.row_height)
3232 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
3233 self.row_height = 0;
3234 }
3235 if self.cursor_y + height + TEXT_GLYPH_ATLAS_PADDING > self.size {
3236 return None;
3237 }
3238
3239 let entry = GlyphAtlasEntry {
3240 x: self.cursor_x,
3241 y: self.cursor_y,
3242 width,
3243 height,
3244 };
3245 self.cursor_x = self
3246 .cursor_x
3247 .saturating_add(width)
3248 .saturating_add(TEXT_GLYPH_ATLAS_PADDING);
3249 self.row_height = self.row_height.max(height);
3250 Some(entry)
3251 }
3252
3253 fn upload_glyph(
3254 &mut self,
3255 key: SoftwareGlyphAtlasKey,
3256 glyph: &SoftwareGlyphAtlasGlyph,
3257 queue: &wgpu::Queue,
3258 executor: &mut WgpuFrameGraphExecutor,
3259 frame_stats: &mut gpu_stats::FrameStats,
3260 ) -> Option<GlyphAtlasEntry> {
3261 if let Some(entry) = self.entry(&key) {
3262 frame_stats.record_text_glyph_atlas_hit();
3263 return Some(entry);
3264 }
3265
3266 let width = u32::try_from(glyph.mask.width).ok()?;
3267 let height = u32::try_from(glyph.mask.height).ok()?;
3268 let entry = self.allocate(width, height)?;
3269 self.upload_scratch.clear();
3270 self.upload_scratch.reserve(
3271 glyph
3272 .mask
3273 .alpha
3274 .len()
3275 .saturating_sub(self.upload_scratch.capacity()),
3276 );
3277 self.upload_scratch.extend(
3278 glyph
3279 .mask
3280 .alpha
3281 .iter()
3282 .map(|alpha| (alpha.clamp(0.0, 1.0) * 255.0).round() as u8),
3283 );
3284
3285 let upload_stats = executor.upload_texture(
3286 queue,
3287 wgpu::TexelCopyTextureInfo {
3288 texture: &self.texture,
3289 mip_level: 0,
3290 origin: wgpu::Origin3d {
3291 x: entry.x,
3292 y: entry.y,
3293 z: 0,
3294 },
3295 aspect: wgpu::TextureAspect::All,
3296 },
3297 &self.upload_scratch,
3298 wgpu::TexelCopyBufferLayout {
3299 offset: 0,
3300 bytes_per_row: Some(entry.width),
3301 rows_per_image: Some(entry.height),
3302 },
3303 wgpu::Extent3d {
3304 width: entry.width,
3305 height: entry.height,
3306 depth_or_array_layers: 1,
3307 },
3308 );
3309 frame_stats.record_command_stats(upload_stats);
3310 frame_stats.record_text_glyph_atlas_miss(entry.width, entry.height);
3311 self.entries.put(key, entry);
3312 Some(entry)
3313 }
3314}
3315
3316struct ImageDrawCmd {
3317 index_start: u32,
3318 scissor: (u32, u32, u32, u32),
3319 image_id: u64,
3320 sampling: ImageSampling,
3321}
3322
3323#[derive(Clone, Copy)]
3324enum GlyphDrawSource {
3325 Shared {
3326 index_start: u32,
3327 index_count: u32,
3328 },
3329 #[cfg(not(target_arch = "wasm32"))]
3330 Retained {
3331 cache_key: TextGlyphRunCacheKey,
3332 uniform_slot: usize,
3333 },
3334}
3335
3336#[derive(Clone, Copy)]
3337struct GlyphDrawCmd {
3338 source: GlyphDrawSource,
3339 scissor: (u32, u32, u32, u32),
3340}
3341
3342impl GlyphDrawCmd {
3343 fn shared(index_start: u32, index_count: u32, scissor: (u32, u32, u32, u32)) -> Self {
3344 Self {
3345 source: GlyphDrawSource::Shared {
3346 index_start,
3347 index_count,
3348 },
3349 scissor,
3350 }
3351 }
3352
3353 #[cfg(not(target_arch = "wasm32"))]
3354 fn retained(
3355 cache_key: TextGlyphRunCacheKey,
3356 uniform_slot: usize,
3357 scissor: (u32, u32, u32, u32),
3358 ) -> Self {
3359 Self {
3360 source: GlyphDrawSource::Retained {
3361 cache_key,
3362 uniform_slot,
3363 },
3364 scissor,
3365 }
3366 }
3367}
3368
3369#[derive(Clone, Copy, Debug, PartialEq)]
3370struct ImageUvRect {
3371 min: [f32; 2],
3372 max: [f32; 2],
3373 sample_bounds: [f32; 4],
3374}
3375
3376struct ShapeBatchBuffers {
3383 shape_buffer: wgpu::Buffer,
3384 gradient_buffer: wgpu::Buffer,
3385 bind_group: wgpu::BindGroup,
3386 shape_capacity: usize,
3387 gradient_capacity: usize,
3388 batch_limits: ShapeBatchLimits,
3389}
3390
3391#[cfg(target_arch = "wasm32")]
3392struct UniformBatchBuffer {
3393 buffer: wgpu::Buffer,
3394 bind_group: wgpu::BindGroup,
3395}
3396
3397#[cfg(target_arch = "wasm32")]
3398struct ImageBatchBuffers {
3399 vertex_buffer: wgpu::Buffer,
3400 index_buffer: wgpu::Buffer,
3401 vertex_capacity: usize,
3402 index_capacity: usize,
3403}
3404
3405#[derive(Clone, Copy, Debug, PartialEq)]
3406struct ViewportUniformParams {
3407 width: u32,
3408 height: u32,
3409 offset: [f32; 2],
3410}
3411
3412#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3413#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
3414enum UploadTarget {
3415 Uniform,
3416 ShapeData,
3417 ShapeGradient,
3418 ImageVertex,
3419 ImageIndex,
3420 #[cfg(not(target_arch = "wasm32"))]
3421 RetainedGlyphUniform,
3422 #[cfg(not(target_arch = "wasm32"))]
3425 ReplayTransform,
3426 #[cfg(not(target_arch = "wasm32"))]
3428 ReplayPaintData(u32),
3429}
3430
3431#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3432#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
3433struct PendingBufferCopy {
3434 source_offset: u64,
3435 target_offset: u64,
3436 size: u64,
3437 target: UploadTarget,
3438}
3439
3440#[derive(Default)]
3441struct StagedBufferUploads {
3442 bytes: Vec<u8>,
3443 copies: Vec<PendingBufferCopy>,
3444}
3445
3446impl StagedBufferUploads {
3447 fn clear(&mut self) {
3448 self.bytes.clear();
3449 self.copies.clear();
3450 }
3451
3452 fn shrink_retained_capacity(&mut self, max_bytes: usize, max_copies: usize) -> bool {
3453 let mut shrunk = false;
3454 if self.bytes.len() <= max_bytes && self.bytes.capacity() > max_bytes {
3455 self.bytes.shrink_to(max_bytes);
3456 shrunk = true;
3457 }
3458 if self.copies.len() <= max_copies && self.copies.capacity() > max_copies {
3459 self.copies.shrink_to(max_copies);
3460 shrunk = true;
3461 }
3462 shrunk
3463 }
3464
3465 fn is_empty(&self) -> bool {
3466 self.copies.is_empty()
3467 }
3468
3469 #[cfg(test)]
3470 fn payload_for_copy(&self, copy: PendingBufferCopy) -> &[u8] {
3471 let start = copy.source_offset as usize;
3472 let end = start + copy.size as usize;
3473 &self.bytes[start..end]
3474 }
3475
3476 #[cfg(not(target_arch = "wasm32"))]
3477 fn stage(&mut self, target: UploadTarget, bytes: &[u8]) {
3478 self.stage_at(target, 0, bytes);
3479 }
3480
3481 #[cfg(not(target_arch = "wasm32"))]
3486 fn record_upload_copy(
3487 &mut self,
3488 target: UploadTarget,
3489 source_offset: u64,
3490 target_offset: u64,
3491 size: u64,
3492 ) {
3493 if size == 0 {
3494 return;
3495 }
3496 self.copies.push(PendingBufferCopy {
3497 source_offset,
3498 target_offset,
3499 size,
3500 target,
3501 });
3502 }
3503
3504 #[cfg(not(target_arch = "wasm32"))]
3505 fn stage_at(&mut self, target: UploadTarget, target_offset: u64, bytes: &[u8]) {
3506 if bytes.is_empty() {
3507 return;
3508 }
3509
3510 debug_assert_eq!(
3511 bytes.len() % wgpu::COPY_BUFFER_ALIGNMENT as usize,
3512 0,
3513 "buffer uploads must be aligned to copy requirements"
3514 );
3515
3516 let aligned_offset = align_usize_to(self.bytes.len(), wgpu::COPY_BUFFER_ALIGNMENT as usize);
3517 if aligned_offset > self.bytes.len() {
3518 self.bytes.resize(aligned_offset, 0);
3519 }
3520
3521 let source_offset = self.bytes.len() as u64;
3522 self.bytes.extend_from_slice(bytes);
3523 self.copies.push(PendingBufferCopy {
3524 source_offset,
3525 target_offset,
3526 size: bytes.len() as u64,
3527 target,
3528 });
3529 }
3530
3531 fn truncate(&mut self, bytes_len: usize, copies_len: usize) {
3532 self.bytes.truncate(bytes_len);
3533 self.copies.truncate(copies_len);
3534 }
3535}
3536
3537fn shape_batch_bind_group_entries<'a>(
3542 shape_buffer: &'a wgpu::Buffer,
3543 gradient_buffer: &'a wgpu::Buffer,
3544 similarity_buffer: &'a wgpu::Buffer,
3545 paint_buffer: Option<&'a wgpu::Buffer>,
3546) -> Vec<wgpu::BindGroupEntry<'a>> {
3547 let mut entries = vec![
3548 wgpu::BindGroupEntry {
3549 binding: 0,
3550 resource: shape_buffer.as_entire_binding(),
3551 },
3552 wgpu::BindGroupEntry {
3553 binding: 1,
3554 resource: gradient_buffer.as_entire_binding(),
3555 },
3556 wgpu::BindGroupEntry {
3557 binding: 2,
3558 resource: similarity_buffer.as_entire_binding(),
3559 },
3560 ];
3561 if let Some(paint_buffer) = paint_buffer {
3562 entries.push(wgpu::BindGroupEntry {
3563 binding: 3,
3564 resource: paint_buffer.as_entire_binding(),
3565 });
3566 }
3567 entries
3568}
3569
3570impl ShapeBatchBuffers {
3571 fn new(
3572 device: &wgpu::Device,
3573 bind_group_layout: &wgpu::BindGroupLayout,
3574 similarity_buffer: &wgpu::Buffer,
3575 paint_buffer: Option<&wgpu::Buffer>,
3576 batch_limits: ShapeBatchLimits,
3577 ) -> Self {
3578 debug_assert_eq!(
3579 paint_buffer.is_some(),
3580 batch_limits.storage,
3581 "the paint binding exists exactly when the layout is in storage mode"
3582 );
3583 let initial_shape_cap = batch_limits.initial_shape_capacity();
3584 let initial_gradient_cap = batch_limits.initial_gradient_capacity();
3585
3586 let shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3587 label: Some("Shape Data Buffer"),
3588 size: (std::mem::size_of::<ShapeData>() * initial_shape_cap) as u64,
3589 usage: batch_limits.data_buffer_usage(),
3590 mapped_at_creation: false,
3591 });
3592
3593 let gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3594 label: Some("Gradient Buffer"),
3595 size: (std::mem::size_of::<GradientStop>() * initial_gradient_cap) as u64,
3596 usage: batch_limits.data_buffer_usage(),
3597 mapped_at_creation: false,
3598 });
3599
3600 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3601 label: Some("Shape Bind Group"),
3602 layout: bind_group_layout,
3603 entries: &shape_batch_bind_group_entries(
3604 &shape_buffer,
3605 &gradient_buffer,
3606 similarity_buffer,
3607 paint_buffer,
3608 ),
3609 });
3610
3611 Self {
3612 shape_buffer,
3613 gradient_buffer,
3614 bind_group,
3615 shape_capacity: initial_shape_cap,
3616 gradient_capacity: initial_gradient_cap,
3617 batch_limits,
3618 }
3619 }
3620
3621 fn ensure_capacity(
3624 &mut self,
3625 device: &wgpu::Device,
3626 bind_group_layout: &wgpu::BindGroupLayout,
3627 similarity_buffer: &wgpu::Buffer,
3628 paint_buffer: Option<&wgpu::Buffer>,
3629 shapes_needed: usize,
3630 gradients_needed: usize,
3631 ) {
3632 let mut need_bind_group_update = false;
3633
3634 if shapes_needed > self.shape_capacity
3638 && self.shape_capacity < self.batch_limits.max_shapes_per_batch
3639 {
3640 let new_cap = shapes_needed
3641 .next_power_of_two()
3642 .min(self.batch_limits.max_shapes_per_batch);
3643 self.shape_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3644 label: Some("Shape Data Buffer"),
3645 size: (std::mem::size_of::<ShapeData>() * new_cap) as u64,
3646 usage: self.batch_limits.data_buffer_usage(),
3647 mapped_at_creation: false,
3648 });
3649 self.shape_capacity = new_cap;
3650 need_bind_group_update = true;
3651 }
3652
3653 if gradients_needed > self.gradient_capacity
3654 && self.gradient_capacity < self.batch_limits.max_gradient_stops
3655 {
3656 let new_cap = gradients_needed
3657 .max(1)
3658 .next_power_of_two()
3659 .min(self.batch_limits.max_gradient_stops);
3660 self.gradient_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3661 label: Some("Gradient Buffer"),
3662 size: (std::mem::size_of::<GradientStop>() * new_cap) as u64,
3663 usage: self.batch_limits.data_buffer_usage(),
3664 mapped_at_creation: false,
3665 });
3666 self.gradient_capacity = new_cap;
3667 need_bind_group_update = true;
3668 }
3669
3670 if need_bind_group_update {
3671 self.bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3672 label: Some("Shape Bind Group"),
3673 layout: bind_group_layout,
3674 entries: &shape_batch_bind_group_entries(
3675 &self.shape_buffer,
3676 &self.gradient_buffer,
3677 similarity_buffer,
3678 paint_buffer,
3679 ),
3680 });
3681 }
3682 }
3683}
3684
3685#[cfg(target_arch = "wasm32")]
3686impl UniformBatchBuffer {
3687 fn new(device: &wgpu::Device, bind_group_layout: &wgpu::BindGroupLayout) -> Self {
3688 let buffer = device.create_buffer(&wgpu::BufferDescriptor {
3689 label: Some("Viewport Uniform Batch Buffer"),
3690 size: std::mem::size_of::<Uniforms>() as u64,
3691 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
3692 mapped_at_creation: false,
3693 });
3694 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
3695 label: Some("Viewport Uniform Batch Bind Group"),
3696 layout: bind_group_layout,
3697 entries: &[wgpu::BindGroupEntry {
3698 binding: 0,
3699 resource: buffer.as_entire_binding(),
3700 }],
3701 });
3702 Self { buffer, bind_group }
3703 }
3704}
3705
3706#[cfg(target_arch = "wasm32")]
3707impl ImageBatchBuffers {
3708 fn new(device: &wgpu::Device) -> Self {
3709 let vertex_capacity = 4;
3710 let index_capacity = 6;
3711 let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3712 label: Some("Image Vertex Batch Buffer"),
3713 size: (std::mem::size_of::<Vertex>() * vertex_capacity) as u64,
3714 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3715 mapped_at_creation: false,
3716 });
3717 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3718 label: Some("Image Index Batch Buffer"),
3719 size: (std::mem::size_of::<u32>() * index_capacity) as u64,
3720 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3721 mapped_at_creation: false,
3722 });
3723 Self {
3724 vertex_buffer,
3725 index_buffer,
3726 vertex_capacity,
3727 index_capacity,
3728 }
3729 }
3730
3731 fn ensure_capacity(
3732 &mut self,
3733 device: &wgpu::Device,
3734 vertices_needed: usize,
3735 indices_needed: usize,
3736 ) {
3737 let hard_max_bytes = HARD_MAX_BUFFER_MB * 1024 * 1024;
3738 if vertices_needed > self.vertex_capacity {
3739 let desired = vertices_needed.next_power_of_two();
3740 let max_count = hard_max_bytes / std::mem::size_of::<Vertex>();
3741 let new_cap = desired.min(max_count);
3742 self.vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3743 label: Some("Image Vertex Batch Buffer"),
3744 size: (std::mem::size_of::<Vertex>() * new_cap) as u64,
3745 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
3746 mapped_at_creation: false,
3747 });
3748 self.vertex_capacity = new_cap;
3749 }
3750 if indices_needed > self.index_capacity {
3751 let desired = indices_needed.next_power_of_two();
3752 let max_count = hard_max_bytes / std::mem::size_of::<u32>();
3753 let new_cap = desired.min(max_count);
3754 self.index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
3755 label: Some("Image Index Batch Buffer"),
3756 size: (std::mem::size_of::<u32>() * new_cap) as u64,
3757 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
3758 mapped_at_creation: false,
3759 });
3760 self.index_capacity = new_cap;
3761 }
3762 }
3763}
3764
3765pub struct GpuRenderer {
3768 pub(crate) device: Arc<wgpu::Device>,
3769 pub(crate) queue: Arc<wgpu::Queue>,
3770 renderer_epoch: u64,
3775 #[cfg(not(target_arch = "wasm32"))]
3781 store_feed_generation: u64,
3782 surface_format: wgpu::TextureFormat,
3783 adapter_backend: wgpu::Backend,
3784 shape_batch_limits: ShapeBatchLimits,
3785 pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3786 pipeline_dst_out: LazyGpuResource<wgpu::RenderPipeline>,
3787 pipeline_solid: LazyGpuResource<wgpu::RenderPipeline>,
3789 #[cfg(not(target_arch = "wasm32"))]
3793 mesh_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3794 #[cfg(not(target_arch = "wasm32"))]
3800 instanced_quads: Option<InstancedQuadPipelines>,
3801 uniform_bind_group_layout: wgpu::BindGroupLayout,
3802 shape_bind_group_layout: wgpu::BindGroupLayout,
3803 dummy_paint_buffer: Option<wgpu::Buffer>,
3806 identity_similarity_buffer: wgpu::Buffer,
3809 #[cfg(not(target_arch = "wasm32"))]
3810 replay_slots: ReplaySlotStore,
3811 image_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3812 image_pipeline_dst_out: LazyGpuResource<wgpu::RenderPipeline>,
3813 glyph_atlas_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3814 #[cfg(not(target_arch = "wasm32"))]
3815 retained_glyph_atlas_pipeline: LazyGpuResource<wgpu::RenderPipeline>,
3816 image_bind_group_layout: wgpu::BindGroupLayout,
3817 #[cfg(not(target_arch = "wasm32"))]
3818 retained_glyph_uniform_bind_group_layout: wgpu::BindGroupLayout,
3819 image_nearest_sampler: wgpu::Sampler,
3820 image_linear_sampler: wgpu::Sampler,
3821 text_fonts: SoftwareTextFontSet,
3822 #[cfg(not(target_arch = "wasm32"))]
3824 upload_buffer: wgpu::Buffer,
3825 #[cfg(not(target_arch = "wasm32"))]
3826 uniform_buffer: wgpu::Buffer,
3827 #[cfg(not(target_arch = "wasm32"))]
3828 uniform_bind_group: wgpu::BindGroup,
3829 #[cfg(not(target_arch = "wasm32"))]
3830 shape_buffers: ShapeBatchBuffers,
3831 #[cfg(not(target_arch = "wasm32"))]
3832 image_vertex_buffer: wgpu::Buffer,
3833 #[cfg(not(target_arch = "wasm32"))]
3834 image_index_buffer: wgpu::Buffer,
3835 #[cfg(not(target_arch = "wasm32"))]
3836 retained_glyph_uniform_buffer: wgpu::Buffer,
3837 #[cfg(not(target_arch = "wasm32"))]
3838 retained_glyph_uniform_bind_group: wgpu::BindGroup,
3839 #[cfg(not(target_arch = "wasm32"))]
3840 retained_glyph_uniform_stride: u64,
3841 #[cfg(not(target_arch = "wasm32"))]
3842 retained_glyph_uniform_capacity: usize,
3843 #[cfg(not(target_arch = "wasm32"))]
3844 retained_glyph_uniform_cursor: usize,
3845 #[cfg(target_arch = "wasm32")]
3846 wasm_uniform_batches: Vec<UniformBatchBuffer>,
3847 #[cfg(target_arch = "wasm32")]
3848 wasm_uniform_batch_cursor: usize,
3849 #[cfg(target_arch = "wasm32")]
3850 wasm_shape_batches: Vec<ShapeBatchBuffers>,
3851 #[cfg(target_arch = "wasm32")]
3852 wasm_shape_batch_cursor: usize,
3853 #[cfg(target_arch = "wasm32")]
3854 wasm_image_batches: Vec<ImageBatchBuffers>,
3855 #[cfg(target_arch = "wasm32")]
3856 wasm_image_batch_cursor: usize,
3857 image_texture_cache: BoundedLruCache<u64, CachedImageTexture>,
3858 image_texture_cache_bytes: usize,
3860 text_image_cache: BoundedLruCache<TextImageCacheKey, CachedTextImage>,
3861 text_glyph_atlas: TextGlyphAtlas,
3862 text_glyph_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedTextGlyphRun>,
3863 #[cfg(not(target_arch = "wasm32"))]
3864 text_glyph_gpu_run_cache: BoundedLruCache<TextGlyphRunCacheKey, CachedGpuTextGlyphRun>,
3865 text_glyph_mask_cache: SoftwareGlyphRasterCache,
3866 text_line_index_cache: TextLineIndexCache,
3867 scratch_shape_data: Vec<ShapeData>,
3868 scratch_gradients: Vec<GradientStop>,
3869 scratch_image_vertices: Vec<Vertex>,
3870 scratch_image_indices: Vec<u32>,
3871 scratch_image_cmds: Vec<ImageDrawCmd>,
3872 scratch_glyph_cmds: Vec<GlyphDrawCmd>,
3873 scratch_text_glyph_run: Vec<SoftwareGlyphAtlasRunGlyph>,
3874 scratch_text_glyph_placements: Vec<SoftwareGlyphAtlasPlacement>,
3875 scratch_text_glyph_quads: Vec<CachedTextGlyphQuad>,
3876 scratch_segment_items: Vec<(usize, SegmentDrawItem)>,
3877 scratch_effect_ranges: Vec<Range<usize>>,
3878 scratch_layer_events: Vec<LayerEvent>,
3879 staged_uploads: StagedBufferUploads,
3880 frame_graph_executor: WgpuFrameGraphExecutor,
3881 deferred_offscreen_releases: Vec<OffscreenTarget>,
3882 effect_renderer: EffectRenderer,
3883 layer_surface_cache: LayerSurfaceCache,
3884 observed_scene_range_cache_misses: BoundedLruCache<LayerRasterCacheKey, ()>,
3885 shadow_surface_cache: BoundedLruCache<ShadowSurfaceCacheKey, CachedShadowSurface>,
3886 shadow_surface_cache_bytes: u64,
3887 frame_stats: gpu_stats::FrameStats,
3888 last_frame_stats: Option<gpu_stats::FrameStatsSnapshot>,
3889 pending_frame_warmup_frames: u8,
3890 frame_count: u64,
3891 gpu_stats_enabled: bool,
3892 warning_state: RendererWarningState,
3893 #[cfg(not(target_arch = "wasm32"))]
3894 replay_upload_stats: ReplayUploadStats,
3895 #[cfg(not(target_arch = "wasm32"))]
3902 replay_color_patches: Vec<crate::scene::ColorPatch>,
3903 #[cfg(not(target_arch = "wasm32"))]
3907 color_patch_scratch: Vec<crate::scene::ColorPatch>,
3908 #[cfg(not(target_arch = "wasm32"))]
3913 replay_ack_confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
3914 #[cfg(not(target_arch = "wasm32"))]
3919 replay_generation_drops: u64,
3920 #[cfg(not(target_arch = "wasm32"))]
3923 retained_bundle_cache: RetainedBundleCache,
3924}
3925
3926#[cfg(not(target_arch = "wasm32"))]
3932#[derive(Default)]
3933struct ReplayUploadStats {
3934 calls: u64,
3935 patched_calls: u64,
3936 patches: u64,
3937 slots: u64,
3938 records: u64,
3939 bytes: u64,
3940 ideal_bytes: u64,
3941 max_frame_bytes: u64,
3942}
3943
3944#[cfg(not(target_arch = "wasm32"))]
3945impl ReplayUploadStats {
3946 const REPORT_CALLS: u64 = 1024;
3956
3957 fn note_frame(&mut self, patches: u64, slots: u64, records: u64, bytes: u64, ideal: u64) {
3958 self.calls += 1;
3959 if patches > 0 {
3960 self.patched_calls += 1;
3961 self.patches += patches;
3962 self.slots += slots;
3963 self.records += records;
3964 self.bytes += bytes;
3965 self.ideal_bytes += ideal;
3966 self.max_frame_bytes = self.max_frame_bytes.max(bytes);
3967 }
3968 if self.calls >= Self::REPORT_CALLS {
3969 let patched = self.patched_calls.max(1);
3970 log::warn!(
3971 "[replay-upload] {} patched of {} drains: avg {:.1} KB/frame (max {:.1} KB), \
3972 color-only would be {:.1} KB/frame; avg {} patches over {} records in {} slots",
3973 self.patched_calls,
3974 self.calls,
3975 self.bytes as f64 / patched as f64 / 1024.0,
3976 self.max_frame_bytes as f64 / 1024.0,
3977 self.ideal_bytes as f64 / patched as f64 / 1024.0,
3978 self.patches / patched,
3979 self.records / patched,
3980 self.slots / patched,
3981 );
3982 *self = Self::default();
3983 }
3984 }
3985}
3986
3987fn image_sampler_descriptor(sampling: ImageSampling) -> wgpu::SamplerDescriptor<'static> {
3988 let filter = match sampling {
3989 ImageSampling::Nearest => wgpu::FilterMode::Nearest,
3990 ImageSampling::Linear => wgpu::FilterMode::Linear,
3991 };
3992 wgpu::SamplerDescriptor {
3993 label: Some(match sampling {
3994 ImageSampling::Nearest => "Nearest Image Sampler",
3995 ImageSampling::Linear => "Linear Image Sampler",
3996 }),
3997 address_mode_u: wgpu::AddressMode::ClampToEdge,
3998 address_mode_v: wgpu::AddressMode::ClampToEdge,
3999 address_mode_w: wgpu::AddressMode::ClampToEdge,
4000 mag_filter: filter,
4001 min_filter: filter,
4002 mipmap_filter: wgpu::MipmapFilterMode::Nearest,
4003 ..Default::default()
4004 }
4005}
4006
4007#[cfg(test)]
4008fn layer_raster_cache_candidate(
4009 layer: &LayerNode,
4010 root_scale: f32,
4011 has_backdrop_underlay: bool,
4012 allow_runtime_cache: bool,
4013) -> Option<(LayerRasterCacheKey, Rect)> {
4014 let mut layer_surface_requirements_cache = HashMap::new();
4015 let surface_requirements =
4016 layer_surface_requirements_cached(layer, &mut layer_surface_requirements_cache);
4017 let runtime_cache_is_safe = allow_runtime_cache
4018 && surface_requirements
4019 .surface_requirements
4020 .has_isolating_requirement()
4021 && !surface_requirements.contains_runtime_shader;
4022 let cache_is_allowed = layer.cache_policy == CachePolicy::Auto
4023 || (allow_runtime_cache && surface_requirements.has_renderer_forced_surface())
4024 || runtime_cache_is_safe;
4025 if !cache_is_allowed {
4026 return None;
4027 }
4028 if layer_uses_external_backdrop_input(layer, has_backdrop_underlay) {
4029 return None;
4030 }
4031 if surface_requirements.contains_runtime_shader {
4034 return None;
4035 }
4036
4037 let logical_rect = estimate_layer_surface_rect(layer);
4038 let pixel_size = surface_target_size(logical_rect, root_scale, u32::MAX);
4039 Some((
4040 LayerRasterCacheKey::new(
4041 layer.node_id,
4042 layer.target_content_hash(),
4043 layer.effect_hash(),
4044 logical_rect,
4045 pixel_size,
4046 ScaleBucket::from_scale(root_scale),
4047 ),
4048 logical_rect,
4049 ))
4050}
4051
4052impl GpuRenderer {
4053 pub fn new(
4054 device: Arc<wgpu::Device>,
4055 queue: Arc<wgpu::Queue>,
4056 surface_format: wgpu::TextureFormat,
4057 adapter_backend: wgpu::Backend,
4058 text_fonts: SoftwareTextFontSet,
4059 renderer_epoch: u64,
4060 store_feed_generation: u64,
4061 ) -> Self {
4062 #[cfg(target_arch = "wasm32")]
4063 let _ = store_feed_generation;
4064 let shape_batch_limits = ShapeBatchLimits::for_device(&device);
4065 let uniform_bind_group_layout =
4066 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4067 label: Some("Uniform Bind Group Layout"),
4068 entries: &[wgpu::BindGroupLayoutEntry {
4069 binding: 0,
4070 visibility: wgpu::ShaderStages::VERTEX,
4071 ty: wgpu::BindingType::Buffer {
4072 ty: wgpu::BufferBindingType::Uniform,
4073 has_dynamic_offset: false,
4074 min_binding_size: None,
4075 },
4076 count: None,
4077 }],
4078 });
4079 #[cfg(not(target_arch = "wasm32"))]
4080 let retained_glyph_uniform_bind_group_layout =
4081 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4082 label: Some("Retained Glyph Dynamic Uniform Bind Group Layout"),
4083 entries: &[wgpu::BindGroupLayoutEntry {
4084 binding: 0,
4085 visibility: wgpu::ShaderStages::VERTEX,
4086 ty: wgpu::BindingType::Buffer {
4087 ty: wgpu::BufferBindingType::Uniform,
4088 has_dynamic_offset: true,
4089 min_binding_size: wgpu::BufferSize::new(
4090 std::mem::size_of::<Uniforms>() as u64
4091 ),
4092 },
4093 count: None,
4094 }],
4095 });
4096
4097 let mut shape_bind_group_layout_entries = vec![
4106 wgpu::BindGroupLayoutEntry {
4107 binding: 0,
4108 visibility: wgpu::ShaderStages::VERTEX_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 {
4117 binding: 1,
4118 visibility: wgpu::ShaderStages::FRAGMENT,
4119 ty: wgpu::BindingType::Buffer {
4120 ty: shape_batch_limits.data_binding_type(),
4121 has_dynamic_offset: false,
4122 min_binding_size: None,
4123 },
4124 count: None,
4125 },
4126 wgpu::BindGroupLayoutEntry {
4131 binding: 2,
4132 visibility: wgpu::ShaderStages::VERTEX,
4133 ty: wgpu::BindingType::Buffer {
4134 ty: wgpu::BufferBindingType::Uniform,
4135 has_dynamic_offset: true,
4136 min_binding_size: wgpu::BufferSize::new(
4137 std::mem::size_of::<SimilarityTransform>() as u64,
4138 ),
4139 },
4140 count: None,
4141 },
4142 ];
4143 if shape_batch_limits.storage {
4149 shape_bind_group_layout_entries.push(wgpu::BindGroupLayoutEntry {
4150 binding: 3,
4151 visibility: wgpu::ShaderStages::VERTEX,
4152 ty: wgpu::BindingType::Buffer {
4153 ty: wgpu::BufferBindingType::Storage { read_only: true },
4154 has_dynamic_offset: false,
4155 min_binding_size: None,
4156 },
4157 count: None,
4158 });
4159 }
4160 let shape_bind_group_layout =
4161 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4162 label: Some("Shape Bind Group Layout"),
4163 entries: &shape_bind_group_layout_entries,
4164 });
4165
4166 let identity_similarity_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4167 label: Some("Identity Similarity Buffer"),
4168 size: std::mem::size_of::<SimilarityTransform>() as u64,
4169 usage: wgpu::BufferUsages::UNIFORM,
4170 mapped_at_creation: true,
4171 });
4172 identity_similarity_buffer
4173 .slice(..)
4174 .get_mapped_range_mut()
4175 .copy_from_slice(bytemuck::bytes_of(&SimilarityTransform::IDENTITY));
4176 identity_similarity_buffer.unmap();
4177
4178 let dummy_paint_buffer = shape_batch_limits.storage.then(|| {
4183 device.create_buffer(&wgpu::BufferDescriptor {
4184 label: Some("Dummy Paint Buffer"),
4185 size: std::mem::size_of::<[f32; 4]>() as u64,
4186 usage: wgpu::BufferUsages::STORAGE,
4187 mapped_at_creation: false,
4188 })
4189 });
4190 #[cfg(not(target_arch = "wasm32"))]
4191 let replay_slot_store = ReplaySlotStore::new(&device);
4192
4193 let pipeline = LazyGpuResource::new("shape/src-over");
4194 let pipeline_dst_out = LazyGpuResource::new("shape/dst-out");
4195 let pipeline_solid = LazyGpuResource::new("shape/solid-src-over");
4196 #[cfg(not(target_arch = "wasm32"))]
4197 let mesh_pipeline = LazyGpuResource::new("shape/mesh");
4198 #[cfg(not(target_arch = "wasm32"))]
4204 let instanced_quads =
4205 (shape_batch_limits.storage && instanced_quads_enabled()).then(|| {
4206 let index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4207 label: Some("Instanced Quad Index Buffer"),
4208 size: std::mem::size_of_val(&INSTANCED_QUAD_INDICES) as u64,
4209 usage: wgpu::BufferUsages::INDEX,
4210 mapped_at_creation: true,
4211 });
4212 index_buffer
4213 .slice(..)
4214 .get_mapped_range_mut()
4215 .copy_from_slice(bytemuck::cast_slice(&INSTANCED_QUAD_INDICES));
4216 index_buffer.unmap();
4217 InstancedQuadPipelines {
4218 pipeline: LazyGpuResource::new("shape/instanced-src-over"),
4219 pipeline_dst_out: LazyGpuResource::new("shape/instanced-dst-out"),
4220 pipeline_solid: LazyGpuResource::new("shape/instanced-solid"),
4221 index_buffer,
4222 }
4223 });
4224
4225 let image_bind_group_layout =
4226 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
4227 label: Some("Image Texture Bind Group Layout"),
4228 entries: &[
4229 wgpu::BindGroupLayoutEntry {
4230 binding: 0,
4231 visibility: wgpu::ShaderStages::FRAGMENT,
4232 ty: wgpu::BindingType::Texture {
4233 multisampled: false,
4234 view_dimension: wgpu::TextureViewDimension::D2,
4235 sample_type: wgpu::TextureSampleType::Float { filterable: true },
4236 },
4237 count: None,
4238 },
4239 wgpu::BindGroupLayoutEntry {
4240 binding: 1,
4241 visibility: wgpu::ShaderStages::FRAGMENT,
4242 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
4243 count: None,
4244 },
4245 ],
4246 });
4247
4248 let image_pipeline = LazyGpuResource::new("image/src-over");
4249 let image_pipeline_dst_out = LazyGpuResource::new("image/dst-out");
4250 let glyph_atlas_pipeline = LazyGpuResource::new("glyph/shared");
4251 #[cfg(not(target_arch = "wasm32"))]
4252 let retained_glyph_atlas_pipeline = LazyGpuResource::new("glyph/retained");
4253
4254 #[cfg(not(target_arch = "wasm32"))]
4255 let upload_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4256 label: Some("Frame Upload Buffer"),
4257 size: INITIAL_UPLOAD_BUFFER_BYTES,
4258 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
4259 mapped_at_creation: false,
4260 });
4261
4262 #[cfg(not(target_arch = "wasm32"))]
4263 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4264 label: Some("Uniform Buffer"),
4265 size: std::mem::size_of::<Uniforms>() as u64,
4266 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4267 mapped_at_creation: false,
4268 });
4269
4270 #[cfg(not(target_arch = "wasm32"))]
4271 let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
4272 label: Some("Uniform Bind Group"),
4273 layout: &uniform_bind_group_layout,
4274 entries: &[wgpu::BindGroupEntry {
4275 binding: 0,
4276 resource: uniform_buffer.as_entire_binding(),
4277 }],
4278 });
4279
4280 #[cfg(not(target_arch = "wasm32"))]
4281 let shape_buffers = ShapeBatchBuffers::new(
4282 &device,
4283 &shape_bind_group_layout,
4284 &identity_similarity_buffer,
4285 dummy_paint_buffer.as_ref(),
4286 shape_batch_limits,
4287 );
4288
4289 let image_nearest_sampler =
4290 device.create_sampler(&image_sampler_descriptor(ImageSampling::Nearest));
4291 let image_linear_sampler =
4292 device.create_sampler(&image_sampler_descriptor(ImageSampling::Linear));
4293 let text_glyph_atlas = TextGlyphAtlas::new(
4294 &device,
4295 &image_bind_group_layout,
4296 &image_nearest_sampler,
4297 TEXT_GLYPH_ATLAS_MIN_SIZE,
4298 );
4299
4300 #[cfg(not(target_arch = "wasm32"))]
4301 let image_vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4302 label: Some("Image Vertex Buffer"),
4303 size: (std::mem::size_of::<Vertex>() * 4) as u64,
4304 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
4305 mapped_at_creation: false,
4306 });
4307
4308 #[cfg(not(target_arch = "wasm32"))]
4309 let image_index_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4310 label: Some("Image Index Buffer"),
4311 size: (std::mem::size_of::<u32>() * 6) as u64,
4312 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
4313 mapped_at_creation: false,
4314 });
4315 #[cfg(not(target_arch = "wasm32"))]
4316 let retained_glyph_uniform_stride = align_usize_to(
4317 std::mem::size_of::<Uniforms>(),
4318 (device.limits().min_uniform_buffer_offset_alignment as usize)
4319 .max(wgpu::COPY_BUFFER_ALIGNMENT as usize),
4320 ) as u64;
4321 #[cfg(not(target_arch = "wasm32"))]
4322 let retained_glyph_uniform_capacity = INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS;
4323 #[cfg(not(target_arch = "wasm32"))]
4324 let retained_glyph_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
4325 label: Some("Retained Glyph Uniform Buffer"),
4326 size: retained_glyph_uniform_stride * retained_glyph_uniform_capacity as u64,
4327 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
4328 mapped_at_creation: false,
4329 });
4330 #[cfg(not(target_arch = "wasm32"))]
4331 let retained_glyph_uniform_bind_group =
4332 device.create_bind_group(&wgpu::BindGroupDescriptor {
4333 label: Some("Retained Glyph Uniform Bind Group"),
4334 layout: &retained_glyph_uniform_bind_group_layout,
4335 entries: &[wgpu::BindGroupEntry {
4336 binding: 0,
4337 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
4338 buffer: &retained_glyph_uniform_buffer,
4339 offset: 0,
4340 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
4341 }),
4342 }],
4343 });
4344
4345 let effect_renderer = EffectRenderer::new(&device, surface_format, adapter_backend);
4346
4347 Self {
4348 device,
4349 queue,
4350 renderer_epoch,
4351 #[cfg(not(target_arch = "wasm32"))]
4352 store_feed_generation,
4353 surface_format,
4354 adapter_backend,
4355 shape_batch_limits,
4356 pipeline,
4357 pipeline_dst_out,
4358 pipeline_solid,
4359 #[cfg(not(target_arch = "wasm32"))]
4360 mesh_pipeline,
4361 #[cfg(not(target_arch = "wasm32"))]
4362 instanced_quads,
4363 uniform_bind_group_layout,
4364 shape_bind_group_layout,
4365 dummy_paint_buffer,
4366 identity_similarity_buffer,
4367 #[cfg(not(target_arch = "wasm32"))]
4368 replay_slots: replay_slot_store,
4369 image_pipeline,
4370 image_pipeline_dst_out,
4371 glyph_atlas_pipeline,
4372 #[cfg(not(target_arch = "wasm32"))]
4373 retained_glyph_atlas_pipeline,
4374 image_bind_group_layout,
4375 #[cfg(not(target_arch = "wasm32"))]
4376 retained_glyph_uniform_bind_group_layout,
4377 image_nearest_sampler,
4378 image_linear_sampler,
4379 text_fonts,
4380 #[cfg(not(target_arch = "wasm32"))]
4381 upload_buffer,
4382 #[cfg(not(target_arch = "wasm32"))]
4383 uniform_buffer,
4384 #[cfg(not(target_arch = "wasm32"))]
4385 uniform_bind_group,
4386 #[cfg(not(target_arch = "wasm32"))]
4387 shape_buffers,
4388 #[cfg(not(target_arch = "wasm32"))]
4389 image_vertex_buffer,
4390 #[cfg(not(target_arch = "wasm32"))]
4391 image_index_buffer,
4392 #[cfg(not(target_arch = "wasm32"))]
4393 retained_glyph_uniform_buffer,
4394 #[cfg(not(target_arch = "wasm32"))]
4395 retained_glyph_uniform_bind_group,
4396 #[cfg(not(target_arch = "wasm32"))]
4397 retained_glyph_uniform_stride,
4398 #[cfg(not(target_arch = "wasm32"))]
4399 retained_glyph_uniform_capacity,
4400 #[cfg(not(target_arch = "wasm32"))]
4401 retained_glyph_uniform_cursor: 0,
4402 #[cfg(target_arch = "wasm32")]
4403 wasm_uniform_batches: Vec::new(),
4404 #[cfg(target_arch = "wasm32")]
4405 wasm_uniform_batch_cursor: 0,
4406 #[cfg(target_arch = "wasm32")]
4407 wasm_shape_batches: Vec::new(),
4408 #[cfg(target_arch = "wasm32")]
4409 wasm_shape_batch_cursor: 0,
4410 #[cfg(target_arch = "wasm32")]
4411 wasm_image_batches: Vec::new(),
4412 #[cfg(target_arch = "wasm32")]
4413 wasm_image_batch_cursor: 0,
4414 image_texture_cache: BoundedLruCache::with_capacity_at_least_one(
4415 MAX_TEXTURE_CACHE_ITEMS,
4416 ),
4417 image_texture_cache_bytes: 0,
4418 text_image_cache: BoundedLruCache::with_capacity_at_least_one(
4419 MAX_TEXT_IMAGE_CACHE_ITEMS,
4420 ),
4421 text_glyph_atlas,
4422 text_glyph_run_cache: BoundedLruCache::with_capacity_at_least_one(
4423 MAX_TEXT_GLYPH_RUN_CACHE_ITEMS,
4424 ),
4425 #[cfg(not(target_arch = "wasm32"))]
4426 text_glyph_gpu_run_cache: BoundedLruCache::with_capacity_at_least_one(
4427 MAX_TEXT_GLYPH_GPU_RUN_CACHE_ITEMS,
4428 ),
4429 text_glyph_mask_cache: SoftwareGlyphRasterCache::with_capacity_at_least_one(
4430 MAX_TEXT_GLYPH_MASK_CACHE_ITEMS,
4431 ),
4432 text_line_index_cache: TextLineIndexCache::new(MAX_TEXT_LINE_INDEX_CACHE_ITEMS),
4433 scratch_shape_data: Vec::new(),
4434 scratch_gradients: Vec::new(),
4435 scratch_image_vertices: Vec::new(),
4436 scratch_image_indices: Vec::new(),
4437 scratch_image_cmds: Vec::new(),
4438 scratch_glyph_cmds: Vec::new(),
4439 scratch_text_glyph_run: Vec::new(),
4440 scratch_text_glyph_placements: Vec::new(),
4441 scratch_text_glyph_quads: Vec::new(),
4442 scratch_segment_items: Vec::new(),
4443 scratch_effect_ranges: Vec::new(),
4444 scratch_layer_events: Vec::new(),
4445 staged_uploads: StagedBufferUploads::default(),
4446 frame_graph_executor: WgpuFrameGraphExecutor::new(),
4447 deferred_offscreen_releases: Vec::new(),
4448 effect_renderer,
4449 layer_surface_cache: LayerSurfaceCache::new(),
4450 observed_scene_range_cache_misses: BoundedLruCache::with_capacity_at_least_one(
4451 MAX_OBSERVED_SCENE_RANGE_CACHE_MISSES,
4452 ),
4453 shadow_surface_cache: BoundedLruCache::with_capacity_at_least_one(
4454 MAX_SHADOW_SURFACE_CACHE_ITEMS,
4455 ),
4456 shadow_surface_cache_bytes: 0,
4457 frame_stats: gpu_stats::FrameStats::default(),
4458 last_frame_stats: None,
4459 pending_frame_warmup_frames: 0,
4460 frame_count: 0,
4461 gpu_stats_enabled: gpu_stats_enabled(),
4462 warning_state: RendererWarningState::default(),
4463 #[cfg(not(target_arch = "wasm32"))]
4464 replay_upload_stats: ReplayUploadStats::default(),
4465 #[cfg(not(target_arch = "wasm32"))]
4466 replay_color_patches: Vec::new(),
4467 #[cfg(not(target_arch = "wasm32"))]
4468 color_patch_scratch: Vec::new(),
4469 #[cfg(not(target_arch = "wasm32"))]
4470 replay_ack_confirmations: Vec::new(),
4471 #[cfg(not(target_arch = "wasm32"))]
4472 replay_generation_drops: 0,
4473 #[cfg(not(target_arch = "wasm32"))]
4474 retained_bundle_cache: RetainedBundleCache::new(),
4475 }
4476 }
4477
4478 fn shape_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
4479 let resource = match blend_mode {
4480 BlendMode::DstOut => &self.pipeline_dst_out,
4481 _ => &self.pipeline,
4482 };
4483 resource.get_or_init(self.adapter_backend, || {
4484 create_shape_pipeline(
4485 &self.device,
4486 self.surface_format,
4487 &self.uniform_bind_group_layout,
4488 &self.shape_bind_group_layout,
4489 blend_mode,
4490 self.shape_batch_limits,
4491 "fs_main",
4492 )
4493 })
4494 }
4495
4496 fn shape_pipeline_solid(&self) -> &wgpu::RenderPipeline {
4501 self.pipeline_solid.get_or_init(self.adapter_backend, || {
4502 create_shape_pipeline(
4503 &self.device,
4504 self.surface_format,
4505 &self.uniform_bind_group_layout,
4506 &self.shape_bind_group_layout,
4507 BlendMode::SrcOver,
4508 self.shape_batch_limits,
4509 "fs_solid",
4510 )
4511 })
4512 }
4513
4514 #[cfg(not(target_arch = "wasm32"))]
4515 fn mesh_pipeline(&self) -> &wgpu::RenderPipeline {
4516 self.mesh_pipeline.get_or_init(self.adapter_backend, || {
4517 create_mesh_shape_pipeline(
4518 &self.device,
4519 self.surface_format,
4520 &self.uniform_bind_group_layout,
4521 &self.shape_bind_group_layout,
4522 self.shape_batch_limits,
4523 )
4524 })
4525 }
4526
4527 #[cfg(not(target_arch = "wasm32"))]
4528 fn instanced_pipeline<'a>(
4529 &'a self,
4530 instanced: &'a InstancedQuadPipelines,
4531 blend_mode: BlendMode,
4532 ) -> &'a wgpu::RenderPipeline {
4533 let resource = match blend_mode {
4534 BlendMode::DstOut => &instanced.pipeline_dst_out,
4535 _ => &instanced.pipeline,
4536 };
4537 resource.get_or_init(self.adapter_backend, || {
4538 create_instanced_shape_pipeline(
4539 &self.device,
4540 self.surface_format,
4541 &self.uniform_bind_group_layout,
4542 &self.shape_bind_group_layout,
4543 blend_mode,
4544 self.shape_batch_limits,
4545 "fs_main",
4546 )
4547 })
4548 }
4549
4550 #[cfg(not(target_arch = "wasm32"))]
4552 fn instanced_pipeline_solid<'a>(
4553 &'a self,
4554 instanced: &'a InstancedQuadPipelines,
4555 ) -> &'a wgpu::RenderPipeline {
4556 instanced
4557 .pipeline_solid
4558 .get_or_init(self.adapter_backend, || {
4559 create_instanced_shape_pipeline(
4560 &self.device,
4561 self.surface_format,
4562 &self.uniform_bind_group_layout,
4563 &self.shape_bind_group_layout,
4564 BlendMode::SrcOver,
4565 self.shape_batch_limits,
4566 "fs_solid",
4567 )
4568 })
4569 }
4570
4571 fn image_pipeline(&self, blend_mode: BlendMode) -> &wgpu::RenderPipeline {
4572 let resource = match blend_mode {
4573 BlendMode::DstOut => &self.image_pipeline_dst_out,
4574 _ => &self.image_pipeline,
4575 };
4576 resource.get_or_init(self.adapter_backend, || {
4577 create_image_pipeline(
4578 &self.device,
4579 self.surface_format,
4580 &self.uniform_bind_group_layout,
4581 &self.image_bind_group_layout,
4582 blend_mode,
4583 )
4584 })
4585 }
4586
4587 fn glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
4588 self.glyph_atlas_pipeline
4589 .get_or_init(self.adapter_backend, || {
4590 create_glyph_atlas_pipeline(
4591 &self.device,
4592 self.surface_format,
4593 &self.uniform_bind_group_layout,
4594 &self.image_bind_group_layout,
4595 )
4596 })
4597 }
4598
4599 #[cfg(not(target_arch = "wasm32"))]
4600 fn retained_glyph_atlas_pipeline(&self) -> &wgpu::RenderPipeline {
4601 self.retained_glyph_atlas_pipeline
4602 .get_or_init(self.adapter_backend, || {
4603 create_glyph_atlas_pipeline(
4604 &self.device,
4605 self.surface_format,
4606 &self.retained_glyph_uniform_bind_group_layout,
4607 &self.image_bind_group_layout,
4608 )
4609 })
4610 }
4611
4612 fn ensure_image_cached(&mut self, image: &ImageBitmap) -> Result<(), String> {
4613 if self.image_texture_cache.get(&image.id()).is_some() {
4614 return Ok(());
4615 }
4616
4617 let size = wgpu::Extent3d {
4618 width: image.width(),
4619 height: image.height(),
4620 depth_or_array_layers: 1,
4621 };
4622
4623 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
4624 label: Some("Image Texture"),
4625 size,
4626 mip_level_count: 1,
4627 sample_count: 1,
4628 dimension: wgpu::TextureDimension::D2,
4629 format: wgpu::TextureFormat::Rgba8Unorm,
4630 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
4631 view_formats: &[],
4632 });
4633
4634 let upload_stats = self.frame_graph_executor.upload_texture(
4635 &self.queue,
4636 wgpu::TexelCopyTextureInfo {
4637 texture: &texture,
4638 mip_level: 0,
4639 origin: wgpu::Origin3d::ZERO,
4640 aspect: wgpu::TextureAspect::All,
4641 },
4642 image.pixels(),
4643 wgpu::TexelCopyBufferLayout {
4644 offset: 0,
4645 bytes_per_row: Some(4 * image.width()),
4646 rows_per_image: Some(image.height()),
4647 },
4648 size,
4649 );
4650 self.frame_stats.record_command_stats(upload_stats);
4651
4652 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
4653 let nearest_bind_group = self.image_bind_group(&view, &self.image_nearest_sampler);
4654 let linear_bind_group = self.image_bind_group(&view, &self.image_linear_sampler);
4655
4656 let bytes = image.width() as usize * image.height() as usize * 4;
4657 if let Some(replaced) = self.image_texture_cache.put(
4658 image.id(),
4659 CachedImageTexture {
4660 _texture: texture,
4661 _view: view,
4662 nearest_bind_group,
4663 linear_bind_group,
4664 bytes,
4665 },
4666 ) {
4667 self.image_texture_cache_bytes = self
4668 .image_texture_cache_bytes
4669 .saturating_sub(replaced.bytes);
4670 }
4671 self.image_texture_cache_bytes += bytes;
4672 while self.image_texture_cache_bytes > MAX_IMAGE_TEXTURE_CACHE_BYTES
4675 && self.image_texture_cache.len() > 1
4676 {
4677 let Some((_, evicted)) = self.image_texture_cache.pop_lru() else {
4678 break;
4679 };
4680 self.image_texture_cache_bytes =
4681 self.image_texture_cache_bytes.saturating_sub(evicted.bytes);
4682 }
4683 Ok(())
4684 }
4685
4686 fn image_bind_group(
4687 &self,
4688 view: &wgpu::TextureView,
4689 sampler: &wgpu::Sampler,
4690 ) -> wgpu::BindGroup {
4691 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
4692 label: Some("Image Texture Bind Group"),
4693 layout: &self.image_bind_group_layout,
4694 entries: &[
4695 wgpu::BindGroupEntry {
4696 binding: 0,
4697 resource: wgpu::BindingResource::TextureView(view),
4698 },
4699 wgpu::BindGroupEntry {
4700 binding: 1,
4701 resource: wgpu::BindingResource::Sampler(sampler),
4702 },
4703 ],
4704 })
4705 }
4706
4707 fn max_texture_dim(&self) -> u32 {
4710 self.effect_renderer.max_texture_dim()
4711 }
4712
4713 fn acquire_offscreen(&mut self, width: u32, height: u32) -> OffscreenTarget {
4714 self.effect_renderer
4715 .acquire_offscreen(&self.device, width, height, Some(&self.frame_stats))
4716 }
4717
4718 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
4719 self.acquire_offscreen(width, height)
4720 }
4721
4722 fn transient_offscreen_descriptor(
4723 &self,
4724 label: &'static str,
4725 width: u32,
4726 height: u32,
4727 ) -> FrameTextureDescriptor {
4728 let max_texture_dim = self.max_texture_dim();
4729 FrameTextureDescriptor::render_attachment(
4730 label,
4731 width.min(max_texture_dim),
4732 height.min(max_texture_dim),
4733 self.surface_format,
4734 )
4735 }
4736
4737 fn defer_offscreen_release(&mut self, target: OffscreenTarget) {
4738 self.deferred_offscreen_releases.push(target);
4739 }
4740
4741 fn flush_deferred_offscreen_releases(&mut self) {
4742 for target in self.deferred_offscreen_releases.drain(..) {
4743 self.effect_renderer.release_offscreen(target);
4744 }
4745 }
4746
4747 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
4748 if let LayerSurfaceTexture::Owned(target) = target {
4749 self.defer_offscreen_release(target);
4750 }
4751 }
4752
4753 fn cached_layer_surface(
4754 &mut self,
4755 key: &LayerRasterCacheKey,
4756 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
4757 self.layer_surface_cache.get(key, &self.frame_stats)
4758 }
4759
4760 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
4761 admit_layer_surface_cache_miss_impl(key, &mut self.observed_scene_range_cache_misses)
4762 }
4763
4764 fn insert_cached_layer_surface(
4765 &mut self,
4766 key: LayerRasterCacheKey,
4767 target: OffscreenTarget,
4768 logical_rect: Rect,
4769 ) -> Rc<OffscreenTarget> {
4770 self.layer_surface_cache
4771 .insert(key, target, logical_rect, &self.frame_stats)
4772 }
4773
4774 fn cached_shadow_surface(
4775 &mut self,
4776 key: &ShadowSurfaceCacheKey,
4777 ) -> Option<Rc<OffscreenTarget>> {
4778 self.shadow_surface_cache
4779 .get(key)
4780 .map(|cached| cached.target.clone())
4781 }
4782
4783 fn cached_shape_shadow_composite(
4784 &mut self,
4785 shadow: &ShadowDraw,
4786 width: u32,
4787 height: u32,
4788 root_scale: f32,
4789 ) -> Option<CachedShadowComposite> {
4790 if shadow.blur_radius <= 0.0 || shadow.shapes.is_empty() || !shadow.texts.is_empty() {
4791 return None;
4792 }
4793
4794 let plan = shape_shadow_surface_plan(
4795 &shadow.shapes,
4796 shadow.clip,
4797 shadow.blur_radius,
4798 width,
4799 height,
4800 root_scale,
4801 self.max_texture_dim(),
4802 )?;
4803 let key = shape_shadow_surface_cache_key(
4804 &shadow.shapes,
4805 plan.source_device_bounds,
4806 plan.pixel_radius,
4807 root_scale,
4808 )?;
4809 let cached = self.cached_shadow_surface(&key)?;
4810 let viewport_offset = [plan.source_device_bounds.x, plan.source_device_bounds.y];
4811 self.frame_stats.record_shadow_shape_cache_hit(
4812 plan.source_device_bounds.width,
4813 plan.source_device_bounds.height,
4814 );
4815
4816 let clip_scissor = shadow
4817 .clip
4818 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
4819 let scissor = clip_scissor.or(plan.processing_scissor);
4820 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
4821 mask.rect[0] -= viewport_offset[0];
4822 mask.rect[1] -= viewport_offset[1];
4823 mask
4824 });
4825 let dest_viewport = Some((
4826 viewport_offset[0],
4827 viewport_offset[1],
4828 plan.source_device_bounds.width as f32,
4829 plan.source_device_bounds.height as f32,
4830 ));
4831
4832 Some(CachedShadowComposite {
4833 source: cached,
4834 scissor,
4835 rounded_mask,
4836 dest_viewport,
4837 })
4838 }
4839
4840 fn insert_cached_shadow_surface(
4841 &mut self,
4842 key: ShadowSurfaceCacheKey,
4843 target: OffscreenTarget,
4844 ) {
4845 let byte_size = offscreen_byte_size(target.width, target.height);
4846 while self.shadow_surface_cache_bytes + byte_size > MAX_SHADOW_SURFACE_CACHE_BYTES {
4847 let Some((_evicted_key, evicted_entry)) = self.shadow_surface_cache.pop_lru() else {
4848 break;
4849 };
4850 self.shadow_surface_cache_bytes = self
4851 .shadow_surface_cache_bytes
4852 .saturating_sub(evicted_entry.byte_size);
4853 }
4854
4855 let cached = CachedShadowSurface {
4856 target: Rc::new(target),
4857 byte_size,
4858 };
4859 if let Some((_replaced_key, replaced_entry)) = self.shadow_surface_cache.push(key, cached) {
4860 self.shadow_surface_cache_bytes = self
4861 .shadow_surface_cache_bytes
4862 .saturating_sub(replaced_entry.byte_size);
4863 }
4864 self.shadow_surface_cache_bytes = self.shadow_surface_cache_bytes.saturating_add(byte_size);
4865 }
4866
4867 fn supports_render_effect(&self, effect: &RenderEffect) -> bool {
4868 is_render_effect_supported(effect)
4869 }
4870}
4871
4872struct RecordingSurfaceBackend<'renderer, 'recorder, C: FrameCommandRecorder> {
4873 renderer: &'renderer mut GpuRenderer,
4874 recorder: &'recorder mut C,
4875}
4876
4877impl<C: FrameCommandRecorder> RecordingSurfaceBackend<'_, '_, C> {
4878 #[allow(clippy::too_many_arguments)]
4879 fn render_range_with_layer_events_to_target_recorded(
4880 &mut self,
4881 target: &OffscreenTarget,
4882 shapes: &[DrawShape],
4883 images: &[ImageDraw],
4884 texts: &[TextDraw],
4885 shadow_draws: &[ShadowDraw],
4886 draw_ops: &[DrawOp],
4887 effect_layers: &[EffectLayer],
4888 backdrop_layers: &[BackdropLayer],
4889 z_start: usize,
4890 z_end: usize,
4891 excluded_effect_layer: Option<usize>,
4892 width: u32,
4893 height: u32,
4894 root_scale: f32,
4895 backdrop_underlay: Option<&OffscreenTarget>,
4896 initial_load_op: wgpu::LoadOp<wgpu::Color>,
4897 ) -> Result<(), String> {
4898 if z_start >= z_end {
4899 if matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
4900 self.clear_target_view_with_load_op(&target.view, initial_load_op);
4901 }
4902 return Ok(());
4903 }
4904
4905 let mut effect_z_ranges = std::mem::take(&mut self.renderer.scratch_effect_ranges);
4906 collect_effect_ranges(
4907 effect_layers,
4908 z_start,
4909 z_end,
4910 excluded_effect_layer,
4911 &mut effect_z_ranges,
4912 );
4913 let mut events = std::mem::take(&mut self.renderer.scratch_layer_events);
4914 collect_layer_events(
4915 effect_layers,
4916 backdrop_layers,
4917 z_start,
4918 z_end,
4919 excluded_effect_layer,
4920 &mut events,
4921 );
4922
4923 let result = (|| -> Result<(), String> {
4924 let mut next_load_op = initial_load_op;
4925 let mut cursor_z = z_start;
4926 for event in &events {
4927 if event.z_index > cursor_z {
4928 self.render_non_effect_segment(
4929 &target.view,
4930 shapes,
4931 images,
4932 texts,
4933 shadow_draws,
4934 &[],
4937 draw_ops,
4938 cursor_z,
4939 event.z_index,
4940 &effect_z_ranges,
4941 width,
4942 height,
4943 root_scale,
4944 next_load_op,
4945 )?;
4946 next_load_op = wgpu::LoadOp::Load;
4947 cursor_z = event.z_index;
4948 } else if event.z_index < cursor_z {
4949 continue;
4950 }
4951
4952 if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
4953 self.clear_target_view_with_load_op(&target.view, next_load_op);
4954 next_load_op = wgpu::LoadOp::Load;
4955 }
4956
4957 match event.kind {
4958 LayerEventKind::Backdrop(index) => {
4959 let layer = &backdrop_layers[index];
4960 let effective_backdrop_underlay = if backdrop_underlay.is_some()
4961 && backdrop_underlay_is_covered_by_local_content(
4962 shapes,
4963 images,
4964 shadow_draws,
4965 draw_ops,
4966 effect_layers,
4967 backdrop_layers,
4968 layer,
4969 ) {
4970 None
4971 } else {
4972 backdrop_underlay
4973 };
4974 execute_apply_backdrop_layer_to_target(
4975 self,
4976 target,
4977 layer,
4978 effective_backdrop_underlay,
4979 width,
4980 height,
4981 root_scale,
4982 None,
4983 )?;
4984 }
4985 LayerEventKind::Effect(index) => {
4986 let layer = &effect_layers[index];
4987 if layer.z_start < cursor_z {
4988 continue;
4989 }
4990 execute_render_effect_layer_to_target(
4991 self,
4992 target,
4993 shapes,
4994 images,
4995 texts,
4996 shadow_draws,
4997 draw_ops,
4998 effect_layers,
4999 backdrop_layers,
5000 index,
5001 backdrop_underlay,
5002 width,
5003 height,
5004 root_scale,
5005 )?;
5006 cursor_z = cursor_z.max(layer.z_end);
5007 }
5008 }
5009 }
5010
5011 if cursor_z < z_end {
5012 self.render_non_effect_segment(
5013 &target.view,
5014 shapes,
5015 images,
5016 texts,
5017 shadow_draws,
5018 &[],
5019 draw_ops,
5020 cursor_z,
5021 z_end,
5022 &effect_z_ranges,
5023 width,
5024 height,
5025 root_scale,
5026 next_load_op,
5027 )?;
5028 } else if matches!(next_load_op, wgpu::LoadOp::Clear(_)) {
5029 self.clear_target_view_with_load_op(&target.view, next_load_op);
5030 }
5031
5032 Ok(())
5033 })();
5034
5035 self.renderer.scratch_effect_ranges = effect_z_ranges;
5036 self.renderer.scratch_layer_events = events;
5037 result
5038 }
5039
5040 #[allow(clippy::too_many_arguments)]
5041 fn record_shader_composite(
5042 &mut self,
5043 source: &OffscreenTarget,
5044 shader: &RuntimeShader,
5045 effect_rect: [f32; 4],
5046 dest_view: &wgpu::TextureView,
5047 alpha: f32,
5048 load_op: wgpu::LoadOp<wgpu::Color>,
5049 scissor: Option<(u32, u32, u32, u32)>,
5050 blend_mode: BlendMode,
5051 dest_viewport: Option<(f32, f32, f32, f32)>,
5052 sample_mode: CompositeSampleMode,
5053 ) {
5054 let device = self.renderer.device.clone();
5055 if let Some(viewport) = direct_shader_composite_viewport(
5056 alpha,
5057 blend_mode,
5058 dest_viewport,
5059 sample_mode,
5060 (source.width, source.height),
5061 ) {
5062 let shader_applied = self
5063 .renderer
5064 .effect_renderer
5065 .encode_shader_src_over_to_view(
5066 self.recorder,
5067 &device,
5068 source,
5069 dest_view,
5070 shader,
5071 effect_rect,
5072 load_op,
5073 scissor,
5074 viewport,
5075 );
5076 if shader_applied {
5077 self.renderer
5078 .effect_renderer
5079 .debug_effects
5080 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5081 self.recorder.record_pass();
5082 self.renderer.effect_renderer.record_composite_pass();
5083 return;
5084 }
5085 }
5086 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5087 "Shader Effect Composite Scratch",
5088 source.width,
5089 source.height,
5090 );
5091 let scratch = self
5092 .recorder
5093 .acquire_transient_offscreen(&device, scratch_descriptor);
5094 let shader_applied = {
5095 self.renderer.effect_renderer.encode_shader(
5096 self.recorder,
5097 &device,
5098 source,
5099 &scratch.view,
5100 shader,
5101 effect_rect,
5102 )
5103 };
5104 let composite_source = if shader_applied {
5105 self.renderer
5106 .effect_renderer
5107 .debug_effects
5108 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5109 self.recorder.record_pass();
5110 &scratch
5111 } else {
5112 source
5113 };
5114 {
5115 self.renderer
5116 .effect_renderer
5117 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
5118 self.recorder,
5119 &device,
5120 composite_source,
5121 dest_view,
5122 alpha,
5123 load_op,
5124 scissor,
5125 None,
5126 supported_blend_mode(blend_mode),
5127 dest_viewport,
5128 sample_mode,
5129 );
5130 }
5131 self.recorder.record_pass();
5132 self.renderer.effect_renderer.record_composite_pass();
5133 self.recorder
5134 .release_transient_offscreen(scratch_descriptor, scratch);
5135 }
5136
5137 #[allow(clippy::too_many_arguments)]
5138 fn record_shader_projective_composite(
5139 &mut self,
5140 source: &OffscreenTarget,
5141 shader: &RuntimeShader,
5142 effect_rect: [f32; 4],
5143 dest_view: &wgpu::TextureView,
5144 viewport: (u32, u32),
5145 source_size: (f32, f32),
5146 inverse_matrix: [[f32; 3]; 3],
5147 dest_bounds: [[f32; 2]; 4],
5148 alpha: f32,
5149 load_op: wgpu::LoadOp<wgpu::Color>,
5150 scissor: Option<(u32, u32, u32, u32)>,
5151 blend_mode: BlendMode,
5152 sample_mode: CompositeSampleMode,
5153 ) {
5154 if projective_dest_bounds_rect(dest_bounds).is_none() {
5155 return;
5156 }
5157 let device = self.renderer.device.clone();
5158 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5159 "Shader Projective Composite Scratch",
5160 source.width,
5161 source.height,
5162 );
5163 let scratch = self
5164 .recorder
5165 .acquire_transient_offscreen(&device, scratch_descriptor);
5166 let shader_applied = {
5167 self.renderer.effect_renderer.encode_shader(
5168 self.recorder,
5169 &device,
5170 source,
5171 &scratch.view,
5172 shader,
5173 effect_rect,
5174 )
5175 };
5176 let composite_source = if shader_applied {
5177 self.renderer
5178 .effect_renderer
5179 .debug_effects
5180 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5181 self.recorder.record_pass();
5182 &scratch
5183 } else {
5184 source
5185 };
5186 let composited = {
5187 self.renderer
5188 .effect_renderer
5189 .encode_composite_to_view_projective(
5190 self.recorder,
5191 &device,
5192 composite_source,
5193 dest_view,
5194 viewport,
5195 source_size,
5196 inverse_matrix,
5197 dest_bounds,
5198 alpha,
5199 load_op,
5200 scissor,
5201 supported_blend_mode(blend_mode),
5202 sample_mode,
5203 )
5204 };
5205 if composited {
5206 self.recorder.record_pass();
5207 self.renderer.effect_renderer.record_composite_pass();
5208 }
5209 self.recorder
5210 .release_transient_offscreen(scratch_descriptor, scratch);
5211 }
5212
5213 #[allow(clippy::too_many_arguments)]
5214 fn record_effect_with_direct_shader_tail_composite(
5215 &mut self,
5216 source: &OffscreenTarget,
5217 first_effect: &RenderEffect,
5218 shader: &RuntimeShader,
5219 effect_rect: [f32; 4],
5220 dest_view: &wgpu::TextureView,
5221 load_op: wgpu::LoadOp<wgpu::Color>,
5222 scissor: Option<(u32, u32, u32, u32)>,
5223 dest_viewport: (f32, f32, f32, f32),
5224 ) -> Result<bool, String> {
5225 let device = self.renderer.device.clone();
5226 let intermediate_descriptor = self.renderer.transient_offscreen_descriptor(
5227 "Render Effect Direct Shader Tail Intermediate",
5228 source.width,
5229 source.height,
5230 );
5231 let intermediate = self
5232 .recorder
5233 .acquire_transient_offscreen(&device, intermediate_descriptor);
5234 let effect_scratch_targets = self
5235 .renderer
5236 .effect_renderer
5237 .acquire_recorded_effect_scratch_targets(
5238 self.recorder,
5239 &device,
5240 first_effect,
5241 source.width,
5242 source.height,
5243 self.renderer.surface_format,
5244 );
5245 let first_passes = {
5246 let mut effect_scratch_refs = effect_scratch_targets.refs();
5247 let pass_count = self.renderer.effect_renderer.encode_effect(
5248 self.recorder,
5249 &device,
5250 source,
5251 &intermediate.view,
5252 first_effect,
5253 effect_rect,
5254 &mut effect_scratch_refs,
5255 );
5256 match pass_count {
5257 Ok(pass_count) => effect_scratch_refs.assert_consumed().map(|()| pass_count),
5258 Err(error) => Err(error),
5259 }
5260 };
5261 let first_passes = match first_passes {
5262 Ok(pass_count) => pass_count,
5263 Err(error) => {
5264 effect_scratch_targets.release_into(self.recorder);
5265 self.recorder
5266 .release_transient_offscreen(intermediate_descriptor, intermediate);
5267 return Err(error);
5268 }
5269 };
5270 let shader_applied = self
5271 .renderer
5272 .effect_renderer
5273 .encode_shader_src_over_to_view(
5274 self.recorder,
5275 &device,
5276 &intermediate,
5277 dest_view,
5278 shader,
5279 effect_rect,
5280 load_op,
5281 scissor,
5282 dest_viewport,
5283 );
5284 self.recorder
5285 .record_passes(first_passes.saturating_add(u32::from(shader_applied)));
5286 effect_scratch_targets.release_into(self.recorder);
5287 self.recorder
5288 .release_transient_offscreen(intermediate_descriptor, intermediate);
5289 if !shader_applied {
5290 return Ok(false);
5291 }
5292 self.renderer
5293 .effect_renderer
5294 .debug_effects
5295 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5296 self.renderer.effect_renderer.record_composite_pass();
5297 Ok(true)
5298 }
5299
5300 #[allow(clippy::too_many_arguments)]
5301 fn record_effect_composite(
5302 &mut self,
5303 source: &OffscreenTarget,
5304 effect: &RenderEffect,
5305 effect_rect: [f32; 4],
5306 dest_view: &wgpu::TextureView,
5307 alpha: f32,
5308 load_op: wgpu::LoadOp<wgpu::Color>,
5309 scissor: Option<(u32, u32, u32, u32)>,
5310 blend_mode: BlendMode,
5311 dest_viewport: Option<(f32, f32, f32, f32)>,
5312 sample_mode: CompositeSampleMode,
5313 ) -> Result<(), String> {
5314 if let (
5315 RenderEffect::Chain { first, second },
5316 Some(viewport),
5317 BlendMode::SrcOver,
5318 CompositeSampleMode::Linear,
5319 ) = (
5320 effect,
5321 dest_viewport,
5322 supported_blend_mode(blend_mode),
5323 sample_mode,
5324 ) {
5325 if let (
5326 RenderEffect::Blur {
5327 radius_x,
5328 radius_y,
5329 edge_treatment,
5330 },
5331 RenderEffect::Shader { shader },
5332 ) = (first.as_ref(), second.as_ref())
5333 {
5334 if *radius_x > 0.0 || *radius_y > 0.0 {
5335 let device = self.renderer.device.clone();
5336 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5337 "Blur Rounded Mask Scratch",
5338 source.width,
5339 source.height,
5340 );
5341 let scratch = self
5342 .recorder
5343 .acquire_transient_offscreen(&device, scratch_descriptor);
5344 let fused = self
5345 .renderer
5346 .effect_renderer
5347 .encode_blur_then_rounded_mask_src_over_to_view(
5348 self.recorder,
5349 &device,
5350 source,
5351 &scratch,
5352 dest_view,
5353 *radius_x,
5354 *radius_y,
5355 *edge_treatment,
5356 shader,
5357 effect_rect,
5358 load_op,
5359 scissor,
5360 viewport,
5361 );
5362 if fused {
5363 self.recorder.record_passes(2);
5364 self.renderer.effect_renderer.record_blur_pass();
5365 self.renderer
5366 .effect_renderer
5367 .debug_effects
5368 .set(self.renderer.effect_renderer.debug_effects.get() + 1);
5369 self.renderer.effect_renderer.record_composite_pass();
5370 self.recorder
5371 .release_transient_offscreen(scratch_descriptor, scratch);
5372 return Ok(());
5373 }
5374 self.recorder
5375 .release_transient_offscreen(scratch_descriptor, scratch);
5376 }
5377 }
5378 }
5379 if let Some((first_effect, shader, viewport)) = direct_shader_tail_composite(
5380 effect,
5381 alpha,
5382 blend_mode,
5383 dest_viewport,
5384 sample_mode,
5385 (source.width, source.height),
5386 ) {
5387 if self.record_effect_with_direct_shader_tail_composite(
5388 source,
5389 first_effect,
5390 shader,
5391 effect_rect,
5392 dest_view,
5393 load_op,
5394 scissor,
5395 viewport,
5396 )? {
5397 return Ok(());
5398 }
5399 }
5400 let device = self.renderer.device.clone();
5401 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5402 "Render Effect Composite Scratch",
5403 source.width,
5404 source.height,
5405 );
5406 let scratch = self
5407 .recorder
5408 .acquire_transient_offscreen(&device, scratch_descriptor);
5409 let effect_scratch_targets = self
5410 .renderer
5411 .effect_renderer
5412 .acquire_recorded_effect_scratch_targets(
5413 self.recorder,
5414 &device,
5415 effect,
5416 source.width,
5417 source.height,
5418 self.renderer.surface_format,
5419 );
5420 let effect_passes = {
5421 let mut effect_scratch_refs = effect_scratch_targets.refs();
5422 let pass_count = self.renderer.effect_renderer.encode_effect(
5423 self.recorder,
5424 &device,
5425 source,
5426 &scratch.view,
5427 effect,
5428 effect_rect,
5429 &mut effect_scratch_refs,
5430 )?;
5431 effect_scratch_refs.assert_consumed()?;
5432 Ok(pass_count)
5433 };
5434 let effect_passes = match effect_passes {
5435 Ok(pass_count) => pass_count,
5436 Err(error) => {
5437 effect_scratch_targets.release_into(self.recorder);
5438 self.recorder
5439 .release_transient_offscreen(scratch_descriptor, scratch);
5440 return Err(error);
5441 }
5442 };
5443 {
5444 self.renderer
5445 .effect_renderer
5446 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
5447 self.recorder,
5448 &device,
5449 &scratch,
5450 dest_view,
5451 alpha,
5452 load_op,
5453 scissor,
5454 None,
5455 supported_blend_mode(blend_mode),
5456 dest_viewport,
5457 sample_mode,
5458 );
5459 }
5460 self.recorder.record_passes(effect_passes.saturating_add(1));
5461 self.renderer.effect_renderer.record_composite_pass();
5462 effect_scratch_targets.release_into(self.recorder);
5463 self.recorder
5464 .release_transient_offscreen(scratch_descriptor, scratch);
5465 Ok(())
5466 }
5467
5468 #[allow(clippy::too_many_arguments)]
5469 fn record_effect_projective_composite(
5470 &mut self,
5471 source: &OffscreenTarget,
5472 effect: &RenderEffect,
5473 effect_rect: [f32; 4],
5474 dest_view: &wgpu::TextureView,
5475 viewport: (u32, u32),
5476 source_size: (f32, f32),
5477 inverse_matrix: [[f32; 3]; 3],
5478 dest_bounds: [[f32; 2]; 4],
5479 alpha: f32,
5480 load_op: wgpu::LoadOp<wgpu::Color>,
5481 scissor: Option<(u32, u32, u32, u32)>,
5482 blend_mode: BlendMode,
5483 sample_mode: CompositeSampleMode,
5484 ) -> Result<(), String> {
5485 if projective_dest_bounds_rect(dest_bounds).is_none() {
5486 return Ok(());
5487 }
5488 let device = self.renderer.device.clone();
5489 let scratch_descriptor = self.renderer.transient_offscreen_descriptor(
5490 "Render Effect Projective Composite Scratch",
5491 source.width,
5492 source.height,
5493 );
5494 let scratch = self
5495 .recorder
5496 .acquire_transient_offscreen(&device, scratch_descriptor);
5497 let effect_scratch_targets = self
5498 .renderer
5499 .effect_renderer
5500 .acquire_recorded_effect_scratch_targets(
5501 self.recorder,
5502 &device,
5503 effect,
5504 source.width,
5505 source.height,
5506 self.renderer.surface_format,
5507 );
5508 let effect_passes = {
5509 let mut effect_scratch_refs = effect_scratch_targets.refs();
5510 let pass_count = self.renderer.effect_renderer.encode_effect(
5511 self.recorder,
5512 &device,
5513 source,
5514 &scratch.view,
5515 effect,
5516 effect_rect,
5517 &mut effect_scratch_refs,
5518 )?;
5519 effect_scratch_refs.assert_consumed()?;
5520 Ok(pass_count)
5521 };
5522 let effect_passes = match effect_passes {
5523 Ok(pass_count) => pass_count,
5524 Err(error) => {
5525 effect_scratch_targets.release_into(self.recorder);
5526 self.recorder
5527 .release_transient_offscreen(scratch_descriptor, scratch);
5528 return Err(error);
5529 }
5530 };
5531 let composited = {
5532 self.renderer
5533 .effect_renderer
5534 .encode_composite_to_view_projective(
5535 self.recorder,
5536 &device,
5537 &scratch,
5538 dest_view,
5539 viewport,
5540 source_size,
5541 inverse_matrix,
5542 dest_bounds,
5543 alpha,
5544 load_op,
5545 scissor,
5546 supported_blend_mode(blend_mode),
5547 sample_mode,
5548 )
5549 };
5550 if composited {
5551 self.recorder.record_passes(effect_passes.saturating_add(1));
5552 self.renderer.effect_renderer.record_composite_pass();
5553 } else {
5554 self.recorder.record_passes(effect_passes);
5555 }
5556 effect_scratch_targets.release_into(self.recorder);
5557 self.recorder
5558 .release_transient_offscreen(scratch_descriptor, scratch);
5559 Ok(())
5560 }
5561}
5562
5563impl<C: FrameCommandRecorder> SurfaceExecutionBackend for RecordingSurfaceBackend<'_, '_, C> {
5564 fn max_texture_dim(&self) -> u32 {
5565 self.renderer.max_texture_dim()
5566 }
5567
5568 fn acquire_retained_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
5569 self.renderer.acquire_retained_surface(width, height)
5570 }
5571
5572 fn acquire_frame_surface(&mut self, width: u32, height: u32) -> OffscreenTarget {
5573 let descriptor =
5574 self.renderer
5575 .transient_offscreen_descriptor("Frame Surface", width, height);
5576 self.recorder
5577 .acquire_transient_offscreen(&self.renderer.device, descriptor)
5578 }
5579
5580 fn release_frame_surface(&mut self, target: OffscreenTarget) {
5581 let descriptor = self.renderer.transient_offscreen_descriptor(
5582 "Frame Surface",
5583 target.width,
5584 target.height,
5585 );
5586 self.recorder
5587 .release_transient_offscreen(descriptor, target);
5588 }
5589
5590 fn release_layer_surface_target(&mut self, target: LayerSurfaceTexture) {
5591 self.renderer.release_layer_surface_target(target);
5592 }
5593
5594 fn cached_layer_surface(
5595 &mut self,
5596 key: &LayerRasterCacheKey,
5597 ) -> Option<(Rc<OffscreenTarget>, Rect)> {
5598 self.renderer.cached_layer_surface(key)
5599 }
5600
5601 fn admit_layer_surface_cache_miss(&mut self, key: &LayerRasterCacheKey) -> bool {
5602 self.renderer.admit_layer_surface_cache_miss(key)
5603 }
5604
5605 fn insert_cached_layer_surface(
5606 &mut self,
5607 key: LayerRasterCacheKey,
5608 target: OffscreenTarget,
5609 logical_rect: Rect,
5610 ) -> Rc<OffscreenTarget> {
5611 self.renderer
5612 .insert_cached_layer_surface(key, target, logical_rect)
5613 }
5614
5615 fn clear_target_view_with_load_op(
5616 &mut self,
5617 target_view: &wgpu::TextureView,
5618 load_op: wgpu::LoadOp<wgpu::Color>,
5619 ) {
5620 {
5621 let _clear = self
5622 .recorder
5623 .encoder()
5624 .begin_render_pass(&wgpu::RenderPassDescriptor {
5625 label: Some("Layer Event Clear Pass"),
5626 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
5627 view: target_view,
5628 resolve_target: None,
5629 depth_slice: None,
5630 ops: wgpu::Operations {
5631 load: load_op,
5632 store: wgpu::StoreOp::Store,
5633 },
5634 })],
5635 depth_stencil_attachment: None,
5636 timestamp_writes: None,
5637 occlusion_query_set: None,
5638 multiview_mask: None,
5639 });
5640 }
5641 self.recorder.record_pass();
5642 }
5643
5644 #[allow(clippy::too_many_arguments)]
5645 fn render_non_effect_segment(
5646 &mut self,
5647 target_view: &wgpu::TextureView,
5648 shapes: &[DrawShape],
5649 images: &[ImageDraw],
5650 texts: &[TextDraw],
5651 shadow_draws: &[ShadowDraw],
5652 retained_draws: &[RetainedDraw],
5653 draw_ops: &[DrawOp],
5654 z_start: usize,
5655 z_end: usize,
5656 effect_z_ranges: &[Range<usize>],
5657 width: u32,
5658 height: u32,
5659 root_scale: f32,
5660 initial_load_op: wgpu::LoadOp<wgpu::Color>,
5661 ) -> Result<(), String> {
5662 self.render_non_effect_segment_with_composites(
5663 target_view,
5664 shapes,
5665 images,
5666 texts,
5667 shadow_draws,
5668 retained_draws,
5669 draw_ops,
5670 z_start,
5671 z_end,
5672 effect_z_ranges,
5673 &[],
5674 &[],
5675 width,
5676 height,
5677 root_scale,
5678 initial_load_op,
5679 )
5680 }
5681
5682 #[allow(clippy::too_many_arguments)]
5683 fn render_non_effect_segment_with_composites(
5684 &mut self,
5685 target_view: &wgpu::TextureView,
5686 shapes: &[DrawShape],
5687 images: &[ImageDraw],
5688 texts: &[TextDraw],
5689 shadow_draws: &[ShadowDraw],
5690 retained_draws: &[RetainedDraw],
5691 draw_ops: &[DrawOp],
5692 z_start: usize,
5693 z_end: usize,
5694 effect_z_ranges: &[Range<usize>],
5695 composites: &[(usize, CompositeBatchItem<'_>)],
5696 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
5697 width: u32,
5698 height: u32,
5699 root_scale: f32,
5700 initial_load_op: wgpu::LoadOp<wgpu::Color>,
5701 ) -> Result<(), String> {
5702 let mut ordered_items = std::mem::take(&mut self.renderer.scratch_segment_items);
5703 collect_non_effect_segment_items(
5704 shapes,
5705 images,
5706 texts,
5707 shadow_draws,
5708 draw_ops,
5709 z_start,
5710 z_end,
5711 effect_z_ranges,
5712 width,
5713 height,
5714 root_scale,
5715 &mut ordered_items,
5716 );
5717 #[cfg(not(target_arch = "wasm32"))]
5718 let raw_shadow_items = ordered_items
5719 .iter()
5720 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
5721 .count();
5722 let culled_shadow_items = retain_renderable_shadow_items(
5723 &mut ordered_items,
5724 shadow_draws,
5725 width,
5726 height,
5727 root_scale,
5728 self.renderer.max_texture_dim(),
5729 );
5730 #[cfg(target_arch = "wasm32")]
5731 let _ = culled_shadow_items;
5732 let mut cached_shadow_composites: Vec<(usize, CachedShadowComposite)> = Vec::new();
5733 ordered_items.extend(
5734 composites
5735 .iter()
5736 .enumerate()
5737 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::Composite(index))),
5738 );
5739 ordered_items.extend(
5740 shader_composites
5741 .iter()
5742 .enumerate()
5743 .map(|(index, (z_index, _))| (*z_index, SegmentDrawItem::ShaderComposite(index))),
5744 );
5745 for (z_index, item) in &mut ordered_items {
5746 let SegmentDrawItem::Shadow(shadow_index) = *item else {
5747 continue;
5748 };
5749 let Some(composite) = self.renderer.cached_shape_shadow_composite(
5750 &shadow_draws[shadow_index],
5751 width,
5752 height,
5753 root_scale,
5754 ) else {
5755 continue;
5756 };
5757 let composite_index = composites.len() + cached_shadow_composites.len();
5758 cached_shadow_composites.push((*z_index, composite));
5759 *item = SegmentDrawItem::Composite(composite_index);
5760 }
5761 let mut merged_composites = Vec::with_capacity(
5762 composites
5763 .len()
5764 .saturating_add(cached_shadow_composites.len()),
5765 );
5766 merged_composites.extend(composites.iter().copied());
5767 merged_composites.extend(
5768 cached_shadow_composites
5769 .iter()
5770 .map(|(z_index, composite)| (*z_index, composite.batch_item())),
5771 );
5772 ordered_items.sort_unstable_by_key(|(z_index, _)| *z_index);
5776 #[cfg(not(target_arch = "wasm32"))]
5777 maybe_print_segment_diag(
5778 z_start..z_end,
5779 &ordered_items,
5780 shapes,
5781 images,
5782 SegmentDiagCounts {
5783 raw_shadow_items,
5784 culled_shadow_items,
5785 cached_shadow_composites: cached_shadow_composites.len(),
5786 composite_items: merged_composites.len(),
5787 shader_composite_items: shader_composites.len(),
5788 },
5789 self.renderer.shape_batch_limits,
5790 );
5791 let result = if ordered_items.is_empty() {
5792 Ok(SegmentCommandEncodeOutcome { first_batch: true })
5793 } else {
5794 self.renderer.encode_non_effect_segment_commands(
5795 self.recorder,
5796 target_view,
5797 &ordered_items,
5798 &merged_composites,
5799 shader_composites,
5800 shapes,
5801 images,
5802 texts,
5803 shadow_draws,
5804 retained_draws,
5805 initial_load_op,
5806 width,
5807 height,
5808 root_scale,
5809 )
5810 };
5811 self.renderer.scratch_segment_items = ordered_items;
5812 let outcome = result?;
5813 if outcome.first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
5814 self.clear_target_view_with_load_op(target_view, initial_load_op);
5815 }
5816 Ok(())
5817 }
5818
5819 fn render_range_with_layer_events_to_target(
5820 &mut self,
5821 target: &OffscreenTarget,
5822 shapes: &[DrawShape],
5823 images: &[ImageDraw],
5824 texts: &[TextDraw],
5825 shadow_draws: &[ShadowDraw],
5826 draw_ops: &[DrawOp],
5827 effect_layers: &[EffectLayer],
5828 backdrop_layers: &[BackdropLayer],
5829 z_start: usize,
5830 z_end: usize,
5831 excluded_effect_layer: Option<usize>,
5832 width: u32,
5833 height: u32,
5834 root_scale: f32,
5835 backdrop_underlay: Option<&OffscreenTarget>,
5836 initial_load_op: wgpu::LoadOp<wgpu::Color>,
5837 ) -> Result<(), String> {
5838 self.render_range_with_layer_events_to_target_recorded(
5839 target,
5840 shapes,
5841 images,
5842 texts,
5843 shadow_draws,
5844 draw_ops,
5845 effect_layers,
5846 backdrop_layers,
5847 z_start,
5848 z_end,
5849 excluded_effect_layer,
5850 width,
5851 height,
5852 root_scale,
5853 backdrop_underlay,
5854 initial_load_op,
5855 )
5856 }
5857
5858 fn render_shadow_draw(
5859 &mut self,
5860 target_view: &wgpu::TextureView,
5861 shadow: &ShadowDraw,
5862 width: u32,
5863 height: u32,
5864 root_scale: f32,
5865 ) {
5866 self.renderer.encode_shadow_draw(
5867 self.recorder,
5868 target_view,
5869 shadow,
5870 width,
5871 height,
5872 root_scale,
5873 );
5874 }
5875
5876 fn composite_to_view_projective(
5877 &mut self,
5878 source: &OffscreenTarget,
5879 dest_view: &wgpu::TextureView,
5880 viewport: (u32, u32),
5881 source_size: (f32, f32),
5882 inverse_matrix: [[f32; 3]; 3],
5883 dest_bounds: [[f32; 2]; 4],
5884 alpha: f32,
5885 load_op: wgpu::LoadOp<wgpu::Color>,
5886 scissor: Option<(u32, u32, u32, u32)>,
5887 blend_mode: BlendMode,
5888 sample_mode: CompositeSampleMode,
5889 ) {
5890 let device = self.renderer.device.clone();
5891 let composited = {
5892 self.renderer
5893 .effect_renderer
5894 .encode_composite_to_view_projective(
5895 self.recorder,
5896 &device,
5897 source,
5898 dest_view,
5899 viewport,
5900 source_size,
5901 inverse_matrix,
5902 dest_bounds,
5903 alpha,
5904 load_op,
5905 scissor,
5906 supported_blend_mode(blend_mode),
5907 sample_mode,
5908 )
5909 };
5910 if composited {
5911 self.recorder.record_pass();
5912 self.renderer.effect_renderer.record_composite_pass();
5913 }
5914 }
5915
5916 fn composite_projective_surfaces_to_view(
5917 &mut self,
5918 dest_view: &wgpu::TextureView,
5919 viewport: (u32, u32),
5920 composites: &[ProjectiveSurfaceComposite<'_>],
5921 ) {
5922 let device = self.renderer.device.clone();
5923 let mut composite_count = 0_u32;
5924 for composite in composites
5925 .iter()
5926 .copied()
5927 .filter(|composite| projective_dest_bounds_rect(composite.dest_bounds).is_some())
5928 {
5929 let composited = {
5930 self.renderer
5931 .effect_renderer
5932 .encode_composite_to_view_projective(
5933 self.recorder,
5934 &device,
5935 composite.source,
5936 dest_view,
5937 viewport,
5938 composite.source_size,
5939 composite.inverse_matrix,
5940 composite.dest_bounds,
5941 composite.alpha,
5942 composite.load_op,
5943 composite.scissor,
5944 supported_blend_mode(composite.blend_mode),
5945 composite.sample_mode,
5946 )
5947 };
5948 if composited {
5949 composite_count = composite_count.saturating_add(1);
5950 }
5951 }
5952 if composite_count > 0 {
5953 self.recorder.record_passes(composite_count);
5954 self.renderer
5955 .effect_renderer
5956 .debug_composites
5957 .set(self.renderer.effect_renderer.debug_composites.get() + composite_count);
5958 }
5959 }
5960
5961 fn composite_surface_batch_to_view(
5962 &mut self,
5963 dest_view: &wgpu::TextureView,
5964 viewport: (u32, u32),
5965 load_op: wgpu::LoadOp<wgpu::Color>,
5966 composites: &[CompositeBatchItem<'_>],
5967 ) {
5968 if composites.is_empty() {
5969 return;
5970 }
5971 let device = self.renderer.device.clone();
5972 self.renderer
5973 .effect_renderer
5974 .encode_composite_batch_to_view_pass(
5975 self.recorder,
5976 &device,
5977 dest_view,
5978 viewport,
5979 load_op,
5980 composites,
5981 );
5982 self.recorder.record_pass();
5983 self.renderer.effect_renderer.record_composite_pass();
5984 }
5985
5986 fn copy_texture_region_to_target(
5987 &mut self,
5988 source: &OffscreenTarget,
5989 source_origin: (u32, u32),
5990 target: &OffscreenTarget,
5991 size: (u32, u32),
5992 ) -> bool {
5993 let (width, height) = size;
5994 if width == 0 || height == 0 || width > target.width || height > target.height {
5995 return false;
5996 }
5997 let Some(source_right) = source_origin.0.checked_add(width) else {
5998 return false;
5999 };
6000 let Some(source_bottom) = source_origin.1.checked_add(height) else {
6001 return false;
6002 };
6003 if source_right > source.width || source_bottom > source.height {
6004 return false;
6005 }
6006
6007 self.recorder.encoder().copy_texture_to_texture(
6008 wgpu::TexelCopyTextureInfo {
6009 texture: source.texture(),
6010 mip_level: 0,
6011 origin: wgpu::Origin3d {
6012 x: source_origin.0,
6013 y: source_origin.1,
6014 z: 0,
6015 },
6016 aspect: wgpu::TextureAspect::All,
6017 },
6018 wgpu::TexelCopyTextureInfo {
6019 texture: target.texture(),
6020 mip_level: 0,
6021 origin: wgpu::Origin3d::ZERO,
6022 aspect: wgpu::TextureAspect::All,
6023 },
6024 wgpu::Extent3d {
6025 width,
6026 height,
6027 depth_or_array_layers: 1,
6028 },
6029 );
6030 true
6031 }
6032
6033 fn shader_composite_batch_to_view(
6034 &mut self,
6035 dest_view: &wgpu::TextureView,
6036 viewport: (u32, u32),
6037 load_op: wgpu::LoadOp<wgpu::Color>,
6038 composites: &[ShaderCompositeBatchItem<'_>],
6039 ) -> bool {
6040 if composites.is_empty() {
6041 return true;
6042 }
6043 let device = self.renderer.device.clone();
6044 let encoded = self
6045 .renderer
6046 .effect_renderer
6047 .encode_shader_batch_src_over_to_view(
6048 self.recorder,
6049 &device,
6050 dest_view,
6051 viewport,
6052 load_op,
6053 composites,
6054 );
6055 if encoded {
6056 self.recorder.record_pass();
6057 self.renderer.effect_renderer.record_composite_pass();
6058 self.renderer
6059 .effect_renderer
6060 .debug_effects
6061 .set(self.renderer.effect_renderer.debug_effects.get() + composites.len() as u32);
6062 }
6063 encoded
6064 }
6065
6066 fn composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
6067 &mut self,
6068 source: &OffscreenTarget,
6069 dest_view: &wgpu::TextureView,
6070 alpha: f32,
6071 load_op: wgpu::LoadOp<wgpu::Color>,
6072 scissor: Option<(u32, u32, u32, u32)>,
6073 rounded_mask: Option<RoundedCompositeMask>,
6074 blend_mode: BlendMode,
6075 dest_viewport: Option<(f32, f32, f32, f32)>,
6076 sample_mode: CompositeSampleMode,
6077 ) {
6078 let device = self.renderer.device.clone();
6079 {
6080 self.renderer
6081 .effect_renderer
6082 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
6083 self.recorder,
6084 &device,
6085 source,
6086 dest_view,
6087 alpha,
6088 load_op,
6089 scissor,
6090 rounded_mask,
6091 supported_blend_mode(blend_mode),
6092 dest_viewport,
6093 sample_mode,
6094 );
6095 }
6096 self.recorder.record_pass();
6097 self.renderer.effect_renderer.record_composite_pass();
6098 }
6099
6100 fn apply_effect_and_composite_to_view(
6101 &mut self,
6102 source: &OffscreenTarget,
6103 effect: &RenderEffect,
6104 effect_rect: [f32; 4],
6105 dest_view: &wgpu::TextureView,
6106 alpha: f32,
6107 load_op: wgpu::LoadOp<wgpu::Color>,
6108 scissor: Option<(u32, u32, u32, u32)>,
6109 blend_mode: BlendMode,
6110 dest_viewport: Option<(f32, f32, f32, f32)>,
6111 sample_mode: CompositeSampleMode,
6112 ) -> Result<(), String> {
6113 self.record_effect_composite(
6114 source,
6115 effect,
6116 effect_rect,
6117 dest_view,
6118 alpha,
6119 load_op,
6120 scissor,
6121 blend_mode,
6122 dest_viewport,
6123 sample_mode,
6124 )
6125 }
6126
6127 fn apply_shader_and_composite_to_view(
6128 &mut self,
6129 source: &OffscreenTarget,
6130 shader: &RuntimeShader,
6131 effect_rect: [f32; 4],
6132 dest_view: &wgpu::TextureView,
6133 alpha: f32,
6134 load_op: wgpu::LoadOp<wgpu::Color>,
6135 scissor: Option<(u32, u32, u32, u32)>,
6136 blend_mode: BlendMode,
6137 dest_viewport: Option<(f32, f32, f32, f32)>,
6138 sample_mode: CompositeSampleMode,
6139 ) {
6140 self.record_shader_composite(
6141 source,
6142 shader,
6143 effect_rect,
6144 dest_view,
6145 alpha,
6146 load_op,
6147 scissor,
6148 blend_mode,
6149 dest_viewport,
6150 sample_mode,
6151 );
6152 }
6153
6154 fn apply_shader_and_composite_to_view_projective(
6155 &mut self,
6156 source: &OffscreenTarget,
6157 shader: &RuntimeShader,
6158 effect_rect: [f32; 4],
6159 dest_view: &wgpu::TextureView,
6160 viewport: (u32, u32),
6161 source_size: (f32, f32),
6162 inverse_matrix: [[f32; 3]; 3],
6163 dest_bounds: [[f32; 2]; 4],
6164 alpha: f32,
6165 load_op: wgpu::LoadOp<wgpu::Color>,
6166 scissor: Option<(u32, u32, u32, u32)>,
6167 blend_mode: BlendMode,
6168 sample_mode: CompositeSampleMode,
6169 ) {
6170 self.record_shader_projective_composite(
6171 source,
6172 shader,
6173 effect_rect,
6174 dest_view,
6175 viewport,
6176 source_size,
6177 inverse_matrix,
6178 dest_bounds,
6179 alpha,
6180 load_op,
6181 scissor,
6182 blend_mode,
6183 sample_mode,
6184 );
6185 }
6186
6187 fn apply_effect_and_composite_to_view_projective(
6188 &mut self,
6189 source: &OffscreenTarget,
6190 effect: &RenderEffect,
6191 effect_rect: [f32; 4],
6192 dest_view: &wgpu::TextureView,
6193 viewport: (u32, u32),
6194 source_size: (f32, f32),
6195 inverse_matrix: [[f32; 3]; 3],
6196 dest_bounds: [[f32; 2]; 4],
6197 alpha: f32,
6198 load_op: wgpu::LoadOp<wgpu::Color>,
6199 scissor: Option<(u32, u32, u32, u32)>,
6200 blend_mode: BlendMode,
6201 sample_mode: CompositeSampleMode,
6202 ) -> Result<(), String> {
6203 self.record_effect_projective_composite(
6204 source,
6205 effect,
6206 effect_rect,
6207 dest_view,
6208 viewport,
6209 source_size,
6210 inverse_matrix,
6211 dest_bounds,
6212 alpha,
6213 load_op,
6214 scissor,
6215 blend_mode,
6216 sample_mode,
6217 )
6218 }
6219
6220 fn is_render_effect_supported(&self, effect: &RenderEffect) -> bool {
6221 self.renderer.supports_render_effect(effect)
6222 }
6223
6224 fn warn_unsupported_effect_once(&self) {
6225 self.renderer.warning_state.warn_unsupported_effect_once();
6226 }
6227
6228 fn record_layer_cache_miss(&self, width: u32, height: u32) {
6229 self.renderer
6230 .frame_stats
6231 .record_layer_cache_miss(width, height);
6232 }
6233
6234 fn record_isolated_layer_render(
6235 &self,
6236 width: u32,
6237 height: u32,
6238 node_id: Option<NodeId>,
6239 logical_rect: Rect,
6240 requirements: SurfaceRequirementSet,
6241 ) {
6242 self.renderer.frame_stats.record_isolated_layer_render(
6243 width,
6244 height,
6245 node_id,
6246 logical_rect,
6247 requirements.into(),
6248 );
6249 }
6250}
6251
6252impl GpuRenderer {
6253 pub fn render(
6254 &mut self,
6255 view: &wgpu::TextureView,
6256 width: u32,
6257 height: u32,
6258 packet: FramePacket,
6259 surface_epoch: u64,
6260 returns: &mut RenderReturns,
6261 ) -> Result<(), String> {
6262 let cancel_reason = if packet.renderer_epoch != self.renderer_epoch {
6268 Some(CancelReason::RendererEpoch)
6269 } else if packet.surface_epoch != surface_epoch {
6270 Some(CancelReason::SurfaceEpoch)
6271 } else if packet.viewport != (width, height) {
6272 Some(CancelReason::Viewport)
6273 } else {
6274 None
6275 };
6276 if let Some(reason) = cancel_reason {
6277 return Self::cancel_packet(packet, reason, returns);
6278 }
6279 returns.frame_id = packet.frame_id;
6280 log::trace!("🎨 Rendering graph to {}x{}", width, height);
6281 let render_start = Instant::now();
6282
6283 #[cfg(target_arch = "wasm32")]
6284 {
6285 self.wasm_uniform_batch_cursor = 0;
6286 self.wasm_shape_batch_cursor = 0;
6287 self.wasm_image_batch_cursor = 0;
6288 }
6289 #[cfg(not(target_arch = "wasm32"))]
6290 {
6291 self.retained_glyph_uniform_cursor = 0;
6292 }
6293
6294 let text_cache_len = packet.text_cache_len;
6298 let result = self.render_graph(view, packet, returns);
6299 let after_graph = Instant::now();
6300 self.flush_deferred_offscreen_releases();
6301
6302 #[cfg(target_arch = "wasm32")]
6303 {
6304 const WASM_BATCH_POOL_MARGIN: usize = 4;
6305 self.wasm_uniform_batches.truncate(
6306 self.wasm_uniform_batch_cursor
6307 .saturating_add(WASM_BATCH_POOL_MARGIN),
6308 );
6309 self.wasm_shape_batches.truncate(
6310 self.wasm_shape_batch_cursor
6311 .saturating_add(WASM_BATCH_POOL_MARGIN),
6312 );
6313 self.wasm_image_batches.truncate(
6314 self.wasm_image_batch_cursor
6315 .saturating_add(WASM_BATCH_POOL_MARGIN),
6316 );
6317 }
6318 self.staged_uploads
6319 .shrink_retained_capacity(RETAINED_STAGED_UPLOAD_BYTES, RETAINED_STAGED_UPLOAD_COPIES);
6320
6321 self.layer_surface_cache.finish_frame(&self.frame_stats);
6322 #[cfg(not(target_arch = "wasm32"))]
6323 self.retained_bundle_cache.end_frame();
6324
6325 self.frame_stats.offscreen_pool_size.set(
6326 self.effect_renderer
6327 .retained_offscreen_count()
6328 .saturating_add(self.frame_graph_executor.retained_texture_count())
6329 as u32,
6330 );
6331 self.frame_stats.offscreen_pool_bytes.set(
6332 (self.effect_renderer.retained_offscreen_bytes() as u64)
6333 .saturating_add(self.frame_graph_executor.retained_texture_bytes()),
6334 );
6335 self.frame_stats
6336 .text_pool_size
6337 .set(self.text_image_cache.len() as u32);
6338 self.frame_stats
6339 .image_cache_size
6340 .set(self.image_texture_cache.len() as u32);
6341 self.frame_stats.text_cache_size.set(text_cache_len as u32);
6342 self.effect_renderer
6343 .merge_and_reset_debug_counters(&self.frame_stats);
6344 self.frame_graph_executor.reset_upload_allocators();
6345 let snapshot = self.frame_stats.snapshot();
6346 self.last_frame_stats = Some(snapshot);
6347 PRESENTED_FRAMES.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6348 update_frame_warmup_budget(&mut self.pending_frame_warmup_frames, &snapshot);
6349 self.frame_stats.maybe_print_snapshot(
6350 snapshot,
6351 &mut self.frame_count,
6352 self.gpu_stats_enabled,
6353 );
6354 if self.gpu_stats_enabled && self.frame_count.is_multiple_of(60) {
6355 gpu_stats::print_gpu_memory_report(&self.device, self.frame_count);
6356 }
6357 self.frame_stats.reset();
6358 let after_stats = Instant::now();
6359 if let Some(total_ms) = should_log_wgpu_render_stage(render_start, after_stats) {
6360 log::warn!(
6361 "[wgpu-render-stage:render] total_ms={total_ms:.2} graph_ms={:.2} cleanup_stats_ms={:.2}",
6362 instant_ms(render_start, after_graph),
6363 instant_ms(after_graph, after_stats),
6364 );
6365 }
6366 if result.is_ok() {
6367 returns.outcome = PresentOutcome::Presented;
6370 }
6371 result
6372 }
6373
6374 fn cancel_packet(
6381 packet: FramePacket,
6382 reason: CancelReason,
6383 returns: &mut RenderReturns,
6384 ) -> Result<(), String> {
6385 let FramePacket {
6386 frame_id,
6387 viewport: _,
6388 renderer_epoch: _,
6389 surface_epoch: _,
6390 root_scale: _,
6391 root,
6392 overlay: _,
6393 replay,
6394 text_cache_len: _,
6395 } = packet;
6396 match root {
6397 PacketRoot::Direct(root) => {
6398 returns.scene = Some(root.scene);
6405 #[cfg(not(target_arch = "wasm32"))]
6406 {
6407 returns.cancelled_replay = Some(replay);
6408 }
6409 }
6410 PacketRoot::Surface(_) => {}
6411 }
6412 #[cfg(target_arch = "wasm32")]
6413 let _ = replay;
6414 returns.ack = None;
6415 returns.frame_id = frame_id;
6416 returns.outcome = PresentOutcome::Cancelled(reason);
6417 Ok(())
6418 }
6419
6420 pub fn last_frame_stats(&self) -> Option<gpu_stats::FrameStatsSnapshot> {
6421 self.last_frame_stats
6422 }
6423
6424 pub fn needs_frame_warmup(&self) -> bool {
6425 self.pending_frame_warmup_frames > 0
6426 }
6427
6428 pub fn debug_cpu_allocation_stats(&self) -> DebugCpuAllocationStats {
6429 let layer_surface_cache_stats = self.layer_surface_cache.debug_stats();
6430 DebugCpuAllocationStats {
6431 scene_graph_node_count: 0,
6432 scene_graph_heap_bytes: 0,
6433 scene_hits_len: 0,
6434 scene_hits_cap: 0,
6435 scene_node_index_len: 0,
6436 scene_node_index_cap: 0,
6437 text_renderer_pool_len: self.text_image_cache.len(),
6438 text_renderer_pool_cap: self.text_image_cache.cap().get(),
6439 swash_image_cache_len: 0,
6440 swash_image_cache_cap: 0,
6441 swash_outline_cache_len: 0,
6442 swash_outline_cache_cap: 0,
6443 image_texture_cache_len: self.image_texture_cache.len(),
6444 image_texture_cache_cap: self.image_texture_cache.cap().get(),
6445 scratch_shape_data_cap: self.scratch_shape_data.capacity(),
6446 scratch_gradients_cap: self.scratch_gradients.capacity(),
6447 scratch_image_vertices_cap: self.scratch_image_vertices.capacity(),
6448 scratch_image_indices_cap: self.scratch_image_indices.capacity(),
6449 scratch_image_cmds_cap: self.scratch_image_cmds.capacity(),
6450 scratch_segment_items_cap: self.scratch_segment_items.capacity(),
6451 scratch_effect_ranges_cap: self.scratch_effect_ranges.capacity(),
6452 scratch_layer_events_cap: self.scratch_layer_events.capacity(),
6453 staged_upload_bytes_cap: self.staged_uploads.bytes.capacity(),
6454 staged_upload_copies_cap: self.staged_uploads.copies.capacity(),
6455 layer_surface_cache_len: layer_surface_cache_stats.entries_len,
6456 layer_surface_cache_cap: layer_surface_cache_stats.entries_cap,
6457 layer_surface_cache_identity_len: layer_surface_cache_stats.identity_len,
6458 layer_surface_cache_identity_cap: layer_surface_cache_stats.identity_cap,
6459 layer_surface_rect_cache_len: 0,
6462 layer_surface_rect_cache_cap: 0,
6463 layer_surface_requirements_cache_len: 0,
6464 layer_surface_requirements_cache_cap: 0,
6465 layer_cache_seen_this_frame_len: layer_surface_cache_stats.seen_this_frame_len,
6466 layer_cache_seen_this_frame_cap: layer_surface_cache_stats.seen_this_frame_cap,
6467 }
6468 }
6469
6470 pub fn render_to_rgba_pixels(
6471 &mut self,
6472 width: u32,
6473 height: u32,
6474 packet: FramePacket,
6475 surface_epoch: u64,
6476 returns: &mut RenderReturns,
6477 ) -> Result<Vec<u8>, String> {
6478 if width == 0 || height == 0 {
6479 return Err("Screenshot size must be non-zero".to_string());
6480 }
6481
6482 let output_texture = self.device.create_texture(&wgpu::TextureDescriptor {
6483 label: Some("Screenshot Output Texture"),
6484 size: wgpu::Extent3d {
6485 width,
6486 height,
6487 depth_or_array_layers: 1,
6488 },
6489 mip_level_count: 1,
6490 sample_count: 1,
6491 dimension: wgpu::TextureDimension::D2,
6492 format: self.surface_format,
6493 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
6494 view_formats: &[],
6495 });
6496 let output_view = output_texture.create_view(&wgpu::TextureViewDescriptor::default());
6497
6498 self.render(&output_view, width, height, packet, surface_epoch, returns)?;
6499
6500 let bytes_per_pixel = 4u32;
6501 let unpadded_bytes_per_row = width
6502 .checked_mul(bytes_per_pixel)
6503 .ok_or_else(|| "Screenshot row byte size overflow".to_string())?;
6504 let padded_bytes_per_row =
6505 align_to(unpadded_bytes_per_row, wgpu::COPY_BYTES_PER_ROW_ALIGNMENT);
6506 let output_buffer_size = padded_bytes_per_row as u64 * height as u64;
6507
6508 let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
6509 label: Some("Screenshot Readback Buffer"),
6510 size: output_buffer_size,
6511 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
6512 mapped_at_creation: false,
6513 });
6514
6515 let device = self.device.clone();
6516 let queue = self.queue.clone();
6517 let mut graph = WgpuFrameGraph::new(Some("Screenshot Copy Encoder"));
6518 let source = graph.import_surface("screenshot-copy-source");
6519 graph.add_fallible_command_pass(Some("Screenshot Copy Pass"), &[source], &[], |context| {
6520 context.encoder.copy_texture_to_buffer(
6521 wgpu::TexelCopyTextureInfo {
6522 texture: &output_texture,
6523 mip_level: 0,
6524 origin: wgpu::Origin3d::ZERO,
6525 aspect: wgpu::TextureAspect::All,
6526 },
6527 wgpu::TexelCopyBufferInfo {
6528 buffer: &output_buffer,
6529 layout: wgpu::TexelCopyBufferLayout {
6530 offset: 0,
6531 bytes_per_row: Some(padded_bytes_per_row),
6532 rows_per_image: Some(height),
6533 },
6534 },
6535 wgpu::Extent3d {
6536 width,
6537 height,
6538 depth_or_array_layers: 1,
6539 },
6540 );
6541 Ok(())
6542 });
6543 let mut executor = std::mem::take(&mut self.frame_graph_executor);
6544 let execution = executor.execute_recorded_graph(&device, &queue, graph);
6545 self.frame_graph_executor = executor;
6546 let execution = execution.map_err(|error| error.to_string())?;
6547 let submission_index = execution.submission;
6548 let copy_stats = execution.stats;
6549 self.last_frame_stats = self
6550 .last_frame_stats
6551 .map(|snapshot| snapshot.with_command_stats_added(copy_stats));
6552
6553 let buffer_slice = output_buffer.slice(..);
6554 let (tx, rx) = mpsc::channel();
6555 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
6556 let _ = tx.send(result);
6557 });
6558 let _ = self.device.poll(wgpu::PollType::Wait {
6559 submission_index: Some(submission_index),
6560 timeout: None,
6561 });
6562
6563 match rx.recv_timeout(Duration::from_secs(3)) {
6564 Ok(Ok(())) => {}
6565 Ok(Err(err)) => return Err(format!("Screenshot map_async failed: {err:?}")),
6566 Err(err) => return Err(format!("Screenshot readback timed out: {err}")),
6567 }
6568
6569 let mapped = buffer_slice.get_mapped_range();
6570 let mut pixels = vec![0u8; (width as usize) * (height as usize) * 4];
6571
6572 let src_row_len = padded_bytes_per_row as usize;
6573 let dst_row_len = unpadded_bytes_per_row as usize;
6574 for row in 0..height as usize {
6575 let src_offset = row * src_row_len;
6576 let dst_offset = row * dst_row_len;
6577 pixels[dst_offset..dst_offset + dst_row_len]
6578 .copy_from_slice(&mapped[src_offset..src_offset + dst_row_len]);
6579 }
6580 drop(mapped);
6581 output_buffer.unmap();
6582
6583 self.convert_surface_pixels_to_rgba(&mut pixels)?;
6584 Ok(pixels)
6585 }
6586
6587 fn render_graph(
6588 &mut self,
6589 surface_view: &wgpu::TextureView,
6590 packet: FramePacket,
6591 returns: &mut RenderReturns,
6592 ) -> Result<(), String> {
6593 let device = self.device.clone();
6594 let queue = self.queue.clone();
6595 let graph_start = Instant::now();
6596
6597 #[cfg(not(target_arch = "wasm32"))]
6598 {
6599 let mut executor = std::mem::take(&mut self.frame_graph_executor);
6600 let mut frame_graph = WgpuFrameGraph::new(Some("Renderer Frame Graph"));
6601 let surface = frame_graph.import_surface("renderer-surface");
6602 frame_graph.add_fallible_recorded_command_pass(
6603 Some("Renderer Frame Pass"),
6604 &[],
6605 &[surface],
6606 |frame_encoder| {
6607 self.render_graph_recorded(surface_view, packet, returns, frame_encoder)
6608 },
6609 );
6610 let after_build = Instant::now();
6611 let execution = executor.execute_recorded_graph(&device, &queue, frame_graph);
6612 let after_execute = Instant::now();
6613 self.frame_graph_executor = executor;
6614 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
6615 log::warn!(
6616 "[wgpu-render-stage:graph] total_ms={total_ms:.2} build_ms={:.2} execute_ms={:.2}",
6617 instant_ms(graph_start, after_build),
6618 instant_ms(after_build, after_execute),
6619 );
6620 }
6621
6622 match execution {
6623 Ok(execution) => {
6624 if execution.stats.pass_count > 0 {
6625 self.frame_stats.record_command_stats(execution.stats);
6626 }
6627 Ok(())
6628 }
6629 Err(crate::frame_graph::FrameGraphError::NoDeclaredPasses) => Ok(()),
6630 Err(error) => Err(error.to_string()),
6631 }
6632 }
6633
6634 #[cfg(target_arch = "wasm32")]
6635 {
6636 let mut executor = std::mem::take(&mut self.frame_graph_executor);
6637 let (result, execution) = {
6638 let mut frame_encoder =
6639 executor.begin(&device, &queue, Some("Renderer Frame Encoder"));
6640 let initial_pass_count = frame_encoder.recorded_pass_count();
6641 let result =
6642 self.render_graph_recorded(surface_view, packet, returns, &mut frame_encoder);
6643 let execution =
6644 if result.is_ok() && frame_encoder.recorded_pass_count() > initial_pass_count {
6645 Some(frame_encoder.finish())
6646 } else {
6647 None
6648 };
6649 (result, execution)
6650 };
6651 let after_execute = Instant::now();
6652 self.frame_graph_executor = executor;
6653 if let Some(total_ms) = should_log_wgpu_render_stage(graph_start, after_execute) {
6654 log::warn!("[wgpu-render-stage:graph] total_ms={total_ms:.2}",);
6655 }
6656 if let Some(execution) = execution {
6657 self.frame_stats.record_command_stats(execution.stats);
6658 }
6659 result
6660 }
6661 }
6662
6663 fn render_graph_recorded<C: FrameCommandRecorder>(
6664 &mut self,
6665 surface_view: &wgpu::TextureView,
6666 packet: FramePacket,
6667 returns: &mut RenderReturns,
6668 frame_encoder: &mut C,
6669 ) -> Result<(), String> {
6670 let recorded_start = Instant::now();
6671
6672 #[cfg(not(target_arch = "wasm32"))]
6684 let mut packet = packet;
6685 #[cfg(not(target_arch = "wasm32"))]
6686 if let PacketRoot::Direct(root) = &packet.root {
6687 let ops = std::mem::take(&mut packet.replay);
6688 let (ack, recycled) =
6689 self.consume_replay_ops(ops, &root.scene.shapes, packet.root_scale);
6690 returns.ack = Some((ack, recycled));
6691 }
6692
6693 let FramePacket {
6694 frame_id,
6695 viewport: (width, height),
6696 renderer_epoch: _,
6697 surface_epoch: _,
6698 root_scale,
6699 root,
6700 overlay,
6701 replay: _,
6702 text_cache_len: _,
6703 } = packet;
6704
6705 let mut backend = RecordingSurfaceBackend {
6706 renderer: self,
6707 recorder: frame_encoder,
6708 };
6709
6710 let surface_packet = match root {
6711 PacketRoot::Direct(root) => {
6712 let direct_render_start = Instant::now();
6713 let result = match execute_render_root_direct(
6714 &mut backend,
6715 surface_view,
6716 *root,
6717 width,
6718 height,
6719 root_scale,
6720 wgpu::LoadOp::Clear(CLEAR_COLOR),
6721 ) {
6722 Ok(scene) => {
6727 returns.scene = Some(scene);
6728 Ok(())
6729 }
6730 Err((error, scene)) => {
6731 returns.scene = Some(scene);
6732 Err(error)
6733 }
6734 };
6735 if result.is_ok() {
6736 if let Some(overlay) = overlay {
6737 Self::render_overlay_packet(
6738 &mut backend,
6739 surface_view,
6740 overlay,
6741 width,
6742 height,
6743 root_scale,
6744 )?;
6745 }
6746 }
6747 let after_direct_render = Instant::now();
6748 if let Some(total_ms) =
6749 should_log_wgpu_render_stage(recorded_start, after_direct_render)
6750 {
6751 log::warn!(
6752 "[wgpu-render-stage:recorded-direct-root] frame={frame_id} total_ms={total_ms:.2} render_ms={:.2}",
6753 instant_ms(direct_render_start, after_direct_render),
6754 );
6755 }
6756 return result;
6757 }
6758 PacketRoot::Surface(surface_packet) => surface_packet,
6759 };
6760 let after_root_collect = Instant::now();
6761
6762 let RootSurfacePacket {
6763 lowered,
6764 source,
6765 transform_to_parent,
6766 node_id,
6767 backdrop,
6768 graphics_layer,
6769 local_bounds,
6770 clip_rect,
6771 shadow_clip,
6772 } = *surface_packet;
6773 let mut lowered = lowered;
6774 lowered.source = source;
6775
6776 let viewport_rect = Rect {
6781 x: 0.0,
6782 y: 0.0,
6783 width: width as f32 / root_scale,
6784 height: height as f32 / root_scale,
6785 };
6786 let root_surface = execute_render_layer_surface(
6787 &mut backend,
6788 &mut lowered,
6789 LayerSurfaceRequest {
6790 root_scale,
6791 backdrop_underlay: None,
6792 allow_runtime_cache: false,
6793 logical_rect_override: Some(viewport_rect),
6794 capture_clip_override: None,
6795 activates_nested_capture: false,
6796 translation_context: TranslationRenderContext::default(),
6797 },
6798 )?;
6799 let root_quad = transform_to_parent.map_rect(root_surface.logical_rect);
6800 let root_dest_quad = scaled_quad(root_quad, root_scale);
6801
6802 let needs_root_composite_target =
6803 backdrop.is_some() || graphics_layer.shadow_elevation > 0.0;
6804
6805 if needs_root_composite_target {
6806 let composite_target = backend.acquire_frame_surface(width, height);
6807 backend.clear_target_view_with_load_op(
6808 &composite_target.view,
6809 wgpu::LoadOp::Clear(CLEAR_COLOR),
6810 );
6811
6812 if let Some(backdrop) = &backdrop {
6813 execute_apply_backdrop_layer_to_target(
6814 &mut backend,
6815 &composite_target,
6816 &BackdropLayer {
6817 node_id,
6818 rect: quad_bounds(transform_to_parent.map_rect(local_bounds)),
6819 clip: clip_rect.map(|clip| quad_bounds(transform_to_parent.map_rect(clip))),
6820 snap_anchor: None,
6821 effect: backdrop.clone(),
6822 z_index: 0,
6823 },
6824 None,
6825 width,
6826 height,
6827 root_scale,
6828 None,
6829 )?;
6830 }
6831
6832 let mut root_shadow_scene = CompositorScene::new();
6833 let root_shadow_clip =
6834 shadow_clip.map(|clip| quad_bounds(transform_to_parent.map_rect(clip)));
6835 push_layer_shadow(
6836 &mut root_shadow_scene,
6837 &graphics_layer,
6838 local_bounds,
6839 quad_bounds(transform_to_parent.map_rect(local_bounds)),
6840 root_shadow_clip,
6841 );
6842 for shadow in &root_shadow_scene.shadow_draws {
6843 backend.render_shadow_draw(
6844 &composite_target.view,
6845 shadow,
6846 width,
6847 height,
6848 root_scale,
6849 );
6850 }
6851
6852 let composite_dest_quad =
6853 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
6854 execute_composite_surface_to_view(
6855 &mut backend,
6856 root_surface.target.target(),
6857 &composite_target.view,
6858 (width, height),
6859 composite_dest_quad,
6860 root_surface.composite_alpha,
6861 wgpu::LoadOp::Load,
6862 None,
6863 root_surface.blend_mode,
6864 root_surface.sample_mode,
6865 )?;
6866 backend.composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
6867 &composite_target,
6868 surface_view,
6869 1.0,
6870 wgpu::LoadOp::Clear(CLEAR_COLOR),
6871 None,
6872 None,
6873 BlendMode::SrcOver,
6874 None,
6875 CompositeSampleMode::Linear,
6876 );
6877 backend.release_frame_surface(composite_target);
6878 } else {
6879 let composite_dest_quad =
6880 snap_motion_stable_dest_quad(root_dest_quad, root_surface.sample_mode);
6881 execute_composite_surface_to_view(
6882 &mut backend,
6883 root_surface.target.target(),
6884 surface_view,
6885 (width, height),
6886 composite_dest_quad,
6887 root_surface.composite_alpha,
6888 wgpu::LoadOp::Clear(CLEAR_COLOR),
6889 None,
6890 root_surface.blend_mode,
6891 root_surface.sample_mode,
6892 )?;
6893 }
6894 backend.release_layer_surface_target(root_surface.target);
6895 if let Some(overlay) = overlay {
6896 Self::render_overlay_packet(
6897 &mut backend,
6898 surface_view,
6899 overlay,
6900 width,
6901 height,
6902 root_scale,
6903 )?;
6904 }
6905 let after_layer_render = Instant::now();
6906 if let Some(total_ms) = should_log_wgpu_render_stage(recorded_start, after_layer_render) {
6907 log::warn!(
6908 "[wgpu-render-stage:recorded-layer-root] total_ms={total_ms:.2} collect_ms={:.2} render_ms={:.2}",
6909 instant_ms(recorded_start, after_root_collect),
6910 instant_ms(after_root_collect, after_layer_render),
6911 );
6912 }
6913 Ok(())
6914 }
6915
6916 fn render_overlay_packet<C: FrameCommandRecorder>(
6920 backend: &mut RecordingSurfaceBackend<'_, '_, C>,
6921 surface_view: &wgpu::TextureView,
6922 overlay: CollectedLayer,
6923 width: u32,
6924 height: u32,
6925 root_scale: f32,
6926 ) -> Result<(), String> {
6927 if !overlay.child_layers.is_empty()
6928 || !root_direct_scene_events_are_supported(&overlay.scene)
6929 || !direct_root_child_underlays_are_supported(&overlay)
6930 {
6931 return Err("dev overlay graph must stay directly renderable".to_string());
6932 }
6933 execute_render_root_direct(
6934 backend,
6935 surface_view,
6936 overlay,
6937 width,
6938 height,
6939 root_scale,
6940 wgpu::LoadOp::Load,
6941 )
6942 .map(|_overlay_scene| ())
6943 .map_err(|(error, _overlay_scene)| error)
6944 }
6945
6946 #[allow(clippy::too_many_arguments)]
6947 fn encode_non_effect_segment_commands<C: FrameCommandRecorder>(
6948 &mut self,
6949 frame_encoder: &mut C,
6950 target_view: &wgpu::TextureView,
6951 ordered_items: &[(usize, SegmentDrawItem)],
6952 composites: &[(usize, CompositeBatchItem<'_>)],
6953 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
6954 shapes: &[DrawShape],
6955 images: &[ImageDraw],
6956 texts: &[TextDraw],
6957 shadow_draws: &[ShadowDraw],
6958 retained_draws: &[RetainedDraw],
6959 initial_load_op: wgpu::LoadOp<wgpu::Color>,
6960 width: u32,
6961 height: u32,
6962 root_scale: f32,
6963 ) -> Result<SegmentCommandEncodeOutcome, String> {
6964 let mut first_batch = true;
6965 for command in
6966 SegmentCommandIter::new(ordered_items, shapes, images, self.shape_batch_limits)
6967 {
6968 match command {
6969 SegmentRenderCommand::DrawChunk(chunk) => {
6970 let load_op = if first_batch {
6971 initial_load_op
6972 } else {
6973 wgpu::LoadOp::Load
6974 };
6975 let outcome = self.render_segment_draw_chunk(
6976 frame_encoder,
6977 target_view,
6978 ordered_items,
6979 composites,
6980 shader_composites,
6981 shapes,
6982 images,
6983 texts,
6984 retained_draws,
6985 chunk,
6986 width,
6987 height,
6988 root_scale,
6989 load_op,
6990 )?;
6991 if outcome.rendered_any {
6992 frame_encoder.record_passes(outcome.pass_count);
6993 first_batch = false;
6994 }
6995 }
6996 SegmentRenderCommand::Shadow(index) => {
6997 if first_batch && matches!(initial_load_op, wgpu::LoadOp::Clear(_)) {
6998 {
6999 let _clear = frame_encoder.encoder().begin_render_pass(
7000 &wgpu::RenderPassDescriptor {
7001 label: Some("Shadow Pre-Clear"),
7002 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7003 view: target_view,
7004 resolve_target: None,
7005 depth_slice: None,
7006 ops: wgpu::Operations {
7007 load: initial_load_op,
7008 store: wgpu::StoreOp::Store,
7009 },
7010 })],
7011 depth_stencil_attachment: None,
7012 timestamp_writes: None,
7013 occlusion_query_set: None,
7014 multiview_mask: None,
7015 },
7016 );
7017 }
7018 frame_encoder.record_pass();
7019 first_batch = false;
7020 }
7021 let pass_count_before = frame_encoder.recorded_pass_count();
7022 self.encode_shadow_draw(
7023 frame_encoder,
7024 target_view,
7025 &shadow_draws[index],
7026 width,
7027 height,
7028 root_scale,
7029 );
7030 if frame_encoder.recorded_pass_count() > pass_count_before {
7031 first_batch = false;
7032 }
7033 }
7034 }
7035 }
7036 Ok(SegmentCommandEncodeOutcome { first_batch })
7037 }
7038
7039 #[cfg(not(target_arch = "wasm32"))]
7040 #[allow(clippy::too_many_arguments)]
7041 fn render_segment_draw_chunk_fused_native<C: FrameCommandRecorder>(
7042 &mut self,
7043 frame_encoder: &mut C,
7044 target_view: &wgpu::TextureView,
7045 ordered_items: &[(usize, SegmentDrawItem)],
7046 composites: &[(usize, CompositeBatchItem<'_>)],
7047 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
7048 shapes: &[DrawShape],
7049 images: &[ImageDraw],
7050 texts: &[TextDraw],
7051 retained_draws: &[RetainedDraw],
7052 chunk: &SegmentDrawChunkPlan,
7053 width: u32,
7054 height: u32,
7055 root_scale: f32,
7056 load_op: wgpu::LoadOp<wgpu::Color>,
7057 ) -> Result<Option<SegmentRenderOutcome>, String> {
7058 let Some(partitions) = native_segment_fusion_partitions(
7059 ordered_items,
7060 shapes,
7061 chunk,
7062 self.shape_batch_limits,
7063 )?
7064 else {
7065 return Ok(None);
7066 };
7067
7068 let mut rendered_any = false;
7069 let mut pass_count = 0_u32;
7070 let mut next_load_op = load_op;
7071 for partition in partitions {
7072 let outcome = self.render_segment_draw_chunk_fused_native_partition(
7073 frame_encoder,
7074 target_view,
7075 ordered_items,
7076 composites,
7077 shader_composites,
7078 shapes,
7079 images,
7080 texts,
7081 retained_draws,
7082 &partition.chunk,
7083 partition.budget,
7084 width,
7085 height,
7086 root_scale,
7087 next_load_op,
7088 )?;
7089 if outcome.rendered_any {
7090 rendered_any = true;
7091 pass_count = pass_count.saturating_add(outcome.pass_count);
7092 next_load_op = wgpu::LoadOp::Load;
7093 }
7094 }
7095
7096 Ok(Some(SegmentRenderOutcome {
7097 rendered_any,
7098 pass_count,
7099 }))
7100 }
7101
7102 #[cfg(not(target_arch = "wasm32"))]
7103 #[allow(clippy::too_many_arguments)]
7104 fn render_segment_draw_chunk_fused_native_partition<C: FrameCommandRecorder>(
7105 &mut self,
7106 frame_encoder: &mut C,
7107 target_view: &wgpu::TextureView,
7108 ordered_items: &[(usize, SegmentDrawItem)],
7109 composites: &[(usize, CompositeBatchItem<'_>)],
7110 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
7111 shapes: &[DrawShape],
7112 images: &[ImageDraw],
7113 texts: &[TextDraw],
7114 retained_draws: &[RetainedDraw],
7115 chunk: &SegmentDrawChunkPlan,
7116 budget: NativeSegmentFusionBudget,
7117 width: u32,
7118 height: u32,
7119 root_scale: f32,
7120 load_op: wgpu::LoadOp<wgpu::Color>,
7121 ) -> Result<SegmentRenderOutcome, String> {
7122 let partition_start = Instant::now();
7123 let mut staged_uploads = self.take_staged_uploads();
7124 staged_uploads.clear();
7125 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
7126 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
7127 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
7128 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
7129
7130 image_vertices.clear();
7131 image_indices.clear();
7132 image_cmds.clear();
7133 glyph_cmds.clear();
7134
7135 let result = (|| {
7136 let viewport = ViewportUniformParams {
7137 width,
7138 height,
7139 offset: [0.0, 0.0],
7140 };
7141 self.prewarm_offscreen_text_glyph_draws_in_chunk(
7142 ordered_items,
7143 texts,
7144 chunk,
7145 viewport,
7146 root_scale,
7147 &mut staged_uploads,
7148 &mut image_vertices,
7149 &mut image_indices,
7150 &mut glyph_cmds,
7151 )?;
7152 let mut shape_refs = Vec::with_capacity(budget.shape_count);
7153 for batch in chunk.iter() {
7154 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
7155 continue;
7156 };
7157 for (_, item) in &ordered_items[start..end] {
7158 let SegmentDrawItem::Shape(shape_index) = item else {
7159 return Err(format!(
7160 "shape batch contains non-shape draw item: {item:?}"
7161 ));
7162 };
7163 shape_refs.push(&shapes[*shape_index]);
7164 }
7165 }
7166 let after_shape_refs = Instant::now();
7167
7168 let mut direct_shape_uploads = StagedBufferUploads::default();
7169 let mut shape_upload_base = 0u64;
7170 if !shape_refs.is_empty() {
7171 let Some((_, upload_base)) = self.prepare_shapes_batch_direct(
7172 frame_encoder,
7173 shape_refs.iter().copied(),
7174 root_scale,
7175 viewport,
7176 &mut direct_shape_uploads,
7177 ) else {
7178 return Err(
7179 "native fused segment shape preparation produced no draw batch".to_string(),
7180 );
7181 };
7182 shape_upload_base = upload_base;
7183 }
7184 let after_shape_prepare = Instant::now();
7185
7186 let mut fused_batches = Vec::with_capacity(chunk.batches.len());
7187 let mut shape_cursor = 0_u32;
7188 let mut composite_cursor = 0usize;
7189 let mut shader_composite_cursor = 0usize;
7190 for batch in chunk.iter() {
7191 match batch {
7192 SegmentBatchPlan::Shape {
7193 start,
7194 end,
7195 blend_mode,
7196 } => {
7197 let mut has_gradient = false;
7198 for (_, item) in &ordered_items[start..end] {
7199 let SegmentDrawItem::Shape(shape_index) = item else {
7200 return Err(format!(
7201 "shape batch contains non-shape draw item: {item:?}"
7202 ));
7203 };
7204 has_gradient |= shape_gradient_stop_count(&shapes[*shape_index]) > 0;
7205 }
7206 let shape_count = end - start;
7207 if shape_count > 0 {
7208 fused_batches.push(FusedSegmentBatch::Shape {
7209 batch: PreparedShapeBatch {
7210 vertex_start: shape_cursor * 6,
7211 vertex_count: shape_count as u32 * 6,
7212 has_gradient,
7213 },
7214 blend_mode,
7215 });
7216 shape_cursor += shape_count as u32;
7217 }
7218 }
7219 SegmentBatchPlan::Image {
7220 start,
7221 end,
7222 blend_mode,
7223 } => {
7224 let cmd_start = image_cmds.len();
7225 for (_, item) in &ordered_items[start..end] {
7226 let SegmentDrawItem::Image(image_index) = item else {
7227 return Err(format!(
7228 "image batch contains non-image draw item: {item:?}"
7229 ));
7230 };
7231 self.append_image_draw_cmd(
7232 &images[*image_index],
7233 viewport,
7234 root_scale,
7235 &mut image_vertices,
7236 &mut image_indices,
7237 &mut image_cmds,
7238 )?;
7239 }
7240 let cmd_end = image_cmds.len();
7241 if cmd_start < cmd_end {
7242 fused_batches.push(FusedSegmentBatch::Image {
7243 cmd_range: cmd_start..cmd_end,
7244 blend_mode,
7245 });
7246 }
7247 }
7248 SegmentBatchPlan::Text { start, end } => {
7249 let glyph_cmd_start = glyph_cmds.len();
7250 let image_cmd_start = image_cmds.len();
7251 let text_draws =
7252 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7253 if !self.append_text_glyph_draws(
7254 text_draws,
7255 viewport,
7256 root_scale,
7257 false,
7258 &mut staged_uploads,
7259 &mut image_vertices,
7260 &mut image_indices,
7261 &mut glyph_cmds,
7262 )? {
7263 let text_draws =
7264 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7265 self.append_text_image_draw_cmds(
7266 text_draws,
7267 viewport,
7268 root_scale,
7269 &mut image_vertices,
7270 &mut image_indices,
7271 &mut image_cmds,
7272 )?;
7273 }
7274 let image_cmd_end = image_cmds.len();
7275 let glyph_cmd_end = glyph_cmds.len();
7276 if image_cmd_start < image_cmd_end || glyph_cmd_start < glyph_cmd_end {
7277 fused_batches.push(FusedSegmentBatch::Text {
7278 image_cmd_range: image_cmd_start..image_cmd_end,
7279 glyph_cmd_range: glyph_cmd_start..glyph_cmd_end,
7280 });
7281 }
7282 }
7283 SegmentBatchPlan::Composite { start, end } => {
7284 for (_, item) in &ordered_items[start..end] {
7285 if !matches!(item, SegmentDrawItem::Composite(_)) {
7286 return Err(format!(
7287 "composite batch contains non-composite draw item: {item:?}"
7288 ));
7289 }
7290 }
7291 let draw_count = end - start;
7292 if draw_count > 0 {
7293 let draw_start = composite_cursor;
7294 composite_cursor += draw_count;
7295 fused_batches.push(FusedSegmentBatch::Composite {
7296 draw_range: draw_start..composite_cursor,
7297 });
7298 }
7299 }
7300 SegmentBatchPlan::ShaderComposite { start, end } => {
7301 for (_, item) in &ordered_items[start..end] {
7302 if !matches!(item, SegmentDrawItem::ShaderComposite(_)) {
7303 return Err(format!(
7304 "shader composite batch contains non-shader-composite draw item: {item:?}"
7305 ));
7306 }
7307 }
7308 let draw_count = end - start;
7309 if draw_count > 0 {
7310 let draw_start = shader_composite_cursor;
7311 shader_composite_cursor += draw_count;
7312 fused_batches.push(FusedSegmentBatch::ShaderComposite {
7313 draw_range: draw_start..shader_composite_cursor,
7314 });
7315 }
7316 }
7317 SegmentBatchPlan::Retained { start, end } => {
7318 self.stage_replay_patches(&mut staged_uploads);
7319 for (_, item) in &ordered_items[start..end] {
7320 let SegmentDrawItem::Retained(index) = item else {
7321 return Err(format!(
7322 "retained batch contains non-retained draw item: {item:?}"
7323 ));
7324 };
7325 let retained = retained_draws.get(*index).ok_or_else(|| {
7326 format!("retained draw index {index} out of bounds")
7327 })?;
7328 if (*index as u32) < MAX_REPLAY_SLOTS
7329 && self.replay_slots.slots.contains_key(&retained.slot)
7330 {
7331 let transform = retained.transform.with_retained_paint();
7332 staged_uploads.stage_at(
7333 UploadTarget::ReplayTransform,
7334 *index as u64 * REPLAY_TRANSFORM_STRIDE,
7335 bytemuck::bytes_of(&transform),
7336 );
7337 }
7338 }
7339 if end > start {
7340 fused_batches.push(FusedSegmentBatch::Retained {
7341 item_range: start..end,
7342 });
7343 }
7344 }
7345 }
7346 }
7347 let after_batch_prepare = Instant::now();
7348
7349 if !image_indices.is_empty() {
7350 self.stage_native_image_buffers(
7351 &mut staged_uploads,
7352 viewport,
7353 &image_vertices,
7354 &image_indices,
7355 );
7356 }
7357
7358 let device = self.device.clone();
7359 let composite_items: Vec<_> = chunk
7360 .iter()
7361 .filter_map(|batch| match batch {
7362 SegmentBatchPlan::Composite { start, end } => Some((start, end)),
7363 _ => None,
7364 })
7365 .flat_map(|(start, end)| {
7366 ordered_items[start..end].iter().filter_map(|(_, item)| {
7367 let SegmentDrawItem::Composite(composite_index) = item else {
7368 return None;
7369 };
7370 composites
7371 .get(*composite_index)
7372 .map(|(_, composite)| *composite)
7373 })
7374 })
7375 .collect();
7376 let prepared_composites = self.effect_renderer.prepare_composite_batch_draws(
7377 frame_encoder,
7378 &device,
7379 load_op,
7380 &composite_items,
7381 );
7382 let shader_items: Vec<_> = chunk
7383 .iter()
7384 .filter_map(|batch| match batch {
7385 SegmentBatchPlan::ShaderComposite { start, end } => Some((start, end)),
7386 _ => None,
7387 })
7388 .flat_map(|(start, end)| {
7389 ordered_items[start..end].iter().filter_map(|(_, item)| {
7390 let SegmentDrawItem::ShaderComposite(composite_index) = item else {
7391 return None;
7392 };
7393 shader_composites
7394 .get(*composite_index)
7395 .map(|(_, composite)| *composite)
7396 })
7397 })
7398 .collect();
7399 let prepared_shaders = self
7400 .effect_renderer
7401 .prepare_shader_batch_draws(frame_encoder, &device, &shader_items)
7402 .ok_or_else(|| "shader composite batch preparation failed".to_string())?;
7403 if !shader_items.is_empty() {
7404 self.effect_renderer.record_composite_pass();
7405 self.effect_renderer
7406 .debug_effects
7407 .set(self.effect_renderer.debug_effects.get() + shader_items.len() as u32);
7408 }
7409 let after_composite_prepare = Instant::now();
7410
7411 if fused_batches.is_empty() {
7412 return Ok(SegmentRenderOutcome {
7413 rendered_any: false,
7414 pass_count: 0,
7415 });
7416 }
7417
7418 self.flush_staged_uploads_at(
7423 frame_encoder.encoder(),
7424 &direct_shape_uploads,
7425 shape_upload_base,
7426 );
7427 let upload_offset =
7428 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7429 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
7430 let after_upload = Instant::now();
7431
7432 let use_retained_bundles = retained_bundles_enabled();
7433 let mut retained_encode_ms = 0.0_f64;
7434 {
7435 let mut render_pass =
7436 frame_encoder
7437 .encoder()
7438 .begin_render_pass(&wgpu::RenderPassDescriptor {
7439 label: Some("Fused Segment Draw Pass"),
7440 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7441 view: target_view,
7442 resolve_target: None,
7443 depth_slice: None,
7444 ops: wgpu::Operations {
7445 load: load_op,
7446 store: wgpu::StoreOp::Store,
7447 },
7448 })],
7449 depth_stencil_attachment: None,
7450 timestamp_writes: None,
7451 occlusion_query_set: None,
7452 multiview_mask: None,
7453 });
7454
7455 for batch in &fused_batches {
7456 match batch {
7457 FusedSegmentBatch::Shape { batch, blend_mode } => {
7458 self.draw_prepared_shapes(
7459 &mut render_pass,
7460 *blend_mode,
7461 *batch,
7462 width,
7463 height,
7464 );
7465 }
7466 FusedSegmentBatch::Image {
7467 cmd_range,
7468 blend_mode,
7469 } => {
7470 self.draw_native_prepared_image_cmd_range(
7471 &mut render_pass,
7472 &image_cmds,
7473 cmd_range.clone(),
7474 *blend_mode,
7475 )?;
7476 }
7477 FusedSegmentBatch::Text {
7478 image_cmd_range,
7479 glyph_cmd_range,
7480 } => {
7481 if !image_cmd_range.is_empty() {
7482 self.draw_native_prepared_image_cmd_range(
7483 &mut render_pass,
7484 &image_cmds,
7485 image_cmd_range.clone(),
7486 BlendMode::SrcOver,
7487 )?;
7488 self.frame_stats.bump_text();
7489 }
7490 if !glyph_cmd_range.is_empty() {
7491 self.draw_native_prepared_glyph_cmd_range(
7492 &mut render_pass,
7493 &glyph_cmds,
7494 glyph_cmd_range.clone(),
7495 )?;
7496 }
7497 }
7498 FusedSegmentBatch::Composite { draw_range } => {
7499 for draw in
7500 prepared_composites.get(draw_range.clone()).ok_or_else(|| {
7501 "composite draw range is outside the prepared command buffer"
7502 .to_string()
7503 })?
7504 {
7505 self.effect_renderer.draw_prepared_composite(
7506 &mut render_pass,
7507 (width, height),
7508 draw,
7509 );
7510 }
7511 }
7512 FusedSegmentBatch::ShaderComposite { draw_range } => {
7513 for draw in prepared_shaders.get(draw_range.clone()).ok_or_else(|| {
7514 "shader composite draw range is outside the prepared command buffer"
7515 .to_string()
7516 })? {
7517 self.effect_renderer.draw_prepared_shader_src_over(
7518 &device,
7519 &mut render_pass,
7520 (width, height),
7521 draw,
7522 );
7523 }
7524 }
7525 FusedSegmentBatch::Retained { item_range } => {
7526 let retained_start = Instant::now();
7532 if use_retained_bundles {
7533 self.draw_retained_stretch_bundled(
7534 &mut render_pass,
7535 ordered_items,
7536 retained_draws,
7537 item_range.clone(),
7538 width,
7539 height,
7540 );
7541 } else {
7542 for (_, item) in &ordered_items[item_range.clone()] {
7543 if let SegmentDrawItem::Retained(index) = item {
7544 if let Some(retained) = retained_draws.get(*index) {
7545 self.draw_retained_batch(
7546 &mut render_pass,
7547 retained,
7548 *index,
7549 width,
7550 height,
7551 );
7552 }
7553 }
7554 }
7555 }
7556 retained_encode_ms += instant_ms(retained_start, Instant::now());
7557 }
7558 }
7559 }
7560 }
7561 let after_pass = Instant::now();
7562 if let Some(total_ms) = should_log_wgpu_render_stage(partition_start, after_pass) {
7563 log::warn!(
7564 "[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={}",
7565 instant_ms(partition_start, after_shape_refs),
7566 instant_ms(after_shape_refs, after_shape_prepare),
7567 instant_ms(after_shape_prepare, after_batch_prepare),
7568 instant_ms(after_batch_prepare, after_composite_prepare),
7569 instant_ms(after_composite_prepare, after_upload),
7570 instant_ms(after_upload, after_pass),
7571 fused_batches.len(),
7572 budget.shape_count,
7573 image_cmds.len(),
7574 glyph_cmds.len(),
7575 staged_uploads.bytes.len(),
7576 );
7577 }
7578
7579 Ok(SegmentRenderOutcome {
7580 rendered_any: true,
7581 pass_count: 1,
7582 })
7583 })();
7584
7585 self.scratch_image_vertices = image_vertices;
7586 self.scratch_image_indices = image_indices;
7587 self.scratch_image_cmds = image_cmds;
7588 self.scratch_glyph_cmds = glyph_cmds;
7589 self.restore_staged_uploads(staged_uploads);
7590 result
7591 }
7592
7593 #[allow(clippy::too_many_arguments)]
7594 fn render_segment_draw_chunk<C: FrameCommandRecorder>(
7595 &mut self,
7596 frame_encoder: &mut C,
7597 target_view: &wgpu::TextureView,
7598 ordered_items: &[(usize, SegmentDrawItem)],
7599 composites: &[(usize, CompositeBatchItem<'_>)],
7600 shader_composites: &[(usize, ShaderCompositeBatchItem<'_>)],
7601 shapes: &[DrawShape],
7602 images: &[ImageDraw],
7603 texts: &[TextDraw],
7604 retained_draws: &[RetainedDraw],
7605 chunk: SegmentDrawChunkPlan,
7606 width: u32,
7607 height: u32,
7608 root_scale: f32,
7609 load_op: wgpu::LoadOp<wgpu::Color>,
7610 ) -> Result<SegmentRenderOutcome, String> {
7611 #[cfg(target_arch = "wasm32")]
7612 let _ = retained_draws;
7613 #[cfg(not(target_arch = "wasm32"))]
7614 if let Some(outcome) = self.render_segment_draw_chunk_fused_native(
7615 frame_encoder,
7616 target_view,
7617 ordered_items,
7618 composites,
7619 shader_composites,
7620 shapes,
7621 images,
7622 texts,
7623 retained_draws,
7624 &chunk,
7625 width,
7626 height,
7627 root_scale,
7628 load_op,
7629 )? {
7630 return Ok(outcome);
7631 }
7632
7633 let mut staged_uploads = self.take_staged_uploads();
7634 let result = (|| {
7635 let mut rendered_any = false;
7636 let mut pass_count = 0_u32;
7637 let mut next_load_op = load_op;
7638 for batch in chunk.iter() {
7639 staged_uploads.clear();
7640 match batch {
7641 SegmentBatchPlan::Shape {
7642 start,
7643 end,
7644 blend_mode,
7645 } => {
7646 let slice = &ordered_items[start..end];
7647 if slice.len() > self.shape_batch_limits.max_shapes_per_batch {
7648 return Err(format!(
7649 "shape batch contains {} shapes, exceeding the renderer limit of {}",
7650 slice.len(),
7651 self.shape_batch_limits.max_shapes_per_batch
7652 ));
7653 }
7654 let viewport = ViewportUniformParams {
7655 width,
7656 height,
7657 offset: [0.0, 0.0],
7658 };
7659 for (_, item) in slice {
7660 if !matches!(item, SegmentDrawItem::Shape(_)) {
7661 return Err(format!(
7662 "shape batch contains non-shape draw item: {item:?}"
7663 ));
7664 }
7665 }
7666 let Some(prepared) = self.prepare_shapes_batch(
7667 slice.iter().filter_map(|(_, item)| match item {
7668 SegmentDrawItem::Shape(shape_index) => Some(&shapes[*shape_index]),
7669 _ => None,
7670 }),
7671 root_scale,
7672 viewport,
7673 &mut staged_uploads,
7674 ) else {
7675 continue;
7676 };
7677 let upload_offset = frame_encoder
7678 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7679 self.flush_staged_uploads_at(
7680 frame_encoder.encoder(),
7681 &staged_uploads,
7682 upload_offset,
7683 );
7684 {
7685 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7686 &wgpu::RenderPassDescriptor {
7687 label: Some("Segment Shape Pass"),
7688 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
7689 view: target_view,
7690 resolve_target: None,
7691 depth_slice: None,
7692 ops: wgpu::Operations {
7693 load: next_load_op,
7694 store: wgpu::StoreOp::Store,
7695 },
7696 })],
7697 depth_stencil_attachment: None,
7698 timestamp_writes: None,
7699 occlusion_query_set: None,
7700 multiview_mask: None,
7701 },
7702 );
7703 self.draw_prepared_shapes(
7704 &mut render_pass,
7705 blend_mode,
7706 prepared,
7707 width,
7708 height,
7709 );
7710 }
7711 pass_count = pass_count.saturating_add(1);
7712 rendered_any = true;
7713 next_load_op = wgpu::LoadOp::Load;
7714 }
7715 SegmentBatchPlan::Image {
7716 start,
7717 end,
7718 blend_mode,
7719 } => {
7720 let viewport = ViewportUniformParams {
7721 width,
7722 height,
7723 offset: [0.0, 0.0],
7724 };
7725 for (_, item) in &ordered_items[start..end] {
7726 if !matches!(item, SegmentDrawItem::Image(_)) {
7727 return Err(format!(
7728 "image batch contains non-image draw item: {item:?}"
7729 ));
7730 }
7731 }
7732 let prepared_images = self.prepare_image_draw_cmds(
7733 ordered_items[start..end]
7734 .iter()
7735 .filter_map(|(_, item)| match item {
7736 SegmentDrawItem::Image(image_index) => {
7737 Some(&images[*image_index])
7738 }
7739 _ => None,
7740 }),
7741 viewport,
7742 root_scale,
7743 &mut staged_uploads,
7744 )?;
7745 if prepared_images.is_empty() {
7746 self.scratch_image_cmds = prepared_images.into_cmds();
7747 continue;
7748 }
7749 let upload_offset = frame_encoder
7750 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7751 self.flush_staged_uploads_at(
7752 frame_encoder.encoder(),
7753 &staged_uploads,
7754 upload_offset,
7755 );
7756 let draw_result = {
7757 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7758 &wgpu::RenderPassDescriptor {
7759 label: Some("Segment Image Pass"),
7760 color_attachments: &[Some(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 depth_stencil_attachment: None,
7770 timestamp_writes: None,
7771 occlusion_query_set: None,
7772 multiview_mask: None,
7773 },
7774 );
7775 self.draw_prepared_images(
7776 &mut render_pass,
7777 &prepared_images,
7778 blend_mode,
7779 )
7780 };
7781 pass_count = pass_count.saturating_add(1);
7782 self.scratch_image_cmds = prepared_images.into_cmds();
7783 draw_result?;
7784 rendered_any = true;
7785 next_load_op = wgpu::LoadOp::Load;
7786 }
7787 SegmentBatchPlan::Text { start, end } => {
7788 let viewport = ViewportUniformParams {
7789 width,
7790 height,
7791 offset: [0.0, 0.0],
7792 };
7793 let text_draws =
7794 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7795 if let Some(prepared_glyphs) = self.prepare_text_glyph_draw_cmds(
7796 text_draws,
7797 viewport,
7798 root_scale,
7799 &mut staged_uploads,
7800 )? {
7801 if prepared_glyphs.is_empty() {
7802 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
7803 continue;
7804 }
7805 let upload_offset = frame_encoder
7806 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7807 self.flush_staged_uploads_at(
7808 frame_encoder.encoder(),
7809 &staged_uploads,
7810 upload_offset,
7811 );
7812 {
7813 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7814 &wgpu::RenderPassDescriptor {
7815 label: Some("Segment Text Glyph Atlas Pass"),
7816 color_attachments: &[Some(
7817 wgpu::RenderPassColorAttachment {
7818 view: target_view,
7819 resolve_target: None,
7820 depth_slice: None,
7821 ops: wgpu::Operations {
7822 load: next_load_op,
7823 store: wgpu::StoreOp::Store,
7824 },
7825 },
7826 )],
7827 depth_stencil_attachment: None,
7828 timestamp_writes: None,
7829 occlusion_query_set: None,
7830 multiview_mask: None,
7831 },
7832 );
7833 self.draw_prepared_glyphs(&mut render_pass, &prepared_glyphs)?;
7834 }
7835 pass_count = pass_count.saturating_add(1);
7836 self.scratch_glyph_cmds = prepared_glyphs.into_cmds();
7837 rendered_any = true;
7838 next_load_op = wgpu::LoadOp::Load;
7839 } else {
7840 let text_draws =
7841 text_draws_for_ordered_range(ordered_items, texts, start, end)?;
7842 let prepared_images = self.prepare_text_image_draw_cmds(
7843 text_draws,
7844 viewport,
7845 root_scale,
7846 &mut staged_uploads,
7847 )?;
7848 if prepared_images.is_empty() {
7849 self.scratch_image_cmds = prepared_images.into_cmds();
7850 continue;
7851 }
7852 let upload_offset = frame_encoder
7853 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7854 self.flush_staged_uploads_at(
7855 frame_encoder.encoder(),
7856 &staged_uploads,
7857 upload_offset,
7858 );
7859 {
7860 let mut render_pass = frame_encoder.encoder().begin_render_pass(
7861 &wgpu::RenderPassDescriptor {
7862 label: Some("Segment Text Pass"),
7863 color_attachments: &[Some(
7864 wgpu::RenderPassColorAttachment {
7865 view: target_view,
7866 resolve_target: None,
7867 depth_slice: None,
7868 ops: wgpu::Operations {
7869 load: next_load_op,
7870 store: wgpu::StoreOp::Store,
7871 },
7872 },
7873 )],
7874 depth_stencil_attachment: None,
7875 timestamp_writes: None,
7876 occlusion_query_set: None,
7877 multiview_mask: None,
7878 },
7879 );
7880 self.draw_prepared_images(
7881 &mut render_pass,
7882 &prepared_images,
7883 BlendMode::SrcOver,
7884 )?;
7885 }
7886 self.frame_stats.bump_text();
7887 pass_count = pass_count.saturating_add(1);
7888 self.scratch_image_cmds = prepared_images.into_cmds();
7889 rendered_any = true;
7890 next_load_op = wgpu::LoadOp::Load;
7891 }
7892 }
7893 SegmentBatchPlan::Composite { start, end } => {
7894 let batch_items: Vec<_> = ordered_items[start..end]
7895 .iter()
7896 .map(|(_, item)| match item {
7897 SegmentDrawItem::Composite(composite_index) => composites
7898 .get(*composite_index)
7899 .map(|(_, composite)| *composite)
7900 .ok_or_else(|| {
7901 "composite item index is outside the composite buffer"
7902 .to_string()
7903 }),
7904 other => Err(format!(
7905 "composite batch contains non-composite draw item: {other:?}"
7906 )),
7907 })
7908 .collect::<Result<_, _>>()?;
7909 let device = self.device.clone();
7910 self.effect_renderer.encode_composite_batch_to_view_pass(
7911 frame_encoder,
7912 &device,
7913 target_view,
7914 (width, height),
7915 next_load_op,
7916 &batch_items,
7917 );
7918 self.effect_renderer.record_composite_pass();
7919 pass_count = pass_count.saturating_add(1);
7920 rendered_any = true;
7921 next_load_op = wgpu::LoadOp::Load;
7922 }
7923 SegmentBatchPlan::ShaderComposite { start, end } => {
7924 let batch_items: Vec<_> = ordered_items[start..end]
7925 .iter()
7926 .map(|(_, item)| match item {
7927 SegmentDrawItem::ShaderComposite(composite_index) => {
7928 shader_composites
7929 .get(*composite_index)
7930 .map(|(_, composite)| *composite)
7931 .ok_or_else(|| {
7932 "shader composite item index is outside the shader composite buffer"
7933 .to_string()
7934 })
7935 }
7936 other => Err(format!(
7937 "shader composite batch contains non-shader-composite draw item: {other:?}"
7938 )),
7939 })
7940 .collect::<Result<Vec<_>, _>>()?;
7941 let device = self.device.clone();
7942 let encoded = self.effect_renderer.encode_shader_batch_src_over_to_view(
7943 frame_encoder,
7944 &device,
7945 target_view,
7946 (width, height),
7947 next_load_op,
7948 &batch_items,
7949 );
7950 if !encoded {
7951 return Err("shader composite batch failed to encode".to_string());
7952 }
7953 self.effect_renderer.record_composite_pass();
7954 self.effect_renderer.debug_effects.set(
7955 self.effect_renderer.debug_effects.get() + batch_items.len() as u32,
7956 );
7957 pass_count = pass_count.saturating_add(1);
7958 rendered_any = true;
7959 next_load_op = wgpu::LoadOp::Load;
7960 }
7961 SegmentBatchPlan::Retained { start, end } => {
7962 #[cfg(target_arch = "wasm32")]
7970 {
7971 let _ = (start, end);
7972 return Err("retained shape batches are native-only".to_string());
7973 }
7974 #[cfg(not(target_arch = "wasm32"))]
7975 {
7976 self.stage_replay_patches(&mut staged_uploads);
7977 for (_, item) in &ordered_items[start..end] {
7978 let SegmentDrawItem::Retained(index) = item else {
7979 return Err(format!(
7980 "retained batch contains non-retained draw item: {item:?}"
7981 ));
7982 };
7983 let retained = retained_draws.get(*index).ok_or_else(|| {
7984 format!("retained draw index {index} out of bounds")
7985 })?;
7986 if (*index as u32) < MAX_REPLAY_SLOTS
7987 && self.replay_slots.slots.contains_key(&retained.slot)
7988 {
7989 let transform = retained.transform.with_retained_paint();
7990 staged_uploads.stage_at(
7991 UploadTarget::ReplayTransform,
7992 *index as u64 * REPLAY_TRANSFORM_STRIDE,
7993 bytemuck::bytes_of(&transform),
7994 );
7995 }
7996 }
7997 let upload_offset = frame_encoder
7998 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
7999 self.flush_staged_uploads_at(
8000 frame_encoder.encoder(),
8001 &staged_uploads,
8002 upload_offset,
8003 );
8004 {
8005 let mut render_pass = frame_encoder.encoder().begin_render_pass(
8006 &wgpu::RenderPassDescriptor {
8007 label: Some("Segment Retained Pass"),
8008 color_attachments: &[Some(
8009 wgpu::RenderPassColorAttachment {
8010 view: target_view,
8011 resolve_target: None,
8012 depth_slice: None,
8013 ops: wgpu::Operations {
8014 load: next_load_op,
8015 store: wgpu::StoreOp::Store,
8016 },
8017 },
8018 )],
8019 depth_stencil_attachment: None,
8020 timestamp_writes: None,
8021 occlusion_query_set: None,
8022 multiview_mask: None,
8023 },
8024 );
8025 for (_, item) in &ordered_items[start..end] {
8026 if let SegmentDrawItem::Retained(index) = item {
8027 if let Some(retained) = retained_draws.get(*index) {
8028 self.draw_retained_batch(
8029 &mut render_pass,
8030 retained,
8031 *index,
8032 width,
8033 height,
8034 );
8035 }
8036 }
8037 }
8038 }
8039 pass_count = pass_count.saturating_add(1);
8040 rendered_any = true;
8041 next_load_op = wgpu::LoadOp::Load;
8042 }
8043 }
8044 }
8045 }
8046 Ok(SegmentRenderOutcome {
8047 rendered_any,
8048 pass_count,
8049 })
8050 })();
8051 self.restore_staged_uploads(staged_uploads);
8052 result
8053 }
8054
8055 fn viewport_uniforms(params: ViewportUniformParams) -> Uniforms {
8056 Uniforms {
8057 viewport: [params.width as f32, params.height as f32],
8058 viewport_offset: params.offset,
8059 }
8060 }
8061
8062 #[cfg(not(target_arch = "wasm32"))]
8063 fn stage_viewport_uniforms(
8064 &self,
8065 staged_uploads: &mut StagedBufferUploads,
8066 params: ViewportUniformParams,
8067 ) {
8068 let uniforms = Self::viewport_uniforms(params);
8069 staged_uploads.stage(UploadTarget::Uniform, bytemuck::bytes_of(&uniforms));
8070 }
8071
8072 #[cfg(not(target_arch = "wasm32"))]
8073 fn stage_retained_glyph_viewport_uniforms(
8074 &mut self,
8075 staged_uploads: &mut StagedBufferUploads,
8076 params: ViewportUniformParams,
8077 ) -> usize {
8078 let slot = self.claim_retained_glyph_uniform_slot();
8079 let uniforms = Self::viewport_uniforms(params);
8080 staged_uploads.stage_at(
8081 UploadTarget::RetainedGlyphUniform,
8082 self.retained_glyph_uniform_offset(slot),
8083 bytemuck::bytes_of(&uniforms),
8084 );
8085 slot
8086 }
8087
8088 #[cfg(not(target_arch = "wasm32"))]
8089 fn claim_retained_glyph_uniform_slot(&mut self) -> usize {
8090 let slot = self.retained_glyph_uniform_cursor;
8091 self.retained_glyph_uniform_cursor = self.retained_glyph_uniform_cursor.saturating_add(1);
8092 self.ensure_retained_glyph_uniform_capacity(slot.saturating_add(1));
8093 slot
8094 }
8095
8096 #[cfg(not(target_arch = "wasm32"))]
8097 fn retained_glyph_uniform_offset(&self, slot: usize) -> u64 {
8098 self.retained_glyph_uniform_stride * slot as u64
8099 }
8100
8101 #[cfg(not(target_arch = "wasm32"))]
8102 fn retained_glyph_uniform_dynamic_offset(&self, slot: usize) -> Result<u32, String> {
8103 let offset = self.retained_glyph_uniform_offset(slot);
8104 u32::try_from(offset).map_err(|_| {
8105 "retained glyph uniform offset exceeded WGPU dynamic offset range".to_string()
8106 })
8107 }
8108
8109 #[cfg(not(target_arch = "wasm32"))]
8110 fn ensure_retained_glyph_uniform_capacity(&mut self, required_slots: usize) {
8111 if required_slots <= self.retained_glyph_uniform_capacity {
8112 return;
8113 }
8114 let new_capacity = required_slots
8115 .next_power_of_two()
8116 .max(INITIAL_RETAINED_GLYPH_UNIFORM_SLOTS);
8117 self.retained_glyph_uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
8118 label: Some("Retained Glyph Uniform Buffer"),
8119 size: self.retained_glyph_uniform_stride * new_capacity as u64,
8120 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
8121 mapped_at_creation: false,
8122 });
8123 self.retained_glyph_uniform_bind_group =
8124 self.device.create_bind_group(&wgpu::BindGroupDescriptor {
8125 label: Some("Retained Glyph Uniform Bind Group"),
8126 layout: &self.retained_glyph_uniform_bind_group_layout,
8127 entries: &[wgpu::BindGroupEntry {
8128 binding: 0,
8129 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
8130 buffer: &self.retained_glyph_uniform_buffer,
8131 offset: 0,
8132 size: wgpu::BufferSize::new(std::mem::size_of::<Uniforms>() as u64),
8133 }),
8134 }],
8135 });
8136 self.retained_glyph_uniform_capacity = new_capacity;
8137 }
8138
8139 #[cfg(target_arch = "wasm32")]
8140 fn prepare_wasm_viewport_uniforms(&mut self, params: ViewportUniformParams) -> usize {
8141 let slot = self.claim_wasm_uniform_batch();
8142 let uniforms = Self::viewport_uniforms(params);
8143 let bytes = bytemuck::bytes_of(&uniforms);
8144 let upload_stats = self.frame_graph_executor.upload_buffer(
8145 &self.queue,
8146 &self.wasm_uniform_batches[slot].buffer,
8147 0,
8148 bytes,
8149 );
8150 self.frame_stats.record_command_stats(upload_stats);
8151 slot
8152 }
8153
8154 #[cfg(target_arch = "wasm32")]
8155 fn claim_wasm_uniform_batch(&mut self) -> usize {
8156 let slot = self.wasm_uniform_batch_cursor;
8157 self.wasm_uniform_batch_cursor += 1;
8158 while self.wasm_uniform_batches.len() <= slot {
8159 self.wasm_uniform_batches.push(UniformBatchBuffer::new(
8160 &self.device,
8161 &self.uniform_bind_group_layout,
8162 ));
8163 }
8164 slot
8165 }
8166
8167 #[cfg(target_arch = "wasm32")]
8168 fn claim_wasm_shape_batch(&mut self) -> usize {
8169 let slot = self.wasm_shape_batch_cursor;
8170 self.wasm_shape_batch_cursor += 1;
8171 while self.wasm_shape_batches.len() <= slot {
8172 self.wasm_shape_batches.push(ShapeBatchBuffers::new(
8173 &self.device,
8174 &self.shape_bind_group_layout,
8175 &self.identity_similarity_buffer,
8176 self.dummy_paint_buffer.as_ref(),
8177 self.shape_batch_limits,
8178 ));
8179 }
8180 slot
8181 }
8182
8183 #[cfg(target_arch = "wasm32")]
8184 fn claim_wasm_image_batch(&mut self) -> usize {
8185 let slot = self.wasm_image_batch_cursor;
8186 self.wasm_image_batch_cursor += 1;
8187 while self.wasm_image_batches.len() <= slot {
8188 self.wasm_image_batches
8189 .push(ImageBatchBuffers::new(&self.device));
8190 }
8191 slot
8192 }
8193
8194 #[cfg(target_arch = "wasm32")]
8195 fn write_wasm_buffer(&self, buffer: &wgpu::Buffer, bytes: &[u8]) {
8196 let upload_stats = self
8197 .frame_graph_executor
8198 .upload_buffer(&self.queue, buffer, 0, bytes);
8199 self.frame_stats.record_command_stats(upload_stats);
8200 }
8201
8202 fn take_staged_uploads(&mut self) -> StagedBufferUploads {
8203 let mut staged_uploads = std::mem::take(&mut self.staged_uploads);
8204 debug_assert!(
8205 staged_uploads.is_empty(),
8206 "renderer-owned staged uploads should be restored as empty scratch storage"
8207 );
8208 staged_uploads.clear();
8209 staged_uploads
8210 }
8211
8212 fn restore_staged_uploads(&mut self, mut staged_uploads: StagedBufferUploads) {
8213 staged_uploads.clear();
8214 self.staged_uploads = staged_uploads;
8215 }
8216
8217 #[cfg(not(target_arch = "wasm32"))]
8218 fn ensure_upload_buffer_capacity(&mut self, required_bytes: u64) {
8219 if required_bytes <= self.upload_buffer.size() {
8220 return;
8221 }
8222
8223 let new_size = required_bytes
8224 .next_power_of_two()
8225 .max(INITIAL_UPLOAD_BUFFER_BYTES);
8226 self.upload_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
8227 label: Some("Frame Upload Buffer"),
8228 size: new_size,
8229 usage: wgpu::BufferUsages::COPY_SRC | wgpu::BufferUsages::COPY_DST,
8230 mapped_at_creation: false,
8231 });
8232 }
8233
8234 fn flush_staged_uploads_at(
8235 &mut self,
8236 encoder: &mut wgpu::CommandEncoder,
8237 staged_uploads: &StagedBufferUploads,
8238 upload_buffer_offset: u64,
8239 ) {
8240 if staged_uploads.is_empty() {
8241 return;
8242 }
8243 debug_assert_eq!(
8244 upload_buffer_offset % wgpu::COPY_BUFFER_ALIGNMENT,
8245 0,
8246 "upload-buffer base offset must satisfy copy alignment"
8247 );
8248
8249 #[cfg(target_arch = "wasm32")]
8250 {
8251 let _ = upload_buffer_offset;
8252 let _ = encoder;
8253 debug_assert!(
8254 staged_uploads.is_empty(),
8255 "wasm draw uploads use retained per-batch resource slots"
8256 );
8257 return;
8258 }
8259
8260 #[cfg(not(target_arch = "wasm32"))]
8261 {
8262 self.ensure_upload_buffer_capacity(
8263 upload_buffer_offset + staged_uploads.bytes.len() as u64,
8264 );
8265 let upload_stats = self.frame_graph_executor.upload_buffer(
8266 &self.queue,
8267 &self.upload_buffer,
8268 upload_buffer_offset,
8269 &staged_uploads.bytes,
8270 );
8271 self.frame_stats.record_command_stats(upload_stats);
8272
8273 for copy in &staged_uploads.copies {
8274 let target_buffer = match copy.target {
8275 UploadTarget::Uniform => &self.uniform_buffer,
8276 UploadTarget::ShapeData => &self.shape_buffers.shape_buffer,
8277 UploadTarget::ShapeGradient => &self.shape_buffers.gradient_buffer,
8278 UploadTarget::ImageVertex => &self.image_vertex_buffer,
8279 UploadTarget::ImageIndex => &self.image_index_buffer,
8280 UploadTarget::RetainedGlyphUniform => &self.retained_glyph_uniform_buffer,
8281 UploadTarget::ReplayTransform => &self.replay_slots.transform_buffer,
8282 UploadTarget::ReplayPaintData(slot) => {
8283 let Some(entry) = self.replay_slots.slots.get(&slot) else {
8286 continue;
8287 };
8288 &entry.paint_buffer
8289 }
8290 };
8291 encoder.copy_buffer_to_buffer(
8292 &self.upload_buffer,
8293 upload_buffer_offset + copy.source_offset,
8294 target_buffer,
8295 copy.target_offset,
8296 copy.size,
8297 );
8298 }
8299 }
8300 }
8301
8302 #[allow(clippy::too_many_arguments)]
8303 fn encode_shadow_draw<C: FrameCommandRecorder>(
8304 &mut self,
8305 frame_encoder: &mut C,
8306 target_view: &wgpu::TextureView,
8307 shadow: &ShadowDraw,
8308 width: u32,
8309 height: u32,
8310 root_scale: f32,
8311 ) {
8312 if shadow.shapes.is_empty() && shadow.texts.is_empty() {
8313 return;
8314 }
8315
8316 let shape_bounds_opt = shadow
8317 .shapes
8318 .iter()
8319 .map(|(shape, _)| shape.rect)
8320 .reduce(|a, b| Rect {
8321 x: a.x.min(b.x),
8322 y: a.y.min(b.y),
8323 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
8324 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
8325 });
8326
8327 let text_bounds_opt = shadow
8328 .texts
8329 .iter()
8330 .map(|text| text.rect)
8331 .reduce(|a, b| Rect {
8332 x: a.x.min(b.x),
8333 y: a.y.min(b.y),
8334 width: (a.x + a.width).max(b.x + b.width) - a.x.min(b.x),
8335 height: (a.y + a.height).max(b.y + b.height) - a.y.min(b.y),
8336 });
8337
8338 let combined_bounds = match (shape_bounds_opt, text_bounds_opt) {
8339 (Some(s), Some(t)) => Some(Rect {
8340 x: s.x.min(t.x),
8341 y: s.y.min(t.y),
8342 width: (s.x + s.width).max(t.x + t.width) - s.x.min(t.x),
8343 height: (s.y + s.height).max(t.y + t.height) - s.y.min(t.y),
8344 }),
8345 (Some(s), None) => Some(s),
8346 (None, Some(t)) => Some(t),
8347 (None, None) => None,
8348 };
8349
8350 let Some(shape_bounds) = combined_bounds else {
8351 return;
8352 };
8353
8354 let blur_margin = blur_extent_margin(shadow.blur_radius);
8355 let source_blur_bounds = Rect {
8356 x: shape_bounds.x - blur_margin,
8357 y: shape_bounds.y - blur_margin,
8358 width: shape_bounds.width + blur_margin * 2.0,
8359 height: shape_bounds.height + blur_margin * 2.0,
8360 };
8361 let mut visible_blur_bounds = source_blur_bounds;
8362 if let Some(clip) = shadow.clip {
8363 let clip_expanded = Rect {
8364 x: clip.x - blur_margin,
8365 y: clip.y - blur_margin,
8366 width: clip.width + blur_margin * 2.0,
8367 height: clip.height + blur_margin * 2.0,
8368 };
8369 let Some(intersection) = visible_blur_bounds.intersect(clip_expanded) else {
8370 return;
8371 };
8372 visible_blur_bounds = intersection;
8373 }
8374 let processing_scissor =
8375 scissor_rect_for_rect(visible_blur_bounds, root_scale, width, height);
8376 if processing_scissor.is_none() {
8377 return;
8378 }
8379
8380 if shadow.blur_radius <= 0.0 {
8382 for (shape, blend_mode) in &shadow.shapes {
8383 self.encode_shapes_pass(
8384 frame_encoder,
8385 target_view,
8386 std::iter::once(shape),
8387 *blend_mode,
8388 width,
8389 height,
8390 root_scale,
8391 wgpu::LoadOp::Load,
8392 [0.0, 0.0],
8393 );
8394 frame_encoder.record_pass();
8395 }
8396 if !shadow.texts.is_empty() {
8397 let mut staged_uploads = self.take_staged_uploads();
8398 let viewport = ViewportUniformParams {
8399 width,
8400 height,
8401 offset: [0.0, 0.0],
8402 };
8403 match self.prepare_text_image_draw_cmds(
8404 shadow.texts.iter(),
8405 viewport,
8406 root_scale,
8407 &mut staged_uploads,
8408 ) {
8409 Ok(prepared_images) if !prepared_images.is_empty() => {
8410 let upload_offset = frame_encoder
8411 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
8412 self.flush_staged_uploads_at(
8413 frame_encoder.encoder(),
8414 &staged_uploads,
8415 upload_offset,
8416 );
8417 let draw_result = {
8418 let mut render_pass = frame_encoder.encoder().begin_render_pass(
8419 &wgpu::RenderPassDescriptor {
8420 label: Some("Zero Blur Shadow Text Image Pass"),
8421 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8422 view: target_view,
8423 resolve_target: None,
8424 depth_slice: None,
8425 ops: wgpu::Operations {
8426 load: wgpu::LoadOp::Load,
8427 store: wgpu::StoreOp::Store,
8428 },
8429 })],
8430 depth_stencil_attachment: None,
8431 timestamp_writes: None,
8432 occlusion_query_set: None,
8433 multiview_mask: None,
8434 },
8435 );
8436 self.draw_prepared_images(
8437 &mut render_pass,
8438 &prepared_images,
8439 BlendMode::SrcOver,
8440 )
8441 };
8442 self.scratch_image_cmds = prepared_images.into_cmds();
8443 if let Err(e) = draw_result {
8444 eprintln!("Failed to draw text for zero-blur shadow: {}", e);
8445 } else {
8446 self.frame_stats.bump_text();
8447 frame_encoder.record_pass();
8448 }
8449 }
8450 Ok(prepared_images) => {
8451 self.scratch_image_cmds = prepared_images.into_cmds();
8452 }
8453 Err(e) => {
8454 eprintln!("Failed to prepare text image for zero-blur shadow: {}", e);
8455 }
8456 }
8457 self.restore_staged_uploads(staged_uploads);
8458 }
8459 return;
8460 }
8461
8462 let Some(device_bounds) =
8464 device_pixel_bounds_for_rect(visible_blur_bounds, width, height, root_scale)
8465 else {
8466 return;
8467 };
8468 let bounds_x = device_bounds.x;
8469 let bounds_y = device_bounds.y;
8470 let bounds_w = device_bounds.width;
8471 let bounds_h = device_bounds.height;
8472 let pixel_radius = shadow.blur_radius * root_scale;
8473
8474 if shadow.texts.is_empty() && !shadow.shapes.is_empty() {
8475 if let Some(plan) = shape_shadow_surface_plan(
8476 &shadow.shapes,
8477 shadow.clip,
8478 shadow.blur_radius,
8479 width,
8480 height,
8481 root_scale,
8482 self.max_texture_dim(),
8483 ) {
8484 if self.encode_shape_only_blurred_shadow_draw(
8485 frame_encoder,
8486 target_view,
8487 shadow,
8488 plan.source_device_bounds,
8489 plan.pixel_radius,
8490 plan.processing_scissor,
8491 width,
8492 height,
8493 root_scale,
8494 ) {
8495 return;
8496 }
8497 }
8498 }
8499
8500 if !shadow.texts.is_empty() {
8501 self.frame_stats.record_shadow_text_blur_fallback();
8502 }
8503
8504 let device = self.device.clone();
8505 let source_descriptor =
8506 self.transient_offscreen_descriptor("Shadow Source", bounds_w, bounds_h);
8507 let source = frame_encoder.acquire_transient_offscreen(&device, source_descriptor);
8508 let viewport_offset = [bounds_x, bounds_y];
8509 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
8510 let source_outcome = self.encode_shadow_shape_source_passes(
8511 frame_encoder,
8512 &source.view,
8513 &shadow.shapes,
8514 bounds_w,
8515 bounds_h,
8516 viewport_offset,
8517 root_scale,
8518 &mut next_load_op,
8519 );
8520 frame_encoder.record_passes(source_outcome.pass_count);
8521 let mut rendered_any = source_outcome.rendered_any;
8522
8523 if !shadow.texts.is_empty() {
8524 let mut shifted_texts = shadow.texts.clone();
8525 for text in &mut shifted_texts {
8526 text.rect.x -= viewport_offset[0] / root_scale;
8527 text.rect.y -= viewport_offset[1] / root_scale;
8528 if let Some(clip) = text.clip.as_mut() {
8529 clip.x -= viewport_offset[0] / root_scale;
8530 clip.y -= viewport_offset[1] / root_scale;
8531 }
8532 }
8533
8534 let mut staged_uploads = self.take_staged_uploads();
8535 let viewport = ViewportUniformParams {
8536 width: bounds_w,
8537 height: bounds_h,
8538 offset: [0.0, 0.0],
8539 };
8540 match self.prepare_text_image_draw_cmds(
8541 shifted_texts.iter(),
8542 viewport,
8543 root_scale,
8544 &mut staged_uploads,
8545 ) {
8546 Ok(prepared_images) if !prepared_images.is_empty() => {
8547 let upload_offset = frame_encoder
8548 .allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
8549 self.flush_staged_uploads_at(
8550 frame_encoder.encoder(),
8551 &staged_uploads,
8552 upload_offset,
8553 );
8554 let draw_result = {
8555 let mut render_pass = frame_encoder.encoder().begin_render_pass(
8556 &wgpu::RenderPassDescriptor {
8557 label: Some("Shadow Source Text Image Pass"),
8558 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8559 view: &source.view,
8560 resolve_target: None,
8561 depth_slice: None,
8562 ops: wgpu::Operations {
8563 load: next_load_op,
8564 store: wgpu::StoreOp::Store,
8565 },
8566 })],
8567 depth_stencil_attachment: None,
8568 timestamp_writes: None,
8569 occlusion_query_set: None,
8570 multiview_mask: None,
8571 },
8572 );
8573 self.draw_prepared_images(
8574 &mut render_pass,
8575 &prepared_images,
8576 BlendMode::SrcOver,
8577 )
8578 };
8579 self.scratch_image_cmds = prepared_images.into_cmds();
8580 if let Err(e) = draw_result {
8581 eprintln!("Failed to draw text for shadow: {}", e);
8582 } else {
8583 self.frame_stats.bump_text();
8584 frame_encoder.record_pass();
8585 rendered_any = true;
8586 }
8587 }
8588 Ok(prepared_images) => {
8589 self.scratch_image_cmds = prepared_images.into_cmds();
8590 }
8591 Err(e) => {
8592 eprintln!("Failed to prepare text image for shadow: {}", e);
8593 }
8594 }
8595 self.restore_staged_uploads(staged_uploads);
8596 }
8597
8598 if !rendered_any {
8599 frame_encoder.release_transient_offscreen(source_descriptor, source);
8600 return;
8601 }
8602
8603 let scratch_descriptor =
8604 self.transient_offscreen_descriptor("Shadow Blur Scratch", bounds_w, bounds_h);
8605 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
8606 {
8607 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
8608 frame_encoder,
8609 &device,
8610 &source,
8611 &scratch,
8612 &source.view,
8613 pixel_radius,
8614 pixel_radius,
8615 TileMode::Decal,
8616 None, );
8618 }
8619 frame_encoder.record_passes(2);
8620
8621 let clip_scissor = shadow
8622 .clip
8623 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
8624 let scissor = clip_scissor.or(processing_scissor);
8625 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
8626 mask.rect[0] -= viewport_offset[0];
8629 mask.rect[1] -= viewport_offset[1];
8630 mask
8631 });
8632 let dest_viewport = Some((
8633 viewport_offset[0],
8634 viewport_offset[1],
8635 bounds_w as f32,
8636 bounds_h as f32,
8637 ));
8638 {
8639 self.effect_renderer
8640 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8641 frame_encoder,
8642 &device,
8643 &source,
8644 target_view,
8645 1.0,
8646 wgpu::LoadOp::Load,
8647 scissor,
8648 rounded_mask,
8649 BlendMode::SrcOver,
8650 dest_viewport,
8651 CompositeSampleMode::Linear,
8652 );
8653 }
8654 frame_encoder.record_pass();
8655 self.effect_renderer.record_blur_pass();
8656 self.effect_renderer.record_composite_pass();
8657 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
8658 frame_encoder.release_transient_offscreen(source_descriptor, source);
8659 }
8660
8661 #[allow(clippy::too_many_arguments)]
8662 fn encode_shadow_shape_source_passes<C: FrameCommandRecorder>(
8663 &mut self,
8664 frame_encoder: &mut C,
8665 source_view: &wgpu::TextureView,
8666 shapes: &[(DrawShape, BlendMode)],
8667 width: u32,
8668 height: u32,
8669 viewport_offset: [f32; 2],
8670 root_scale: f32,
8671 next_load_op: &mut wgpu::LoadOp<wgpu::Color>,
8672 ) -> ShadowSourceRenderOutcome {
8673 if shapes.is_empty() {
8674 return ShadowSourceRenderOutcome {
8675 rendered_any: false,
8676 pass_count: 0,
8677 };
8678 }
8679
8680 let mut staged_uploads = self.take_staged_uploads();
8681 let mut rendered_any = false;
8682 let mut pass_count = 0_u32;
8683 let mut start = 0usize;
8684 while start < shapes.len() {
8685 let blend_mode = supported_blend_mode(shapes[start].1);
8686 let mut end = start + 1;
8687 while end < shapes.len()
8688 && end - start < self.shape_batch_limits.max_shapes_per_batch
8689 && supported_blend_mode(shapes[end].1) == blend_mode
8690 {
8691 end += 1;
8692 }
8693
8694 staged_uploads.clear();
8695 let viewport = ViewportUniformParams {
8696 width,
8697 height,
8698 offset: viewport_offset,
8699 };
8700 let Some(prepared_shape) = self.prepare_shapes_batch(
8701 shapes[start..end]
8702 .iter()
8703 .map(|(shape, _blend_mode)| shape)
8704 .filter(|shape| shape_draw_is_visible_in_viewport(shape, viewport, root_scale)),
8705 root_scale,
8706 viewport,
8707 &mut staged_uploads,
8708 ) else {
8709 start = end;
8710 continue;
8711 };
8712
8713 let upload_offset =
8714 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
8715 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
8716
8717 {
8718 let mut render_pass =
8719 frame_encoder
8720 .encoder()
8721 .begin_render_pass(&wgpu::RenderPassDescriptor {
8722 label: Some("Shadow Source Shape Pass"),
8723 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
8724 view: source_view,
8725 resolve_target: None,
8726 depth_slice: None,
8727 ops: wgpu::Operations {
8728 load: *next_load_op,
8729 store: wgpu::StoreOp::Store,
8730 },
8731 })],
8732 depth_stencil_attachment: None,
8733 timestamp_writes: None,
8734 occlusion_query_set: None,
8735 multiview_mask: None,
8736 });
8737 self.draw_prepared_shapes(
8738 &mut render_pass,
8739 blend_mode,
8740 prepared_shape,
8741 width,
8742 height,
8743 );
8744 }
8745
8746 pass_count = pass_count.saturating_add(1);
8747 rendered_any = true;
8748 *next_load_op = wgpu::LoadOp::Load;
8749 start = end;
8750 }
8751
8752 self.restore_staged_uploads(staged_uploads);
8753 ShadowSourceRenderOutcome {
8754 rendered_any,
8755 pass_count,
8756 }
8757 }
8758
8759 #[allow(clippy::too_many_arguments)]
8760 fn encode_shape_only_blurred_shadow_draw<C: FrameCommandRecorder>(
8761 &mut self,
8762 frame_encoder: &mut C,
8763 target_view: &wgpu::TextureView,
8764 shadow: &ShadowDraw,
8765 device_bounds: DevicePixelBounds,
8766 pixel_radius: f32,
8767 processing_scissor: Option<(u32, u32, u32, u32)>,
8768 width: u32,
8769 height: u32,
8770 root_scale: f32,
8771 ) -> bool {
8772 let bounds_w = device_bounds.width;
8773 let bounds_h = device_bounds.height;
8774 let viewport_offset = [device_bounds.x, device_bounds.y];
8775 let cache_key =
8776 shape_shadow_surface_cache_key(&shadow.shapes, device_bounds, pixel_radius, root_scale);
8777
8778 if let Some(key) = cache_key {
8779 if let Some(cached) = self.cached_shadow_surface(&key) {
8780 self.frame_stats
8781 .record_shadow_shape_cache_hit(bounds_w, bounds_h);
8782 let clip_scissor = shadow
8783 .clip
8784 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
8785 let scissor = clip_scissor.or(processing_scissor);
8786 let rounded_mask =
8787 inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
8788 mask.rect[0] -= viewport_offset[0];
8789 mask.rect[1] -= viewport_offset[1];
8790 mask
8791 });
8792 let dest_viewport = Some((
8793 viewport_offset[0],
8794 viewport_offset[1],
8795 bounds_w as f32,
8796 bounds_h as f32,
8797 ));
8798 {
8799 self.effect_renderer
8800 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8801 frame_encoder,
8802 &self.device,
8803 &cached,
8804 target_view,
8805 1.0,
8806 wgpu::LoadOp::Load,
8807 scissor,
8808 rounded_mask,
8809 BlendMode::SrcOver,
8810 dest_viewport,
8811 CompositeSampleMode::Nearest,
8812 );
8813 }
8814 frame_encoder.record_pass();
8815 self.effect_renderer.record_composite_pass();
8816 return true;
8817 }
8818 self.frame_stats
8819 .record_shadow_shape_cache_miss(bounds_w, bounds_h);
8820 self.frame_stats.maybe_print_shadow_shape_cache_miss(
8821 bounds_w,
8822 bounds_h,
8823 key.content_hash,
8824 pixel_radius,
8825 viewport_offset,
8826 shadow.shapes.len(),
8827 shadow.clip,
8828 );
8829 }
8830
8831 let device = self.device.clone();
8832 let source_descriptor =
8833 self.transient_offscreen_descriptor("Shape Shadow Source", bounds_w, bounds_h);
8834 let source_is_cacheable = cache_key.is_some();
8835 let source = if source_is_cacheable {
8836 self.acquire_retained_surface(bounds_w, bounds_h)
8837 } else {
8838 frame_encoder.acquire_transient_offscreen(&device, source_descriptor)
8839 };
8840 let scratch_descriptor =
8841 self.transient_offscreen_descriptor("Shape Shadow Blur Scratch", bounds_w, bounds_h);
8842 let scratch = frame_encoder.acquire_transient_offscreen(&device, scratch_descriptor);
8843 let mut next_load_op = wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT);
8844 let source_outcome = self.encode_shadow_shape_source_passes(
8845 frame_encoder,
8846 &source.view,
8847 &shadow.shapes,
8848 bounds_w,
8849 bounds_h,
8850 viewport_offset,
8851 root_scale,
8852 &mut next_load_op,
8853 );
8854 frame_encoder.record_passes(source_outcome.pass_count);
8855
8856 if !source_outcome.rendered_any {
8857 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
8858 if source_is_cacheable {
8859 self.defer_offscreen_release(source);
8860 } else {
8861 frame_encoder.release_transient_offscreen(source_descriptor, source);
8862 }
8863 return true;
8864 }
8865
8866 {
8867 self.effect_renderer.encode_blur_scissored_ping_pong_passes(
8868 frame_encoder,
8869 &device,
8870 &source,
8871 &scratch,
8872 &source.view,
8873 pixel_radius,
8874 pixel_radius,
8875 TileMode::Decal,
8876 None,
8877 );
8878 }
8879 frame_encoder.record_passes(2);
8880
8881 let clip_scissor = shadow
8882 .clip
8883 .and_then(|clip| scissor_rect_for_rect(clip, root_scale, width, height));
8884 let scissor = clip_scissor.or(processing_scissor);
8885 let rounded_mask = inner_shadow_composite_mask(shadow, root_scale).map(|mut mask| {
8886 mask.rect[0] -= viewport_offset[0];
8887 mask.rect[1] -= viewport_offset[1];
8888 mask
8889 });
8890 let dest_viewport = Some((
8891 viewport_offset[0],
8892 viewport_offset[1],
8893 bounds_w as f32,
8894 bounds_h as f32,
8895 ));
8896 {
8897 self.effect_renderer
8898 .encode_composite_to_view_scissored_with_alpha_and_mask_and_blend_mode(
8899 frame_encoder,
8900 &device,
8901 &source,
8902 target_view,
8903 1.0,
8904 wgpu::LoadOp::Load,
8905 scissor,
8906 rounded_mask,
8907 BlendMode::SrcOver,
8908 dest_viewport,
8909 CompositeSampleMode::Nearest,
8910 );
8911 }
8912 frame_encoder.record_pass();
8913
8914 self.effect_renderer.record_blur_pass();
8915 self.effect_renderer.record_composite_pass();
8916 frame_encoder.release_transient_offscreen(scratch_descriptor, scratch);
8917 if let Some(key) = cache_key {
8918 self.insert_cached_shadow_surface(key, source);
8919 } else {
8920 frame_encoder.release_transient_offscreen(source_descriptor, source);
8921 }
8922 true
8923 }
8924
8925 fn prepare_shapes_batch<'a, I>(
8926 &mut self,
8927 layer_shapes: I,
8928 root_scale: f32,
8929 viewport: ViewportUniformParams,
8930 staged_uploads: &mut StagedBufferUploads,
8931 ) -> Option<PreparedShapeBatch>
8932 where
8933 I: Iterator<Item = &'a DrawShape>,
8934 {
8935 #[cfg(target_arch = "wasm32")]
8936 let _ = staged_uploads;
8937
8938 let shape_refs: Vec<&DrawShape> = layer_shapes
8943 .take(self.shape_batch_limits.max_shapes_per_batch)
8944 .collect();
8945 let shape_count = shape_refs.len();
8946 if shape_count == 0 {
8947 return None;
8948 }
8949
8950 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
8954 let mut total_gradient_stops = 0u32;
8955 gradient_offsets.push(0);
8956 for shape in &shape_refs {
8957 total_gradient_stops += shape_gradient_stop_count(shape) as u32;
8958 gradient_offsets.push(total_gradient_stops);
8959 }
8960
8961 self.scratch_shape_data.clear();
8962 self.scratch_shape_data
8963 .resize(shape_count, ShapeData::zeroed());
8964 self.scratch_gradients.clear();
8965 self.scratch_gradients
8966 .resize(total_gradient_stops as usize, GradientStop::zeroed());
8967
8968 convert_shapes_into_outputs(
8969 &shape_refs,
8970 &gradient_offsets,
8971 root_scale,
8972 &mut self.scratch_shape_data,
8973 &mut self.scratch_gradients,
8974 );
8975
8976 #[cfg(not(target_arch = "wasm32"))]
8977 {
8978 self.shape_buffers.ensure_capacity(
8979 &self.device,
8980 &self.shape_bind_group_layout,
8981 &self.identity_similarity_buffer,
8982 self.dummy_paint_buffer.as_ref(),
8983 shape_count,
8984 self.scratch_gradients.len().max(1),
8985 );
8986 self.stage_viewport_uniforms(staged_uploads, viewport);
8987 staged_uploads.stage(
8988 UploadTarget::ShapeData,
8989 bytemuck::cast_slice(&self.scratch_shape_data),
8990 );
8991 if !self.scratch_gradients.is_empty() {
8992 staged_uploads.stage(
8993 UploadTarget::ShapeGradient,
8994 bytemuck::cast_slice(&self.scratch_gradients),
8995 );
8996 }
8997 }
8998
8999 #[cfg(target_arch = "wasm32")]
9000 let shape_slot = {
9001 let slot = self.claim_wasm_shape_batch();
9002 {
9003 let buffers = &mut self.wasm_shape_batches[slot];
9004 buffers.ensure_capacity(
9005 &self.device,
9006 &self.shape_bind_group_layout,
9007 &self.identity_similarity_buffer,
9008 self.dummy_paint_buffer.as_ref(),
9009 shape_count,
9010 self.scratch_gradients.len().max(1),
9011 );
9012 }
9013 let buffers = &self.wasm_shape_batches[slot];
9014 self.write_wasm_buffer(
9015 &buffers.shape_buffer,
9016 bytemuck::cast_slice(&self.scratch_shape_data),
9017 );
9018 if !self.scratch_gradients.is_empty() {
9019 self.write_wasm_buffer(
9020 &buffers.gradient_buffer,
9021 bytemuck::cast_slice(&self.scratch_gradients),
9022 );
9023 }
9024 slot
9025 };
9026
9027 #[cfg(target_arch = "wasm32")]
9028 let uniform_slot = self.prepare_wasm_viewport_uniforms(viewport);
9029
9030 Some(PreparedShapeBatch {
9031 vertex_start: 0,
9032 vertex_count: shape_count as u32 * 6,
9033 has_gradient: total_gradient_stops > 0,
9034 #[cfg(target_arch = "wasm32")]
9035 shape_slot,
9036 #[cfg(target_arch = "wasm32")]
9037 uniform_slot,
9038 })
9039 }
9040
9041 #[cfg(not(target_arch = "wasm32"))]
9048 fn prepare_shapes_batch_direct<'a, I, C: FrameCommandRecorder>(
9049 &mut self,
9050 frame_encoder: &mut C,
9051 layer_shapes: I,
9052 root_scale: f32,
9053 viewport: ViewportUniformParams,
9054 staged_uploads: &mut StagedBufferUploads,
9055 ) -> Option<(PreparedShapeBatch, u64)>
9056 where
9057 I: Iterator<Item = &'a DrawShape>,
9058 {
9059 let shape_refs: Vec<&DrawShape> = layer_shapes
9060 .take(self.shape_batch_limits.max_shapes_per_batch)
9061 .collect();
9062 let shape_count = shape_refs.len();
9063 if shape_count == 0 {
9064 return None;
9065 }
9066
9067 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
9068 let mut total_gradient_stops = 0u32;
9069 gradient_offsets.push(0);
9070 for shape in &shape_refs {
9071 total_gradient_stops += shape_gradient_stop_count(shape) as u32;
9072 gradient_offsets.push(total_gradient_stops);
9073 }
9074
9075 self.shape_buffers.ensure_capacity(
9076 &self.device,
9077 &self.shape_bind_group_layout,
9078 &self.identity_similarity_buffer,
9079 self.dummy_paint_buffer.as_ref(),
9080 shape_count,
9081 (total_gradient_stops as usize).max(1),
9082 );
9083
9084 self.scratch_shape_data.clear();
9085 self.scratch_shape_data
9086 .resize(shape_count, ShapeData::zeroed());
9087 self.scratch_gradients.clear();
9088 self.scratch_gradients
9089 .resize(total_gradient_stops as usize, GradientStop::zeroed());
9090 convert_shapes_into_outputs(
9091 &shape_refs,
9092 &gradient_offsets,
9093 root_scale,
9094 &mut self.scratch_shape_data,
9095 &mut self.scratch_gradients,
9096 );
9097
9098 let uniform_len = std::mem::size_of::<Uniforms>() as u64;
9105 let shape_len = (shape_count * std::mem::size_of::<ShapeData>()) as u64;
9106 let gradient_len = total_gradient_stops as u64 * std::mem::size_of::<GradientStop>() as u64;
9107 let total_len = uniform_len + shape_len + gradient_len;
9108 let upload_base = frame_encoder.allocate_staged_upload_bytes(total_len);
9109 self.ensure_upload_buffer_capacity(upload_base + total_len);
9110
9111 let shape_off = uniform_len;
9112 let gradient_off = shape_off + shape_len;
9113
9114 let uniforms = Self::viewport_uniforms(viewport);
9115 let mut upload_stats = self.frame_graph_executor.upload_buffer(
9116 &self.queue,
9117 &self.upload_buffer,
9118 upload_base,
9119 bytemuck::bytes_of(&uniforms),
9120 );
9121 upload_stats.upload_bytes += self
9122 .frame_graph_executor
9123 .upload_buffer(
9124 &self.queue,
9125 &self.upload_buffer,
9126 upload_base + shape_off,
9127 bytemuck::cast_slice(&self.scratch_shape_data),
9128 )
9129 .upload_bytes;
9130 if !self.scratch_gradients.is_empty() {
9131 upload_stats.upload_bytes += self
9132 .frame_graph_executor
9133 .upload_buffer(
9134 &self.queue,
9135 &self.upload_buffer,
9136 upload_base + gradient_off,
9137 bytemuck::cast_slice(&self.scratch_gradients),
9138 )
9139 .upload_bytes;
9140 }
9141 self.frame_stats.record_command_stats(upload_stats);
9142
9143 staged_uploads.record_upload_copy(UploadTarget::Uniform, 0, 0, uniform_len);
9144 staged_uploads.record_upload_copy(UploadTarget::ShapeData, shape_off, 0, shape_len);
9145 staged_uploads.record_upload_copy(
9146 UploadTarget::ShapeGradient,
9147 gradient_off,
9148 0,
9149 gradient_len,
9150 );
9151
9152 Some((
9153 PreparedShapeBatch {
9154 vertex_start: 0,
9155 vertex_count: shape_count as u32 * 6,
9156 has_gradient: total_gradient_stops > 0,
9157 },
9158 upload_base,
9159 ))
9160 }
9161
9162 pub(crate) fn replay_supported(&self) -> bool {
9168 #[cfg(target_arch = "wasm32")]
9172 {
9173 false
9174 }
9175 #[cfg(not(target_arch = "wasm32"))]
9176 {
9177 self.shape_batch_limits.storage
9178 }
9179 }
9180
9181 pub(crate) fn restore_replay_ack_confirmations(
9187 &mut self,
9188 confirmations: Vec<crate::frame_packet::ReplayConfirmation>,
9189 ) {
9190 #[cfg(not(target_arch = "wasm32"))]
9191 {
9192 self.replay_ack_confirmations = confirmations;
9193 }
9194 #[cfg(target_arch = "wasm32")]
9195 let _ = confirmations;
9196 }
9197
9198 #[cfg(not(target_arch = "wasm32"))]
9213 fn consume_replay_ops(
9214 &mut self,
9215 mut ops: crate::frame_packet::ReplayFrameOps,
9216 shapes: &[DrawShape],
9217 root_scale: f32,
9218 ) -> (
9219 crate::frame_packet::ReplayAck,
9220 crate::frame_packet::ReplayFrameOps,
9221 ) {
9222 if ops.generation < self.store_feed_generation {
9223 self.replay_generation_drops += 1;
9229 log::warn!(
9230 "[command-feed] dropping replay ops of generation {} against store \
9231 generation {} ({} captures, {} patches, {} releases; lifetime drops {})",
9232 ops.generation,
9233 self.store_feed_generation,
9234 ops.captures.len(),
9235 ops.color_patches.len(),
9236 ops.releases.len(),
9237 self.replay_generation_drops,
9238 );
9239 ops.captures.clear();
9240 ops.color_patches.clear();
9241 ops.releases.clear();
9242 return (
9243 crate::frame_packet::ReplayAck {
9244 generation: self.store_feed_generation,
9245 confirmations: Vec::new(),
9246 },
9247 ops,
9248 );
9249 }
9250 if ops.generation > self.store_feed_generation {
9251 self.store_feed_generation = ops.generation;
9258 }
9259 let generation = ops.generation;
9260 for slot in ops.releases.drain(..) {
9263 self.release_replay_slot(slot);
9264 }
9265 let mut confirmations = std::mem::take(&mut self.replay_ack_confirmations);
9268 debug_assert!(confirmations.is_empty());
9269 for capture in ops.captures.drain(..) {
9270 if capture.frame != ops.frame {
9271 log::warn!(
9278 "[command-feed] dropping stale capture for slot {} of {:?} \
9279 (queued frame {}, ops frame {})",
9280 capture.key.1,
9281 capture.key.0,
9282 capture.frame,
9283 ops.frame,
9284 );
9285 continue;
9286 }
9287 let end = capture.shape_start + capture.shape_count;
9288 let Some(slice) = shapes.get(capture.shape_start..end) else {
9289 continue;
9290 };
9291 let refs: Vec<&DrawShape> = slice.iter().collect();
9292 let Some(gpu_slot) = self.capture_replay_slot(&refs, root_scale) else {
9293 continue;
9294 };
9295 confirmations.push((capture.key, gpu_slot));
9296 }
9297 self.replay_color_patches.clear();
9305 std::mem::swap(&mut self.replay_color_patches, &mut ops.color_patches);
9306 (
9307 crate::frame_packet::ReplayAck {
9308 generation,
9309 confirmations,
9310 },
9311 ops,
9312 )
9313 }
9314
9315 #[cfg(not(target_arch = "wasm32"))]
9320 pub(crate) fn replay_generation_drops(&self) -> u64 {
9321 self.replay_generation_drops
9322 }
9323
9324 #[cfg(not(target_arch = "wasm32"))]
9332 pub(crate) fn replay_ops_roundtrip_for_tests(&mut self, generation_skew: u64) -> usize {
9333 let generation = self.store_feed_generation.wrapping_add(generation_skew);
9334 let ops = crate::shape_replay::SHAPE_REPLAY
9335 .with(|state| state.borrow_mut().take_frame_ops(generation));
9336 let (ack, recycled) = self.consume_replay_ops(ops, &[], 1.0);
9337 let confirmed = ack.confirmations.len();
9338 self.replay_ack_confirmations = crate::shape_replay::SHAPE_REPLAY
9339 .with(|state| state.borrow_mut().apply_ack(ack, recycled));
9340 confirmed
9341 }
9342
9343 #[cfg(not(target_arch = "wasm32"))]
9350 fn stage_replay_patches(&mut self, staged_uploads: &mut StagedBufferUploads) {
9351 std::mem::swap(
9359 &mut self.replay_color_patches,
9360 &mut self.color_patch_scratch,
9361 );
9362 let total_patches = self.color_patch_scratch.len();
9363 if total_patches == 0 {
9364 self.replay_upload_stats.note_frame(0, 0, 0, 0, 0);
9365 return;
9366 }
9367
9368 #[derive(Clone, Copy)]
9374 struct DirtySpan {
9375 paint_min: u32,
9376 paint_max: u32,
9377 }
9378 const CLEAN: DirtySpan = DirtySpan {
9379 paint_min: u32::MAX,
9380 paint_max: 0,
9381 };
9382 let mut dirty: std::collections::HashMap<
9383 u32,
9384 DirtySpan,
9385 cranpose_ui_graphics::FxBuildHasher,
9386 > = std::collections::HashMap::default();
9387
9388 for patch in &self.color_patch_scratch {
9390 let Some(slot) = self.replay_slots.slots.get_mut(&patch.slot) else {
9391 continue;
9392 };
9393 let Some(paint) = slot.paint_mirror.get_mut(patch.shape_index as usize) else {
9394 continue;
9395 };
9396 *paint = patch.color;
9397 let span = dirty.entry(patch.slot).or_insert(CLEAN);
9398 span.paint_min = span.paint_min.min(patch.shape_index);
9399 span.paint_max = span.paint_max.max(patch.shape_index);
9400 }
9401
9402 let mut uploaded_records = 0u64;
9403 let mut uploaded_bytes = 0u64;
9404 let slots_touched = dirty.len() as u64;
9405 for (slot_id, span) in dirty {
9406 let Some(slot) = self.replay_slots.slots.get(&slot_id) else {
9407 continue;
9408 };
9409 if span.paint_min <= span.paint_max {
9410 let range = span.paint_min as usize..span.paint_max as usize + 1;
9411 uploaded_records += range.len() as u64;
9412 uploaded_bytes += (range.len() * std::mem::size_of::<[f32; 4]>()) as u64;
9413 staged_uploads.stage_at(
9414 UploadTarget::ReplayPaintData(slot_id),
9415 range.start as u64 * std::mem::size_of::<[f32; 4]>() as u64,
9416 bytemuck::cast_slice(&slot.paint_mirror[range]),
9417 );
9418 }
9419 }
9420 let ideal_bytes = total_patches as u64 * 16;
9423 self.replay_upload_stats.note_frame(
9424 total_patches as u64,
9425 slots_touched,
9426 uploaded_records,
9427 uploaded_bytes,
9428 ideal_bytes,
9429 );
9430 if cranpose_core::env_flag!("CRANPOSE_COMMAND_REPLAY_DIAG") {
9431 log::warn!(
9432 "[replay-upload] frame: {} patches -> {} records / {:.1} KB staged \
9433 across {} slots (color-only {:.1} KB)",
9434 total_patches,
9435 uploaded_records,
9436 uploaded_bytes as f64 / 1024.0,
9437 slots_touched,
9438 ideal_bytes as f64 / 1024.0,
9439 );
9440 }
9441 self.color_patch_scratch.clear();
9442 }
9443
9444 #[cfg(not(target_arch = "wasm32"))]
9447 pub(crate) fn capture_replay_slot(
9448 &mut self,
9449 shape_refs: &[&DrawShape],
9450 root_scale: f32,
9451 ) -> Option<u32> {
9452 if !self.shape_batch_limits.storage || shape_refs.is_empty() {
9453 return None;
9454 }
9455 let id = self.replay_slots.free_ids.pop()?;
9456 let shape_count = shape_refs.len();
9457
9458 let mut gradient_offsets: Vec<u32> = Vec::with_capacity(shape_count + 1);
9459 let mut total_gradient_stops = 0u32;
9460 gradient_offsets.push(0);
9461 for shape in shape_refs {
9462 total_gradient_stops += shape_gradient_stop_count(shape) as u32;
9463 gradient_offsets.push(total_gradient_stops);
9464 }
9465
9466 let mut shape_data = vec![ShapeData::zeroed(); shape_count];
9467 let mut gradients = vec![GradientStop::zeroed(); (total_gradient_stops as usize).max(1)];
9468 convert_shapes_into_outputs(
9469 shape_refs,
9470 &gradient_offsets,
9471 root_scale,
9472 &mut shape_data,
9473 &mut gradients,
9474 );
9475
9476 let shape_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9477 label: Some("Replay Shape Buffer"),
9478 size: (std::mem::size_of::<ShapeData>() * shape_count) as u64,
9479 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
9480 mapped_at_creation: true,
9481 });
9482 shape_buffer
9483 .slice(..)
9484 .get_mapped_range_mut()
9485 .copy_from_slice(bytemuck::cast_slice(&shape_data));
9486 shape_buffer.unmap();
9487
9488 let gradient_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9489 label: Some("Replay Gradient Buffer"),
9490 size: (std::mem::size_of::<GradientStop>() * gradients.len()) as u64,
9491 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
9492 mapped_at_creation: true,
9493 });
9494 gradient_buffer
9495 .slice(..)
9496 .get_mapped_range_mut()
9497 .copy_from_slice(bytemuck::cast_slice(&gradients));
9498 gradient_buffer.unmap();
9499
9500 let mesh = if arc_mesh_enabled() {
9501 match build_arc_mesh_vertices(&shape_data) {
9502 Some(build) => {
9503 let cut = if build.quad_area > 0.0 {
9504 (1.0 - build.mesh_area / build.quad_area) * 100.0
9505 } else {
9506 0.0
9507 };
9508 log::warn!(
9514 "[arc-mesh] slot {id}: {} arcs meshed ({} segs), {} passthrough; \
9515 {} unique verts / {} indices (quad path: {} verts); \
9516 quad_px {:.0} -> mesh_px {:.0} (-{:.1}%)",
9517 build.meshed_arcs,
9518 build.meshed_segments,
9519 build.passthrough,
9520 build.vertices.len(),
9521 build.indices.len(),
9522 shape_count * 6,
9523 build.quad_area,
9524 build.mesh_area,
9525 cut,
9526 );
9527 (build.meshed_arcs > 0).then(|| {
9530 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9531 label: Some("Replay Mesh Vertex Buffer"),
9532 size: (std::mem::size_of::<MeshVertex>() * build.vertices.len()) as u64,
9533 usage: wgpu::BufferUsages::VERTEX,
9534 mapped_at_creation: true,
9535 });
9536 vertex_buffer
9537 .slice(..)
9538 .get_mapped_range_mut()
9539 .copy_from_slice(bytemuck::cast_slice(&build.vertices));
9540 vertex_buffer.unmap();
9541 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9542 label: Some("Replay Mesh Index Buffer"),
9543 size: (std::mem::size_of::<u32>() * build.indices.len()) as u64,
9544 usage: wgpu::BufferUsages::INDEX,
9545 mapped_at_creation: true,
9546 });
9547 index_buffer
9548 .slice(..)
9549 .get_mapped_range_mut()
9550 .copy_from_slice(bytemuck::cast_slice(&build.indices));
9551 index_buffer.unmap();
9552 ReplaySlotMesh {
9553 vertex_buffer,
9554 index_buffer,
9555 index_prefix: build.index_prefix,
9556 }
9557 })
9558 }
9559 None => {
9560 log::warn!(
9561 "[arc-mesh] slot {id}: geometry byte budget overflowed for \
9562 {shape_count} shapes; whole slot falls back to quad passthrough"
9563 );
9564 None
9565 }
9566 }
9567 } else {
9568 None
9569 };
9570
9571 let paint: Vec<[f32; 4]> = shape_data.iter().map(|shape| shape.color).collect();
9574 let paint_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
9575 label: Some("Replay Paint Buffer"),
9576 size: (std::mem::size_of::<[f32; 4]>() * shape_count) as u64,
9577 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
9578 mapped_at_creation: true,
9579 });
9580 paint_buffer
9581 .slice(..)
9582 .get_mapped_range_mut()
9583 .copy_from_slice(bytemuck::cast_slice(&paint));
9584 paint_buffer.unmap();
9585
9586 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
9587 label: Some("Replay Shape Bind Group"),
9588 layout: &self.shape_bind_group_layout,
9589 entries: &[
9590 wgpu::BindGroupEntry {
9591 binding: 0,
9592 resource: shape_buffer.as_entire_binding(),
9593 },
9594 wgpu::BindGroupEntry {
9595 binding: 1,
9596 resource: gradient_buffer.as_entire_binding(),
9597 },
9598 wgpu::BindGroupEntry {
9602 binding: 2,
9603 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
9604 buffer: &self.replay_slots.transform_buffer,
9605 offset: 0,
9606 size: Some(
9607 std::num::NonZeroU64::new(
9608 std::mem::size_of::<SimilarityTransform>() as u64
9609 )
9610 .expect("similarity transform is non-empty"),
9611 ),
9612 }),
9613 },
9614 wgpu::BindGroupEntry {
9615 binding: 3,
9616 resource: paint_buffer.as_entire_binding(),
9617 },
9618 ],
9619 });
9620
9621 let capture_epoch = self.replay_slots.next_capture_epoch;
9622 self.replay_slots.next_capture_epoch += 1;
9623 self.replay_slots.slots.insert(
9624 id,
9625 ReplaySlot {
9626 paint_buffer,
9627 bind_group,
9628 shape_count: shape_count as u32,
9629 paint_mirror: paint,
9630 mesh,
9631 capture_epoch,
9632 has_gradient: total_gradient_stops > 0,
9633 },
9634 );
9635 Some(id)
9636 }
9637
9638 #[cfg(not(target_arch = "wasm32"))]
9640 pub(crate) fn release_replay_slot(&mut self, id: u32) {
9641 if self.replay_slots.slots.remove(&id).is_some() {
9642 self.replay_slots.free_ids.push(id);
9643 self.retained_bundle_cache.clear();
9649 }
9650 }
9651
9652 #[cfg(not(target_arch = "wasm32"))]
9655 #[doc(hidden)]
9656 pub fn instanced_quads_active(&self) -> bool {
9657 self.instanced_quads.is_some()
9658 }
9659
9660 #[cfg(not(target_arch = "wasm32"))]
9663 #[doc(hidden)]
9664 pub fn replay_slot_mesh_stats(&self) -> (usize, usize) {
9665 let meshed = self
9666 .replay_slots
9667 .slots
9668 .values()
9669 .filter(|slot| slot.mesh.is_some())
9670 .count();
9671 (meshed, self.replay_slots.slots.len())
9672 }
9673
9674 #[cfg(not(target_arch = "wasm32"))]
9678 fn draw_retained_batch(
9679 &self,
9680 render_pass: &mut wgpu::RenderPass<'_>,
9681 retained: &RetainedDraw,
9682 retained_index: usize,
9683 width: u32,
9684 height: u32,
9685 ) {
9686 let Some(slot) = self.replay_slots.slots.get(&retained.slot) else {
9687 return;
9688 };
9689 if retained_index as u32 >= MAX_REPLAY_SLOTS {
9690 return;
9691 }
9692 let first = retained.first_shape.min(slot.shape_count);
9693 let last = retained
9694 .first_shape
9695 .saturating_add(retained.shape_count)
9696 .min(slot.shape_count);
9697 if first >= last {
9698 return;
9699 }
9700 self.frame_stats.bump_shapes();
9701 self.frame_stats.add_draw_calls(1);
9702 render_pass.set_scissor_rect(0, 0, width, height);
9703 let mesh = slot.mesh.as_ref().map(|mesh| (mesh, self.mesh_pipeline()));
9711 match &mesh {
9712 Some((_, mesh_pipeline)) => render_pass.set_pipeline(mesh_pipeline),
9713 None => match &self.instanced_quads {
9714 Some(instanced) if !slot.has_gradient => {
9715 render_pass.set_pipeline(self.instanced_pipeline_solid(instanced))
9716 }
9717 Some(instanced) => {
9718 render_pass.set_pipeline(self.instanced_pipeline(instanced, BlendMode::SrcOver))
9719 }
9720 None if !slot.has_gradient => render_pass.set_pipeline(self.shape_pipeline_solid()),
9721 None => render_pass.set_pipeline(self.shape_pipeline(BlendMode::SrcOver)),
9722 },
9723 }
9724 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
9725 render_pass.set_bind_group(
9726 1,
9727 &slot.bind_group,
9728 &[retained_index as u32 * REPLAY_TRANSFORM_STRIDE as u32],
9729 );
9730 match mesh {
9731 Some((mesh, _)) => {
9732 render_pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
9733 render_pass
9734 .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
9735 render_pass.draw_indexed(
9736 mesh.index_prefix[first as usize]..mesh.index_prefix[last as usize],
9737 0,
9738 0..1,
9739 );
9740 }
9741 None => match &self.instanced_quads {
9742 Some(instanced) => {
9743 render_pass.set_index_buffer(
9744 instanced.index_buffer.slice(..),
9745 wgpu::IndexFormat::Uint16,
9746 );
9747 render_pass.draw_indexed(0..6, 0, first..last);
9748 }
9749 None => render_pass.draw(first * 6..last * 6, 0..1),
9750 },
9751 }
9752 }
9753
9754 #[cfg(not(target_arch = "wasm32"))]
9761 fn retained_bundle_key(
9762 &self,
9763 ordered_items: &[(usize, SegmentDrawItem)],
9764 retained_draws: &[RetainedDraw],
9765 item_range: Range<usize>,
9766 ) -> RetainedBundleKey {
9767 let mut ops = Vec::with_capacity(item_range.len());
9768 for (_, item) in &ordered_items[item_range] {
9769 let SegmentDrawItem::Retained(index) = item else {
9770 continue;
9771 };
9772 let Some(retained) = retained_draws.get(*index) else {
9773 continue;
9774 };
9775 let slot = self.replay_slots.slots.get(&retained.slot);
9776 let (first, last) = match slot {
9777 Some(slot) => (
9778 retained.first_shape.min(slot.shape_count),
9779 retained
9780 .first_shape
9781 .saturating_add(retained.shape_count)
9782 .min(slot.shape_count),
9783 ),
9784 None => (
9785 retained.first_shape,
9786 retained.first_shape.saturating_add(retained.shape_count),
9787 ),
9788 };
9789 ops.push(RetainedBundleOpKey {
9790 slot: retained.slot,
9791 capture_epoch: slot.map(|slot| slot.capture_epoch),
9792 first,
9793 last,
9794 retained_index: *index as u32,
9795 has_mesh: slot.is_some_and(|slot| slot.mesh.is_some())
9796 && self.shape_batch_limits.storage,
9797 });
9798 }
9799 RetainedBundleKey { ops }
9800 }
9801
9802 #[cfg(not(target_arch = "wasm32"))]
9809 fn build_retained_bundle(&self, key: &RetainedBundleKey) -> wgpu::RenderBundle {
9810 let mut encoder =
9811 self.device
9812 .create_render_bundle_encoder(&wgpu::RenderBundleEncoderDescriptor {
9813 label: Some("Retained Stretch Bundle"),
9814 color_formats: &[Some(self.surface_format)],
9818 depth_stencil: None,
9819 sample_count: 1,
9820 multiview: None,
9821 });
9822 for op in &key.ops {
9823 if op.capture_epoch.is_none()
9824 || op.retained_index >= MAX_REPLAY_SLOTS
9825 || op.first >= op.last
9826 {
9827 continue;
9828 }
9829 let Some(slot) = self.replay_slots.slots.get(&op.slot) else {
9830 continue;
9831 };
9832 let mesh = slot.mesh.as_ref().map(|mesh| (mesh, self.mesh_pipeline()));
9833 match &mesh {
9834 Some((_, mesh_pipeline)) => encoder.set_pipeline(mesh_pipeline),
9835 None => match &self.instanced_quads {
9839 Some(instanced) if !slot.has_gradient => {
9840 encoder.set_pipeline(self.instanced_pipeline_solid(instanced))
9841 }
9842 Some(instanced) => {
9843 encoder.set_pipeline(self.instanced_pipeline(instanced, BlendMode::SrcOver))
9844 }
9845 None if !slot.has_gradient => encoder.set_pipeline(self.shape_pipeline_solid()),
9846 None => encoder.set_pipeline(self.shape_pipeline(BlendMode::SrcOver)),
9847 },
9848 }
9849 encoder.set_bind_group(0, &self.uniform_bind_group, &[]);
9850 encoder.set_bind_group(
9851 1,
9852 &slot.bind_group,
9853 &[op.retained_index * REPLAY_TRANSFORM_STRIDE as u32],
9854 );
9855 match mesh {
9856 Some((mesh, _)) => {
9857 encoder.set_vertex_buffer(0, mesh.vertex_buffer.slice(..));
9858 encoder
9859 .set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
9860 encoder.draw_indexed(
9861 mesh.index_prefix[op.first as usize]..mesh.index_prefix[op.last as usize],
9862 0,
9863 0..1,
9864 );
9865 }
9866 None => match &self.instanced_quads {
9871 Some(instanced) => {
9872 encoder.set_index_buffer(
9873 instanced.index_buffer.slice(..),
9874 wgpu::IndexFormat::Uint16,
9875 );
9876 encoder.draw_indexed(0..6, 0, op.first..op.last);
9877 }
9878 None => encoder.draw(op.first * 6..op.last * 6, 0..1),
9879 },
9880 }
9881 }
9882 encoder.finish(&wgpu::RenderBundleDescriptor {
9883 label: Some("Retained Stretch Bundle"),
9884 })
9885 }
9886
9887 #[cfg(not(target_arch = "wasm32"))]
9895 fn draw_retained_stretch_bundled(
9896 &mut self,
9897 render_pass: &mut wgpu::RenderPass<'_>,
9898 ordered_items: &[(usize, SegmentDrawItem)],
9899 retained_draws: &[RetainedDraw],
9900 item_range: Range<usize>,
9901 width: u32,
9902 height: u32,
9903 ) {
9904 let key = self.retained_bundle_key(ordered_items, retained_draws, item_range);
9905 if !self.retained_bundle_cache.hit(&key) {
9906 let bundle = self.build_retained_bundle(&key);
9907 self.retained_bundle_cache.insert(key.clone(), bundle);
9908 }
9909 for op in &key.ops {
9912 if op.capture_epoch.is_some()
9913 && op.retained_index < MAX_REPLAY_SLOTS
9914 && op.first < op.last
9915 {
9916 self.frame_stats.bump_shapes();
9917 self.frame_stats.add_draw_calls(1);
9918 }
9919 }
9920 render_pass.set_scissor_rect(0, 0, width, height);
9925 if let Some(bundle) = self.retained_bundle_cache.get(&key) {
9926 render_pass.execute_bundles(std::iter::once(bundle));
9927 }
9928 }
9929
9930 #[cfg(not(target_arch = "wasm32"))]
9933 #[doc(hidden)]
9934 pub fn retained_bundle_stats(&self) -> (u64, u64) {
9935 self.retained_bundle_cache.stats()
9936 }
9937
9938 fn draw_prepared_shapes(
9939 &self,
9940 render_pass: &mut wgpu::RenderPass<'_>,
9941 blend_mode: BlendMode,
9942 batch: PreparedShapeBatch,
9943 width: u32,
9944 height: u32,
9945 ) {
9946 self.frame_stats.bump_shapes();
9947 self.frame_stats.add_draw_calls(1);
9948 render_pass.set_scissor_rect(0, 0, width, height);
9949 #[cfg(not(target_arch = "wasm32"))]
9950 let (uniform_bind_group, shape_buffers) = (&self.uniform_bind_group, &self.shape_buffers);
9951 #[cfg(target_arch = "wasm32")]
9952 let (uniform_bind_group, shape_buffers) = (
9953 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
9954 &self.wasm_shape_batches[batch.shape_slot],
9955 );
9956 #[cfg(not(target_arch = "wasm32"))]
9961 if let Some(instanced) = &self.instanced_quads {
9962 assert!(
9963 batch.vertex_start.is_multiple_of(6) && batch.vertex_count.is_multiple_of(6),
9964 "shape batches are whole shapes: vertex range {}..+{} must be \
9965 six-aligned to convert to an instance range",
9966 batch.vertex_start,
9967 batch.vertex_count,
9968 );
9969 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
9970 render_pass.set_pipeline(self.instanced_pipeline_solid(instanced));
9971 } else {
9972 render_pass.set_pipeline(self.instanced_pipeline(instanced, blend_mode));
9973 }
9974 render_pass.set_bind_group(0, uniform_bind_group, &[]);
9975 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
9978 let first_shape = batch.vertex_start / 6;
9979 let shape_count = batch.vertex_count / 6;
9980 render_pass
9981 .set_index_buffer(instanced.index_buffer.slice(..), wgpu::IndexFormat::Uint16);
9982 render_pass.draw_indexed(0..6, 0, first_shape..first_shape + shape_count);
9983 return;
9984 }
9985 if blend_mode == BlendMode::SrcOver && !batch.has_gradient {
9986 render_pass.set_pipeline(self.shape_pipeline_solid());
9987 } else {
9988 render_pass.set_pipeline(self.shape_pipeline(blend_mode));
9989 }
9990 render_pass.set_bind_group(0, uniform_bind_group, &[]);
9991 render_pass.set_bind_group(1, &shape_buffers.bind_group, &[0]);
9994 render_pass.draw(
9997 batch.vertex_start..batch.vertex_start + batch.vertex_count,
9998 0..1,
9999 );
10000 }
10001
10002 #[allow(clippy::too_many_arguments)]
10005 fn encode_shapes_pass<'a, I, C: FrameCommandRecorder>(
10006 &mut self,
10007 frame_encoder: &mut C,
10008 target_view: &wgpu::TextureView,
10009 layer_shapes: I,
10010 blend_mode: BlendMode,
10011 width: u32,
10012 height: u32,
10013 root_scale: f32,
10014 load_op: wgpu::LoadOp<wgpu::Color>,
10015 viewport_offset: [f32; 2],
10016 ) where
10017 I: Iterator<Item = &'a DrawShape>,
10018 {
10019 let mut staged_uploads = self.take_staged_uploads();
10020 let viewport = ViewportUniformParams {
10021 width,
10022 height,
10023 offset: viewport_offset,
10024 };
10025 let Some(batch) = self.prepare_shapes_batch(
10026 layer_shapes
10027 .filter(|shape| shape_draw_is_visible_in_viewport(shape, viewport, root_scale)),
10028 root_scale,
10029 viewport,
10030 &mut staged_uploads,
10031 ) else {
10032 self.restore_staged_uploads(staged_uploads);
10033 return;
10034 };
10035 let upload_offset =
10036 frame_encoder.allocate_staged_upload_bytes(staged_uploads.bytes.len() as u64);
10037 self.flush_staged_uploads_at(frame_encoder.encoder(), &staged_uploads, upload_offset);
10038 self.restore_staged_uploads(staged_uploads);
10039 let mut render_pass =
10040 frame_encoder
10041 .encoder()
10042 .begin_render_pass(&wgpu::RenderPassDescriptor {
10043 label: Some("Shape Pass"),
10044 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
10045 view: target_view,
10046 resolve_target: None,
10047 depth_slice: None,
10048 ops: wgpu::Operations {
10049 load: load_op,
10050 store: wgpu::StoreOp::Store,
10051 },
10052 })],
10053 depth_stencil_attachment: None,
10054 timestamp_writes: None,
10055 occlusion_query_set: None,
10056 multiview_mask: None,
10057 });
10058 self.draw_prepared_shapes(&mut render_pass, blend_mode, batch, width, height);
10059 }
10060
10061 fn draw_prepared_images(
10062 &mut self,
10063 render_pass: &mut wgpu::RenderPass<'_>,
10064 batch: &PreparedImageBatch,
10065 blend_mode: BlendMode,
10066 ) -> Result<(), String> {
10067 if batch.cmds.is_empty() {
10068 return Ok(());
10069 }
10070 self.frame_stats.bump_images();
10071 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
10072 render_pass.set_pipeline(self.image_pipeline(blend_mode));
10073 #[cfg(not(target_arch = "wasm32"))]
10074 let (uniform_bind_group, vertex_buffer, index_buffer) = (
10075 &self.uniform_bind_group,
10076 &self.image_vertex_buffer,
10077 &self.image_index_buffer,
10078 );
10079 #[cfg(target_arch = "wasm32")]
10080 let (uniform_bind_group, vertex_buffer, index_buffer) = (
10081 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
10082 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
10083 &self.wasm_image_batches[batch.image_slot].index_buffer,
10084 );
10085 render_pass.set_bind_group(0, uniform_bind_group, &[]);
10086 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10087 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
10088
10089 for cmd in &batch.cmds {
10090 let (sx, sy, sw, sh) = cmd.scissor;
10091 render_pass.set_scissor_rect(sx, sy, sw, sh);
10092
10093 let cached = self
10094 .image_texture_cache
10095 .get(&cmd.image_id)
10096 .ok_or_else(|| "image texture missing from cache".to_string())?;
10097 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
10098 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
10099 }
10100 Ok(())
10101 }
10102
10103 fn draw_prepared_glyphs(
10104 &mut self,
10105 render_pass: &mut wgpu::RenderPass<'_>,
10106 batch: &PreparedGlyphBatch,
10107 ) -> Result<(), String> {
10108 if batch.cmds.is_empty() {
10109 return Ok(());
10110 }
10111 #[cfg(not(target_arch = "wasm32"))]
10112 {
10113 self.draw_native_prepared_glyph_cmd_range(
10114 render_pass,
10115 &batch.cmds,
10116 0..batch.cmds.len(),
10117 )?;
10118 }
10119 #[cfg(target_arch = "wasm32")]
10120 {
10121 self.frame_stats.bump_text();
10122 self.frame_stats.add_draw_calls(batch.cmds.len() as u32);
10123 render_pass.set_pipeline(self.glyph_atlas_pipeline());
10124 let (uniform_bind_group, vertex_buffer, index_buffer) = (
10125 &self.wasm_uniform_batches[batch.uniform_slot].bind_group,
10126 &self.wasm_image_batches[batch.image_slot].vertex_buffer,
10127 &self.wasm_image_batches[batch.image_slot].index_buffer,
10128 );
10129 render_pass.set_bind_group(0, uniform_bind_group, &[]);
10130 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
10131 render_pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10132 render_pass.set_vertex_buffer(0, vertex_buffer.slice(..));
10133
10134 for cmd in &batch.cmds {
10135 let (sx, sy, sw, sh) = cmd.scissor;
10136 render_pass.set_scissor_rect(sx, sy, sw, sh);
10137 let GlyphDrawSource::Shared {
10138 index_start,
10139 index_count,
10140 } = cmd.source;
10141 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
10142 }
10143 }
10144 Ok(())
10145 }
10146
10147 #[cfg(not(target_arch = "wasm32"))]
10148 fn draw_native_prepared_image_cmd_range(
10149 &mut self,
10150 render_pass: &mut wgpu::RenderPass<'_>,
10151 cmds: &[ImageDrawCmd],
10152 cmd_range: Range<usize>,
10153 blend_mode: BlendMode,
10154 ) -> Result<(), String> {
10155 let Some(cmds) = cmds.get(cmd_range) else {
10156 return Err("image command range is outside the prepared command buffer".to_string());
10157 };
10158 if cmds.is_empty() {
10159 return Ok(());
10160 }
10161
10162 self.frame_stats.bump_images();
10163 self.frame_stats.add_draw_calls(cmds.len() as u32);
10164 render_pass.set_pipeline(self.image_pipeline(blend_mode));
10165 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
10166 render_pass.set_index_buffer(self.image_index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10167 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
10168
10169 for cmd in cmds {
10170 let (sx, sy, sw, sh) = cmd.scissor;
10171 render_pass.set_scissor_rect(sx, sy, sw, sh);
10172
10173 let cached = self
10174 .image_texture_cache
10175 .get(&cmd.image_id)
10176 .ok_or_else(|| "image texture missing from cache".to_string())?;
10177 render_pass.set_bind_group(1, cached.bind_group(cmd.sampling), &[]);
10178 render_pass.draw_indexed(cmd.index_start..(cmd.index_start + 6), 0, 0..1);
10179 }
10180 Ok(())
10181 }
10182
10183 #[cfg(not(target_arch = "wasm32"))]
10184 fn draw_native_prepared_glyph_cmd_range(
10185 &mut self,
10186 render_pass: &mut wgpu::RenderPass<'_>,
10187 cmds: &[GlyphDrawCmd],
10188 cmd_range: Range<usize>,
10189 ) -> Result<(), String> {
10190 let Some(cmds) = cmds.get(cmd_range) else {
10191 return Err("glyph command range is outside the prepared command buffer".to_string());
10192 };
10193 if cmds.is_empty() {
10194 return Ok(());
10195 }
10196
10197 self.frame_stats.bump_text();
10198 self.frame_stats.add_draw_calls(cmds.len() as u32);
10199
10200 let mut shared_buffers_bound = false;
10201 let mut retained_pipeline_bound = false;
10202 for cmd in cmds {
10203 let (sx, sy, sw, sh) = cmd.scissor;
10204 render_pass.set_scissor_rect(sx, sy, sw, sh);
10205 match cmd.source {
10206 GlyphDrawSource::Shared {
10207 index_start,
10208 index_count,
10209 } => {
10210 if retained_pipeline_bound || !shared_buffers_bound {
10211 render_pass.set_pipeline(self.glyph_atlas_pipeline());
10212 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
10213 retained_pipeline_bound = false;
10214 }
10215 if !shared_buffers_bound {
10216 render_pass.set_bind_group(0, &self.uniform_bind_group, &[]);
10217 render_pass.set_index_buffer(
10218 self.image_index_buffer.slice(..),
10219 wgpu::IndexFormat::Uint32,
10220 );
10221 render_pass.set_vertex_buffer(0, self.image_vertex_buffer.slice(..));
10222 shared_buffers_bound = true;
10223 }
10224 render_pass.draw_indexed(index_start..(index_start + index_count), 0, 0..1);
10225 }
10226 GlyphDrawSource::Retained {
10227 cache_key,
10228 uniform_slot,
10229 } => {
10230 shared_buffers_bound = false;
10231 if !retained_pipeline_bound {
10232 render_pass.set_pipeline(self.retained_glyph_atlas_pipeline());
10233 render_pass.set_bind_group(1, &self.text_glyph_atlas.bind_group, &[]);
10234 retained_pipeline_bound = true;
10235 }
10236 let cached = self
10237 .text_glyph_gpu_run_cache
10238 .peek(&cache_key)
10239 .ok_or_else(|| "retained glyph buffer missing from cache".to_string())?;
10240 let dynamic_offset =
10241 self.retained_glyph_uniform_dynamic_offset(uniform_slot)?;
10242 render_pass.set_bind_group(
10243 0,
10244 &self.retained_glyph_uniform_bind_group,
10245 &[dynamic_offset],
10246 );
10247 render_pass
10248 .set_index_buffer(cached.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
10249 render_pass.set_vertex_buffer(0, cached.vertex_buffer.slice(..));
10250 render_pass.draw_indexed(0..cached.index_count, 0, 0..1);
10251 }
10252 }
10253 }
10254 Ok(())
10255 }
10256
10257 fn append_image_draw_cmd(
10258 &mut self,
10259 image_draw: &ImageDraw,
10260 viewport: ViewportUniformParams,
10261 root_scale: f32,
10262 image_vertices: &mut Vec<Vertex>,
10263 image_indices: &mut Vec<u32>,
10264 image_cmds: &mut Vec<ImageDrawCmd>,
10265 ) -> Result<(), String> {
10266 let snap_delta = image_draw
10267 .snap_anchor
10268 .map(|anchor| snap_delta_for_anchor(anchor, root_scale))
10269 .unwrap_or_default();
10270 let rect = image_draw.rect.translate(snap_delta.x, snap_delta.y);
10271 if rect.width <= 0.0 || rect.height <= 0.0 || image_draw.alpha <= 0.0 {
10272 return Ok(());
10273 }
10274
10275 let (tint, cpu_filter) = tint_for_image(image_draw.color_filter, image_draw.alpha);
10276 if tint[3] <= 0.0 {
10277 return Ok(());
10278 }
10279
10280 let prepared_image = if let Some(filter) = cpu_filter {
10281 apply_filter_to_bitmap(&image_draw.image, filter)?
10282 } else {
10283 image_draw.image.clone()
10284 };
10285 self.ensure_image_cached(&prepared_image)?;
10286
10287 let mut adjusted_image = ImageDraw {
10288 rect,
10289 local_rect: image_draw.local_rect.translate(snap_delta.x, snap_delta.y),
10290 quad: translate_quad(image_draw.quad, snap_delta),
10291 snap_anchor: image_draw.snap_anchor,
10292 image: image_draw.image.clone(),
10293 alpha: image_draw.alpha,
10294 color_filter: image_draw.color_filter,
10295 sampling: image_draw.sampling,
10296 z_index: image_draw.z_index,
10297 clip: image_draw.clip,
10298 blend_mode: image_draw.blend_mode,
10299 src_rect: image_draw.src_rect,
10300 motion_context_animated: image_draw.motion_context_animated,
10301 };
10302 snap_nearest_image_to_device_pixels(&mut adjusted_image, root_scale);
10303 let Some(scissor) =
10304 scissor_rect_for_image(&adjusted_image, root_scale, viewport.width, viewport.height)
10305 else {
10306 return Ok(());
10307 };
10308
10309 let Some(uv_rect) = image_uv_rect(&image_draw.image, image_draw.src_rect) else {
10310 return Ok(());
10311 };
10312 let device_quad =
10313 nearest_image_device_quad(&adjusted_image, root_scale).unwrap_or_else(|| {
10314 if adjusted_image.snap_anchor.is_some() {
10315 canonicalized_scaled_quad(adjusted_image.quad, root_scale)
10316 } else {
10317 scaled_quad(adjusted_image.quad, root_scale)
10318 }
10319 });
10320
10321 let base_vertex = image_vertices.len() as u32;
10322 let index_start = image_indices.len() as u32;
10323 image_indices.extend_from_slice(&[
10324 base_vertex,
10325 base_vertex + 1,
10326 base_vertex + 2,
10327 base_vertex + 2,
10328 base_vertex + 1,
10329 base_vertex + 3,
10330 ]);
10331 image_vertices.extend_from_slice(&[
10332 Vertex {
10333 position: device_quad[0],
10334 color: tint,
10335 uv: [uv_rect.min[0], uv_rect.min[1]],
10336 uv_bounds: uv_rect.sample_bounds,
10337 },
10338 Vertex {
10339 position: device_quad[1],
10340 color: tint,
10341 uv: [uv_rect.max[0], uv_rect.min[1]],
10342 uv_bounds: uv_rect.sample_bounds,
10343 },
10344 Vertex {
10345 position: device_quad[2],
10346 color: tint,
10347 uv: [uv_rect.min[0], uv_rect.max[1]],
10348 uv_bounds: uv_rect.sample_bounds,
10349 },
10350 Vertex {
10351 position: device_quad[3],
10352 color: tint,
10353 uv: [uv_rect.max[0], uv_rect.max[1]],
10354 uv_bounds: uv_rect.sample_bounds,
10355 },
10356 ]);
10357
10358 image_cmds.push(ImageDrawCmd {
10359 index_start,
10360 scissor,
10361 image_id: prepared_image.id(),
10362 sampling: image_draw.sampling,
10363 });
10364 Ok(())
10365 }
10366
10367 #[cfg(not(target_arch = "wasm32"))]
10368 fn stage_native_image_buffers(
10369 &mut self,
10370 staged_uploads: &mut StagedBufferUploads,
10371 viewport: ViewportUniformParams,
10372 image_vertices: &[Vertex],
10373 image_indices: &[u32],
10374 ) {
10375 if image_indices.is_empty() {
10376 return;
10377 }
10378
10379 self.stage_viewport_uniforms(staged_uploads, viewport);
10380 let needed_bytes = std::mem::size_of_val(image_vertices) as u64;
10385 if needed_bytes > self.image_vertex_buffer.size() {
10386 self.image_vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10387 label: Some("Image Vertex Buffer"),
10388 size: needed_bytes.next_power_of_two(),
10389 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
10390 mapped_at_creation: false,
10391 });
10392 }
10393 let needed_index_bytes = std::mem::size_of_val(image_indices) as u64;
10394 if needed_index_bytes > self.image_index_buffer.size() {
10395 self.image_index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10396 label: Some("Image Index Buffer"),
10397 size: needed_index_bytes.next_power_of_two(),
10398 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
10399 mapped_at_creation: false,
10400 });
10401 }
10402
10403 staged_uploads.stage(
10404 UploadTarget::ImageVertex,
10405 bytemuck::cast_slice(image_vertices),
10406 );
10407 staged_uploads.stage(
10408 UploadTarget::ImageIndex,
10409 bytemuck::cast_slice(image_indices),
10410 );
10411 }
10412
10413 fn prepare_image_draw_cmds<'a, I>(
10416 &mut self,
10417 layer_images: I,
10418 viewport: ViewportUniformParams,
10419 root_scale: f32,
10420 staged_uploads: &mut StagedBufferUploads,
10421 ) -> Result<PreparedImageBatch, String>
10422 where
10423 I: Iterator<Item = &'a ImageDraw>,
10424 {
10425 #[cfg(target_arch = "wasm32")]
10426 let _ = staged_uploads;
10427
10428 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
10429 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
10430 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
10431 image_vertices.clear();
10432 image_indices.clear();
10433 image_cmds.clear();
10434
10435 for image_draw in layer_images {
10436 self.append_image_draw_cmd(
10437 image_draw,
10438 viewport,
10439 root_scale,
10440 &mut image_vertices,
10441 &mut image_indices,
10442 &mut image_cmds,
10443 )?;
10444 }
10445
10446 #[cfg(not(target_arch = "wasm32"))]
10447 if !image_cmds.is_empty() {
10448 self.stage_native_image_buffers(
10449 staged_uploads,
10450 viewport,
10451 &image_vertices,
10452 &image_indices,
10453 );
10454 }
10455
10456 #[cfg(target_arch = "wasm32")]
10457 let image_slot = if image_cmds.is_empty() {
10458 0
10459 } else {
10460 let slot = self.claim_wasm_image_batch();
10461 {
10462 let buffers = &mut self.wasm_image_batches[slot];
10463 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
10464 }
10465 let buffers = &self.wasm_image_batches[slot];
10466 self.write_wasm_buffer(
10467 &buffers.vertex_buffer,
10468 bytemuck::cast_slice(&image_vertices),
10469 );
10470 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
10471 slot
10472 };
10473
10474 #[cfg(target_arch = "wasm32")]
10475 let uniform_slot = if image_cmds.is_empty() {
10476 0
10477 } else {
10478 self.prepare_wasm_viewport_uniforms(viewport)
10479 };
10480
10481 self.scratch_image_vertices = image_vertices;
10482 self.scratch_image_indices = image_indices;
10483 Ok(PreparedImageBatch {
10484 cmds: image_cmds,
10485 #[cfg(target_arch = "wasm32")]
10486 image_slot,
10487 #[cfg(target_arch = "wasm32")]
10488 uniform_slot,
10489 })
10490 }
10491
10492 fn glyph_atlas_entry_for(
10493 &mut self,
10494 glyph: &SoftwareGlyphAtlasGlyph,
10495 ) -> Result<GlyphAtlasEntry, String> {
10496 if let Some(entry) = self.text_glyph_atlas.upload_glyph(
10497 glyph.key,
10498 glyph,
10499 &self.queue,
10500 &mut self.frame_graph_executor,
10501 &mut self.frame_stats,
10502 ) {
10503 return Ok(entry);
10504 }
10505
10506 self.text_glyph_atlas.reset(
10507 &self.device,
10508 &self.image_bind_group_layout,
10509 &self.image_nearest_sampler,
10510 );
10511 Err("text glyph atlas filled and was reset".to_string())
10512 }
10513
10514 fn glyph_atlas_entry_for_cached(
10515 &mut self,
10516 glyph: &SoftwareGlyphAtlasPlacement,
10517 ) -> Option<GlyphAtlasEntry> {
10518 let entry = self.text_glyph_atlas.entry(&glyph.key)?;
10519 self.frame_stats.record_text_glyph_atlas_hit();
10520 Some(entry)
10521 }
10522
10523 fn glyph_atlas_entry_for_placement(
10524 &mut self,
10525 glyph: &SoftwareGlyphAtlasPlacement,
10526 ) -> Result<GlyphAtlasEntry, String> {
10527 if let Some(entry) = self.glyph_atlas_entry_for_cached(glyph) {
10528 return Ok(entry);
10529 }
10530
10531 let Some(upload_glyph) = self.text_glyph_mask_cache.atlas_glyph_for_placement(glyph) else {
10532 return Err("text glyph placement has no retained raster mask".to_string());
10533 };
10534 self.glyph_atlas_entry_for(&upload_glyph)
10535 }
10536
10537 fn prepare_text_glyph_quads(
10538 &mut self,
10539 run_key: TextGlyphRunCacheKey,
10540 atlas_generation: u64,
10541 cached_glyph_run: Option<&[SoftwareGlyphAtlasPlacement]>,
10542 collected_run: &[SoftwareGlyphAtlasRunGlyph],
10543 generated_quads: &mut Vec<CachedTextGlyphQuad>,
10544 ) -> Result<Rc<[CachedTextGlyphQuad]>, String> {
10545 generated_quads.clear();
10546 if let Some(glyph_run) = cached_glyph_run {
10547 for glyph in glyph_run {
10548 if glyph.width == 0 || glyph.height == 0 || glyph.color.3 <= 0.0 {
10549 continue;
10550 }
10551 let entry = self.glyph_atlas_entry_for_placement(glyph)?;
10552 generated_quads.push(cached_text_glyph_quad(
10557 glyph,
10558 entry,
10559 self.text_glyph_atlas.size(),
10560 ));
10561 }
10562 } else {
10563 for run_glyph in collected_run {
10564 let placement = run_glyph.placement();
10565 if placement.width == 0 || placement.height == 0 || placement.color.3 <= 0.0 {
10566 continue;
10567 }
10568 let entry = match run_glyph {
10569 SoftwareGlyphAtlasRunGlyph::Cached(placement) => {
10570 self.glyph_atlas_entry_for_placement(placement)?
10571 }
10572 SoftwareGlyphAtlasRunGlyph::New(glyph) => self.glyph_atlas_entry_for(glyph)?,
10573 };
10574 generated_quads.push(cached_text_glyph_quad(
10575 &placement,
10576 entry,
10577 self.text_glyph_atlas.size(),
10578 ));
10579 }
10580 }
10581
10582 let quads: Rc<[CachedTextGlyphQuad]> = Rc::from(generated_quads.clone().into_boxed_slice());
10583 if let Some(cached) = self.text_glyph_run_cache.get_mut(&run_key) {
10584 cached.quads = Some(Rc::clone(&quads));
10585 cached.atlas_generation = atlas_generation;
10586 }
10587 Ok(quads)
10588 }
10589
10590 #[allow(clippy::too_many_arguments)]
10591 fn append_text_glyph_quad_run(
10592 &mut self,
10593 source_raster_rect: Rect,
10594 quads: &[CachedTextGlyphQuad],
10595 clip: Option<Rect>,
10596 viewport: ViewportUniformParams,
10597 root_scale: f32,
10598 image_vertices: &mut Vec<Vertex>,
10599 image_indices: &mut Vec<u32>,
10600 record_cached_hits: bool,
10601 ) -> usize {
10602 let mut appended = 0usize;
10603 for quad in quads {
10604 if !cached_text_glyph_quad_is_visible_in_viewport(
10605 source_raster_rect,
10606 quad,
10607 clip,
10608 viewport,
10609 root_scale,
10610 ) {
10611 continue;
10612 }
10613 if append_cached_text_glyph_quad(
10614 source_raster_rect,
10615 quad,
10616 image_vertices,
10617 image_indices,
10618 ) {
10619 if record_cached_hits {
10620 self.frame_stats.record_text_glyph_atlas_hit();
10621 }
10622 appended = appended.saturating_add(1);
10623 }
10624 }
10625 appended
10626 }
10627
10628 #[cfg(not(target_arch = "wasm32"))]
10629 fn retained_glyph_viewport(
10630 viewport: ViewportUniformParams,
10631 source_raster_rect: Rect,
10632 ) -> ViewportUniformParams {
10633 ViewportUniformParams {
10634 width: viewport.width,
10635 height: viewport.height,
10636 offset: [
10637 viewport.offset[0] - source_raster_rect.x,
10638 viewport.offset[1] - source_raster_rect.y,
10639 ],
10640 }
10641 }
10642
10643 #[cfg(not(target_arch = "wasm32"))]
10644 fn retained_text_glyph_run_ready(&mut self, cache_key: TextGlyphRunCacheKey) -> bool {
10645 let atlas_generation = self.text_glyph_atlas.generation();
10646 self.text_glyph_gpu_run_cache
10647 .peek(&cache_key)
10648 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
10649 }
10650
10651 #[cfg(not(target_arch = "wasm32"))]
10652 #[allow(clippy::too_many_arguments)]
10653 fn emit_retained_text_glyph_run_if_ready(
10654 &mut self,
10655 cache_key: TextGlyphRunCacheKey,
10656 quads: &[CachedTextGlyphQuad],
10657 clip: Option<Rect>,
10658 viewport: ViewportUniformParams,
10659 source_raster_rect: Rect,
10660 scissor: (u32, u32, u32, u32),
10661 staged_uploads: &mut StagedBufferUploads,
10662 glyph_cmds: &mut Vec<GlyphDrawCmd>,
10663 ) -> bool {
10664 if !should_use_retained_text_glyph_run(quads.len(), clip) {
10665 return false;
10666 }
10667 if !self.retained_text_glyph_run_ready(cache_key)
10668 && !self.ensure_retained_text_glyph_run(cache_key, quads)
10669 {
10670 return false;
10671 }
10672
10673 let uniform_slot = self.stage_retained_glyph_viewport_uniforms(
10674 staged_uploads,
10675 Self::retained_glyph_viewport(viewport, source_raster_rect),
10676 );
10677 glyph_cmds.push(GlyphDrawCmd::retained(cache_key, uniform_slot, scissor));
10678 true
10679 }
10680
10681 #[cfg(not(target_arch = "wasm32"))]
10682 fn ensure_retained_text_glyph_run(
10683 &mut self,
10684 cache_key: TextGlyphRunCacheKey,
10685 quads: &[CachedTextGlyphQuad],
10686 ) -> bool {
10687 let atlas_generation = self.text_glyph_atlas.generation();
10688 if self
10689 .text_glyph_gpu_run_cache
10690 .peek(&cache_key)
10691 .is_some_and(|cached| cached.atlas_generation == atlas_generation)
10692 {
10693 return true;
10694 }
10695
10696 let mut vertices = Vec::with_capacity(quads.len().saturating_mul(4));
10697 let mut indices = Vec::with_capacity(quads.len().saturating_mul(6));
10698 let origin = Rect {
10699 x: 0.0,
10700 y: 0.0,
10701 width: 0.0,
10702 height: 0.0,
10703 };
10704 for quad in quads {
10705 append_cached_text_glyph_quad(origin, quad, &mut vertices, &mut indices);
10706 }
10707 if indices.is_empty() {
10708 return false;
10709 }
10710
10711 let vertex_bytes = bytemuck::cast_slice(&vertices);
10712 let index_bytes = bytemuck::cast_slice(&indices);
10713 let vertex_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10714 label: Some("Retained Text Glyph Vertex Buffer"),
10715 size: vertex_bytes.len() as u64,
10716 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
10717 mapped_at_creation: false,
10718 });
10719 let index_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
10720 label: Some("Retained Text Glyph Index Buffer"),
10721 size: index_bytes.len() as u64,
10722 usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST,
10723 mapped_at_creation: false,
10724 });
10725 let vertex_upload =
10726 self.frame_graph_executor
10727 .upload_buffer(&self.queue, &vertex_buffer, 0, vertex_bytes);
10728 self.frame_stats.record_command_stats(vertex_upload);
10729 let index_upload =
10730 self.frame_graph_executor
10731 .upload_buffer(&self.queue, &index_buffer, 0, index_bytes);
10732 self.frame_stats.record_command_stats(index_upload);
10733
10734 self.text_glyph_gpu_run_cache.put(
10735 cache_key,
10736 CachedGpuTextGlyphRun {
10737 vertex_buffer,
10738 index_buffer,
10739 index_count: indices.len() as u32,
10740 atlas_generation,
10741 },
10742 );
10743 true
10744 }
10745
10746 #[allow(clippy::too_many_arguments)]
10747 fn append_text_glyph_draws<'a, I>(
10748 &mut self,
10749 layer_texts: I,
10750 viewport: ViewportUniformParams,
10751 root_scale: f32,
10752 allow_offscreen_prewarm: bool,
10753 staged_uploads: &mut StagedBufferUploads,
10754 image_vertices: &mut Vec<Vertex>,
10755 image_indices: &mut Vec<u32>,
10756 glyph_cmds: &mut Vec<GlyphDrawCmd>,
10757 ) -> Result<bool, String>
10758 where
10759 I: IntoIterator<Item = &'a TextDraw>,
10760 {
10761 let append_start = Instant::now();
10762 let initial_vertex_len = image_vertices.len();
10763 let initial_index_len = image_indices.len();
10764 let initial_cmd_len = glyph_cmds.len();
10765 let initial_staged_bytes_len = staged_uploads.bytes.len();
10766 let initial_staged_copies_len = staged_uploads.copies.len();
10767 let mut collected_run = std::mem::take(&mut self.scratch_text_glyph_run);
10768 let mut collected_placements = std::mem::take(&mut self.scratch_text_glyph_placements);
10769 let mut generated_quads = std::mem::take(&mut self.scratch_text_glyph_quads);
10770 generated_quads.clear();
10771 let mut visited = 0usize;
10772 let mut emitted_glyphs = 0usize;
10773 let mut prewarmed_glyphs = 0usize;
10774 let mut run_hits = 0usize;
10775 let mut run_misses = 0usize;
10776
10777 for text_draw in layer_texts {
10778 visited = visited.saturating_add(1);
10779 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
10780 self.text_raster_geometry(text_draw, root_scale)
10781 else {
10782 continue;
10783 };
10784 if !static_text_motion {
10785 image_vertices.truncate(initial_vertex_len);
10786 image_indices.truncate(initial_index_len);
10787 glyph_cmds.truncate(initial_cmd_len);
10788 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
10789 self.scratch_text_glyph_run = collected_run;
10790 self.scratch_text_glyph_placements = collected_placements;
10791 self.scratch_text_glyph_quads = generated_quads;
10792 return Ok(false);
10793 }
10794 let is_visible =
10795 text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale);
10796 let draw_action = text_glyph_draw_action(
10797 is_visible,
10798 text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale),
10799 allow_offscreen_prewarm,
10800 );
10801 if draw_action == TextGlyphDrawAction::Skip {
10802 continue;
10803 }
10804
10805 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
10806 let source_draw = raster_source.draw.as_ref();
10807 let source_raster_rect = raster_source.raster_rect;
10808
10809 let run_key = Self::text_glyph_run_cache_key(
10810 source_draw,
10811 source_raster_rect,
10812 text_scale,
10813 static_text_motion,
10814 );
10815 let atlas_generation = self.text_glyph_atlas.generation();
10816 let mut cached_quad_run = None;
10817 let mut miss_collect_ms = None;
10818 let mut miss_cached_glyphs = 0usize;
10819 let mut miss_new_glyphs = 0usize;
10820 let cached_glyph_run = if let Some(cached) = self.text_glyph_run_cache.get(&run_key) {
10821 run_hits = run_hits.saturating_add(1);
10822 if cached.atlas_generation == atlas_generation {
10823 cached_quad_run = cached.quads.as_ref().map(Rc::clone);
10824 }
10825 Some(Rc::clone(&cached.glyphs))
10826 } else {
10827 run_misses = run_misses.saturating_add(1);
10828 collected_run.clear();
10829 let collect_start = Instant::now();
10830 let collect_result = collect_solid_text_atlas_run(
10831 source_draw.text.as_ref(),
10832 source_raster_rect,
10833 &source_draw.text_style,
10834 source_draw.color,
10835 source_draw.font_size,
10836 text_scale,
10837 &self.text_fonts,
10838 &mut self.text_glyph_mask_cache,
10839 &mut collected_run,
10840 );
10841 miss_collect_ms = Some(instant_ms(collect_start, Instant::now()));
10842 if collect_result.is_none() {
10843 if text_atlas_fallback_diag_enabled() {
10844 let preview: String = source_draw.text.text.chars().take(96).collect();
10845 log::warn!(
10846 "[text-atlas-fallback] node={:?} visible={} prewarm={} spans={} links={} text_len={} preview={:?} span_style={:?} paragraph_style={:?}",
10847 source_draw.node_id,
10848 is_visible,
10849 draw_action == TextGlyphDrawAction::PrewarmOffscreen,
10850 source_draw.text.span_styles.len(),
10851 source_draw.text.links.len(),
10852 source_draw.text.text.len(),
10853 preview,
10854 source_draw.text_style.span_style,
10855 source_draw.text_style.paragraph_style,
10856 );
10857 }
10858 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
10859 continue;
10860 }
10861 image_vertices.truncate(initial_vertex_len);
10862 image_indices.truncate(initial_index_len);
10863 glyph_cmds.truncate(initial_cmd_len);
10864 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
10865 self.scratch_text_glyph_run = collected_run;
10866 self.scratch_text_glyph_placements = collected_placements;
10867 self.scratch_text_glyph_quads = generated_quads;
10868 return Ok(false);
10869 }
10870 if text_glyph_run_diag_enabled() {
10871 miss_cached_glyphs = collected_run
10872 .iter()
10873 .filter(|glyph| matches!(glyph, SoftwareGlyphAtlasRunGlyph::Cached(_)))
10874 .count();
10875 miss_new_glyphs = collected_run.len().saturating_sub(miss_cached_glyphs);
10876 }
10877 collected_placements.clear();
10878 collected_placements.extend(
10879 collected_run
10880 .iter()
10881 .map(SoftwareGlyphAtlasRunGlyph::placement),
10882 );
10883 let glyphs: Rc<[SoftwareGlyphAtlasPlacement]> =
10884 Rc::from(collected_placements.clone().into_boxed_slice());
10885 self.text_glyph_run_cache.put(
10886 run_key,
10887 CachedTextGlyphRun {
10888 glyphs,
10889 quads: None,
10890 atlas_generation: 0,
10891 },
10892 );
10893 None
10894 };
10895
10896 if draw_action == TextGlyphDrawAction::PrewarmOffscreen {
10897 let prewarm_quads = if let Some(quad_run) = cached_quad_run {
10898 quad_run
10899 } else {
10900 let prepare_start = Instant::now();
10901 match self.prepare_text_glyph_quads(
10902 run_key,
10903 atlas_generation,
10904 cached_glyph_run.as_deref(),
10905 &collected_run,
10906 &mut generated_quads,
10907 ) {
10908 Ok(quads) => {
10909 if let Some(collect_ms) = miss_collect_ms {
10910 if text_glyph_run_diag_enabled() {
10911 log::warn!(
10912 "[text-glyph-run-diag] visible=false glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
10913 quads.len(),
10914 miss_cached_glyphs,
10915 miss_new_glyphs,
10916 collect_ms,
10917 instant_ms(prepare_start, Instant::now()),
10918 );
10919 }
10920 }
10921 quads
10922 }
10923 Err(_) => continue,
10924 }
10925 };
10926 #[cfg(not(target_arch = "wasm32"))]
10927 if should_use_retained_text_glyph_run(prewarm_quads.len(), source_draw.clip) {
10928 self.ensure_retained_text_glyph_run(run_key, prewarm_quads.as_ref());
10929 }
10930 prewarmed_glyphs = prewarmed_glyphs.saturating_add(prewarm_quads.len());
10931 continue;
10932 }
10933
10934 let draw_rect = Rect {
10935 x: source_raster_rect.x / root_scale,
10936 y: source_raster_rect.y / root_scale,
10937 width: source_raster_rect.width / root_scale,
10938 height: source_raster_rect.height / root_scale,
10939 };
10940 let Some(scissor) = scissor_rect_for_layer(
10941 draw_rect,
10942 source_draw.clip,
10943 root_scale,
10944 viewport.width,
10945 viewport.height,
10946 ) else {
10947 continue;
10948 };
10949
10950 #[cfg(not(target_arch = "wasm32"))]
10951 if let Some(quad_run) = cached_quad_run.as_ref() {
10952 if should_use_retained_text_glyph_run(quad_run.len(), source_draw.clip)
10953 && self.emit_retained_text_glyph_run_if_ready(
10954 run_key,
10955 quad_run.as_ref(),
10956 source_draw.clip,
10957 viewport,
10958 source_raster_rect,
10959 scissor,
10960 staged_uploads,
10961 glyph_cmds,
10962 )
10963 {
10964 emitted_glyphs = emitted_glyphs.saturating_add(quad_run.len());
10965 continue;
10966 }
10967 }
10968
10969 let index_start = image_indices.len() as u32;
10970 if let Some(quad_run) = cached_quad_run {
10971 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
10972 source_raster_rect,
10973 quad_run.as_ref(),
10974 source_draw.clip,
10975 viewport,
10976 root_scale,
10977 image_vertices,
10978 image_indices,
10979 true,
10980 ));
10981 } else {
10982 let prepare_start = Instant::now();
10983 let Ok(quad_run) = self.prepare_text_glyph_quads(
10984 run_key,
10985 atlas_generation,
10986 cached_glyph_run.as_deref(),
10987 &collected_run,
10988 &mut generated_quads,
10989 ) else {
10990 image_vertices.truncate(initial_vertex_len);
10991 image_indices.truncate(initial_index_len);
10992 glyph_cmds.truncate(initial_cmd_len);
10993 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
10994 self.scratch_text_glyph_run = collected_run;
10995 self.scratch_text_glyph_placements = collected_placements;
10996 self.scratch_text_glyph_quads = generated_quads;
10997 return Ok(false);
10998 };
10999 if let Some(collect_ms) = miss_collect_ms {
11000 if text_glyph_run_diag_enabled() {
11001 log::warn!(
11002 "[text-glyph-run-diag] visible=true glyphs={} cached={} new={} collect_ms={:.2} prepare_ms={:.2}",
11003 quad_run.len(),
11004 miss_cached_glyphs,
11005 miss_new_glyphs,
11006 collect_ms,
11007 instant_ms(prepare_start, Instant::now()),
11008 );
11009 }
11010 }
11011 emitted_glyphs = emitted_glyphs.saturating_add(self.append_text_glyph_quad_run(
11012 source_raster_rect,
11013 quad_run.as_ref(),
11014 source_draw.clip,
11015 viewport,
11016 root_scale,
11017 image_vertices,
11018 image_indices,
11019 false,
11020 ));
11021 }
11022 let index_count = image_indices.len() as u32 - index_start;
11023 if index_count > 0 {
11024 glyph_cmds.push(GlyphDrawCmd::shared(index_start, index_count, scissor));
11025 }
11026 }
11027
11028 self.scratch_text_glyph_run = collected_run;
11029 self.scratch_text_glyph_placements = collected_placements;
11030 self.scratch_text_glyph_quads = generated_quads;
11031 let append_end = Instant::now();
11032 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
11033 log::warn!(
11034 "[wgpu-render-stage:text-glyph-atlas] total_ms={total_ms:.2} visited={} cmds={} glyphs={} prewarmed={} run_hits={} run_misses={}",
11035 visited,
11036 glyph_cmds.len().saturating_sub(initial_cmd_len),
11037 emitted_glyphs,
11038 prewarmed_glyphs,
11039 run_hits,
11040 run_misses,
11041 );
11042 }
11043 Ok(true)
11044 }
11045
11046 #[cfg(not(target_arch = "wasm32"))]
11047 fn text_glyph_prewarm_decision(
11048 &self,
11049 text_draw: &TextDraw,
11050 viewport: ViewportUniformParams,
11051 root_scale: f32,
11052 ) -> TextGlyphPrewarmDecision {
11053 let Some((logical_rect, _, clip, _, static_text_motion)) =
11054 self.text_raster_geometry(text_draw, root_scale)
11055 else {
11056 return TextGlyphPrewarmDecision::MissingGeometry;
11057 };
11058 if !static_text_motion {
11059 return TextGlyphPrewarmDecision::DynamicMotion;
11060 }
11061 if text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
11062 return TextGlyphPrewarmDecision::Visible;
11063 }
11064 if text_draw_should_prewarm_in_viewport(logical_rect, clip, viewport, root_scale) {
11065 TextGlyphPrewarmDecision::Candidate
11066 } else {
11067 TextGlyphPrewarmDecision::OutsidePrewarmWindow
11068 }
11069 }
11070
11071 #[cfg(not(target_arch = "wasm32"))]
11072 #[allow(clippy::too_many_arguments)]
11073 fn prewarm_offscreen_text_glyph_draws_in_chunk(
11074 &mut self,
11075 ordered_items: &[(usize, SegmentDrawItem)],
11076 texts: &[TextDraw],
11077 chunk: &SegmentDrawChunkPlan,
11078 viewport: ViewportUniformParams,
11079 root_scale: f32,
11080 staged_uploads: &mut StagedBufferUploads,
11081 image_vertices: &mut Vec<Vertex>,
11082 image_indices: &mut Vec<u32>,
11083 glyph_cmds: &mut Vec<GlyphDrawCmd>,
11084 ) -> Result<(), String> {
11085 let prewarm_start = Instant::now();
11086 let diag_enabled = cranpose_core::env_flag!("CRANPOSE_TEXT_PREWARM_DIAG");
11087 let mut text_items = 0usize;
11088 let mut candidates = 0usize;
11089 let mut missing_geometry = 0usize;
11090 let mut dynamic_motion = 0usize;
11091 let mut visible = 0usize;
11092 let mut outside = 0usize;
11093 let mut already_prepared = 0usize;
11094 let mut admitted_candidates = 0usize;
11095 let mut skipped_unbounded = 0usize;
11096 let mut skipped_budget = 0usize;
11097 let initial_vertex_len = image_vertices.len();
11098 let initial_index_len = image_indices.len();
11099 let initial_cmd_len = glyph_cmds.len();
11100 let initial_staged_bytes_len = staged_uploads.bytes.len();
11101 let initial_staged_copies_len = staged_uploads.copies.len();
11102 'batches: for batch in chunk.iter() {
11103 let SegmentBatchPlan::Text { start, end } = batch else {
11104 continue;
11105 };
11106 for (_, item) in &ordered_items[start..end] {
11107 if offscreen_text_glyph_prewarm_budget_exhausted(prewarm_start, admitted_candidates)
11108 {
11109 skipped_budget = skipped_budget.saturating_add(1);
11110 break 'batches;
11111 }
11112 let SegmentDrawItem::Text(text_index) = item else {
11113 return Err(format!(
11114 "text prewarm batch contains non-text draw item: {item:?}"
11115 ));
11116 };
11117 let Some(text_draw) = texts.get(*text_index) else {
11118 continue;
11119 };
11120 text_items = text_items.saturating_add(1);
11121 match self.text_glyph_prewarm_decision(text_draw, viewport, root_scale) {
11122 TextGlyphPrewarmDecision::Candidate => {}
11123 TextGlyphPrewarmDecision::MissingGeometry => {
11124 missing_geometry = missing_geometry.saturating_add(1);
11125 continue;
11126 }
11127 TextGlyphPrewarmDecision::DynamicMotion => {
11128 dynamic_motion = dynamic_motion.saturating_add(1);
11129 continue;
11130 }
11131 TextGlyphPrewarmDecision::Visible => {
11132 visible = visible.saturating_add(1);
11133 continue;
11134 }
11135 TextGlyphPrewarmDecision::OutsidePrewarmWindow => {
11136 outside = outside.saturating_add(1);
11137 continue;
11138 }
11139 }
11140
11141 candidates = candidates.saturating_add(1);
11142 let Some((_, raster_rect, _, text_scale, static_text_motion)) =
11143 self.text_raster_geometry(text_draw, root_scale)
11144 else {
11145 missing_geometry = missing_geometry.saturating_add(1);
11146 continue;
11147 };
11148 let raster_source = text_glyph_raster_source(text_draw, raster_rect);
11149 let source_draw = raster_source.draw.as_ref();
11150 let run_key = Self::text_glyph_run_cache_key(
11151 source_draw,
11152 raster_source.raster_rect,
11153 text_scale,
11154 static_text_motion,
11155 );
11156 let atlas_generation = self.text_glyph_atlas.generation();
11157 let cached_glyphs = if let Some(cached) = self.text_glyph_run_cache.peek(&run_key) {
11158 if cached.atlas_generation == atlas_generation && cached.quads.is_some() {
11159 already_prepared = already_prepared.saturating_add(1);
11160 continue;
11161 }
11162 Some(cached.glyphs.len())
11163 } else {
11164 None
11165 };
11166 if !offscreen_text_glyph_prewarm_work_is_bounded(
11167 cached_glyphs,
11168 source_draw.text.text.len(),
11169 ) {
11170 skipped_unbounded = skipped_unbounded.saturating_add(1);
11171 continue;
11172 }
11173 admitted_candidates = admitted_candidates.saturating_add(1);
11174 self.append_text_glyph_draws(
11175 std::iter::once(text_draw),
11176 viewport,
11177 root_scale,
11178 true,
11179 staged_uploads,
11180 image_vertices,
11181 image_indices,
11182 glyph_cmds,
11183 )?;
11184 image_vertices.truncate(initial_vertex_len);
11185 image_indices.truncate(initial_index_len);
11186 glyph_cmds.truncate(initial_cmd_len);
11187 staged_uploads.truncate(initial_staged_bytes_len, initial_staged_copies_len);
11188 }
11189 }
11190
11191 if diag_enabled && text_items > 0 {
11192 log::warn!(
11193 "[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}"
11194 );
11195 }
11196 if admitted_candidates > 0 {
11197 if let Some(total_ms) = should_log_wgpu_render_stage(prewarm_start, Instant::now()) {
11198 log::warn!(
11199 "[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}"
11200 );
11201 }
11202 }
11203 Ok(())
11204 }
11205
11206 fn prepare_text_glyph_draw_cmds<'a, I>(
11207 &mut self,
11208 layer_texts: I,
11209 viewport: ViewportUniformParams,
11210 root_scale: f32,
11211 staged_uploads: &mut StagedBufferUploads,
11212 ) -> Result<Option<PreparedGlyphBatch>, String>
11213 where
11214 I: IntoIterator<Item = &'a TextDraw>,
11215 {
11216 #[cfg(target_arch = "wasm32")]
11217 let _ = staged_uploads;
11218
11219 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
11220 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
11221 let mut glyph_cmds = std::mem::take(&mut self.scratch_glyph_cmds);
11222 image_vertices.clear();
11223 image_indices.clear();
11224 glyph_cmds.clear();
11225
11226 if !self.append_text_glyph_draws(
11227 layer_texts,
11228 viewport,
11229 root_scale,
11230 false,
11231 staged_uploads,
11232 &mut image_vertices,
11233 &mut image_indices,
11234 &mut glyph_cmds,
11235 )? {
11236 self.scratch_image_vertices = image_vertices;
11237 self.scratch_image_indices = image_indices;
11238 self.scratch_glyph_cmds = glyph_cmds;
11239 return Ok(None);
11240 }
11241
11242 #[cfg(not(target_arch = "wasm32"))]
11243 if !image_indices.is_empty() {
11244 self.stage_native_image_buffers(
11245 staged_uploads,
11246 viewport,
11247 &image_vertices,
11248 &image_indices,
11249 );
11250 }
11251
11252 #[cfg(target_arch = "wasm32")]
11253 let image_slot = if glyph_cmds.is_empty() {
11254 0
11255 } else {
11256 let slot = self.claim_wasm_image_batch();
11257 {
11258 let buffers = &mut self.wasm_image_batches[slot];
11259 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
11260 }
11261 let buffers = &self.wasm_image_batches[slot];
11262 self.write_wasm_buffer(
11263 &buffers.vertex_buffer,
11264 bytemuck::cast_slice(&image_vertices),
11265 );
11266 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
11267 slot
11268 };
11269
11270 #[cfg(target_arch = "wasm32")]
11271 let uniform_slot = if glyph_cmds.is_empty() {
11272 0
11273 } else {
11274 self.prepare_wasm_viewport_uniforms(viewport)
11275 };
11276
11277 self.scratch_image_vertices = image_vertices;
11278 self.scratch_image_indices = image_indices;
11279 Ok(Some(PreparedGlyphBatch {
11280 cmds: glyph_cmds,
11281 #[cfg(target_arch = "wasm32")]
11282 image_slot,
11283 #[cfg(target_arch = "wasm32")]
11284 uniform_slot,
11285 }))
11286 }
11287
11288 #[allow(clippy::too_many_arguments)]
11289 fn append_image_bitmap_draw_cmd(
11290 &mut self,
11291 image: &ImageBitmap,
11292 rect: Rect,
11293 clip: Option<Rect>,
11294 sampling: ImageSampling,
11295 viewport: ViewportUniformParams,
11296 root_scale: f32,
11297 image_vertices: &mut Vec<Vertex>,
11298 image_indices: &mut Vec<u32>,
11299 image_cmds: &mut Vec<ImageDrawCmd>,
11300 ) -> Result<(), String> {
11301 if rect.width <= 0.0 || rect.height <= 0.0 {
11302 return Ok(());
11303 }
11304
11305 self.ensure_image_cached(image)?;
11306
11307 let (device_quad, scissor_rect) =
11308 if sampling == ImageSampling::Nearest && root_scale.is_finite() && root_scale > 0.0 {
11309 let left_px = (rect.x * root_scale).round();
11310 let top_px = (rect.y * root_scale).round();
11311 let width_px = (rect.width * root_scale).round().max(1.0);
11312 let height_px = (rect.height * root_scale).round().max(1.0);
11313 let snapped_rect = Rect {
11314 x: left_px / root_scale,
11315 y: top_px / root_scale,
11316 width: width_px / root_scale,
11317 height: height_px / root_scale,
11318 };
11319 let right_px = left_px + width_px;
11320 let bottom_px = top_px + height_px;
11321 (
11322 [
11323 [left_px, top_px],
11324 [right_px, top_px],
11325 [left_px, bottom_px],
11326 [right_px, bottom_px],
11327 ],
11328 snapped_rect,
11329 )
11330 } else {
11331 (
11332 rect_to_quad(rect).map(|[x, y]| [x * root_scale, y * root_scale]),
11333 rect,
11334 )
11335 };
11336
11337 let Some(scissor) = scissor_rect_for_layer(
11338 scissor_rect,
11339 clip,
11340 root_scale,
11341 viewport.width,
11342 viewport.height,
11343 ) else {
11344 return Ok(());
11345 };
11346 let Some(uv_rect) = image_uv_rect(image, None) else {
11347 return Ok(());
11348 };
11349
11350 let base_vertex = image_vertices.len() as u32;
11351 let index_start = image_indices.len() as u32;
11352 image_indices.extend_from_slice(&[
11353 base_vertex,
11354 base_vertex + 1,
11355 base_vertex + 2,
11356 base_vertex + 2,
11357 base_vertex + 1,
11358 base_vertex + 3,
11359 ]);
11360 let color = [1.0, 1.0, 1.0, 1.0];
11361 image_vertices.extend_from_slice(&[
11362 Vertex {
11363 position: device_quad[0],
11364 color,
11365 uv: [uv_rect.min[0], uv_rect.min[1]],
11366 uv_bounds: uv_rect.sample_bounds,
11367 },
11368 Vertex {
11369 position: device_quad[1],
11370 color,
11371 uv: [uv_rect.max[0], uv_rect.min[1]],
11372 uv_bounds: uv_rect.sample_bounds,
11373 },
11374 Vertex {
11375 position: device_quad[2],
11376 color,
11377 uv: [uv_rect.min[0], uv_rect.max[1]],
11378 uv_bounds: uv_rect.sample_bounds,
11379 },
11380 Vertex {
11381 position: device_quad[3],
11382 color,
11383 uv: [uv_rect.max[0], uv_rect.max[1]],
11384 uv_bounds: uv_rect.sample_bounds,
11385 },
11386 ]);
11387 image_cmds.push(ImageDrawCmd {
11388 index_start,
11389 scissor,
11390 image_id: image.id(),
11391 sampling,
11392 });
11393 Ok(())
11394 }
11395
11396 #[allow(clippy::too_many_arguments)]
11397 fn append_text_image_draw_cmds<'a, I>(
11398 &mut self,
11399 layer_texts: I,
11400 viewport: ViewportUniformParams,
11401 root_scale: f32,
11402 image_vertices: &mut Vec<Vertex>,
11403 image_indices: &mut Vec<u32>,
11404 image_cmds: &mut Vec<ImageDrawCmd>,
11405 ) -> Result<(), String>
11406 where
11407 I: Iterator<Item = &'a TextDraw>,
11408 {
11409 let append_start = Instant::now();
11410 let initial_len = image_cmds.len();
11411 let mut visited = 0usize;
11412 let mut hit_count = 0usize;
11413 let mut miss_count = 0usize;
11414 for text_draw in layer_texts {
11415 visited = visited.saturating_add(1);
11416 let _ = text_draw.node_id;
11417 let Some((logical_rect, raster_rect, clip, text_scale, static_text_motion)) =
11418 self.text_raster_geometry(text_draw, root_scale)
11419 else {
11420 continue;
11421 };
11422 if !text_draw_is_visible_in_viewport(logical_rect, clip, viewport, root_scale) {
11423 continue;
11424 }
11425
11426 let raster_source = self.text_image_raster_source(
11427 text_draw,
11428 logical_rect,
11429 raster_rect,
11430 clip,
11431 root_scale,
11432 static_text_motion,
11433 );
11434 let source_draw = raster_source.draw.as_ref();
11435 let source_raster_rect = raster_source.raster_rect;
11436
11437 let cache_key = Self::text_image_cache_key(
11438 source_draw,
11439 source_raster_rect,
11440 text_scale,
11441 static_text_motion,
11442 );
11443 let image = if let Some(cached) = self.text_image_cache.get(&cache_key) {
11444 self.frame_stats
11445 .record_text_image_cache_hit(cached.image.width(), cached.image.height());
11446 hit_count = hit_count.saturating_add(1);
11447 cached.image.clone()
11448 } else {
11449 let Some(image) =
11450 self.rasterize_text_draw_to_image(source_draw, source_raster_rect, text_scale)
11451 else {
11452 continue;
11453 };
11454 self.frame_stats
11455 .record_text_image_cache_miss(image.width(), image.height());
11456 miss_count = miss_count.saturating_add(1);
11457 self.text_image_cache.put(
11458 cache_key,
11459 CachedTextImage {
11460 image: image.clone(),
11461 },
11462 );
11463 image
11464 };
11465
11466 let draw_origin = if static_text_motion {
11467 Point::new(
11468 source_raster_rect.x / root_scale,
11469 source_raster_rect.y / root_scale,
11470 )
11471 } else {
11472 Point::new(logical_rect.x, logical_rect.y)
11473 };
11474 let draw_rect = Rect {
11475 x: draw_origin.x,
11476 y: draw_origin.y,
11477 width: image.width() as f32 / root_scale,
11478 height: image.height() as f32 / root_scale,
11479 };
11480 self.append_image_bitmap_draw_cmd(
11481 &image,
11482 draw_rect,
11483 clip,
11484 ImageSampling::Nearest,
11485 viewport,
11486 root_scale,
11487 image_vertices,
11488 image_indices,
11489 image_cmds,
11490 )?;
11491 }
11492 let append_end = Instant::now();
11493 if let Some(total_ms) = should_log_wgpu_render_stage(append_start, append_end) {
11494 log::warn!(
11495 "[wgpu-render-stage:text-images] total_ms={total_ms:.2} visited={} emitted={} hits={} misses={}",
11496 visited,
11497 image_cmds.len().saturating_sub(initial_len),
11498 hit_count,
11499 miss_count,
11500 );
11501 }
11502 Ok(())
11503 }
11504
11505 fn text_image_raster_source<'a>(
11506 &mut self,
11507 text_draw: &'a TextDraw,
11508 logical_rect: Rect,
11509 raster_rect: Rect,
11510 clip: Option<Rect>,
11511 root_scale: f32,
11512 static_text_motion: bool,
11513 ) -> TextRasterSource<'a> {
11514 let Some(clip) = clip else {
11515 return TextRasterSource {
11516 draw: Cow::Borrowed(text_draw),
11517 raster_rect,
11518 };
11519 };
11520 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
11521 return TextRasterSource {
11522 draw: Cow::Borrowed(text_draw),
11523 raster_rect,
11524 };
11525 }
11526
11527 let line_starts = self.text_line_index_cache.line_starts(&text_draw.text);
11528 clipped_text_raster_source_with_line_starts(
11529 text_draw,
11530 logical_rect,
11531 raster_rect,
11532 clip,
11533 root_scale,
11534 line_starts.as_ref(),
11535 )
11536 }
11537
11538 fn prepare_text_image_draw_cmds<'a, I>(
11539 &mut self,
11540 layer_texts: I,
11541 viewport: ViewportUniformParams,
11542 root_scale: f32,
11543 staged_uploads: &mut StagedBufferUploads,
11544 ) -> Result<PreparedImageBatch, String>
11545 where
11546 I: Iterator<Item = &'a TextDraw>,
11547 {
11548 #[cfg(target_arch = "wasm32")]
11549 let _ = staged_uploads;
11550
11551 let mut image_vertices = std::mem::take(&mut self.scratch_image_vertices);
11552 let mut image_indices = std::mem::take(&mut self.scratch_image_indices);
11553 let mut image_cmds = std::mem::take(&mut self.scratch_image_cmds);
11554 image_vertices.clear();
11555 image_indices.clear();
11556 image_cmds.clear();
11557
11558 self.append_text_image_draw_cmds(
11559 layer_texts,
11560 viewport,
11561 root_scale,
11562 &mut image_vertices,
11563 &mut image_indices,
11564 &mut image_cmds,
11565 )?;
11566
11567 #[cfg(not(target_arch = "wasm32"))]
11568 if !image_cmds.is_empty() {
11569 self.stage_native_image_buffers(
11570 staged_uploads,
11571 viewport,
11572 &image_vertices,
11573 &image_indices,
11574 );
11575 }
11576
11577 #[cfg(target_arch = "wasm32")]
11578 let image_slot = if image_cmds.is_empty() {
11579 0
11580 } else {
11581 let slot = self.claim_wasm_image_batch();
11582 {
11583 let buffers = &mut self.wasm_image_batches[slot];
11584 buffers.ensure_capacity(&self.device, image_vertices.len(), image_indices.len());
11585 }
11586 let buffers = &self.wasm_image_batches[slot];
11587 self.write_wasm_buffer(
11588 &buffers.vertex_buffer,
11589 bytemuck::cast_slice(&image_vertices),
11590 );
11591 self.write_wasm_buffer(&buffers.index_buffer, bytemuck::cast_slice(&image_indices));
11592 slot
11593 };
11594
11595 #[cfg(target_arch = "wasm32")]
11596 let uniform_slot = if image_cmds.is_empty() {
11597 0
11598 } else {
11599 self.prepare_wasm_viewport_uniforms(viewport)
11600 };
11601
11602 self.scratch_image_vertices = image_vertices;
11603 self.scratch_image_indices = image_indices;
11604 Ok(PreparedImageBatch {
11605 cmds: image_cmds,
11606 #[cfg(target_arch = "wasm32")]
11607 image_slot,
11608 #[cfg(target_arch = "wasm32")]
11609 uniform_slot,
11610 })
11611 }
11612
11613 fn text_raster_geometry(
11614 &self,
11615 text_draw: &TextDraw,
11616 root_scale: f32,
11617 ) -> Option<(Rect, Rect, Option<Rect>, f32, bool)> {
11618 text_raster_geometry_for_draw(text_draw, root_scale)
11619 }
11620
11621 fn text_image_cache_key(
11622 text_draw: &TextDraw,
11623 raster_rect: Rect,
11624 text_scale: f32,
11625 static_text_motion: bool,
11626 ) -> TextImageCacheKey {
11627 let mut state = default_hash::new();
11628 text_draw.text.render_hash().hash(&mut state);
11629 text_draw.text_style.render_hash().hash(&mut state);
11630 text_draw.color.render_hash().hash(&mut state);
11631 hash_text_raster_geometry_for_cache(raster_rect, static_text_motion, &mut state);
11632 text_draw.font_size.to_bits().hash(&mut state);
11633 text_scale.to_bits().hash(&mut state);
11634 text_draw.layout_options.hash(&mut state);
11635 TextImageCacheKey(state.finish())
11636 }
11637
11638 fn text_glyph_run_cache_key(
11639 text_draw: &TextDraw,
11640 raster_rect: Rect,
11641 text_scale: f32,
11642 static_text_motion: bool,
11643 ) -> TextGlyphRunCacheKey {
11644 TextGlyphRunCacheKey(
11645 Self::text_image_cache_key(text_draw, raster_rect, text_scale, static_text_motion).0,
11646 )
11647 }
11648
11649 fn rasterize_text_draw_to_image(
11650 &mut self,
11651 text_draw: &TextDraw,
11652 raster_rect: Rect,
11653 text_scale: f32,
11654 ) -> Option<ImageBitmap> {
11655 if text_draw.text.span_styles.is_empty() {
11656 let font = self.text_fonts.resolve(&text_draw.text_style)?;
11657 return rasterize_text_to_image_with_glyph_cache(
11658 text_draw.text.text.as_str(),
11659 raster_rect,
11660 &text_draw.text_style,
11661 text_draw.color,
11662 text_draw.font_size,
11663 text_scale,
11664 font,
11665 &mut self.text_glyph_mask_cache,
11666 );
11667 }
11668
11669 if let Some(image) = rasterize_annotated_text_to_image_with_glyph_cache(
11670 text_draw.text.as_ref(),
11671 raster_rect,
11672 &text_draw.text_style,
11673 text_draw.color,
11674 text_draw.font_size,
11675 text_scale,
11676 &self.text_fonts,
11677 &mut self.text_glyph_mask_cache,
11678 ) {
11679 return Some(image);
11680 }
11681
11682 rasterize_spanned_text_to_image(
11683 text_draw,
11684 raster_rect,
11685 text_scale,
11686 &self.text_fonts,
11687 &mut self.text_glyph_mask_cache,
11688 )
11689 }
11690}
11691
11692fn rasterize_spanned_text_to_image(
11693 text_draw: &TextDraw,
11694 raster_rect: Rect,
11695 text_scale: f32,
11696 fonts: &SoftwareTextFontSet,
11697 glyph_cache: &mut SoftwareGlyphRasterCache,
11698) -> Option<ImageBitmap> {
11699 let width = raster_rect.width.ceil().max(1.0) as u32;
11700 let height = raster_rect.height.ceil().max(1.0) as u32;
11701 let mut canvas = vec![0_u8; (width as usize) * (height as usize) * 4];
11702 let boundaries = text_draw.text.span_boundaries();
11703 let base_line_height = text_draw
11704 .text_style
11705 .resolve_line_height(14.0, text_draw.font_size)
11706 .max(1.0);
11707 let mut current_line_height = base_line_height;
11708 let mut cursor_x = raster_rect.x;
11709 let mut cursor_y = raster_rect.y;
11710
11711 for window in boundaries.windows(2) {
11712 let start = window[0];
11713 let end = window[1];
11714 if start == end {
11715 continue;
11716 }
11717
11718 let chunk = &text_draw.text.text[start..end];
11719 let mut merged_span = text_draw.text_style.span_style.clone();
11720 for span in &text_draw.text.span_styles {
11721 if span.range.start <= start && span.range.end >= end {
11722 merged_span = merged_span.merge(&span.item);
11723 }
11724 }
11725
11726 let mut chunk_style = text_draw.text_style.clone();
11727 chunk_style.span_style = merged_span;
11728
11729 for part in chunk.split_inclusive('\n') {
11730 let has_newline = part.ends_with('\n');
11731 let content = if has_newline {
11732 &part[..part.len().saturating_sub(1)]
11733 } else {
11734 part
11735 };
11736
11737 if !content.is_empty() {
11738 let chunk_font_size = chunk_style.resolve_font_size(text_draw.font_size);
11739 let Some(font) = fonts.resolve(&chunk_style) else {
11740 continue;
11741 };
11742 let metrics = measure_text_with_font(content, &chunk_style, chunk_font_size, font);
11743 let segment_rect = Rect {
11744 x: cursor_x,
11745 y: cursor_y,
11746 width: (metrics.width * text_scale).ceil().max(1.0),
11747 height: (metrics.height * text_scale).ceil().max(1.0),
11748 };
11749 if let Some(segment_image) = rasterize_text_to_image_with_glyph_cache(
11750 content,
11751 segment_rect,
11752 &chunk_style,
11753 chunk_style.resolve_text_color(text_draw.color),
11754 chunk_font_size,
11755 text_scale,
11756 font,
11757 glyph_cache,
11758 ) {
11759 composite_text_segment(
11760 &mut canvas,
11761 width,
11762 height,
11763 raster_rect,
11764 segment_rect,
11765 &segment_image,
11766 );
11767 }
11768 cursor_x += metrics.width * text_scale;
11769 current_line_height = current_line_height.max(metrics.line_height.max(1.0));
11770 }
11771
11772 if has_newline {
11773 cursor_x = raster_rect.x;
11774 cursor_y += current_line_height * text_scale;
11775 current_line_height = base_line_height;
11776 }
11777 }
11778 }
11779
11780 ImageBitmap::from_rgba8(width, height, canvas).ok()
11781}
11782
11783struct TextRasterSource<'a> {
11784 draw: Cow<'a, TextDraw>,
11785 raster_rect: Rect,
11786}
11787
11788fn text_glyph_raster_source(text_draw: &TextDraw, raster_rect: Rect) -> TextRasterSource<'_> {
11789 TextRasterSource {
11790 draw: Cow::Borrowed(text_draw),
11791 raster_rect,
11792 }
11793}
11794
11795#[cfg(test)]
11796fn clipped_text_raster_source<'a>(
11797 text_draw: &'a TextDraw,
11798 logical_rect: Rect,
11799 raster_rect: Rect,
11800 clip: Option<Rect>,
11801 root_scale: f32,
11802 static_text_motion: bool,
11803) -> TextRasterSource<'a> {
11804 let Some(clip) = clip else {
11805 return TextRasterSource {
11806 draw: Cow::Borrowed(text_draw),
11807 raster_rect,
11808 };
11809 };
11810 if !static_text_motion || text_draw.text.text.as_str().find('\n').is_none() {
11811 return TextRasterSource {
11812 draw: Cow::Borrowed(text_draw),
11813 raster_rect,
11814 };
11815 }
11816 let line_starts = line_start_offsets(text_draw.text.text.as_str());
11817 clipped_text_raster_source_with_line_starts(
11818 text_draw,
11819 logical_rect,
11820 raster_rect,
11821 clip,
11822 root_scale,
11823 &line_starts,
11824 )
11825}
11826
11827fn clipped_text_raster_source_with_line_starts<'a>(
11828 text_draw: &'a TextDraw,
11829 logical_rect: Rect,
11830 raster_rect: Rect,
11831 clip: Rect,
11832 root_scale: f32,
11833 line_starts: &[usize],
11834) -> TextRasterSource<'a> {
11835 if line_starts.len() < MIN_MULTILINE_TEXT_LINES_FOR_CLIPPED_RASTER {
11836 return TextRasterSource {
11837 draw: Cow::Borrowed(text_draw),
11838 raster_rect,
11839 };
11840 }
11841
11842 let Some(visible_rect) = logical_rect.intersect(clip) else {
11843 return TextRasterSource {
11844 draw: Cow::Borrowed(text_draw),
11845 raster_rect,
11846 };
11847 };
11848
11849 let line_count = line_starts.len().max(1);
11850 let line_height = logical_rect.height / line_count as f32;
11851 if !line_height.is_finite() || line_height <= 0.0 {
11852 return TextRasterSource {
11853 draw: Cow::Borrowed(text_draw),
11854 raster_rect,
11855 };
11856 }
11857
11858 let visible_top = ((visible_rect.y - logical_rect.y) / line_height).floor() as isize;
11859 let visible_bottom =
11860 ((visible_rect.y + visible_rect.height - logical_rect.y) / line_height).ceil() as isize;
11861 let start_line = visible_top.saturating_sub(1).max(0) as usize;
11862 let end_line = (visible_bottom + 1).max(start_line as isize + 1) as usize;
11863 let end_line = end_line.min(line_count);
11864 if start_line == 0 && end_line >= line_count {
11865 return TextRasterSource {
11866 draw: Cow::Borrowed(text_draw),
11867 raster_rect,
11868 };
11869 }
11870
11871 let byte_start = line_starts[start_line];
11872 let byte_end = line_end_offset(text_draw.text.text.as_str(), line_starts, end_line - 1);
11873 if byte_start >= byte_end {
11874 return TextRasterSource {
11875 draw: Cow::Borrowed(text_draw),
11876 raster_rect,
11877 };
11878 }
11879
11880 let slice_y = logical_rect.y + start_line as f32 * line_height;
11881 let slice_height = (end_line - start_line) as f32 * line_height;
11882 let mut slice_raster_rect = Rect {
11883 x: logical_rect.x * root_scale,
11884 y: slice_y * root_scale,
11885 width: logical_rect.width * root_scale,
11886 height: slice_height * root_scale,
11887 };
11888 slice_raster_rect.x = slice_raster_rect.x.round();
11889 slice_raster_rect.y = slice_raster_rect.y.round();
11890 slice_raster_rect.width = slice_raster_rect.width.ceil().max(1.0);
11891 slice_raster_rect.height = slice_raster_rect.height.ceil().max(1.0);
11892
11893 let mut sliced_draw = text_draw.clone();
11894 sliced_draw.rect = Rect {
11895 x: logical_rect.x,
11896 y: slice_y,
11897 width: logical_rect.width,
11898 height: slice_height,
11899 };
11900 sliced_draw.text = Arc::new(text_draw.text.subsequence(byte_start..byte_end));
11901
11902 TextRasterSource {
11903 draw: Cow::Owned(sliced_draw),
11904 raster_rect: slice_raster_rect,
11905 }
11906}
11907
11908fn line_start_offsets(text: &str) -> Vec<usize> {
11909 let mut starts =
11910 Vec::with_capacity(text.as_bytes().iter().filter(|b| **b == b'\n').count() + 1);
11911 starts.push(0);
11912 starts.extend(
11913 text.char_indices()
11914 .filter_map(|(index, ch)| (ch == '\n').then_some(index + ch.len_utf8())),
11915 );
11916 starts
11917}
11918
11919fn line_end_offset(text: &str, line_starts: &[usize], line: usize) -> usize {
11920 line_starts.get(line + 1).copied().unwrap_or(text.len())
11921}
11922
11923fn composite_text_segment(
11924 canvas: &mut [u8],
11925 canvas_width: u32,
11926 canvas_height: u32,
11927 canvas_rect: Rect,
11928 segment_rect: Rect,
11929 segment_image: &ImageBitmap,
11930) {
11931 let offset_x = (segment_rect.x - canvas_rect.x).round() as i32;
11932 let offset_y = (segment_rect.y - canvas_rect.y).round() as i32;
11933 let src = segment_image.pixels();
11934 for sy in 0..segment_image.height() as i32 {
11935 let dy = offset_y + sy;
11936 if dy < 0 || dy >= canvas_height as i32 {
11937 continue;
11938 }
11939 for sx in 0..segment_image.width() as i32 {
11940 let dx = offset_x + sx;
11941 if dx < 0 || dx >= canvas_width as i32 {
11942 continue;
11943 }
11944 let src_index = ((sy as u32 * segment_image.width() + sx as u32) * 4) as usize;
11945 let dst_index = ((dy as u32 * canvas_width + dx as u32) * 4) as usize;
11946 blend_rgba_pixel(
11947 &mut canvas[dst_index..dst_index + 4],
11948 &src[src_index..src_index + 4],
11949 );
11950 }
11951 }
11952}
11953
11954fn blend_rgba_pixel(dst: &mut [u8], src: &[u8]) {
11955 let src_alpha = src[3] as f32 / 255.0;
11956 if src_alpha <= 0.0 {
11957 return;
11958 }
11959 let dst_alpha = dst[3] as f32 / 255.0;
11960 let out_alpha = src_alpha + dst_alpha * (1.0 - src_alpha);
11961 if out_alpha <= f32::EPSILON {
11962 dst.copy_from_slice(&[0, 0, 0, 0]);
11963 return;
11964 }
11965
11966 for channel in 0..3 {
11967 let src_channel = src[channel] as f32 / 255.0;
11968 let dst_channel = dst[channel] as f32 / 255.0;
11969 let src_premult = src_channel * src_alpha;
11970 let dst_premult = dst_channel * dst_alpha;
11971 dst[channel] =
11972 (((src_premult + dst_premult * (1.0 - src_alpha)) / out_alpha).clamp(0.0, 1.0) * 255.0)
11973 .round() as u8;
11974 }
11975 dst[3] = (out_alpha.clamp(0.0, 1.0) * 255.0).round() as u8;
11976}
11977
11978fn align_to(value: u32, alignment: u32) -> u32 {
11979 debug_assert!(alignment > 0);
11980 value.div_ceil(alignment) * alignment
11981}
11982
11983#[cfg(not(target_arch = "wasm32"))]
11984fn align_usize_to(value: usize, alignment: usize) -> usize {
11985 debug_assert!(alignment > 0);
11986 value.div_ceil(alignment) * alignment
11987}
11988
11989impl GpuRenderer {
11990 fn convert_surface_pixels_to_rgba(&self, pixels: &mut [u8]) -> Result<(), String> {
11991 match self.surface_format {
11992 wgpu::TextureFormat::Rgba8Unorm | wgpu::TextureFormat::Rgba8UnormSrgb => Ok(()),
11993 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb => {
11994 for pixel in pixels.chunks_exact_mut(4) {
11995 pixel.swap(0, 2);
11996 }
11997 Ok(())
11998 }
11999 format => Err(format!(
12000 "Screenshot readback unsupported for texture format: {format:?}"
12001 )),
12002 }
12003 }
12004}
12005
12006fn is_in_effect_range(z_index: usize, effect_z_ranges: &[Range<usize>]) -> bool {
12007 effect_z_ranges.iter().any(|range| range.contains(&z_index))
12008}
12009
12010#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12011enum SegmentDrawItem {
12012 Shape(usize),
12013 Image(usize),
12014 Text(usize),
12015 Shadow(usize),
12016 Composite(usize),
12017 ShaderComposite(usize),
12018 Retained(usize),
12019}
12020
12021#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12022enum SegmentBatchPlan {
12023 Shape {
12024 start: usize,
12025 end: usize,
12026 blend_mode: BlendMode,
12027 },
12028 Image {
12029 start: usize,
12030 end: usize,
12031 blend_mode: BlendMode,
12032 },
12033 Text {
12034 start: usize,
12035 end: usize,
12036 },
12037 Composite {
12038 start: usize,
12039 end: usize,
12040 },
12041 ShaderComposite {
12042 start: usize,
12043 end: usize,
12044 },
12045 Retained {
12048 start: usize,
12049 end: usize,
12050 },
12051}
12052
12053#[derive(Clone, Debug, Default, PartialEq, Eq)]
12054struct SegmentDrawChunkPlan {
12055 batches: Vec<SegmentBatchPlan>,
12056}
12057
12058struct SegmentRenderOutcome {
12059 rendered_any: bool,
12060 pass_count: u32,
12061}
12062
12063struct SegmentCommandEncodeOutcome {
12064 first_batch: bool,
12065}
12066
12067#[cfg(not(target_arch = "wasm32"))]
12068#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12069enum TextGlyphPrewarmDecision {
12070 Candidate,
12071 MissingGeometry,
12072 DynamicMotion,
12073 Visible,
12074 OutsidePrewarmWindow,
12075}
12076
12077#[cfg(not(target_arch = "wasm32"))]
12078#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12079struct NativeSegmentFusionBudget {
12080 shape_count: usize,
12081 gradient_stop_count: usize,
12082}
12083
12084#[cfg(not(target_arch = "wasm32"))]
12085#[derive(Clone, Debug, PartialEq, Eq)]
12086struct NativeSegmentFusionPartition {
12087 chunk: SegmentDrawChunkPlan,
12088 budget: NativeSegmentFusionBudget,
12089}
12090
12091#[cfg(not(target_arch = "wasm32"))]
12092#[derive(Clone, Debug, PartialEq, Eq)]
12093enum FusedSegmentBatch {
12094 Shape {
12095 batch: PreparedShapeBatch,
12096 blend_mode: BlendMode,
12097 },
12098 Image {
12099 cmd_range: Range<usize>,
12100 blend_mode: BlendMode,
12101 },
12102 Text {
12103 image_cmd_range: Range<usize>,
12104 glyph_cmd_range: Range<usize>,
12105 },
12106 Composite {
12107 draw_range: Range<usize>,
12108 },
12109 ShaderComposite {
12110 draw_range: Range<usize>,
12111 },
12112 Retained {
12113 item_range: Range<usize>,
12114 },
12115}
12116
12117struct ShadowSourceRenderOutcome {
12118 rendered_any: bool,
12119 pass_count: u32,
12120}
12121
12122impl SegmentDrawChunkPlan {
12123 fn is_empty(&self) -> bool {
12124 self.batches.is_empty()
12125 }
12126
12127 fn push(&mut self, batch: SegmentBatchPlan) {
12128 self.batches.push(batch);
12129 }
12130
12131 fn iter(&self) -> impl Iterator<Item = SegmentBatchPlan> + '_ {
12132 self.batches.iter().copied()
12133 }
12134}
12135
12136#[derive(Clone, Debug, PartialEq, Eq)]
12137enum SegmentRenderCommand {
12138 DrawChunk(SegmentDrawChunkPlan),
12139 Shadow(usize),
12140}
12141
12142struct SegmentCommandIter<'a> {
12143 ordered_items: &'a [(usize, SegmentDrawItem)],
12144 shapes: &'a [DrawShape],
12145 images: &'a [ImageDraw],
12146 cursor: usize,
12147 batch_limits: ShapeBatchLimits,
12148}
12149
12150impl<'a> SegmentCommandIter<'a> {
12151 fn new(
12152 ordered_items: &'a [(usize, SegmentDrawItem)],
12153 shapes: &'a [DrawShape],
12154 images: &'a [ImageDraw],
12155 batch_limits: ShapeBatchLimits,
12156 ) -> Self {
12157 Self {
12158 ordered_items,
12159 shapes,
12160 images,
12161 cursor: 0,
12162 batch_limits,
12163 }
12164 }
12165}
12166
12167impl Iterator for SegmentCommandIter<'_> {
12168 type Item = SegmentRenderCommand;
12169
12170 fn next(&mut self) -> Option<Self::Item> {
12171 if self.cursor >= self.ordered_items.len() {
12172 return None;
12173 }
12174
12175 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
12176 self.cursor += 1;
12177 return Some(SegmentRenderCommand::Shadow(index));
12178 }
12179
12180 let mut chunk = SegmentDrawChunkPlan::default();
12181 while self.cursor < self.ordered_items.len() {
12182 if let SegmentDrawItem::Shadow(index) = self.ordered_items[self.cursor].1 {
12183 if chunk.is_empty() {
12184 self.cursor += 1;
12185 return Some(SegmentRenderCommand::Shadow(index));
12186 }
12187 break;
12188 }
12189
12190 let Some((batch, next_cursor)) = segment_batch_plan_at_cursor(
12191 self.ordered_items,
12192 self.shapes,
12193 self.images,
12194 self.cursor,
12195 self.batch_limits,
12196 ) else {
12197 break;
12198 };
12199 chunk.push(batch);
12200 self.cursor = next_cursor;
12201 }
12202
12203 Some(SegmentRenderCommand::DrawChunk(chunk))
12204 }
12205}
12206
12207#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12208struct PreparedShapeBatch {
12209 vertex_start: u32,
12212 vertex_count: u32,
12213 has_gradient: bool,
12216 #[cfg(target_arch = "wasm32")]
12217 shape_slot: usize,
12218 #[cfg(target_arch = "wasm32")]
12219 uniform_slot: usize,
12220}
12221
12222struct PreparedImageBatch {
12223 cmds: Vec<ImageDrawCmd>,
12224 #[cfg(target_arch = "wasm32")]
12225 image_slot: usize,
12226 #[cfg(target_arch = "wasm32")]
12227 uniform_slot: usize,
12228}
12229
12230impl PreparedImageBatch {
12231 fn is_empty(&self) -> bool {
12232 self.cmds.is_empty()
12233 }
12234
12235 fn into_cmds(self) -> Vec<ImageDrawCmd> {
12236 self.cmds
12237 }
12238}
12239
12240struct PreparedGlyphBatch {
12241 cmds: Vec<GlyphDrawCmd>,
12242 #[cfg(target_arch = "wasm32")]
12243 image_slot: usize,
12244 #[cfg(target_arch = "wasm32")]
12245 uniform_slot: usize,
12246}
12247
12248impl PreparedGlyphBatch {
12249 fn is_empty(&self) -> bool {
12250 self.cmds.is_empty()
12251 }
12252
12253 fn into_cmds(self) -> Vec<GlyphDrawCmd> {
12254 self.cmds
12255 }
12256}
12257
12258#[cfg(not(target_arch = "wasm32"))]
12259fn gradient_stop_count_for_shape(shape: &DrawShape) -> usize {
12260 match &shape.brush {
12261 Brush::Solid(_) => 0,
12262 Brush::LinearGradient { colors, .. }
12263 | Brush::RadialGradient { colors, .. }
12264 | Brush::SweepGradient { colors, .. } => colors.len(),
12265 }
12266}
12267
12268#[cfg(not(target_arch = "wasm32"))]
12269fn native_segment_fusion_budget(
12270 ordered_items: &[(usize, SegmentDrawItem)],
12271 shapes: &[DrawShape],
12272 chunk: &SegmentDrawChunkPlan,
12273 batch_limits: ShapeBatchLimits,
12274) -> Result<Option<NativeSegmentFusionBudget>, String> {
12275 let mut shape_count = 0usize;
12276 let mut gradient_stop_count = 0usize;
12277
12278 for batch in chunk.iter() {
12279 let SegmentBatchPlan::Shape { start, end, .. } = batch else {
12280 continue;
12281 };
12282 for (_, item) in &ordered_items[start..end] {
12283 let SegmentDrawItem::Shape(shape_index) = item else {
12284 return Err(format!(
12285 "shape batch contains non-shape draw item: {item:?}"
12286 ));
12287 };
12288 let shape = &shapes[*shape_index];
12289 shape_count = shape_count.saturating_add(1);
12290 gradient_stop_count =
12291 gradient_stop_count.saturating_add(gradient_stop_count_for_shape(shape));
12292 }
12293 }
12294
12295 if shape_count > batch_limits.max_shapes_per_batch
12296 || gradient_stop_count > batch_limits.max_gradient_stops
12297 {
12298 return Ok(None);
12299 }
12300
12301 Ok(Some(NativeSegmentFusionBudget {
12302 shape_count,
12303 gradient_stop_count,
12304 }))
12305}
12306
12307#[cfg(not(target_arch = "wasm32"))]
12308fn push_native_segment_fusion_partition(
12309 partitions: &mut Vec<NativeSegmentFusionPartition>,
12310 current: &mut SegmentDrawChunkPlan,
12311 current_budget: &mut NativeSegmentFusionBudget,
12312) {
12313 if current.is_empty() {
12314 return;
12315 }
12316
12317 partitions.push(NativeSegmentFusionPartition {
12318 chunk: std::mem::take(current),
12319 budget: *current_budget,
12320 });
12321 *current_budget = NativeSegmentFusionBudget {
12322 shape_count: 0,
12323 gradient_stop_count: 0,
12324 };
12325}
12326
12327#[cfg(not(target_arch = "wasm32"))]
12328fn native_segment_fusion_partitions(
12329 ordered_items: &[(usize, SegmentDrawItem)],
12330 shapes: &[DrawShape],
12331 chunk: &SegmentDrawChunkPlan,
12332 batch_limits: ShapeBatchLimits,
12333) -> Result<Option<Vec<NativeSegmentFusionPartition>>, String> {
12334 if let Some(budget) = native_segment_fusion_budget(ordered_items, shapes, chunk, batch_limits)?
12335 {
12336 return Ok(Some(vec![NativeSegmentFusionPartition {
12337 chunk: chunk.clone(),
12338 budget,
12339 }]));
12340 }
12341
12342 let mut partitions = Vec::new();
12343 let mut current = SegmentDrawChunkPlan::default();
12344 let mut current_budget = NativeSegmentFusionBudget {
12345 shape_count: 0,
12346 gradient_stop_count: 0,
12347 };
12348
12349 for batch in chunk.iter() {
12350 let SegmentBatchPlan::Shape {
12351 start,
12352 end,
12353 blend_mode,
12354 } = batch
12355 else {
12356 current.push(batch);
12357 continue;
12358 };
12359
12360 let mut run_start = start;
12361 for (item_cursor, (_, item)) in ordered_items.iter().enumerate().take(end).skip(start) {
12362 let SegmentDrawItem::Shape(shape_index) = *item else {
12363 return Err(format!(
12364 "shape batch contains non-shape draw item: {:?}",
12365 item
12366 ));
12367 };
12368 let gradient_stop_count = gradient_stop_count_for_shape(&shapes[shape_index]);
12369 if gradient_stop_count > batch_limits.max_gradient_stops {
12370 return Ok(None);
12371 }
12372
12373 let fits_shape_count =
12374 current_budget.shape_count.saturating_add(1) <= batch_limits.max_shapes_per_batch;
12375 let fits_gradient_count = current_budget
12376 .gradient_stop_count
12377 .saturating_add(gradient_stop_count)
12378 <= batch_limits.max_gradient_stops;
12379 if !fits_shape_count || !fits_gradient_count {
12380 if run_start < item_cursor {
12381 current.push(SegmentBatchPlan::Shape {
12382 start: run_start,
12383 end: item_cursor,
12384 blend_mode,
12385 });
12386 }
12387 push_native_segment_fusion_partition(
12388 &mut partitions,
12389 &mut current,
12390 &mut current_budget,
12391 );
12392 run_start = item_cursor;
12393 }
12394
12395 current_budget.shape_count = current_budget.shape_count.saturating_add(1);
12396 current_budget.gradient_stop_count = current_budget
12397 .gradient_stop_count
12398 .saturating_add(gradient_stop_count);
12399 }
12400
12401 if run_start < end {
12402 current.push(SegmentBatchPlan::Shape {
12403 start: run_start,
12404 end,
12405 blend_mode,
12406 });
12407 }
12408 }
12409
12410 push_native_segment_fusion_partition(&mut partitions, &mut current, &mut current_budget);
12411 Ok(Some(partitions))
12412}
12413
12414fn segment_batch_plan_at_cursor(
12415 ordered_items: &[(usize, SegmentDrawItem)],
12416 shapes: &[DrawShape],
12417 images: &[ImageDraw],
12418 start: usize,
12419 batch_limits: ShapeBatchLimits,
12420) -> Option<(SegmentBatchPlan, usize)> {
12421 match ordered_items[start].1 {
12422 SegmentDrawItem::Shape(index) => {
12423 let blend_mode = supported_blend_mode(shapes[index].blend_mode);
12424 let mut end = start + 1;
12425 let shape_limit = (start + batch_limits.max_shapes_per_batch).min(ordered_items.len());
12426 while end < shape_limit {
12427 match ordered_items[end].1 {
12428 SegmentDrawItem::Shape(next_index)
12429 if supported_blend_mode(shapes[next_index].blend_mode) == blend_mode =>
12430 {
12431 end += 1;
12432 }
12433 _ => break,
12434 }
12435 }
12436 Some((
12437 SegmentBatchPlan::Shape {
12438 start,
12439 end,
12440 blend_mode,
12441 },
12442 end,
12443 ))
12444 }
12445 SegmentDrawItem::Image(index) => {
12446 let blend_mode = supported_blend_mode(images[index].blend_mode);
12447 let mut end = start + 1;
12448 while end < ordered_items.len() {
12449 match ordered_items[end].1 {
12450 SegmentDrawItem::Image(next_index)
12451 if supported_blend_mode(images[next_index].blend_mode) == blend_mode =>
12452 {
12453 end += 1;
12454 }
12455 _ => break,
12456 }
12457 }
12458 Some((
12459 SegmentBatchPlan::Image {
12460 start,
12461 end,
12462 blend_mode,
12463 },
12464 end,
12465 ))
12466 }
12467 SegmentDrawItem::Text(_) => {
12468 let mut end = start + 1;
12469 while end < ordered_items.len() {
12470 if matches!(ordered_items[end].1, SegmentDrawItem::Text(_)) {
12471 end += 1;
12472 } else {
12473 break;
12474 }
12475 }
12476 Some((SegmentBatchPlan::Text { start, end }, end))
12477 }
12478 SegmentDrawItem::Composite(_) => {
12479 let mut end = start + 1;
12480 while end < ordered_items.len() {
12481 if matches!(ordered_items[end].1, SegmentDrawItem::Composite(_)) {
12482 end += 1;
12483 } else {
12484 break;
12485 }
12486 }
12487 Some((SegmentBatchPlan::Composite { start, end }, end))
12488 }
12489 SegmentDrawItem::ShaderComposite(_) => {
12490 let mut end = start + 1;
12491 while end < ordered_items.len() {
12492 if matches!(ordered_items[end].1, SegmentDrawItem::ShaderComposite(_)) {
12493 end += 1;
12494 } else {
12495 break;
12496 }
12497 }
12498 Some((SegmentBatchPlan::ShaderComposite { start, end }, end))
12499 }
12500 SegmentDrawItem::Retained(_) => {
12501 let mut end = start + 1;
12502 while end < ordered_items.len() {
12503 if matches!(ordered_items[end].1, SegmentDrawItem::Retained(_)) {
12504 end += 1;
12505 } else {
12506 break;
12507 }
12508 }
12509 Some((SegmentBatchPlan::Retained { start, end }, end))
12510 }
12511 SegmentDrawItem::Shadow(_) => None,
12512 }
12513}
12514
12515#[allow(clippy::too_many_arguments)]
12516fn collect_non_effect_segment_items(
12517 shapes: &[DrawShape],
12518 _images: &[ImageDraw],
12519 _texts: &[TextDraw],
12520 _shadow_draws: &[ShadowDraw],
12521 draw_ops: &[DrawOp],
12522 z_start: usize,
12523 z_end: usize,
12524 effect_z_ranges: &[Range<usize>],
12525 width: u32,
12526 height: u32,
12527 root_scale: f32,
12528 scratch: &mut Vec<(usize, SegmentDrawItem)>,
12529) {
12530 scratch.clear();
12531 let viewport = ViewportUniformParams {
12532 width,
12533 height,
12534 offset: [0.0, 0.0],
12535 };
12536
12537 scratch.extend(draw_ops.iter().filter_map(|op| {
12538 if op.z_index < z_start
12539 || op.z_index >= z_end
12540 || is_in_effect_range(op.z_index, effect_z_ranges)
12541 {
12542 return None;
12543 }
12544 let item = match op.kind {
12545 DrawOpKind::Shape(index) => {
12546 let shape = shapes.get(index)?;
12547 if !shape_draw_is_visible_in_viewport(shape, viewport, root_scale) {
12548 return None;
12549 }
12550 SegmentDrawItem::Shape(index)
12551 }
12552 DrawOpKind::Image(index) => SegmentDrawItem::Image(index),
12553 DrawOpKind::Text(index) => SegmentDrawItem::Text(index),
12554 DrawOpKind::Shadow(index) => SegmentDrawItem::Shadow(index),
12555 DrawOpKind::Retained(index) => SegmentDrawItem::Retained(index),
12556 };
12557 Some((op.z_index, item))
12558 }));
12559}
12560
12561fn retain_renderable_shadow_items(
12562 ordered_items: &mut Vec<(usize, SegmentDrawItem)>,
12563 shadow_draws: &[ShadowDraw],
12564 width: u32,
12565 height: u32,
12566 root_scale: f32,
12567 max_texture_dim: u32,
12568) -> usize {
12569 let original_len = ordered_items.len();
12570 ordered_items.retain(|(_, item)| match item {
12571 SegmentDrawItem::Shadow(index) => shadow_draws.get(*index).is_some_and(|shadow| {
12572 shadow_draw_may_render(shadow, width, height, root_scale, max_texture_dim)
12573 }),
12574 _ => true,
12575 });
12576 original_len.saturating_sub(ordered_items.len())
12577}
12578
12579#[cfg(not(target_arch = "wasm32"))]
12580#[derive(Clone, Copy)]
12581struct SegmentDiagCounts {
12582 raw_shadow_items: usize,
12583 culled_shadow_items: usize,
12584 cached_shadow_composites: usize,
12585 composite_items: usize,
12586 shader_composite_items: usize,
12587}
12588
12589#[cfg(not(target_arch = "wasm32"))]
12590fn maybe_print_segment_diag(
12591 z_range: Range<usize>,
12592 ordered_items: &[(usize, SegmentDrawItem)],
12593 shapes: &[DrawShape],
12594 images: &[ImageDraw],
12595 counts: SegmentDiagCounts,
12596 batch_limits: ShapeBatchLimits,
12597) {
12598 if !cranpose_core::env_flag!("CRANPOSE_SEGMENT_DIAG") {
12599 return;
12600 }
12601 let line = SEGMENT_DIAG_LINES.fetch_add(1, Ordering::Relaxed);
12602 if line >= 64 {
12603 return;
12604 }
12605
12606 let remaining_shadow_items = ordered_items
12607 .iter()
12608 .filter(|(_, item)| matches!(item, SegmentDrawItem::Shadow(_)))
12609 .count();
12610 let commands: Vec<_> =
12611 SegmentCommandIter::new(ordered_items, shapes, images, batch_limits).collect();
12612 let draw_chunks = commands
12613 .iter()
12614 .filter(|command| matches!(command, SegmentRenderCommand::DrawChunk(_)))
12615 .count();
12616 let shadow_commands = commands
12617 .iter()
12618 .filter(|command| matches!(command, SegmentRenderCommand::Shadow(_)))
12619 .count();
12620 let mut native_partitions = 0usize;
12621 let mut native_unfused_chunks = 0usize;
12622 for command in &commands {
12623 let SegmentRenderCommand::DrawChunk(chunk) = command else {
12624 continue;
12625 };
12626 match native_segment_fusion_partitions(ordered_items, shapes, chunk, batch_limits) {
12627 Ok(Some(partitions)) => native_partitions += partitions.len(),
12628 Ok(None) | Err(_) => native_unfused_chunks += 1,
12629 }
12630 }
12631
12632 eprintln!(
12633 "[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={}",
12634 z_range.start,
12635 z_range.end,
12636 ordered_items.len(),
12637 counts.raw_shadow_items,
12638 counts.culled_shadow_items,
12639 counts.cached_shadow_composites,
12640 remaining_shadow_items,
12641 counts.composite_items,
12642 counts.shader_composite_items,
12643 draw_chunks,
12644 shadow_commands,
12645 native_partitions,
12646 native_unfused_chunks,
12647 );
12648}
12649
12650pub(crate) fn has_backdrop_layer_in_range(
12651 backdrop_layers: &[BackdropLayer],
12652 z_start: usize,
12653 z_end: usize,
12654) -> bool {
12655 backdrop_layers
12656 .iter()
12657 .any(|layer| layer.z_index >= z_start && layer.z_index < z_end)
12658}
12659
12660pub(crate) fn scissor_rect_for_rect(
12661 rect: Rect,
12662 root_scale: f32,
12663 width: u32,
12664 height: u32,
12665) -> Option<(u32, u32, u32, u32)> {
12666 let mut left = canonicalize_device_coordinate(rect.x * root_scale);
12667 let mut top = canonicalize_device_coordinate(rect.y * root_scale);
12668 let mut right = canonicalize_device_coordinate((rect.x + rect.width) * root_scale);
12669 let mut bottom = canonicalize_device_coordinate((rect.y + rect.height) * root_scale);
12670
12671 left = left.max(0.0).min(width as f32).floor();
12672 top = top.max(0.0).min(height as f32).floor();
12673 right = right.max(0.0).min(width as f32).ceil();
12674 bottom = bottom.max(0.0).min(height as f32).ceil();
12675
12676 if right <= left || bottom <= top {
12677 return None;
12678 }
12679
12680 Some((
12681 left as u32,
12682 top as u32,
12683 (right - left) as u32,
12684 (bottom - top) as u32,
12685 ))
12686}
12687
12688fn scissor_rect_for_layer(
12689 rect: Rect,
12690 clip: Option<Rect>,
12691 root_scale: f32,
12692 width: u32,
12693 height: u32,
12694) -> Option<(u32, u32, u32, u32)> {
12695 let clipped_rect = match clip {
12696 Some(clip_rect) => rect.intersect(clip_rect)?,
12697 None => rect,
12698 };
12699
12700 scissor_rect_for_rect(clipped_rect, root_scale, width, height)
12701}
12702
12703fn tint_for_image(
12704 color_filter: Option<ColorFilter>,
12705 alpha: f32,
12706) -> ([f32; 4], Option<ColorFilter>) {
12707 let alpha = alpha.clamp(0.0, 1.0);
12708 match color_filter {
12709 Some(filter) if filter.supports_gpu_vertex_modulation() => {
12710 let Some(tint) = filter.gpu_vertex_tint() else {
12711 return ([1.0, 1.0, 1.0, alpha], Some(filter));
12712 };
12713 (
12714 [
12715 tint[0].clamp(0.0, 1.0),
12716 tint[1].clamp(0.0, 1.0),
12717 tint[2].clamp(0.0, 1.0),
12718 (tint[3] * alpha).clamp(0.0, 1.0),
12719 ],
12720 None,
12721 )
12722 }
12723 Some(filter) => ([1.0, 1.0, 1.0, alpha], Some(filter)),
12724 None => ([1.0, 1.0, 1.0, alpha], None),
12725 }
12726}
12727
12728fn image_uv_rect(image: &ImageBitmap, src_rect: Option<Rect>) -> Option<ImageUvRect> {
12729 let Some(src) = src_rect else {
12730 return Some(ImageUvRect {
12731 min: [0.0, 0.0],
12732 max: [1.0, 1.0],
12733 sample_bounds: [0.0, 0.0, 1.0, 1.0],
12734 });
12735 };
12736
12737 let (u_min, u_max, u_bound_min, u_bound_max) =
12738 source_axis_uv(src.x, src.width, image.width() as f32)?;
12739 let (v_min, v_max, v_bound_min, v_bound_max) =
12740 source_axis_uv(src.y, src.height, image.height() as f32)?;
12741
12742 Some(ImageUvRect {
12743 min: [u_min, v_min],
12744 max: [u_max, v_max],
12745 sample_bounds: [u_bound_min, v_bound_min, u_bound_max, v_bound_max],
12746 })
12747}
12748
12749fn glyph_atlas_uv_rect(entry: GlyphAtlasEntry, atlas_size: u32) -> ImageUvRect {
12754 let atlas_width = atlas_size as f32;
12755 let atlas_height = atlas_size as f32;
12756 let min = [entry.x as f32 / atlas_width, entry.y as f32 / atlas_height];
12757 let max = [
12758 (entry.x + entry.width) as f32 / atlas_width,
12759 (entry.y + entry.height) as f32 / atlas_height,
12760 ];
12761 let center_min = [
12762 (entry.x as f32 + 0.5) / atlas_width,
12763 (entry.y as f32 + 0.5) / atlas_height,
12764 ];
12765 let center_max = [
12766 (entry.x as f32 + entry.width as f32 - 0.5).max(entry.x as f32 + 0.5) / atlas_width,
12767 (entry.y as f32 + entry.height as f32 - 0.5).max(entry.y as f32 + 0.5) / atlas_height,
12768 ];
12769 ImageUvRect {
12770 min,
12771 max,
12772 sample_bounds: [center_min[0], center_min[1], center_max[0], center_max[1]],
12773 }
12774}
12775
12776fn snap_nearest_image_to_device_pixels(image: &mut ImageDraw, root_scale: f32) {
12777 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
12778 return;
12779 }
12780
12781 let Some(rect) = axis_aligned_quad_rect(image.quad) else {
12782 return;
12783 };
12784
12785 let left_px = (rect.x * root_scale).round();
12786 let top_px = (rect.y * root_scale).round();
12787 let width_px = (rect.width * root_scale).round().max(1.0);
12788 let height_px = (rect.height * root_scale).round().max(1.0);
12789 let snapped = Rect {
12790 x: left_px / root_scale,
12791 y: top_px / root_scale,
12792 width: width_px / root_scale,
12793 height: height_px / root_scale,
12794 };
12795
12796 image.rect = snapped;
12797 image.local_rect = Rect {
12798 x: image.local_rect.x + snapped.x - rect.x,
12799 y: image.local_rect.y + snapped.y - rect.y,
12800 width: snapped.width,
12801 height: snapped.height,
12802 };
12803 image.quad = crate::rect_to_quad(snapped);
12804}
12805
12806fn nearest_image_device_quad(image: &ImageDraw, root_scale: f32) -> Option<[[f32; 2]; 4]> {
12807 if image.sampling != ImageSampling::Nearest || !root_scale.is_finite() || root_scale <= 0.0 {
12808 return None;
12809 }
12810
12811 let rect = axis_aligned_quad_rect(image.quad)?;
12812 let left_px = (rect.x * root_scale).round();
12813 let top_px = (rect.y * root_scale).round();
12814 let width_px = (rect.width * root_scale).round().max(1.0);
12815 let height_px = (rect.height * root_scale).round().max(1.0);
12816 let right_px = left_px + width_px;
12817 let bottom_px = top_px + height_px;
12818 Some([
12819 [left_px, top_px],
12820 [right_px, top_px],
12821 [left_px, bottom_px],
12822 [right_px, bottom_px],
12823 ])
12824}
12825
12826fn source_axis_uv(start: f32, extent: f32, image_extent: f32) -> Option<(f32, f32, f32, f32)> {
12827 if !start.is_finite()
12828 || !extent.is_finite()
12829 || !image_extent.is_finite()
12830 || extent == 0.0
12831 || image_extent <= 0.0
12832 {
12833 return None;
12834 }
12835
12836 let end = start + extent;
12837 let edge_min = start.min(end).clamp(0.0, image_extent);
12838 let edge_max = start.max(end).clamp(0.0, image_extent);
12839 if edge_max <= edge_min {
12840 return None;
12841 }
12842
12843 let center_min = edge_min + 0.5;
12844 let center_max = edge_max - 0.5;
12845 let (bound_min, bound_max) = if center_min <= center_max {
12846 (center_min, center_max)
12847 } else {
12848 let center = (edge_min + edge_max) * 0.5;
12849 (center, center)
12850 };
12851
12852 Some((
12853 edge_min / image_extent,
12854 edge_max / image_extent,
12855 bound_min / image_extent,
12856 bound_max / image_extent,
12857 ))
12858}
12859
12860fn apply_filter_to_bitmap(image: &ImageBitmap, filter: ColorFilter) -> Result<ImageBitmap, String> {
12861 let mut filtered = Vec::with_capacity(image.pixels().len());
12862 for pixel in image.pixels().chunks_exact(4) {
12863 let rgba = [
12864 pixel[0] as f32 / 255.0,
12865 pixel[1] as f32 / 255.0,
12866 pixel[2] as f32 / 255.0,
12867 pixel[3] as f32 / 255.0,
12868 ];
12869 let out = filter.apply_rgba(rgba);
12870 filtered.push((out[0].clamp(0.0, 1.0) * 255.0).round() as u8);
12871 filtered.push((out[1].clamp(0.0, 1.0) * 255.0).round() as u8);
12872 filtered.push((out[2].clamp(0.0, 1.0) * 255.0).round() as u8);
12873 filtered.push((out[3].clamp(0.0, 1.0) * 255.0).round() as u8);
12874 }
12875 ImageBitmap::from_rgba8(image.width(), image.height(), filtered)
12876 .map_err(|error| format!("failed to build filtered bitmap: {error}"))
12877}
12878
12879fn scissor_rect_for_image(
12880 image: &ImageDraw,
12881 root_scale: f32,
12882 width: u32,
12883 height: u32,
12884) -> Option<(u32, u32, u32, u32)> {
12885 scissor_rect_for_layer(image.rect, image.clip, root_scale, width, height)
12886}
12887
12888fn inner_shadow_composite_mask(
12889 shadow: &ShadowDraw,
12890 root_scale: f32,
12891) -> Option<RoundedCompositeMask> {
12892 if !shadow
12893 .shapes
12894 .iter()
12895 .any(|(_, mode)| *mode == BlendMode::DstOut)
12896 {
12897 return None;
12898 }
12899 let (fill, _) = shadow.shapes.first()?;
12900 let rect = fill.local_rect;
12901 if rect.width <= 0.0 || rect.height <= 0.0 {
12902 return None;
12903 }
12904
12905 let radii = fill.shape.map_or([0.0; 4], |rounded| {
12906 let resolved = rounded.resolve(rect.width, rect.height);
12907 [
12908 resolved.top_left * root_scale,
12909 resolved.top_right * root_scale,
12910 resolved.bottom_left * root_scale,
12911 resolved.bottom_right * root_scale,
12912 ]
12913 });
12914
12915 Some(RoundedCompositeMask {
12916 rect: [
12917 rect.x * root_scale,
12918 rect.y * root_scale,
12919 rect.width * root_scale,
12920 rect.height * root_scale,
12921 ],
12922 radii,
12923 })
12924}
12925
12926#[cfg(test)]
12927mod tests {
12928 use super::*;
12929 use crate::normalized_scene::visible_draw_rect;
12930 use cranpose_foundation::lazy::{remember_lazy_list_state, LazyListScope, LazyListState};
12931 use cranpose_render_common::graph::{DrawPrimitiveNode, IsolationReasons, TextPrimitiveNode};
12932 use cranpose_render_common::raster_cache::LayerRasterCacheHashes;
12933 use cranpose_render_common::scene_builder::build_graph_from_applier;
12934 use cranpose_ui::text::{
12935 AnnotatedString, BaselineShift, RangeStyle, Shadow, SpanStyle, TextDecoration,
12936 TextDrawStyle, TextGeometricTransform, TextMotion, TextUnit,
12937 };
12938 use cranpose_ui::{
12939 LayoutEngine, LazyColumn, LazyColumnSpec, Modifier, Size, Text, TextLayoutOptions,
12940 TextStyle,
12941 };
12942 use cranpose_ui_graphics::{
12943 Brush, Color, CornerRadii, DrawPrimitive, Rect, RenderEffect, RoundedCornerShape,
12944 RuntimeShader,
12945 };
12946
12947 fn chunk(batches: &[SegmentBatchPlan]) -> SegmentDrawChunkPlan {
12948 let mut chunk = SegmentDrawChunkPlan::default();
12949 for batch in batches {
12950 chunk.push(*batch);
12951 }
12952 chunk
12953 }
12954
12955 fn with_test_app_context<R>(block: impl FnOnce() -> R) -> R {
12956 let app_context = cranpose_ui::AppContext::new();
12957 app_context.enter(block)
12958 }
12959
12960 fn assert_snap_anchor_close(actual: Option<SnapAnchor>, expected_origin: Point, message: &str) {
12961 let Some(actual) = actual else {
12962 panic!("{message}: missing snap anchor");
12963 };
12964 let expected = SnapAnchor::rigid(expected_origin);
12965 assert_eq!(
12966 actual.device_pixel_step, expected.device_pixel_step,
12967 "{message}: device pixel step changed"
12968 );
12969 assert!(
12970 (actual.origin.x - expected.origin.x).abs() <= 1e-4
12971 && (actual.origin.y - expected.origin.y).abs() <= 1e-4,
12972 "{message}: expected origin {:?}, got {:?}",
12973 expected.origin,
12974 actual.origin
12975 );
12976 }
12977
12978 fn effect_layer(z_start: usize, z_end: usize) -> EffectLayer {
12979 EffectLayer {
12980 rect: Rect {
12981 x: 0.0,
12982 y: 0.0,
12983 width: 10.0,
12984 height: 10.0,
12985 },
12986 clip: None,
12987 snap_anchor: None,
12988 effect: Some(RenderEffect::blur(4.0)),
12989 blend_mode: BlendMode::SrcOver,
12990 composite_alpha: 1.0,
12991 z_start,
12992 z_end,
12993 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
12994 }
12995 }
12996
12997 #[test]
12998 fn direct_shader_composite_accepts_box4_when_viewport_preserves_source_pixels() {
12999 assert_eq!(
13000 direct_shader_composite_viewport(
13001 1.0,
13002 BlendMode::SrcOver,
13003 Some((12.0, 18.0, 64.0, 32.0)),
13004 CompositeSampleMode::Box4,
13005 (64, 32),
13006 ),
13007 Some((12.0, 18.0, 64.0, 32.0))
13008 );
13009 }
13010
13011 #[test]
13012 fn direct_shader_composite_rejects_box4_when_viewport_resamples_source() {
13013 assert_eq!(
13014 direct_shader_composite_viewport(
13015 1.0,
13016 BlendMode::SrcOver,
13017 Some((12.0, 18.0, 64.5, 32.0)),
13018 CompositeSampleMode::Box4,
13019 (64, 32),
13020 ),
13021 None
13022 );
13023 assert_eq!(
13024 direct_shader_composite_viewport(
13025 1.0,
13026 BlendMode::SrcOver,
13027 Some((12.25, 18.0, 64.0, 32.0)),
13028 CompositeSampleMode::Box4,
13029 (64, 32),
13030 ),
13031 None
13032 );
13033 }
13034
13035 fn test_text_draw(rect: Rect, text_motion: TextMotion) -> TextDraw {
13036 let mut text_style = TextStyle::default();
13037 text_style.paragraph_style.text_motion = Some(text_motion);
13038 TextDraw {
13039 node_id: 42,
13040 rect,
13041 snap_anchor: None,
13042 translated_content_context: false,
13043 text: Arc::new(AnnotatedString::new("stable markdown row".to_string()).render_string()),
13044 color: Color::WHITE,
13045 text_style,
13046 font_size: 14.0,
13047 scale: 1.0,
13048 layout_options: TextLayoutOptions::default(),
13049 z_index: 0,
13050 clip: None,
13051 }
13052 }
13053
13054 #[test]
13055 fn static_text_image_cache_key_ignores_absolute_scroll_position() {
13056 let base = test_text_draw(
13057 Rect {
13058 x: 12.25,
13059 y: 40.75,
13060 width: 220.0,
13061 height: 24.0,
13062 },
13063 TextMotion::Static,
13064 );
13065 let scrolled = test_text_draw(
13066 Rect {
13067 x: 12.75,
13068 y: -318.5,
13069 width: 220.0,
13070 height: 24.0,
13071 },
13072 TextMotion::Static,
13073 );
13074
13075 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, true);
13076 let scrolled_key = GpuRenderer::text_image_cache_key(&scrolled, scrolled.rect, 1.0, true);
13077
13078 assert_eq!(
13079 base_key, scrolled_key,
13080 "scrolling static text must reuse the same raster cache entry"
13081 );
13082 }
13083
13084 #[test]
13085 fn static_text_glyph_run_cache_key_ignores_absolute_scroll_position() {
13086 let base = test_text_draw(
13087 Rect {
13088 x: 12.25,
13089 y: 40.75,
13090 width: 220.0,
13091 height: 24.0,
13092 },
13093 TextMotion::Static,
13094 );
13095 let scrolled = test_text_draw(
13096 Rect {
13097 x: 12.75,
13098 y: -318.5,
13099 width: 220.0,
13100 height: 24.0,
13101 },
13102 TextMotion::Static,
13103 );
13104
13105 let base_key = GpuRenderer::text_glyph_run_cache_key(&base, base.rect, 1.0, true);
13106 let scrolled_key =
13107 GpuRenderer::text_glyph_run_cache_key(&scrolled, scrolled.rect, 1.0, true);
13108
13109 assert_eq!(
13110 base_key, scrolled_key,
13111 "scrolling static text must reuse the same retained glyph run"
13112 );
13113 }
13114
13115 #[test]
13116 fn static_multiline_text_glyph_source_keeps_full_text_when_image_source_slices() {
13117 let rect = Rect {
13118 x: 8.0,
13119 y: 100.0,
13120 width: 240.0,
13121 height: 1_000.0,
13122 };
13123 let mut draw = test_text_draw(rect, TextMotion::Static);
13124 let lines = (0..100)
13125 .map(|line| format!("line-{line:03}"))
13126 .collect::<Vec<_>>()
13127 .join("\n");
13128 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
13129
13130 let raster_rect = Rect {
13131 x: 16.0,
13132 y: 200.0,
13133 width: 480.0,
13134 height: 2_000.0,
13135 };
13136 let clipped = clipped_text_raster_source(
13137 &draw,
13138 rect,
13139 raster_rect,
13140 Some(Rect {
13141 x: 0.0,
13142 y: 610.0,
13143 width: 800.0,
13144 height: 40.0,
13145 }),
13146 2.0,
13147 true,
13148 );
13149 let glyph = text_glyph_raster_source(&draw, raster_rect);
13150
13151 assert!(
13152 matches!(clipped.draw, Cow::Owned(_)),
13153 "the image source should still slice large clipped multiline text"
13154 );
13155 assert!(
13156 matches!(glyph.draw, Cow::Borrowed(_)),
13157 "the glyph source must keep a stable full-text run key while scrolling"
13158 );
13159
13160 let clipped_key = GpuRenderer::text_glyph_run_cache_key(
13161 clipped.draw.as_ref(),
13162 clipped.raster_rect,
13163 2.0,
13164 true,
13165 );
13166 let glyph_key = GpuRenderer::text_glyph_run_cache_key(
13167 glyph.draw.as_ref(),
13168 glyph.raster_rect,
13169 2.0,
13170 true,
13171 );
13172
13173 assert_ne!(
13174 clipped_key, glyph_key,
13175 "image slicing must not force glyph rendering onto per-scroll line-window cache keys"
13176 );
13177 }
13178
13179 #[cfg(not(target_arch = "wasm32"))]
13180 #[test]
13181 fn retained_glyph_viewport_offsets_relative_vertices_by_source_origin() {
13182 let viewport = ViewportUniformParams {
13183 width: 800,
13184 height: 600,
13185 offset: [10.0, 20.0],
13186 };
13187 let source = Rect {
13188 x: 40.0,
13189 y: 90.0,
13190 width: 120.0,
13191 height: 48.0,
13192 };
13193
13194 let retained = GpuRenderer::retained_glyph_viewport(viewport, source);
13195
13196 assert_eq!(retained.width, viewport.width);
13197 assert_eq!(retained.height, viewport.height);
13198 assert_eq!(retained.offset, [-30.0, -70.0]);
13199 }
13200
13201 #[cfg(not(target_arch = "wasm32"))]
13202 #[test]
13203 fn tiny_text_glyph_runs_stay_in_shared_uploads() {
13204 assert!(
13205 !should_use_retained_text_glyph_run(8, None),
13206 "tiny labels must stay in the shared fused batch"
13207 );
13208 }
13209
13210 #[cfg(not(target_arch = "wasm32"))]
13211 #[test]
13212 fn line_sized_text_glyph_runs_stay_in_shared_uploads() {
13213 assert!(
13214 !should_use_retained_text_glyph_run(64, None),
13215 "Markdown scroll frames contain many line-sized text runs; retaining each one creates per-run buffer binds instead of one shared glyph batch"
13216 );
13217 }
13218
13219 #[cfg(not(target_arch = "wasm32"))]
13220 #[test]
13221 fn large_clipped_text_glyph_runs_stay_in_shared_uploads() {
13222 assert!(
13223 !should_use_retained_text_glyph_run(
13224 MIN_RETAINED_TEXT_GLYPH_QUADS.saturating_mul(2),
13225 Some(Rect {
13226 x: 0.0,
13227 y: 0.0,
13228 width: 200.0,
13229 height: 100.0,
13230 }),
13231 ),
13232 "clipped lazy-list text must not draw a full retained run outside the viewport"
13233 );
13234 }
13235
13236 #[test]
13237 fn normal_text_glyph_draw_skips_offscreen_prewarm_candidates() {
13238 assert_eq!(
13239 text_glyph_draw_action(false, true, false),
13240 TextGlyphDrawAction::Skip,
13241 "normal draw traversal must not prepare offscreen text"
13242 );
13243 }
13244
13245 #[test]
13246 fn bounded_text_glyph_prewarm_admits_offscreen_candidates() {
13247 assert_eq!(
13248 text_glyph_draw_action(false, true, true),
13249 TextGlyphDrawAction::PrewarmOffscreen,
13250 "only the bounded prewarm path may prepare offscreen text"
13251 );
13252 }
13253
13254 #[test]
13255 fn visible_text_glyph_draws_are_always_admitted() {
13256 assert_eq!(
13257 text_glyph_draw_action(true, false, false),
13258 TextGlyphDrawAction::DrawVisible
13259 );
13260 assert_eq!(
13261 text_glyph_draw_action(true, true, true),
13262 TextGlyphDrawAction::DrawVisible
13263 );
13264 }
13265
13266 #[cfg(not(target_arch = "wasm32"))]
13267 #[test]
13268 fn offscreen_text_prewarm_skips_large_uncached_text_runs() {
13269 assert!(
13270 !offscreen_text_glyph_prewarm_work_is_bounded(
13271 None,
13272 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS + 1,
13273 ),
13274 "offscreen prewarm must not collect large uncached text runs in an input frame"
13275 );
13276 }
13277
13278 #[cfg(not(target_arch = "wasm32"))]
13279 #[test]
13280 fn offscreen_text_prewarm_admits_small_uncached_text_runs() {
13281 assert!(
13282 offscreen_text_glyph_prewarm_work_is_bounded(
13283 None,
13284 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_UNCACHED_CHARS,
13285 ),
13286 "small labels can be warmed without risking a frame-budget spike"
13287 );
13288 }
13289
13290 #[cfg(not(target_arch = "wasm32"))]
13291 #[test]
13292 fn offscreen_text_prewarm_skips_large_cached_runs_without_quads() {
13293 assert!(
13294 !offscreen_text_glyph_prewarm_work_is_bounded(
13295 Some(MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CACHED_GLYPHS + 1),
13296 0,
13297 ),
13298 "cached glyph placements can still be too large to prepare during input frames"
13299 );
13300 }
13301
13302 #[cfg(not(target_arch = "wasm32"))]
13303 #[test]
13304 fn offscreen_text_prewarm_stops_after_candidate_budget() {
13305 assert!(
13306 offscreen_text_glyph_prewarm_budget_exhausted(
13307 Instant::now(),
13308 MAX_OFFSCREEN_TEXT_GLYPH_PREWARM_CANDIDATES,
13309 ),
13310 "prewarm must be bounded by candidate count even when each candidate is cheap"
13311 );
13312 }
13313
13314 #[test]
13315 fn clipped_cached_glyph_quads_are_filtered_to_viewport() {
13316 fn quad(y: i32) -> CachedTextGlyphQuad {
13317 CachedTextGlyphQuad {
13318 x: 8,
13319 y,
13320 width: 20,
13321 height: 10,
13322 color: (1.0, 1.0, 1.0, 1.0),
13323 uv: ImageUvRect {
13324 min: [0.0, 0.0],
13325 max: [1.0, 1.0],
13326 sample_bounds: [0.0, 0.0, 1.0, 1.0],
13327 },
13328 }
13329 }
13330
13331 let source = Rect {
13332 x: 0.0,
13333 y: 0.0,
13334 width: 320.0,
13335 height: 400.0,
13336 };
13337 let clip = Some(Rect {
13338 x: 0.0,
13339 y: 0.0,
13340 width: 320.0,
13341 height: 80.0,
13342 });
13343 let viewport = ViewportUniformParams {
13344 width: 320,
13345 height: 80,
13346 offset: [0.0, 0.0],
13347 };
13348
13349 assert!(cached_text_glyph_quad_is_visible_in_viewport(
13350 source,
13351 &quad(40),
13352 clip,
13353 viewport,
13354 1.0,
13355 ));
13356 assert!(
13357 !cached_text_glyph_quad_is_visible_in_viewport(source, &quad(140), clip, viewport, 1.0,),
13358 "glyphs outside the effective clip should not enter the frame command stream"
13359 );
13360 }
13361
13362 #[test]
13363 fn small_scene_range_cache_miss_observes_first_render() {
13364 let key = LayerRasterCacheKey::scene_range(
13365 0xCACE,
13366 Rect {
13367 x: 0.0,
13368 y: 0.0,
13369 width: 120.0,
13370 height: 80.0,
13371 },
13372 (120, 80),
13373 ScaleBucket::from_scale(1.0),
13374 );
13375
13376 assert!(
13377 !first_cache_miss_admission(&key),
13378 "a small scene-range miss should render directly first instead of materializing a tiny one-frame retained target"
13379 );
13380 assert!(
13381 repeated_cache_miss_admission(&key),
13382 "a repeated small scene-range miss is stable enough to materialize into the retained cache"
13383 );
13384 }
13385
13386 #[test]
13387 fn large_scene_range_cache_miss_requires_repeated_stable_key() {
13388 let key = LayerRasterCacheKey::scene_range(
13389 0xCACE,
13390 Rect {
13391 x: 0.0,
13392 y: 0.0,
13393 width: 1200.0,
13394 height: 900.0,
13395 },
13396 (1200, 900),
13397 ScaleBucket::from_scale(1.0),
13398 );
13399
13400 assert!(
13401 !first_cache_miss_admission(&key),
13402 "a large first scene-range miss should render directly instead of materializing a multi-MB one-frame cache entry"
13403 );
13404 assert!(
13405 repeated_cache_miss_admission(&key),
13406 "a repeated scene-range miss is stable enough to materialize into the retained cache"
13407 );
13408 }
13409
13410 #[test]
13411 fn renderer_warmup_frame_is_requested_for_cache_miss_stats_only() {
13412 let stats = gpu_stats::FrameStats::default();
13413 let mut snapshot = stats.snapshot();
13414 assert!(
13415 !frame_stats_need_warmup_frame(&snapshot),
13416 "a clean frame must not keep a static scene redrawing"
13417 );
13418
13419 snapshot.layer_cache_misses = 1;
13420 assert!(frame_stats_need_warmup_frame(&snapshot));
13421 snapshot.layer_cache_misses = 0;
13422
13423 snapshot.shadow_shape_cache_misses = 1;
13424 assert!(frame_stats_need_warmup_frame(&snapshot));
13425 snapshot.shadow_shape_cache_misses = 0;
13426
13427 snapshot.text_image_cache_misses = 1;
13428 assert!(frame_stats_need_warmup_frame(&snapshot));
13429 snapshot.text_image_cache_misses = 0;
13430
13431 snapshot.text_glyph_atlas_misses = 1;
13432 assert!(frame_stats_need_warmup_frame(&snapshot));
13433 }
13434
13435 #[test]
13436 fn renderer_warmup_budget_is_consumed_by_a_repeated_cache_miss() {
13437 let stats = gpu_stats::FrameStats::default();
13438 let mut snapshot = stats.snapshot();
13439 snapshot.layer_cache_misses = 1;
13440 let mut pending_frames = 0;
13441
13442 update_frame_warmup_budget(&mut pending_frames, &snapshot);
13443 assert_eq!(pending_frames, CACHE_MISS_WARMUP_FRAMES);
13444
13445 update_frame_warmup_budget(&mut pending_frames, &snapshot);
13446 assert_eq!(
13447 pending_frames, 0,
13448 "a cache miss during the warmup frame must not replenish its budget"
13449 );
13450 }
13451
13452 #[test]
13453 fn non_scene_layer_surface_cache_miss_admits_first_render() {
13454 let key = LayerRasterCacheKey::new(
13455 Some(77),
13456 0xC0FFEE,
13457 0,
13458 Rect {
13459 x: 0.0,
13460 y: 0.0,
13461 width: 120.0,
13462 height: 80.0,
13463 },
13464 (120, 80),
13465 ScaleBucket::from_scale(1.0),
13466 );
13467
13468 assert!(
13469 first_cache_miss_admission(&key),
13470 "ordinary retained layer surfaces should still cache on first miss"
13471 );
13472 }
13473
13474 #[test]
13475 fn text_image_cache_key_is_content_addressed_not_node_addressed() {
13476 let first = test_text_draw(
13477 Rect {
13478 x: 12.25,
13479 y: 40.75,
13480 width: 220.0,
13481 height: 24.0,
13482 },
13483 TextMotion::Static,
13484 );
13485 let mut second = first.clone();
13486 second.node_id = first.node_id + 1;
13487
13488 let first_key = GpuRenderer::text_image_cache_key(&first, first.rect, 1.0, true);
13489 let second_key = GpuRenderer::text_image_cache_key(&second, second.rect, 1.0, true);
13490
13491 assert_eq!(
13492 first_key, second_key,
13493 "text raster cache keys must be based on rendered pixels, not node identity"
13494 );
13495 }
13496
13497 #[test]
13498 fn animated_text_image_cache_key_keeps_fractional_phase_only() {
13499 let base = test_text_draw(
13500 Rect {
13501 x: 12.25,
13502 y: 40.75,
13503 width: 220.0,
13504 height: 24.0,
13505 },
13506 TextMotion::Animated,
13507 );
13508 let integer_translated = test_text_draw(
13509 Rect {
13510 x: 44.25,
13511 y: 88.75,
13512 width: 220.0,
13513 height: 24.0,
13514 },
13515 TextMotion::Animated,
13516 );
13517 let phase_shifted = test_text_draw(
13518 Rect {
13519 x: 44.5,
13520 y: 88.75,
13521 width: 220.0,
13522 height: 24.0,
13523 },
13524 TextMotion::Animated,
13525 );
13526
13527 let base_key = GpuRenderer::text_image_cache_key(&base, base.rect, 1.0, false);
13528 let translated_key = GpuRenderer::text_image_cache_key(
13529 &integer_translated,
13530 integer_translated.rect,
13531 1.0,
13532 false,
13533 );
13534 let phase_shifted_key =
13535 GpuRenderer::text_image_cache_key(&phase_shifted, phase_shifted.rect, 1.0, false);
13536
13537 assert_eq!(
13538 base_key, translated_key,
13539 "integer translation should not invalidate animated text raster cache entries"
13540 );
13541 assert_ne!(
13542 base_key, phase_shifted_key,
13543 "fractional phase affects animated text rasterization and must stay in the key"
13544 );
13545 }
13546
13547 #[test]
13548 fn animated_translated_text_raster_geometry_applies_snap_anchor() {
13549 let mut base = test_text_draw(
13550 Rect {
13551 x: 14.25,
13552 y: 16.50,
13553 width: 220.0,
13554 height: 24.0,
13555 },
13556 TextMotion::Animated,
13557 );
13558 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.50)));
13559
13560 let mut scrolled = test_text_draw(
13561 Rect {
13562 x: 14.25,
13563 y: 15.80,
13564 width: 220.0,
13565 height: 24.0,
13566 },
13567 TextMotion::Animated,
13568 );
13569 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 15.80)));
13570
13571 let (base_logical, base_raster, _, _, base_static) =
13572 text_raster_geometry_for_draw(&base, 1.0).expect("base text geometry");
13573 let (scrolled_logical, scrolled_raster, _, _, scrolled_static) =
13574 text_raster_geometry_for_draw(&scrolled, 1.0).expect("scrolled text geometry");
13575
13576 assert!(!base_static);
13577 assert!(!scrolled_static);
13578 assert!((base_logical.x - 14.0).abs() < f32::EPSILON);
13579 assert!((base_logical.y - 17.0).abs() < f32::EPSILON);
13580 assert!((scrolled_logical.x - 14.0).abs() < f32::EPSILON);
13581 assert!((scrolled_logical.y - 16.0).abs() < f32::EPSILON);
13582 assert_eq!(base_raster.x.fract(), 0.0);
13583 assert_eq!(base_raster.y.fract(), 0.0);
13584 assert_eq!(scrolled_raster.x.fract(), 0.0);
13585 assert_eq!(scrolled_raster.y.fract(), 0.0);
13586
13587 let base_key = GpuRenderer::text_image_cache_key(&base, base_raster, 1.0, false);
13588 let scrolled_key =
13589 GpuRenderer::text_image_cache_key(&scrolled, scrolled_raster, 1.0, false);
13590 assert_eq!(
13591 base_key, scrolled_key,
13592 "translated animated text should keep a stable raster phase while scrolling"
13593 );
13594 }
13595
13596 #[test]
13597 fn translated_static_text_moves_one_device_pixel_at_half_pixel_phase() {
13598 let root_scale = 1.25;
13599 let mut base = test_text_draw(
13600 Rect {
13601 x: 14.0,
13602 y: 276.0,
13603 width: 220.0,
13604 height: 24.0,
13605 },
13606 TextMotion::Static,
13607 );
13608 base.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.600_006)));
13609
13610 let mut scrolled = test_text_draw(
13611 Rect {
13612 x: 14.0,
13613 y: 275.2,
13614 width: 220.0,
13615 height: 24.0,
13616 },
13617 TextMotion::Static,
13618 );
13619 scrolled.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 126.799_99)));
13620
13621 let (_, base_raster, _, _, _) =
13622 text_raster_geometry_for_draw(&base, root_scale).expect("base text geometry");
13623 let (_, scrolled_raster, _, _, _) =
13624 text_raster_geometry_for_draw(&scrolled, root_scale).expect("scrolled text geometry");
13625
13626 assert_eq!(
13627 base_raster.y - scrolled_raster.y,
13628 1.0,
13629 "one physical pixel of rigid scrolling must move static text by one raster pixel"
13630 );
13631 }
13632
13633 #[test]
13634 fn translated_text_snap_does_not_move_its_fixed_ancestor_clip() {
13635 let root_scale = 1.25;
13636 let fixed_clip = Rect {
13637 x: 8.0,
13638 y: 20.0,
13639 width: 300.0,
13640 height: 680.0,
13641 };
13642 let mut draw = test_text_draw(
13643 Rect {
13644 x: 14.0,
13645 y: 276.0,
13646 width: 220.0,
13647 height: 24.0,
13648 },
13649 TextMotion::Static,
13650 );
13651 draw.snap_anchor = Some(SnapAnchor::rigid(Point::new(0.0, 127.4)));
13652 draw.clip = Some(fixed_clip);
13653
13654 let (_, _, clip, _, _) =
13655 text_raster_geometry_for_draw(&draw, root_scale).expect("clipped text geometry");
13656
13657 assert_eq!(
13658 clip,
13659 Some(fixed_clip),
13660 "content pixel snapping must not translate a fixed ancestor clip"
13661 );
13662 }
13663
13664 #[test]
13665 fn clipped_static_multiline_text_raster_source_limits_visible_line_window() {
13666 let rect = Rect {
13667 x: 8.0,
13668 y: 100.0,
13669 width: 240.0,
13670 height: 1_000.0,
13671 };
13672 let mut draw = test_text_draw(rect, TextMotion::Static);
13673 let lines = (0..100)
13674 .map(|line| format!("line-{line:03}"))
13675 .collect::<Vec<_>>()
13676 .join("\n");
13677 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
13678
13679 let raster_rect = Rect {
13680 x: 16.0,
13681 y: 200.0,
13682 width: 480.0,
13683 height: 2_000.0,
13684 };
13685 let source = clipped_text_raster_source(
13686 &draw,
13687 rect,
13688 raster_rect,
13689 Some(Rect {
13690 x: 0.0,
13691 y: 610.0,
13692 width: 800.0,
13693 height: 40.0,
13694 }),
13695 2.0,
13696 true,
13697 );
13698
13699 let Cow::Owned(sliced_draw) = source.draw else {
13700 panic!("clipped static multiline text should rasterize only the visible line window");
13701 };
13702 let sliced_text = sliced_draw.text.text.as_str();
13703 assert!(sliced_text.contains("line-050"));
13704 assert!(sliced_text.contains("line-055"));
13705 assert!(!sliced_text.contains("line-000"));
13706 assert!(!sliced_text.contains("line-099"));
13707 assert_eq!(source.raster_rect.x, raster_rect.x);
13708 assert!(source.raster_rect.y > raster_rect.y);
13709 assert!(source.raster_rect.height < raster_rect.height);
13710 }
13711
13712 #[test]
13713 fn clipped_static_multiline_text_raster_source_slices_short_multiline_text() {
13714 let rect = Rect {
13715 x: 8.0,
13716 y: 100.0,
13717 width: 240.0,
13718 height: 320.0,
13719 };
13720 let mut draw = test_text_draw(rect, TextMotion::Static);
13721 let lines = (0..24)
13722 .map(|line| format!("code-line-{line:02}"))
13723 .collect::<Vec<_>>()
13724 .join("\n");
13725 draw.text = Arc::new(AnnotatedString::from(lines).render_string());
13726
13727 let raster_rect = Rect {
13728 x: 16.0,
13729 y: 200.0,
13730 width: 480.0,
13731 height: 640.0,
13732 };
13733 let source = clipped_text_raster_source(
13734 &draw,
13735 rect,
13736 raster_rect,
13737 Some(Rect {
13738 x: 0.0,
13739 y: 190.0,
13740 width: 800.0,
13741 height: 120.0,
13742 }),
13743 2.0,
13744 true,
13745 );
13746
13747 let Cow::Owned(sliced_draw) = source.draw else {
13748 panic!("clipped multiline text should rasterize only the visible line window");
13749 };
13750 assert!(sliced_draw.text.text.as_str().contains("code-line-06"));
13751 assert!(!sliced_draw.text.text.as_str().contains("code-line-00"));
13752 assert!(!sliced_draw.text.text.as_str().contains("code-line-23"));
13753 assert_eq!(source.raster_rect.x, raster_rect.x);
13754 assert!(source.raster_rect.y > raster_rect.y);
13755 assert!(source.raster_rect.height < raster_rect.height);
13756 }
13757
13758 #[test]
13759 fn text_line_index_cache_reuses_retained_index_for_same_text_instance() {
13760 let mut cache = TextLineIndexCache::new(4);
13761 let text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
13762
13763 let first = cache.line_starts(&text);
13764 let second = cache.line_starts(&text);
13765
13766 assert_eq!(first.as_ref(), &[0, 2, 4]);
13767 assert!(
13768 Rc::ptr_eq(&first, &second),
13769 "retained text should not rebuild its line index on every clipped frame"
13770 );
13771 }
13772
13773 #[test]
13774 fn text_line_index_cache_is_retained_text_instance_local() {
13775 let mut cache = TextLineIndexCache::new(4);
13776 let first_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
13777 let second_text = Arc::new(AnnotatedString::from("a\nb\nc").render_string());
13778
13779 let first = cache.line_starts(&first_text);
13780 let second = cache.line_starts(&second_text);
13781
13782 assert_eq!(first.as_ref(), second.as_ref());
13783 assert!(
13784 !Rc::ptr_eq(&first, &second),
13785 "line index lookup should not hash large text contents to find unrelated retained nodes"
13786 );
13787 }
13788
13789 #[test]
13790 fn device_pixel_bounds_for_rect_snaps_origin_and_extents() {
13791 let bounds = device_pixel_bounds_for_rect(
13792 Rect {
13793 x: 10.25,
13794 y: 14.6,
13795 width: 20.1,
13796 height: 9.2,
13797 },
13798 200,
13799 120,
13800 2.0,
13801 )
13802 .expect("rect should intersect the viewport");
13803
13804 assert_eq!(
13805 bounds,
13806 DevicePixelBounds {
13807 x: 20.0,
13808 y: 29.0,
13809 width: 41,
13810 height: 19,
13811 }
13812 );
13813 }
13814
13815 #[test]
13816 fn visible_layer_rect_intersects_clip_and_viewport() {
13817 let visible = visible_layer_rect(
13818 Rect {
13819 x: -10.0,
13820 y: 5.0,
13821 width: 80.0,
13822 height: 40.0,
13823 },
13824 Some(Rect {
13825 x: 4.0,
13826 y: 8.0,
13827 width: 20.0,
13828 height: 50.0,
13829 }),
13830 2.0,
13831 60,
13832 40,
13833 )
13834 .expect("visible rect");
13835
13836 assert_eq!(
13837 visible,
13838 Rect {
13839 x: 4.0,
13840 y: 8.0,
13841 width: 20.0,
13842 height: 12.0,
13843 }
13844 );
13845 }
13846
13847 #[test]
13848 fn clamp_effect_surface_scale_caps_large_surfaces_but_keeps_base_scale() {
13849 let clamped = clamp_effect_surface_scale(
13850 Rect {
13851 x: 0.0,
13852 y: 0.0,
13853 width: 1200.0,
13854 height: 900.0,
13855 },
13856 1.0,
13857 8.0,
13858 16_384,
13859 );
13860
13861 assert!(
13862 clamped < 8.0,
13863 "large translated effect layers must be capped to avoid OOM, got {clamped}"
13864 );
13865 assert!(
13866 clamped >= 1.0,
13867 "effect surfaces must not fall below destination resolution, got {clamped}"
13868 );
13869 }
13870
13871 #[test]
13872 fn clamp_effect_surface_scale_keeps_decorated_text_capture_scale() {
13873 let clamped = clamp_effect_surface_scale(
13874 Rect {
13875 x: 0.0,
13876 y: 0.0,
13877 width: 446.0,
13878 height: 44.0,
13879 },
13880 1.0,
13881 9.0,
13882 16_384,
13883 );
13884
13885 assert_eq!(
13886 clamped, 9.0,
13887 "decorated text motion-stable captures must keep full scale"
13888 );
13889 }
13890
13891 fn backdrop_layer(z_index: usize) -> BackdropLayer {
13892 BackdropLayer {
13893 node_id: Some(700 + z_index),
13894 rect: Rect {
13895 x: 0.0,
13896 y: 0.0,
13897 width: 10.0,
13898 height: 10.0,
13899 },
13900 clip: None,
13901 snap_anchor: None,
13902 effect: RenderEffect::blur(2.0),
13903 z_index,
13904 }
13905 }
13906
13907 fn test_shape(z_index: usize, blend_mode: BlendMode) -> DrawShape {
13908 DrawShape {
13909 rect: Rect {
13910 x: 0.0,
13911 y: 0.0,
13912 width: 8.0,
13913 height: 8.0,
13914 },
13915 local_rect: Rect {
13916 x: 0.0,
13917 y: 0.0,
13918 width: 8.0,
13919 height: 8.0,
13920 },
13921 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
13922 snap_anchor: None,
13923 brush: Brush::solid(Color::BLACK),
13924 shape: None,
13925 stroke: None,
13926 arc: None,
13927 z_index,
13928 clip: None,
13929 blend_mode,
13930 motion_context_animated: false,
13931 }
13932 }
13933
13934 #[test]
13935 fn shape_shadow_content_hash_ignores_viewport_translation() {
13936 fn translate_shape(shape: &DrawShape, dx: f32, dy: f32) -> DrawShape {
13937 let mut translated = shape.clone();
13938 translated.rect.x += dx;
13939 translated.rect.y += dy;
13940 translated.local_rect.x += dx;
13941 translated.local_rect.y += dy;
13942 for point in &mut translated.quad {
13943 point[0] += dx;
13944 point[1] += dy;
13945 }
13946 translated.snap_anchor = translated.snap_anchor.map(|anchor| {
13947 SnapAnchor::rigid(Point::new(anchor.origin.x + dx, anchor.origin.y + dy))
13948 });
13949 translated.clip = translated.clip.map(|mut clip| {
13950 clip.x += dx;
13951 clip.y += dy;
13952 clip
13953 });
13954 translated
13955 }
13956
13957 let mut first = test_shape(1, BlendMode::SrcOver);
13958 first.rect = Rect {
13959 x: 10.0,
13960 y: 20.0,
13961 width: 80.0,
13962 height: 40.0,
13963 };
13964 first.local_rect = first.rect;
13965 first.quad = [[10.0, 20.0], [90.0, 20.0], [10.0, 60.0], [90.0, 60.0]];
13966 first.snap_anchor = Some(SnapAnchor::rigid(Point::new(7.0, 11.0)));
13967 first.shape = Some(RoundedCornerShape::uniform(8.0));
13968 first.clip = Some(Rect {
13969 x: 8.0,
13970 y: 18.0,
13971 width: 86.0,
13972 height: 44.0,
13973 });
13974 let mut cutout = test_shape(2, BlendMode::DstOut);
13975 cutout.rect = Rect {
13976 x: 18.0,
13977 y: 26.0,
13978 width: 62.0,
13979 height: 22.0,
13980 };
13981 cutout.local_rect = cutout.rect;
13982 cutout.quad = [[18.0, 26.0], [80.0, 26.0], [18.0, 48.0], [80.0, 48.0]];
13983 cutout.shape = Some(RoundedCornerShape::uniform(4.0));
13984
13985 let dx = 37.0;
13986 let dy = -11.5;
13987 let translated = translate_shape(&first, dx, dy);
13988 let translated_cutout = translate_shape(&cutout, dx, dy);
13989
13990 let root_scale = 1.25;
13991 let first_shapes = vec![
13992 (first.clone(), BlendMode::SrcOver),
13993 (cutout, BlendMode::DstOut),
13994 ];
13995 let translated_shapes = vec![
13996 (translated.clone(), BlendMode::SrcOver),
13997 (translated_cutout, BlendMode::DstOut),
13998 ];
13999
14000 let first_hash = shape_shadow_content_hash(&first_shapes, root_scale);
14001 let translated_hash = shape_shadow_content_hash(&translated_shapes, root_scale);
14002
14003 assert_eq!(first_hash, translated_hash);
14004
14005 let mut changed_shapes = translated_shapes;
14006 changed_shapes[0].0.rect.width += 1.0;
14007 let changed_hash = shape_shadow_content_hash(&changed_shapes, root_scale);
14008
14009 assert_ne!(first_hash, changed_hash);
14010 }
14011
14012 #[test]
14013 fn shape_shadow_content_hash_is_stable_under_fractional_scale_scroll() {
14014 fn shadow_shapes_at(y: f32) -> Vec<(DrawShape, BlendMode)> {
14020 let mut shape = test_shape(1, BlendMode::SrcOver);
14021 shape.rect = Rect {
14022 x: 24.0,
14023 y,
14024 width: 180.0,
14025 height: 90.0,
14026 };
14027 shape.local_rect = shape.rect;
14028 shape.quad = crate::rect_to_quad(shape.rect);
14029 shape.shape = Some(RoundedCornerShape::uniform(14.0));
14030 vec![(shape, BlendMode::SrcOver)]
14031 }
14032
14033 let root_scale = 130.0f32 / 96.0;
14034 let blur_radius = 18.0f32;
14035 let pixel_radius = blur_radius * root_scale;
14036
14037 let key_at = |y: f32| {
14038 let shapes = shadow_shapes_at(y);
14039 let plan =
14040 shape_shadow_surface_plan(&shapes, None, blur_radius, 1600, 1600, root_scale, 8192)
14041 .expect("surface plan");
14042 shape_shadow_surface_cache_key(
14043 &shapes,
14044 plan.source_device_bounds,
14045 pixel_radius,
14046 root_scale,
14047 )
14048 .expect("cache key")
14049 };
14050
14051 let base = key_at(640.0);
14056 for step in 1..=12 {
14057 let scrolled = key_at(640.0 - step as f32 * 4.0);
14058 assert_eq!(
14059 base, scrolled,
14060 "scrolled shadow cache key must stay stable at fractional scale (step {step})"
14061 );
14062 }
14063 }
14064
14065 #[test]
14066 fn shape_shadow_cache_key_uses_unclipped_source_bounds_for_scrolled_clip() {
14067 fn translated_card_shadow(y: f32) -> Vec<(DrawShape, BlendMode)> {
14068 let mut shape = test_shape(1, BlendMode::SrcOver);
14069 shape.rect = Rect {
14070 x: 24.0,
14071 y,
14072 width: 280.0,
14073 height: 120.0,
14074 };
14075 shape.local_rect = shape.rect;
14076 shape.quad = [[24.0, y], [304.0, y], [24.0, y + 120.0], [304.0, y + 120.0]];
14077 shape.shape = Some(RoundedCornerShape::uniform(18.0));
14078 vec![(shape, BlendMode::SrcOver)]
14079 }
14080
14081 let root_scale = 1.0;
14082 let blur_radius = 18.0;
14083 let viewport_clip = Rect {
14084 x: 0.0,
14085 y: 96.0,
14086 width: 360.0,
14087 height: 720.0,
14088 };
14089 let key_for = |y: f32| {
14090 let shapes = translated_card_shadow(y);
14091 let plan = shape_shadow_surface_plan(
14092 &shapes,
14093 Some(viewport_clip),
14094 blur_radius,
14095 360,
14096 900,
14097 root_scale,
14098 4096,
14099 )
14100 .expect("surface plan");
14101 shape_shadow_surface_cache_key(
14102 &shapes,
14103 plan.source_device_bounds,
14104 plan.pixel_radius,
14105 root_scale,
14106 )
14107 .expect("cache key")
14108 };
14109
14110 assert_eq!(key_for(740.0), key_for(756.0));
14113 }
14114
14115 #[test]
14116 fn shape_visibility_uses_nonzero_viewport_offset_for_cropped_offscreen() {
14117 let mut shape = test_shape(1, BlendMode::SrcOver);
14118 shape.rect = Rect {
14119 x: 24.0,
14120 y: 740.0,
14121 width: 280.0,
14122 height: 120.0,
14123 };
14124 shape.local_rect = shape.rect;
14125 shape.quad = [[24.0, 740.0], [304.0, 740.0], [24.0, 860.0], [304.0, 860.0]];
14126 let viewport = ViewportUniformParams {
14127 width: 316,
14128 height: 228,
14129 offset: [6.0, 686.0],
14130 };
14131
14132 assert!(shape_draw_is_visible_in_viewport(&shape, viewport, 1.0));
14133 }
14134
14135 #[test]
14136 fn text_prewarm_uses_nonzero_viewport_offset_for_cropped_offscreen() {
14137 let viewport = ViewportUniformParams {
14138 width: 316,
14139 height: 228,
14140 offset: [6.0, 686.0],
14141 };
14142 let text_rect = Rect {
14143 x: 24.0,
14144 y: 740.0,
14145 width: 280.0,
14146 height: 40.0,
14147 };
14148
14149 assert!(text_draw_is_visible_in_viewport(
14150 text_rect, None, viewport, 1.0
14151 ));
14152 assert!(text_draw_should_prewarm_in_viewport(
14153 text_rect, None, viewport, 1.0
14154 ));
14155 }
14156
14157 fn test_shadow_draw(shapes: Vec<(DrawShape, BlendMode)>) -> ShadowDraw {
14158 ShadowDraw {
14159 shapes,
14160 texts: vec![],
14161 blur_radius: 8.0,
14162 clip: None,
14163 z_index: 0,
14164 }
14165 }
14166
14167 fn test_image(z_index: usize, blend_mode: BlendMode) -> ImageDraw {
14168 ImageDraw {
14169 rect: Rect {
14170 x: 0.0,
14171 y: 0.0,
14172 width: 8.0,
14173 height: 8.0,
14174 },
14175 local_rect: Rect {
14176 x: 0.0,
14177 y: 0.0,
14178 width: 8.0,
14179 height: 8.0,
14180 },
14181 quad: [[0.0, 0.0], [8.0, 0.0], [0.0, 8.0], [8.0, 8.0]],
14182 snap_anchor: None,
14183 image: ImageBitmap::from_rgba8(1, 1, vec![255, 255, 255, 255]).expect("image"),
14184 alpha: 1.0,
14185 color_filter: None,
14186 sampling: ImageSampling::Nearest,
14187 z_index,
14188 clip: None,
14189 blend_mode,
14190 src_rect: None,
14191 motion_context_animated: false,
14192 }
14193 }
14194
14195 #[test]
14196 fn image_sampler_descriptors_match_requested_sampling() {
14197 let nearest = image_sampler_descriptor(ImageSampling::Nearest);
14198 assert_eq!(nearest.mag_filter, wgpu::FilterMode::Nearest);
14199 assert_eq!(nearest.min_filter, wgpu::FilterMode::Nearest);
14200
14201 let linear = image_sampler_descriptor(ImageSampling::Linear);
14202 assert_eq!(linear.mag_filter, wgpu::FilterMode::Linear);
14203 assert_eq!(linear.min_filter, wgpu::FilterMode::Linear);
14204 }
14205
14206 #[test]
14207 fn image_uv_rect_clamps_source_rect_to_texel_centers() {
14208 let image = ImageBitmap::from_rgba8(24, 16, vec![0; 24 * 16 * 4]).expect("image");
14209 let uv = image_uv_rect(
14210 &image,
14211 Some(Rect {
14212 x: 0.0,
14213 y: 0.0,
14214 width: 16.0,
14215 height: 16.0,
14216 }),
14217 )
14218 .expect("uv rect");
14219
14220 assert_eq!(uv.min, [0.0, 0.0]);
14221 assert_eq!(uv.max, [16.0 / 24.0, 1.0]);
14222 assert_eq!(
14223 uv.sample_bounds,
14224 [0.5 / 24.0, 0.5 / 16.0, 15.5 / 24.0, 15.5 / 16.0]
14225 );
14226 }
14227
14228 #[test]
14229 fn image_uv_rect_keeps_full_image_unclamped() {
14230 let image = ImageBitmap::from_rgba8(2, 2, vec![0; 16]).expect("image");
14231 let uv = image_uv_rect(&image, None).expect("uv rect");
14232
14233 assert_eq!(uv.min, [0.0, 0.0]);
14234 assert_eq!(uv.max, [1.0, 1.0]);
14235 assert_eq!(uv.sample_bounds, [0.0, 0.0, 1.0, 1.0]);
14236 }
14237
14238 fn test_text(z_index: usize) -> TextDraw {
14239 TextDraw {
14240 node_id: 0,
14241 rect: Rect {
14242 x: 0.0,
14243 y: 0.0,
14244 width: 8.0,
14245 height: 8.0,
14246 },
14247 snap_anchor: None,
14248 translated_content_context: false,
14249 text: Arc::new(cranpose_ui::text::AnnotatedString::from("t").render_string()),
14250 color: Color::WHITE,
14251 text_style: cranpose_ui::TextStyle::default(),
14252 font_size: 12.0,
14253 scale: 1.0,
14254 layout_options: cranpose_ui::TextLayoutOptions::default(),
14255 z_index,
14256 clip: None,
14257 }
14258 }
14259
14260 #[test]
14261 fn text_draw_visibility_rejects_text_outside_clip_before_rasterization() {
14262 let viewport = ViewportUniformParams {
14263 width: 320,
14264 height: 240,
14265 offset: [0.0, 0.0],
14266 };
14267 let text_rect = Rect {
14268 x: 0.0,
14269 y: 260.0,
14270 width: 200.0,
14271 height: 40.0,
14272 };
14273 let clip = Some(Rect {
14274 x: 0.0,
14275 y: 0.0,
14276 width: 320.0,
14277 height: 200.0,
14278 });
14279
14280 assert!(
14281 !text_draw_is_visible_in_viewport(text_rect, clip, viewport, 1.0),
14282 "lazy-list beyond-bound text outside the clip must not be rasterized"
14283 );
14284 }
14285
14286 #[test]
14287 fn text_draw_prewarm_accepts_clipped_text_near_viewport() {
14288 let viewport = ViewportUniformParams {
14289 width: 320,
14290 height: 240,
14291 offset: [0.0, 0.0],
14292 };
14293 let text_rect = Rect {
14294 x: 0.0,
14295 y: 260.0,
14296 width: 200.0,
14297 height: 40.0,
14298 };
14299 let clip = Some(Rect {
14300 x: 0.0,
14301 y: 0.0,
14302 width: 320.0,
14303 height: 200.0,
14304 });
14305
14306 assert!(!text_draw_is_visible_in_viewport(
14307 text_rect, clip, viewport, 1.0
14308 ));
14309 assert!(text_draw_should_prewarm_in_viewport(
14310 text_rect, clip, viewport, 1.0
14311 ));
14312 }
14313
14314 #[test]
14315 fn text_draw_prewarm_rejects_far_clipped_text() {
14316 let viewport = ViewportUniformParams {
14317 width: 320,
14318 height: 240,
14319 offset: [0.0, 0.0],
14320 };
14321 let text_rect = Rect {
14322 x: 0.0,
14323 y: 1600.0,
14324 width: 200.0,
14325 height: 40.0,
14326 };
14327 let clip = Some(Rect {
14328 x: 0.0,
14329 y: 0.0,
14330 width: 320.0,
14331 height: 200.0,
14332 });
14333
14334 assert!(!text_draw_should_prewarm_in_viewport(
14335 text_rect, clip, viewport, 1.0
14336 ));
14337 }
14338
14339 #[test]
14340 fn text_draw_visibility_rejects_unclipped_text_outside_viewport() {
14341 let viewport = ViewportUniformParams {
14342 width: 320,
14343 height: 240,
14344 offset: [0.0, 0.0],
14345 };
14346 let text_rect = Rect {
14347 x: 0.0,
14348 y: 241.0,
14349 width: 200.0,
14350 height: 40.0,
14351 };
14352
14353 assert!(
14354 !text_draw_is_visible_in_viewport(text_rect, None, viewport, 1.0),
14355 "unclipped text outside the target viewport must not be rasterized"
14356 );
14357 }
14358
14359 #[test]
14360 fn text_draw_visibility_keeps_partially_visible_text() {
14361 let viewport = ViewportUniformParams {
14362 width: 320,
14363 height: 240,
14364 offset: [0.0, 0.0],
14365 };
14366 let text_rect = Rect {
14367 x: 0.0,
14368 y: 220.0,
14369 width: 200.0,
14370 height: 40.0,
14371 };
14372
14373 assert!(text_draw_is_visible_in_viewport(
14374 text_rect, None, viewport, 1.0
14375 ));
14376 }
14377
14378 fn test_draw_ops(
14379 shapes: &[DrawShape],
14380 images: &[ImageDraw],
14381 texts: &[TextDraw],
14382 shadows: &[ShadowDraw],
14383 ) -> Vec<DrawOp> {
14384 let mut ops = Vec::new();
14385 ops.extend(shapes.iter().enumerate().map(|(index, shape)| DrawOp {
14386 z_index: shape.z_index,
14387 kind: DrawOpKind::Shape(index),
14388 }));
14389 ops.extend(images.iter().enumerate().map(|(index, image)| DrawOp {
14390 z_index: image.z_index,
14391 kind: DrawOpKind::Image(index),
14392 }));
14393 ops.extend(texts.iter().enumerate().map(|(index, text)| DrawOp {
14394 z_index: text.z_index,
14395 kind: DrawOpKind::Text(index),
14396 }));
14397 ops.extend(shadows.iter().enumerate().map(|(index, shadow)| DrawOp {
14398 z_index: shadow.z_index,
14399 kind: DrawOpKind::Shadow(index),
14400 }));
14401 ops.sort_by_key(|op| op.z_index);
14402 ops
14403 }
14404
14405 fn test_layer(local_bounds: Rect, children: Vec<RenderNode>) -> LayerNode {
14406 crate::test_support::layer_node(
14407 local_bounds,
14408 ProjectiveTransform::identity(),
14409 GraphicsLayer::default(),
14410 children,
14411 )
14412 }
14413
14414 fn cacheable_layer(
14415 node_id: cranpose_core::NodeId,
14416 local_bounds: Rect,
14417 children: Vec<RenderNode>,
14418 ) -> LayerNode {
14419 let mut layer = test_layer(local_bounds, children);
14420 layer.node_id = Some(node_id);
14421 layer.cache_policy = cranpose_render_common::graph::CachePolicy::Auto;
14422 layer.recompute_raster_cache_hashes();
14423 layer
14424 }
14425
14426 fn text_layer_with_style(text: AnnotatedString, text_style: TextStyle) -> LayerNode {
14427 test_layer(
14428 Rect {
14429 x: 0.0,
14430 y: 0.0,
14431 width: 64.0,
14432 height: 32.0,
14433 },
14434 vec![RenderNode::Primitive(PrimitiveEntry {
14435 phase: PrimitivePhase::BeforeChildren,
14436 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
14437 node_id: 1,
14438 rect: Rect {
14439 x: 2.0,
14440 y: 3.0,
14441 width: 48.0,
14442 height: 18.0,
14443 },
14444 text: std::rc::Rc::new(text),
14445 text_style,
14446 font_size: 14.0,
14447 layout_options: TextLayoutOptions::default(),
14448 clip: None,
14449 })),
14450 })],
14451 )
14452 }
14453
14454 fn snapped_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
14455 LayerNode {
14456 node_id: Some(77),
14457 local_bounds: Rect {
14458 x: 0.0,
14459 y: 0.0,
14460 width: 48.0,
14461 height: 24.0,
14462 },
14463 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
14464 motion_context_animated: animated,
14465 translated_content_context,
14466 translated_content_offset: Point::default(),
14467 content_offset: Point::default(),
14468 scene_children_origin: cranpose_ui_graphics::Point::default(),
14469 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
14470 graphics_layer: GraphicsLayer::default(),
14471 clip_to_bounds: false,
14472 shadow_clip: None,
14473 hit_test: None,
14474 has_hit_targets: false,
14475 isolation: IsolationReasons::default(),
14476 cache_policy: CachePolicy::None,
14477 cache_hashes: LayerRasterCacheHashes::default(),
14478 cache_hashes_valid: false,
14479 children: vec![
14480 RenderNode::Primitive(PrimitiveEntry {
14481 phase: PrimitivePhase::BeforeChildren,
14482 node: PrimitiveNode::Draw(DrawPrimitiveNode {
14483 primitive: DrawPrimitive::RoundRect {
14484 rect: Rect {
14485 x: 0.0,
14486 y: 0.0,
14487 width: 48.0,
14488 height: 24.0,
14489 },
14490 brush: Brush::solid(Color(0.28, 0.30, 0.46, 0.88)),
14491 radii: CornerRadii::uniform(6.0),
14492 stroke: None,
14493 },
14494 clip: None,
14495 }),
14496 }),
14497 RenderNode::Primitive(PrimitiveEntry {
14498 phase: PrimitivePhase::BeforeChildren,
14499 node: PrimitiveNode::Draw(DrawPrimitiveNode {
14500 primitive: DrawPrimitive::Image {
14501 rect: Rect {
14502 x: 2.0,
14503 y: 2.0,
14504 width: 12.0,
14505 height: 12.0,
14506 },
14507 image: ImageBitmap::from_rgba8(
14508 2,
14509 2,
14510 vec![
14511 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255,
14512 255,
14513 ],
14514 )
14515 .expect("image"),
14516 alpha: 1.0,
14517 color_filter: None,
14518 sampling: ImageSampling::Linear,
14519 src_rect: None,
14520 },
14521 clip: None,
14522 }),
14523 }),
14524 RenderNode::Primitive(PrimitiveEntry {
14525 phase: PrimitivePhase::BeforeChildren,
14526 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
14527 node_id: 77,
14528 rect: Rect {
14529 x: 6.0,
14530 y: 4.0,
14531 width: 36.0,
14532 height: 16.0,
14533 },
14534 text: std::rc::Rc::new(AnnotatedString::from("48 px")),
14535 text_style: TextStyle::default(),
14536 font_size: 14.0,
14537 layout_options: TextLayoutOptions::default(),
14538 clip: None,
14539 })),
14540 }),
14541 ],
14542 }
14543 }
14544
14545 fn snapped_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
14546 let text_leaf = snapped_text_leaf(animated, translated_content_context);
14547 test_layer(
14548 Rect {
14549 x: 0.0,
14550 y: 0.0,
14551 width: 96.0,
14552 height: 64.0,
14553 },
14554 vec![RenderNode::Layer(Box::new(text_leaf))],
14555 )
14556 }
14557
14558 fn translated_content_local_surface_root() -> LayerNode {
14559 let mut effectful_text = text_layer_with_style(
14560 AnnotatedString::from("shadow"),
14561 TextStyle::from_span_style(SpanStyle {
14562 shadow: Some(Shadow {
14563 color: Color::BLACK,
14564 offset: Point::new(1.0, 2.0),
14565 blur_radius: 3.0,
14566 }),
14567 ..SpanStyle::default()
14568 }),
14569 );
14570 effectful_text.translated_content_context = true;
14571
14572 let translated_content = LayerNode {
14573 node_id: Some(78),
14574 local_bounds: Rect {
14575 x: 0.0,
14576 y: 0.0,
14577 width: 96.0,
14578 height: 64.0,
14579 },
14580 transform_to_parent: ProjectiveTransform::translation(14.25, 16.5),
14581 motion_context_animated: false,
14582 translated_content_context: true,
14583 translated_content_offset: Point::default(),
14584 content_offset: Point::default(),
14585 scene_children_origin: cranpose_ui_graphics::Point::default(),
14586 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
14587 graphics_layer: GraphicsLayer::default(),
14588 clip_to_bounds: false,
14589 shadow_clip: None,
14590 hit_test: None,
14591 has_hit_targets: false,
14592 isolation: IsolationReasons::default(),
14593 cache_policy: CachePolicy::None,
14594 cache_hashes: LayerRasterCacheHashes::default(),
14595 cache_hashes_valid: false,
14596 children: vec![RenderNode::Layer(Box::new(effectful_text))],
14597 };
14598
14599 test_layer(
14600 Rect {
14601 x: 0.0,
14602 y: 0.0,
14603 width: 160.0,
14604 height: 120.0,
14605 },
14606 vec![RenderNode::Layer(Box::new(translated_content))],
14607 )
14608 }
14609
14610 #[test]
14611 fn scissor_rect_for_layer_intersects_with_clip() {
14612 let rect = Rect {
14613 x: 10.0,
14614 y: 10.0,
14615 width: 30.0,
14616 height: 20.0,
14617 };
14618 let clip = Rect {
14619 x: 20.0,
14620 y: 15.0,
14621 width: 100.0,
14622 height: 100.0,
14623 };
14624
14625 let scissor = scissor_rect_for_layer(rect, Some(clip), 1.0, 200, 200);
14626 assert_eq!(scissor, Some((20, 15, 20, 15)));
14627 }
14628
14629 #[test]
14630 fn visible_draw_rect_no_clip_returns_original() {
14631 let rect = Rect {
14632 x: 100.0,
14633 y: 200.0,
14634 width: 300.0,
14635 height: 400.0,
14636 };
14637 assert_eq!(visible_draw_rect(rect, None), Some(rect));
14638 }
14639
14640 #[test]
14641 fn visible_draw_rect_with_clip_intersects() {
14642 let rect = Rect {
14643 x: 0.0,
14644 y: 0.0,
14645 width: 2000.0,
14646 height: 5000.0,
14647 };
14648 let clip = Rect {
14649 x: 0.0,
14650 y: 0.0,
14651 width: 800.0,
14652 height: 600.0,
14653 };
14654 let visible = visible_draw_rect(rect, Some(clip)).expect("should have visible area");
14655 assert_eq!(visible.width, 800.0);
14656 assert_eq!(visible.height, 600.0);
14657 }
14658
14659 #[test]
14660 fn visible_draw_rect_fully_clipped_returns_none() {
14661 let rect = Rect {
14662 x: 1000.0,
14663 y: 1000.0,
14664 width: 200.0,
14665 height: 200.0,
14666 };
14667 let clip = Rect {
14668 x: 0.0,
14669 y: 0.0,
14670 width: 800.0,
14671 height: 600.0,
14672 };
14673 assert!(visible_draw_rect(rect, Some(clip)).is_none());
14674 }
14675
14676 #[test]
14677 fn scene_bounds_respects_clip_on_shapes() {
14678 let mut scene = CompositorScene::new();
14679 scene.shapes.push(DrawShape {
14681 rect: Rect {
14682 x: 10.0,
14683 y: 10.0,
14684 width: 100.0,
14685 height: 50.0,
14686 },
14687 clip: Some(Rect {
14688 x: 0.0,
14689 y: 0.0,
14690 width: 800.0,
14691 height: 600.0,
14692 }),
14693 ..test_shape(0, BlendMode::SrcOver)
14694 });
14695 scene.shapes.push(DrawShape {
14697 rect: Rect {
14698 x: 0.0,
14699 y: 3000.0,
14700 width: 100.0,
14701 height: 50.0,
14702 },
14703 clip: Some(Rect {
14704 x: 0.0,
14705 y: 0.0,
14706 width: 800.0,
14707 height: 600.0,
14708 }),
14709 ..test_shape(1, BlendMode::SrcOver)
14710 });
14711 let bounds = scene_bounds(&scene).expect("should have bounds");
14712 assert!(bounds.y + bounds.height <= 600.0);
14715 }
14716
14717 #[test]
14718 fn scene_bounds_scroll_content_clipped_to_viewport() {
14719 let mut scene = CompositorScene::new();
14722 let viewport_clip = Rect {
14723 x: 0.0,
14724 y: 0.0,
14725 width: 800.0,
14726 height: 600.0,
14727 };
14728 for i in 0..20 {
14729 scene.shapes.push(DrawShape {
14730 rect: Rect {
14731 x: 0.0,
14732 y: i as f32 * 300.0,
14733 width: 800.0,
14734 height: 200.0,
14735 },
14736 clip: Some(viewport_clip),
14737 ..test_shape(i, BlendMode::SrcOver)
14738 });
14739 }
14740 let bounds = scene_bounds(&scene).expect("should have bounds");
14741 assert_eq!(bounds.x, 0.0);
14744 assert_eq!(bounds.y, 0.0);
14745 assert!(bounds.width <= 800.0);
14746 assert!(bounds.height <= 600.0);
14747 }
14748
14749 #[test]
14750 fn scene_bounds_stable_across_scroll_offsets() {
14751 let viewport_clip = Rect {
14754 x: 0.0,
14755 y: 0.0,
14756 width: 400.0,
14757 height: 50.0,
14758 };
14759 let compute_bounds_at_offset = |scroll_x: f32| {
14760 let mut scene = CompositorScene::new();
14761 for i in 0..10 {
14762 scene.shapes.push(DrawShape {
14763 rect: Rect {
14764 x: i as f32 * 100.0 - scroll_x,
14765 y: 0.0,
14766 width: 80.0,
14767 height: 40.0,
14768 },
14769 clip: Some(viewport_clip),
14770 ..test_shape(i, BlendMode::SrcOver)
14771 });
14772 }
14773 scene_bounds(&scene).expect("bounds")
14774 };
14775 let bounds_at_0 = compute_bounds_at_offset(0.0);
14776 let bounds_at_300 = compute_bounds_at_offset(300.0);
14777 let bounds_at_600 = compute_bounds_at_offset(600.0);
14778 assert!(
14780 (bounds_at_0.width - bounds_at_300.width).abs() < 1.0,
14781 "bounds width changed with scroll: {} vs {}",
14782 bounds_at_0.width,
14783 bounds_at_300.width
14784 );
14785 assert!(
14786 (bounds_at_0.width - bounds_at_600.width).abs() < 1.0,
14787 "bounds width changed with scroll: {} vs {}",
14788 bounds_at_0.width,
14789 bounds_at_600.width
14790 );
14791 }
14792
14793 #[test]
14794 fn collect_effect_ranges_respects_excluded_effect() {
14795 let layers = vec![effect_layer(10, 40), effect_layer(20, 30)];
14796 let mut ranges = Vec::new();
14797 collect_effect_ranges(&layers, 10, 40, Some(0), &mut ranges);
14798 assert_eq!(ranges.len(), 1);
14799 assert_eq!(ranges[0], 20..30);
14800 }
14801
14802 #[test]
14803 fn collect_layer_events_includes_nested_when_parent_excluded() {
14804 let effects = vec![effect_layer(10, 40), effect_layer(20, 30)];
14805 let backdrops = vec![backdrop_layer(25)];
14806 let mut events = Vec::new();
14807 collect_layer_events(&effects, &backdrops, 10, 40, Some(0), &mut events);
14808 assert_eq!(events.len(), 2);
14809
14810 match events[0].kind {
14811 LayerEventKind::Effect(index) => assert_eq!(index, 1),
14812 LayerEventKind::Backdrop(_) => panic!("expected nested effect as first event"),
14813 }
14814 match events[1].kind {
14815 LayerEventKind::Backdrop(index) => assert_eq!(index, 0),
14816 LayerEventKind::Effect(_) => panic!("expected backdrop as second event"),
14817 }
14818 }
14819
14820 fn pure_text_leaf(animated: bool, translated_content_context: bool) -> LayerNode {
14821 LayerNode {
14822 node_id: Some(177),
14823 local_bounds: Rect {
14824 x: 0.0,
14825 y: 0.0,
14826 width: 96.0,
14827 height: 32.0,
14828 },
14829 transform_to_parent: ProjectiveTransform::translation(11.4, 23.6),
14830 motion_context_animated: animated,
14831 translated_content_context,
14832 translated_content_offset: Point::default(),
14833 content_offset: Point::default(),
14834 scene_children_origin: cranpose_ui_graphics::Point::default(),
14835 scene_children_layer_translation: cranpose_ui_graphics::Point::default(),
14836 graphics_layer: GraphicsLayer::default(),
14837 clip_to_bounds: false,
14838 shadow_clip: None,
14839 hit_test: None,
14840 has_hit_targets: false,
14841 isolation: IsolationReasons::default(),
14842 cache_policy: CachePolicy::None,
14843 cache_hashes: LayerRasterCacheHashes::default(),
14844 cache_hashes_valid: false,
14845 children: vec![RenderNode::Primitive(PrimitiveEntry {
14846 phase: PrimitivePhase::BeforeChildren,
14847 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
14848 node_id: 177,
14849 rect: Rect {
14850 x: 0.0,
14851 y: 0.0,
14852 width: 96.0,
14853 height: 24.0,
14854 },
14855 clip: None,
14856 text: std::rc::Rc::new(AnnotatedString::from("Pure text")),
14857 text_style: TextStyle::default(),
14858 font_size: 14.0,
14859 layout_options: TextLayoutOptions::default(),
14860 })),
14861 })],
14862 }
14863 }
14864
14865 fn pure_text_leaf_root(animated: bool, translated_content_context: bool) -> LayerNode {
14866 let text_leaf = pure_text_leaf(animated, translated_content_context);
14867 test_layer(
14868 Rect {
14869 x: 0.0,
14870 y: 0.0,
14871 width: 160.0,
14872 height: 96.0,
14873 },
14874 vec![RenderNode::Layer(Box::new(text_leaf))],
14875 )
14876 }
14877
14878 #[test]
14879 fn collect_layer_events_sorts_backdrop_before_effect_at_same_z() {
14880 let effects = vec![effect_layer(10, 20)];
14881 let backdrops = vec![backdrop_layer(10)];
14882 let mut events = Vec::new();
14883 collect_layer_events(&effects, &backdrops, 0, 30, None, &mut events);
14884 assert_eq!(events.len(), 2);
14885
14886 match events[0].kind {
14887 LayerEventKind::Backdrop(_) => {}
14888 LayerEventKind::Effect(_) => panic!("expected backdrop to run before effect"),
14889 }
14890 match events[1].kind {
14891 LayerEventKind::Effect(_) => {}
14892 LayerEventKind::Backdrop(_) => panic!("expected effect as second event"),
14893 }
14894 }
14895
14896 #[test]
14897 fn collect_layer_events_prefers_outer_effect_when_same_start_z() {
14898 let effects = vec![effect_layer(10, 20), effect_layer(10, 40)];
14901 let mut events = Vec::new();
14902 collect_layer_events(&effects, &[], 0, 50, None, &mut events);
14903
14904 assert_eq!(events.len(), 2);
14905 match events[0].kind {
14906 LayerEventKind::Effect(index) => assert_eq!(index, 1),
14907 LayerEventKind::Backdrop(_) => panic!("expected outer effect first"),
14908 }
14909 match events[1].kind {
14910 LayerEventKind::Effect(index) => assert_eq!(index, 0),
14911 LayerEventKind::Backdrop(_) => panic!("expected child effect second"),
14912 }
14913 }
14914
14915 #[test]
14916 fn collect_layer_events_prefers_later_effect_when_ranges_match() {
14917 let effects = vec![effect_layer(10, 20), effect_layer(10, 20)];
14918 let mut events = Vec::new();
14919 collect_layer_events(&effects, &[], 0, 30, None, &mut events);
14920
14921 assert_eq!(events.len(), 2);
14922 match events[0].kind {
14923 LayerEventKind::Effect(index) => assert_eq!(index, 1),
14924 LayerEventKind::Backdrop(_) => panic!("expected later effect first"),
14925 }
14926 match events[1].kind {
14927 LayerEventKind::Effect(index) => assert_eq!(index, 0),
14928 LayerEventKind::Backdrop(_) => panic!("expected earlier effect second"),
14929 }
14930 }
14931
14932 #[test]
14933 fn has_backdrop_layer_in_range_detects_nested_layers() {
14934 let backdrops = vec![backdrop_layer(5), backdrop_layer(15), backdrop_layer(25)];
14935 assert!(has_backdrop_layer_in_range(&backdrops, 10, 20));
14936 assert!(has_backdrop_layer_in_range(&backdrops, 0, 6));
14937 assert!(!has_backdrop_layer_in_range(&backdrops, 20, 25));
14938 }
14939
14940 #[test]
14941 fn layer_contains_descendant_backdrop_ignores_self_backdrop() {
14942 let mut self_backdrop = test_layer(
14943 Rect {
14944 x: 0.0,
14945 y: 0.0,
14946 width: 10.0,
14947 height: 10.0,
14948 },
14949 vec![],
14950 );
14951 self_backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
14952 assert!(!layer_contains_descendant_backdrop(&self_backdrop));
14953
14954 let mut child = test_layer(
14955 Rect {
14956 x: 0.0,
14957 y: 0.0,
14958 width: 8.0,
14959 height: 8.0,
14960 },
14961 vec![],
14962 );
14963 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
14964
14965 let parent = test_layer(
14966 Rect {
14967 x: 0.0,
14968 y: 0.0,
14969 width: 20.0,
14970 height: 20.0,
14971 },
14972 vec![RenderNode::Layer(Box::new(child))],
14973 );
14974 assert!(layer_contains_descendant_backdrop(&parent));
14975 }
14976
14977 fn child_layer_composite(
14978 layer: &LayerNode,
14979 z_index: usize,
14980 rect: Rect,
14981 needs_nested_underlay: bool,
14982 ) -> crate::normalized_scene::ChildLayerComposite {
14983 let mut requirements_cache = cranpose_core::collections::map::HashMap::new();
14984 let surface_requirements =
14985 crate::surface_plan::layer_surface_requirements_cached(layer, &mut requirements_cache);
14986 crate::normalized_scene::ChildLayerComposite {
14987 z_index,
14988 logical_rect: Rect {
14989 x: 0.0,
14990 y: 0.0,
14991 width: rect.width,
14992 height: rect.height,
14993 },
14994 dest_quad: rect_to_quad(rect),
14995 snap_anchor: None,
14996 composite_snap_origin: None,
14997 backdrop_rect: rect,
14998 visual_clip: None,
14999 surface_clip: None,
15000 shadow_draws: Vec::new(),
15001 needs_nested_underlay,
15002 node_id: layer.node_id,
15003 backdrop: layer.backdrop().cloned(),
15004 has_effect: layer.effect().is_some(),
15005 effect_contains_runtime_shader: layer
15006 .effect()
15007 .is_some_and(|effect| effect.contains_runtime_shader()),
15008 target_content_hash: layer.target_content_hash(),
15009 effect_hash: layer.effect_hash(),
15010 motion_source_content_hash: Some(layer.motion_source_content_hash()),
15011 contains_descendant_backdrop: layer_contains_descendant_backdrop(layer),
15012 cache_policy: layer.cache_policy,
15013 surface_requirements,
15014 rounded_clip: crate::surface_executor::backend::LayerSurfaceRoundedClip::from_layer(
15015 layer,
15016 ),
15017 isolation: cranpose_render_common::layer_composition::effective_layer_isolation(
15018 &layer.graphics_layer,
15019 ),
15020 translated_content_context: layer.translated_content_context,
15021 own_translated_content_axes: crate::surface_plan::translated_content_axes_for_layer(
15022 layer,
15023 ),
15024 clip_rect: layer.clip_rect(),
15025 local_bounds: layer.local_bounds,
15026 surface_scale: crate::surface_plan::layer_surface_scale(layer),
15027 source: crate::normalized_scene::LoweredChildSource::default(),
15028 }
15029 }
15030
15031 #[test]
15032 fn root_direct_preflight_allows_first_translated_child_underlay() {
15033 let child = test_layer(
15034 Rect {
15035 x: 0.0,
15036 y: 0.0,
15037 width: 400.0,
15038 height: 280.0,
15039 },
15040 vec![],
15041 );
15042 let collected = CollectedLayer {
15043 scene: CompositorScene::new(),
15044 child_layers: vec![child_layer_composite(
15045 &child,
15046 3,
15047 Rect {
15048 x: 48.0,
15049 y: 96.0,
15050 width: 400.0,
15051 height: 280.0,
15052 },
15053 true,
15054 )],
15055 };
15056
15057 assert!(direct_root_child_underlays_are_supported(&collected));
15058 }
15059
15060 #[test]
15061 fn root_direct_preflight_allows_axis_aligned_prior_child_underlay() {
15062 let first = test_layer(
15063 Rect {
15064 x: 0.0,
15065 y: 0.0,
15066 width: 80.0,
15067 height: 40.0,
15068 },
15069 vec![],
15070 );
15071 let backdrop_child = test_layer(
15072 Rect {
15073 x: 0.0,
15074 y: 0.0,
15075 width: 400.0,
15076 height: 280.0,
15077 },
15078 vec![],
15079 );
15080 let collected = CollectedLayer {
15081 scene: CompositorScene::new(),
15082 child_layers: vec![
15083 child_layer_composite(
15084 &first,
15085 1,
15086 Rect {
15087 x: 8.0,
15088 y: 16.0,
15089 width: 80.0,
15090 height: 40.0,
15091 },
15092 false,
15093 ),
15094 child_layer_composite(
15095 &backdrop_child,
15096 4,
15097 Rect {
15098 x: 48.0,
15099 y: 96.0,
15100 width: 400.0,
15101 height: 280.0,
15102 },
15103 true,
15104 ),
15105 ],
15106 };
15107
15108 assert!(direct_root_child_underlays_are_supported(&collected));
15109 }
15110
15111 #[test]
15112 fn root_direct_preflight_rejects_effectful_prior_child_underlay() {
15113 let mut first = test_layer(
15114 Rect {
15115 x: 0.0,
15116 y: 0.0,
15117 width: 80.0,
15118 height: 40.0,
15119 },
15120 vec![],
15121 );
15122 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
15123 let backdrop_child = test_layer(
15124 Rect {
15125 x: 0.0,
15126 y: 0.0,
15127 width: 400.0,
15128 height: 280.0,
15129 },
15130 vec![],
15131 );
15132 let collected = CollectedLayer {
15133 scene: CompositorScene::new(),
15134 child_layers: vec![
15135 child_layer_composite(
15136 &first,
15137 1,
15138 Rect {
15139 x: 64.0,
15140 y: 112.0,
15141 width: 80.0,
15142 height: 40.0,
15143 },
15144 false,
15145 ),
15146 child_layer_composite(
15147 &backdrop_child,
15148 4,
15149 Rect {
15150 x: 48.0,
15151 y: 96.0,
15152 width: 400.0,
15153 height: 280.0,
15154 },
15155 true,
15156 ),
15157 ],
15158 };
15159
15160 assert!(!direct_root_child_underlays_are_supported(&collected));
15161 }
15162
15163 #[test]
15164 fn root_direct_preflight_ignores_non_overlapping_effectful_prior_child_underlay() {
15165 let mut first = test_layer(
15166 Rect {
15167 x: 0.0,
15168 y: 0.0,
15169 width: 80.0,
15170 height: 40.0,
15171 },
15172 vec![],
15173 );
15174 first.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
15175 let backdrop_child = test_layer(
15176 Rect {
15177 x: 0.0,
15178 y: 0.0,
15179 width: 400.0,
15180 height: 280.0,
15181 },
15182 vec![],
15183 );
15184 let collected = CollectedLayer {
15185 scene: CompositorScene::new(),
15186 child_layers: vec![
15187 child_layer_composite(
15188 &first,
15189 1,
15190 Rect {
15191 x: 8.0,
15192 y: 16.0,
15193 width: 80.0,
15194 height: 40.0,
15195 },
15196 false,
15197 ),
15198 child_layer_composite(
15199 &backdrop_child,
15200 4,
15201 Rect {
15202 x: 48.0,
15203 y: 96.0,
15204 width: 400.0,
15205 height: 280.0,
15206 },
15207 true,
15208 ),
15209 ],
15210 };
15211
15212 assert!(direct_root_child_underlays_are_supported(&collected));
15213 }
15214
15215 #[test]
15216 fn root_direct_preflight_rejects_underlay_that_would_replay_prior_scene_effects() {
15217 let backdrop_child = test_layer(
15218 Rect {
15219 x: 0.0,
15220 y: 0.0,
15221 width: 400.0,
15222 height: 280.0,
15223 },
15224 vec![],
15225 );
15226 let mut scene = CompositorScene::new();
15227 scene.next_z = 1;
15228 scene.push_effect_layer(
15229 Rect {
15230 x: 0.0,
15231 y: 0.0,
15232 width: 120.0,
15233 height: 120.0,
15234 },
15235 None,
15236 Some(RenderEffect::blur(2.0)),
15237 BlendMode::SrcOver,
15238 1.0,
15239 0,
15240 1,
15241 );
15242 let collected = CollectedLayer {
15243 scene,
15244 child_layers: vec![child_layer_composite(
15245 &backdrop_child,
15246 4,
15247 Rect {
15248 x: 48.0,
15249 y: 96.0,
15250 width: 400.0,
15251 height: 280.0,
15252 },
15253 true,
15254 )],
15255 };
15256
15257 assert!(!direct_root_child_underlays_are_supported(&collected));
15258 }
15259
15260 #[test]
15261 fn root_direct_eligibility_does_not_reject_descendant_backdrop() {
15262 let mut backdrop = test_layer(
15263 Rect {
15264 x: 0.0,
15265 y: 0.0,
15266 width: 40.0,
15267 height: 40.0,
15268 },
15269 vec![],
15270 );
15271 backdrop.graphics_layer.backdrop_effect = Some(RenderEffect::blur(4.0));
15272 let child = test_layer(
15273 Rect {
15274 x: 0.0,
15275 y: 0.0,
15276 width: 120.0,
15277 height: 96.0,
15278 },
15279 vec![RenderNode::Layer(Box::new(backdrop))],
15280 );
15281 let root = test_layer(
15282 Rect {
15283 x: 0.0,
15284 y: 0.0,
15285 width: 240.0,
15286 height: 160.0,
15287 },
15288 vec![RenderNode::Layer(Box::new(child))],
15289 );
15290 let mut cache = HashMap::new();
15291
15292 assert!(root_can_render_directly_cached(&root, &mut cache));
15293 }
15294
15295 #[test]
15296 fn root_direct_scene_events_allow_root_local_effects() {
15297 let mut scene = CompositorScene::new();
15298 scene.effect_layers.push(EffectLayer {
15299 rect: Rect {
15300 x: 20.0,
15301 y: 30.0,
15302 width: 120.0,
15303 height: 80.0,
15304 },
15305 clip: None,
15306 snap_anchor: None,
15307 effect: Some(RenderEffect::blur(6.0)),
15308 blend_mode: BlendMode::SrcOver,
15309 composite_alpha: 1.0,
15310 z_start: 0,
15311 z_end: 1,
15312 requirements: SurfaceRequirementSet::default().with(SurfaceRequirement::RenderEffect),
15313 });
15314
15315 assert!(root_direct_scene_events_are_supported(&scene));
15316 }
15317
15318 #[test]
15319 fn root_direct_scene_events_reject_root_local_backdrops() {
15320 let mut scene = CompositorScene::new();
15321 scene.backdrop_layers.push(BackdropLayer {
15322 node_id: Some(99),
15323 rect: Rect {
15324 x: 20.0,
15325 y: 30.0,
15326 width: 120.0,
15327 height: 80.0,
15328 },
15329 clip: None,
15330 snap_anchor: None,
15331 effect: RenderEffect::blur(6.0),
15332 z_index: 1,
15333 });
15334
15335 assert!(!root_direct_scene_events_are_supported(&scene));
15336 }
15337
15338 #[test]
15339 fn estimate_layer_surface_rect_includes_transformed_child_bounds() {
15340 let mut child = test_layer(
15341 Rect {
15342 x: 0.0,
15343 y: 0.0,
15344 width: 10.0,
15345 height: 6.0,
15346 },
15347 vec![RenderNode::Primitive(PrimitiveEntry {
15348 phase: PrimitivePhase::BeforeChildren,
15349 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15350 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15351 rect: Rect {
15352 x: 0.0,
15353 y: 0.0,
15354 width: 10.0,
15355 height: 6.0,
15356 },
15357 brush: Brush::solid(Color::WHITE),
15358 stroke: None,
15359 },
15360 clip: None,
15361 }),
15362 })],
15363 );
15364 child.transform_to_parent = ProjectiveTransform::translation(18.0, 7.0);
15365
15366 let parent = test_layer(
15367 Rect {
15368 x: 0.0,
15369 y: 0.0,
15370 width: 4.0,
15371 height: 4.0,
15372 },
15373 vec![RenderNode::Layer(Box::new(child))],
15374 );
15375
15376 assert_eq!(
15377 estimate_layer_surface_rect(&parent),
15378 Rect {
15379 x: 18.0,
15380 y: 7.0,
15381 width: 10.0,
15382 height: 6.0,
15383 }
15384 );
15385 }
15386
15387 #[test]
15388 fn estimate_layer_surface_rect_clips_translated_clip_layers_without_hidden_leading_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: 24.0,
15402 y: 0.0,
15403 width: 200.0,
15404 height: 480.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: 24.0,
15421 y: 0.0,
15422 width: 96.0,
15423 height: 72.0,
15424 }
15425 );
15426 }
15427
15428 #[test]
15429 fn estimate_layer_surface_rect_clips_active_horizontal_scroll_content() {
15430 let mut layer = test_layer(
15431 Rect {
15432 x: 0.0,
15433 y: 0.0,
15434 width: 120.0,
15435 height: 72.0,
15436 },
15437 vec![RenderNode::Primitive(PrimitiveEntry {
15438 phase: PrimitivePhase::BeforeChildren,
15439 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15440 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15441 rect: Rect {
15442 x: -24.0,
15443 y: 0.0,
15444 width: 200.0,
15445 height: 480.0,
15446 },
15447 brush: Brush::solid(Color::WHITE),
15448 stroke: None,
15449 },
15450 clip: None,
15451 }),
15452 })],
15453 );
15454 layer.translated_content_context = true;
15455 layer.motion_context_animated = true;
15456 layer.clip_to_bounds = true;
15457
15458 assert_eq!(
15459 estimate_layer_surface_rect(&layer),
15460 Rect {
15461 x: 0.0,
15462 y: 0.0,
15463 width: 120.0,
15464 height: 72.0,
15465 }
15466 );
15467 }
15468
15469 #[test]
15470 fn estimate_layer_surface_rect_clips_active_vertical_scroll_content() {
15471 let mut layer = test_layer(
15472 Rect {
15473 x: 0.0,
15474 y: 0.0,
15475 width: 120.0,
15476 height: 72.0,
15477 },
15478 vec![RenderNode::Primitive(PrimitiveEntry {
15479 phase: PrimitivePhase::BeforeChildren,
15480 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15481 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15482 rect: Rect {
15483 x: 0.0,
15484 y: -24.0,
15485 width: 120.0,
15486 height: 200.0,
15487 },
15488 brush: Brush::solid(Color::WHITE),
15489 stroke: None,
15490 },
15491 clip: None,
15492 }),
15493 })],
15494 );
15495 layer.translated_content_context = true;
15496 layer.motion_context_animated = true;
15497 layer.clip_to_bounds = true;
15498
15499 assert_eq!(
15500 estimate_layer_surface_rect(&layer),
15501 Rect {
15502 x: 0.0,
15503 y: 0.0,
15504 width: 120.0,
15505 height: 72.0,
15506 }
15507 );
15508 }
15509
15510 #[test]
15511 fn estimate_layer_surface_rect_keeps_shallow_scroll_capture_origin_stable() {
15512 fn shallow_scroll_surface_rect(content_y: f32) -> Rect {
15513 let mut layer = test_layer(
15514 Rect {
15515 x: 0.0,
15516 y: 0.0,
15517 width: 120.0,
15518 height: 72.0,
15519 },
15520 vec![RenderNode::Primitive(PrimitiveEntry {
15521 phase: PrimitivePhase::BeforeChildren,
15522 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15523 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15524 rect: Rect {
15525 x: 0.0,
15526 y: content_y,
15527 width: 120.0,
15528 height: 200.0,
15529 },
15530 brush: Brush::solid(Color::WHITE),
15531 stroke: None,
15532 },
15533 clip: None,
15534 }),
15535 })],
15536 );
15537 layer.translated_content_context = true;
15538 layer.motion_context_animated = true;
15539 layer.clip_to_bounds = true;
15540 estimate_layer_surface_rect(&layer)
15541 }
15542
15543 assert_eq!(
15544 shallow_scroll_surface_rect(-24.0),
15545 shallow_scroll_surface_rect(-25.0),
15546 "shallow scroll capture bounds must not move the offscreen surface origin on adjacent scroll positions"
15547 );
15548 }
15549
15550 #[test]
15551 fn estimate_layer_surface_rect_clips_active_xy_scroll_content() {
15552 let mut layer = test_layer(
15553 Rect {
15554 x: 0.0,
15555 y: 0.0,
15556 width: 120.0,
15557 height: 72.0,
15558 },
15559 vec![RenderNode::Primitive(PrimitiveEntry {
15560 phase: PrimitivePhase::BeforeChildren,
15561 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15562 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15563 rect: Rect {
15564 x: -16.0,
15565 y: -24.0,
15566 width: 180.0,
15567 height: 240.0,
15568 },
15569 brush: Brush::solid(Color::WHITE),
15570 stroke: None,
15571 },
15572 clip: None,
15573 }),
15574 })],
15575 );
15576 layer.translated_content_context = true;
15577 layer.motion_context_animated = true;
15578 layer.clip_to_bounds = true;
15579
15580 assert_eq!(
15581 estimate_layer_surface_rect(&layer),
15582 Rect {
15583 x: 0.0,
15584 y: 0.0,
15585 width: 120.0,
15586 height: 72.0,
15587 }
15588 );
15589 }
15590
15591 #[test]
15592 fn estimate_layer_surface_rect_clips_deep_hidden_active_scroll_content() {
15593 let mut layer = test_layer(
15594 Rect {
15595 x: 0.0,
15596 y: 0.0,
15597 width: 120.0,
15598 height: 72.0,
15599 },
15600 vec![RenderNode::Primitive(PrimitiveEntry {
15601 phase: PrimitivePhase::BeforeChildren,
15602 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15603 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15604 rect: Rect {
15605 x: 0.0,
15606 y: -1200.0,
15607 width: 120.0,
15608 height: 1400.0,
15609 },
15610 brush: Brush::solid(Color::WHITE),
15611 stroke: None,
15612 },
15613 clip: None,
15614 }),
15615 })],
15616 );
15617 layer.translated_content_context = true;
15618 layer.motion_context_animated = true;
15619 layer.clip_to_bounds = true;
15620
15621 assert_eq!(
15622 estimate_layer_surface_rect(&layer),
15623 Rect {
15624 x: 0.0,
15625 y: 0.0,
15626 width: 120.0,
15627 height: 72.0,
15628 }
15629 );
15630 }
15631
15632 #[test]
15633 fn estimate_layer_surface_rect_keeps_deep_scroll_capture_origin_stable() {
15634 fn deep_scroll_surface_rect(content_y: f32) -> Rect {
15635 let mut layer = test_layer(
15636 Rect {
15637 x: 0.0,
15638 y: 0.0,
15639 width: 120.0,
15640 height: 72.0,
15641 },
15642 vec![RenderNode::Primitive(PrimitiveEntry {
15643 phase: PrimitivePhase::BeforeChildren,
15644 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15645 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15646 rect: Rect {
15647 x: 0.0,
15648 y: content_y,
15649 width: 120.0,
15650 height: 1400.0,
15651 },
15652 brush: Brush::solid(Color::WHITE),
15653 stroke: None,
15654 },
15655 clip: None,
15656 }),
15657 })],
15658 );
15659 layer.translated_content_context = true;
15660 layer.motion_context_animated = true;
15661 layer.clip_to_bounds = true;
15662 estimate_layer_surface_rect(&layer)
15663 }
15664
15665 assert_eq!(
15666 deep_scroll_surface_rect(-1200.0),
15667 deep_scroll_surface_rect(-1201.0),
15668 "deep scroll capture bounds must not re-phase the offscreen surface origin on adjacent scroll positions"
15669 );
15670 }
15671
15672 #[test]
15673 fn motion_stable_capture_bounds_bounds_shadows_for_clipped_effect_layer() {
15674 let mut layer = test_layer(
15675 Rect {
15676 x: 0.0,
15677 y: 0.0,
15678 width: 120.0,
15679 height: 72.0,
15680 },
15681 vec![],
15682 );
15683 layer.clip_to_bounds = true;
15684 layer.graphics_layer.clip = true;
15685 layer.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
15686
15687 let mut shadow_shape = test_shape(0, BlendMode::SrcOver);
15688 shadow_shape.rect = Rect {
15689 x: -24.0,
15690 y: -1200.0,
15691 width: 180.0,
15692 height: 1400.0,
15693 };
15694 let mut scene = CompositorScene::new();
15695 scene
15696 .shadow_draws
15697 .push(test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]));
15698
15699 let requirements = SurfaceRequirementSet::default()
15700 .with(SurfaceRequirement::RenderEffect)
15701 .with(SurfaceRequirement::MotionStableCapture);
15702
15703 assert_eq!(
15704 motion_stable_capture_bounds(
15705 &layer,
15706 &scene,
15707 &[],
15708 requirements,
15709 TranslatedContentAxes::default(),
15710 None,
15711 ),
15712 Some(Rect {
15713 x: -360.0,
15714 y: -216.0,
15715 width: 480.0,
15716 height: 288.0,
15717 })
15718 );
15719 }
15720
15721 #[test]
15722 fn vertical_motion_stable_capture_uses_viewport_cross_axis_bounds() {
15723 let mut layer = test_layer(
15724 Rect {
15725 x: 0.0,
15726 y: 0.0,
15727 width: 200.0,
15728 height: 100.0,
15729 },
15730 vec![],
15731 );
15732 layer.clip_to_bounds = true;
15733 layer.graphics_layer.clip = true;
15734
15735 let mut shape = test_shape(0, BlendMode::SrcOver);
15736 shape.rect = Rect {
15737 x: 60.0,
15738 y: -80.0,
15739 width: 80.0,
15740 height: 220.0,
15741 };
15742 let mut scene = CompositorScene::new();
15743 scene.shapes.push(shape);
15744
15745 let requirements =
15746 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
15747
15748 assert_eq!(
15749 motion_stable_capture_bounds(
15750 &layer,
15751 &scene,
15752 &[],
15753 requirements,
15754 TranslatedContentAxes { x: false, y: true },
15755 None,
15756 ),
15757 Some(Rect {
15758 x: -96.0,
15759 y: -64.0,
15760 width: 296.0,
15761 height: 164.0,
15762 })
15763 );
15764 }
15765
15766 #[test]
15767 fn vertical_motion_stable_capture_uses_external_surface_clip() {
15768 let layer = test_layer(
15769 Rect {
15770 x: 0.0,
15771 y: 0.0,
15772 width: 200.0,
15773 height: 100.0,
15774 },
15775 vec![],
15776 );
15777
15778 let mut shape = test_shape(0, BlendMode::SrcOver);
15779 shape.rect = Rect {
15780 x: 60.0,
15781 y: -80.0,
15782 width: 80.0,
15783 height: 220.0,
15784 };
15785 let mut scene = CompositorScene::new();
15786 scene.shapes.push(shape);
15787
15788 let requirements =
15789 SurfaceRequirementSet::default().with(SurfaceRequirement::MotionStableCapture);
15790
15791 assert_eq!(
15792 motion_stable_capture_bounds(
15793 &layer,
15794 &scene,
15795 &[],
15796 requirements,
15797 TranslatedContentAxes { x: false, y: true },
15798 Some(Rect {
15799 x: 0.0,
15800 y: 0.0,
15801 width: 200.0,
15802 height: 100.0,
15803 }),
15804 ),
15805 Some(Rect {
15806 x: -96.0,
15807 y: -64.0,
15808 width: 296.0,
15809 height: 164.0,
15810 })
15811 );
15812 }
15813
15814 #[test]
15815 fn estimate_layer_surface_rect_expands_for_child_layer_shadow() {
15816 let mut child = test_layer(
15817 Rect {
15818 x: 0.0,
15819 y: 0.0,
15820 width: 12.0,
15821 height: 8.0,
15822 },
15823 vec![],
15824 );
15825 child.transform_to_parent = ProjectiveTransform::translation(20.0, 9.0);
15826 child.graphics_layer.shadow_elevation = 6.0;
15827
15828 let parent = test_layer(
15829 Rect {
15830 x: 0.0,
15831 y: 0.0,
15832 width: 4.0,
15833 height: 4.0,
15834 },
15835 vec![RenderNode::Layer(Box::new(child))],
15836 );
15837
15838 let rect = estimate_layer_surface_rect(&parent);
15839 assert!(rect.x < 20.0);
15840 assert!(rect.y < 9.0);
15841 assert!(rect.width > 12.0);
15842 assert!(rect.height > 8.0);
15843 }
15844
15845 #[test]
15846 fn estimate_layer_surface_rect_respects_local_bounds_for_effect_layers() {
15847 let mut layer = test_layer(
15848 Rect {
15849 x: 0.0,
15850 y: 0.0,
15851 width: 28.0,
15852 height: 28.0,
15853 },
15854 vec![RenderNode::Primitive(PrimitiveEntry {
15855 phase: PrimitivePhase::BeforeChildren,
15856 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15857 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15858 rect: Rect {
15859 x: 10.0,
15860 y: 10.0,
15861 width: 10.0,
15862 height: 10.0,
15863 },
15864 brush: Brush::solid(Color::WHITE),
15865 stroke: None,
15866 },
15867 clip: None,
15868 }),
15869 })],
15870 );
15871 layer.graphics_layer.render_effect = Some(RenderEffect::blur(12.0));
15872
15873 assert_eq!(
15874 estimate_layer_surface_rect(&layer),
15875 Rect {
15876 x: 0.0,
15877 y: 0.0,
15878 width: 28.0,
15879 height: 28.0,
15880 }
15881 );
15882 }
15883
15884 #[test]
15885 fn layer_raster_cache_candidate_ignores_parent_transform() {
15886 let primitive = PrimitiveEntry {
15887 phase: PrimitivePhase::BeforeChildren,
15888 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15889 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15890 rect: Rect {
15891 x: 2.0,
15892 y: 3.0,
15893 width: 6.0,
15894 height: 4.0,
15895 },
15896 brush: Brush::solid(Color::BLACK),
15897 stroke: None,
15898 },
15899 clip: None,
15900 }),
15901 };
15902 let base = cacheable_layer(
15903 41,
15904 Rect {
15905 x: 0.0,
15906 y: 0.0,
15907 width: 20.0,
15908 height: 20.0,
15909 },
15910 vec![RenderNode::Primitive(primitive.clone())],
15911 );
15912 let mut moved = base.clone();
15913 moved.transform_to_parent = ProjectiveTransform::translation(32.0, 18.0);
15914
15915 assert_eq!(
15916 layer_raster_cache_candidate(&base, 1.25, false, false),
15917 layer_raster_cache_candidate(&moved, 1.25, false, false)
15918 );
15919 }
15920
15921 #[test]
15922 fn layer_raster_cache_candidate_changes_for_translated_content_offset() {
15923 let primitive = PrimitiveEntry {
15924 phase: PrimitivePhase::BeforeChildren,
15925 node: PrimitiveNode::Draw(DrawPrimitiveNode {
15926 primitive: cranpose_ui_graphics::DrawPrimitive::Rect {
15927 rect: Rect {
15928 x: 2.0,
15929 y: 3.0,
15930 width: 6.0,
15931 height: 4.0,
15932 },
15933 brush: Brush::solid(Color::BLACK),
15934 stroke: None,
15935 },
15936 clip: None,
15937 }),
15938 };
15939 let mut base = cacheable_layer(
15940 42,
15941 Rect {
15942 x: 0.0,
15943 y: 0.0,
15944 width: 20.0,
15945 height: 20.0,
15946 },
15947 vec![RenderNode::Primitive(primitive)],
15948 );
15949 base.translated_content_context = true;
15950 base.translated_content_offset = Point::new(0.0, -8.0);
15951 base.recompute_raster_cache_hashes();
15952
15953 let mut moved = base.clone();
15954 moved.translated_content_offset = Point::new(0.0, -16.0);
15955 moved.recompute_raster_cache_hashes();
15956
15957 assert_ne!(
15958 layer_raster_cache_candidate(&base, 1.25, false, false),
15959 layer_raster_cache_candidate(&moved, 1.25, false, false),
15960 "full-surface layer cache candidates must not alias different scroll offsets"
15961 );
15962 }
15963
15964 #[test]
15965 fn layer_raster_cache_candidate_changes_for_child_transform() {
15966 let mut child = cacheable_layer(
15967 8,
15968 Rect {
15969 x: 0.0,
15970 y: 0.0,
15971 width: 12.0,
15972 height: 10.0,
15973 },
15974 vec![],
15975 );
15976 child.transform_to_parent = ProjectiveTransform::translation(4.0, 6.0);
15977 let base = cacheable_layer(
15978 7,
15979 Rect {
15980 x: 0.0,
15981 y: 0.0,
15982 width: 20.0,
15983 height: 20.0,
15984 },
15985 vec![RenderNode::Layer(Box::new(child.clone()))],
15986 );
15987 let mut moved_child = child;
15988 moved_child.transform_to_parent = ProjectiveTransform::translation(9.0, 6.0);
15989 let moved = cacheable_layer(
15990 7,
15991 Rect {
15992 x: 0.0,
15993 y: 0.0,
15994 width: 20.0,
15995 height: 20.0,
15996 },
15997 vec![RenderNode::Layer(Box::new(moved_child))],
15998 );
15999
16000 assert_ne!(
16001 layer_raster_cache_candidate(&base, 1.0, false, false),
16002 layer_raster_cache_candidate(&moved, 1.0, false, false)
16003 );
16004 }
16005
16006 #[test]
16007 fn layer_raster_cache_candidate_rejects_external_backdrop_dependency() {
16008 let mut child = cacheable_layer(
16009 12,
16010 Rect {
16011 x: 0.0,
16012 y: 0.0,
16013 width: 8.0,
16014 height: 8.0,
16015 },
16016 vec![],
16017 );
16018 child.graphics_layer.backdrop_effect = Some(RenderEffect::blur(2.0));
16019 let parent = cacheable_layer(
16020 11,
16021 Rect {
16022 x: 0.0,
16023 y: 0.0,
16024 width: 16.0,
16025 height: 16.0,
16026 },
16027 vec![RenderNode::Layer(Box::new(child))],
16028 );
16029
16030 assert!(layer_raster_cache_candidate(&parent, 1.0, false, false).is_some());
16031 assert!(layer_raster_cache_candidate(&parent, 1.0, true, false).is_none());
16032 }
16033
16034 #[test]
16035 fn layer_raster_cache_candidate_does_not_force_translation_only_text_surfaces() {
16036 let text = RenderNode::Primitive(PrimitiveEntry {
16037 phase: PrimitivePhase::BeforeChildren,
16038 node: PrimitiveNode::Text(Box::new(TextPrimitiveNode {
16039 node_id: 77,
16040 rect: Rect {
16041 x: 2.0,
16042 y: 3.0,
16043 width: 48.0,
16044 height: 18.0,
16045 },
16046 text: std::rc::Rc::new(AnnotatedString::from("runtime cache")),
16047 text_style: TextStyle::default(),
16048 font_size: 14.0,
16049 layout_options: TextLayoutOptions::default(),
16050 clip: None,
16051 })),
16052 });
16053 let mut layer = test_layer(
16054 Rect {
16055 x: 0.0,
16056 y: 0.0,
16057 width: 64.0,
16058 height: 32.0,
16059 },
16060 vec![text],
16061 );
16062 layer.node_id = Some(77);
16063 layer.recompute_raster_cache_hashes();
16064
16065 assert!(
16066 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
16067 "root path should not isolate plain translation-only text layers"
16068 );
16069 assert!(
16070 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
16071 "child path should also render plain translation-only text layers directly"
16072 );
16073 }
16074
16075 #[test]
16076 fn layer_raster_cache_candidate_allows_stable_runtime_child_effect_surfaces() {
16077 let mut layer = test_layer(
16078 Rect {
16079 x: 0.0,
16080 y: 0.0,
16081 width: 64.0,
16082 height: 32.0,
16083 },
16084 vec![RenderNode::Primitive(PrimitiveEntry {
16085 phase: PrimitivePhase::BeforeChildren,
16086 node: PrimitiveNode::Draw(DrawPrimitiveNode {
16087 primitive: DrawPrimitive::Rect {
16088 rect: Rect {
16089 x: 0.0,
16090 y: 0.0,
16091 width: 64.0,
16092 height: 32.0,
16093 },
16094 brush: Brush::solid(Color::WHITE),
16095 stroke: None,
16096 },
16097 clip: None,
16098 }),
16099 })],
16100 );
16101 layer.node_id = Some(78);
16102 layer.graphics_layer.render_effect = Some(RenderEffect::blur(4.0));
16103 layer.recompute_raster_cache_hashes();
16104
16105 assert!(
16106 layer_raster_cache_candidate(&layer, 1.0, false, false).is_none(),
16107 "root direct path should not force-cache ordinary stable effects"
16108 );
16109 assert!(
16110 layer_raster_cache_candidate(&layer, 1.0, false, true).is_some(),
16111 "child surface rendering should retain stable non-runtime effects"
16112 );
16113 }
16114
16115 #[test]
16116 fn layer_raster_cache_candidate_rejects_runtime_shader_child_effect_surfaces() {
16117 let mut layer = test_layer(
16118 Rect {
16119 x: 0.0,
16120 y: 0.0,
16121 width: 64.0,
16122 height: 32.0,
16123 },
16124 vec![],
16125 );
16126 layer.node_id = Some(79);
16127 layer.graphics_layer.render_effect = Some(RenderEffect::runtime_shader(
16128 RuntimeShader::new("runtime shader"),
16129 ));
16130 layer.recompute_raster_cache_hashes();
16131
16132 assert!(
16133 layer_raster_cache_candidate(&layer, 1.0, false, true).is_none(),
16134 "runtime shaders must not fill the retained layer cache with per-frame uniform variants"
16135 );
16136 }
16137
16138 #[test]
16139 fn layer_surface_requirements_keep_plain_text_on_direct_path() {
16140 let layer = text_layer_with_style(AnnotatedString::from("plain"), TextStyle::default());
16141
16142 let requirements = layer_surface_requirements(&layer);
16143
16144 assert_eq!(requirements.direct_translation, Some(Point::default()));
16145 assert!(requirements
16146 .surface_requirements
16147 .contains(SurfaceRequirement::PixelStableComposite));
16148 assert!(!requirements
16149 .surface_requirements
16150 .has_isolating_requirement());
16151 }
16152
16153 #[test]
16154 fn layer_surface_requirements_keep_translated_plain_text_leaf_on_direct_path() {
16155 let layer = pure_text_leaf(false, true);
16156
16157 let requirements = layer_surface_requirements(&layer);
16158
16159 assert_eq!(
16160 requirements.direct_translation,
16161 Some(Point::new(11.4, 23.6))
16162 );
16163 assert!(
16164 requirements
16165 .surface_requirements
16166 .contains(SurfaceRequirement::PixelStableComposite)
16167 && !requirements
16168 .surface_requirements
16169 .has_isolating_requirement(),
16170 "translated plain text should stay on the direct path and isolate only the glyph draw"
16171 );
16172 }
16173
16174 #[test]
16175 fn layer_surface_requirements_keep_translated_text_leaf_with_background_on_direct_path() {
16176 let layer = snapped_text_leaf(false, true);
16177
16178 let requirements = layer_surface_requirements(&layer);
16179
16180 assert_eq!(
16181 requirements.direct_translation,
16182 Some(Point::new(14.25, 16.5))
16183 );
16184 assert!(
16185 requirements
16186 .surface_requirements
16187 .contains(SurfaceRequirement::PixelStableComposite)
16188 && !requirements
16189 .surface_requirements
16190 .has_isolating_requirement(),
16191 "translated text with direct sibling decoration/background should keep the layer direct"
16192 );
16193 }
16194
16195 #[test]
16196 fn translated_plain_text_uses_bounded_snap_surface() {
16197 let root = pure_text_leaf_root(true, true);
16198 let mut rect_cache = HashMap::new();
16199 let mut requirements_cache = HashMap::new();
16200 let collected =
16201 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16202
16203 assert_eq!(collected.child_layers.len(), 1);
16204 assert!(collected.scene.texts.is_empty());
16205 assert!(collected.scene.effect_layers.is_empty());
16206 assert_snap_anchor_close(
16207 collected.child_layers[0].snap_anchor,
16208 Point::new(11.4, 23.6),
16209 "translated plain text's bounded local surface should composite at the content-origin snap phase",
16210 );
16211 }
16212
16213 #[test]
16217 #[ignore]
16218 fn shape_run_collect_timing_harness() {
16219 use cranpose_render_common::graph::DrawPrimitiveNode;
16220 use cranpose_render_common::layer_composition::local_content_layer_for;
16221 use cranpose_ui_graphics::Stroke;
16222
16223 let bounds = Rect {
16224 x: 0.0,
16225 y: 0.0,
16226 width: 1080.0,
16227 height: 2244.0,
16228 };
16229 let graphics_layer = GraphicsLayer::default();
16230
16231 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
16234 for i in 0..3000u32 {
16235 let f = i as f32;
16236 let brush = if i % 8 == 0 {
16237 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
16238 } else {
16239 Brush::Solid(Color(0.5, 0.2, 0.8, 1.0))
16240 };
16241 let center = Point::new(540.0 + (f % 400.0), 1122.0 + (f % 350.0));
16242 let radius = 8.0 + (i % 23) as f32;
16243 let half = radius + 4.0;
16244 nodes.push(DrawPrimitiveNode {
16245 primitive: DrawPrimitive::Arc {
16246 rect: Rect {
16247 x: center.x - half,
16248 y: center.y - half,
16249 width: half * 2.0,
16250 height: half * 2.0,
16251 },
16252 brush,
16253 center,
16254 radius,
16255 start_angle: f * 0.07,
16256 sweep_angle: 0.5 + (i % 5) as f32,
16257 stroke: (i % 3 != 0).then(|| Stroke::new(4.0)),
16258 inner_radius: if i % 3 == 0 { radius * 0.6 } else { 0.0 },
16259 },
16260 clip: None,
16261 });
16262 }
16263
16264 let children: Vec<RenderNode> = nodes
16265 .iter()
16266 .map(|node| {
16267 RenderNode::Primitive(PrimitiveEntry {
16268 phase: PrimitivePhase::BeforeChildren,
16269 node: PrimitiveNode::Draw(node.clone()),
16270 })
16271 })
16272 .collect();
16273 let layer = crate::test_support::layer_node(
16274 bounds,
16275 ProjectiveTransform::identity(),
16276 graphics_layer,
16277 children,
16278 );
16279
16280 const ITERS: usize = 300;
16281
16282 let local_layer = local_content_layer_for(&layer.graphics_layer);
16284 let start = Instant::now();
16285 let mut sink_shapes = 0usize;
16286 for _ in 0..ITERS {
16287 let mut scene = CompositorScene::new();
16288 for node in &nodes {
16289 crate::pipeline::push_draw_primitive(
16290 &node.primitive,
16291 bounds,
16292 &local_layer,
16293 None,
16294 &mut scene,
16295 None,
16296 false,
16297 );
16298 }
16299 sink_shapes = scene.shapes.len();
16300 }
16301 let serial = start.elapsed();
16302
16303 let mut rect_cache = HashMap::new();
16304 let mut requirements_cache = HashMap::new();
16305 let start = Instant::now();
16306 let mut run_shapes = 0usize;
16307 for _ in 0..ITERS {
16308 let collected = collect_layer_contents(
16309 &layer,
16310 None,
16311 None,
16312 &mut rect_cache,
16313 &mut requirements_cache,
16314 );
16315 run_shapes = collected.scene.shapes.len();
16316 }
16317 let run = start.elapsed();
16318
16319 println!(
16320 "per-primitive: {:?}/iter ({sink_shapes} shapes) shape-run: {:?}/iter ({run_shapes} shapes)",
16321 serial / ITERS as u32,
16322 run / ITERS as u32,
16323 );
16324 }
16325
16326 fn assert_shape_run_collect_matches_per_primitive_emission() {
16328 use cranpose_render_common::graph::DrawPrimitiveNode;
16329 use cranpose_render_common::layer_composition::local_content_layer_for;
16330 use cranpose_render_common::primitive_emit::{resolve_primitive_clip, PrimitiveClipSpace};
16331 use cranpose_ui_graphics::{CornerRadii, Stroke};
16332
16333 let bounds = Rect {
16334 x: 0.0,
16335 y: 0.0,
16336 width: 800.0,
16337 height: 800.0,
16338 };
16339 let graphics_layer = GraphicsLayer {
16342 scale: 1.25,
16343 translation_x: 3.5,
16344 translation_y: -2.0,
16345 alpha: 0.9,
16346 rotation_z: 0.35,
16347 ..GraphicsLayer::default()
16348 };
16349
16350 let mut nodes: Vec<DrawPrimitiveNode> = Vec::new();
16351 for i in 0..600u32 {
16352 let f = i as f32;
16353 let brush = if i % 11 == 0 {
16354 Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])
16355 } else {
16356 Brush::Solid(Color(0.1 + (i % 7) as f32 * 0.1, 0.5, 0.9, 1.0))
16357 };
16358 let stroke = (i % 5 == 0).then(|| Stroke::new(1.0 + (i % 3) as f32));
16359 let primitive = match i % 3 {
16360 0 => DrawPrimitive::Rect {
16361 rect: Rect {
16362 x: f % 37.0,
16363 y: f % 53.0,
16364 width: 8.0 + f % 9.0,
16365 height: 6.0 + f % 5.0,
16366 },
16367 brush,
16368 stroke,
16369 },
16370 1 => DrawPrimitive::RoundRect {
16371 rect: Rect {
16372 x: f % 41.0,
16373 y: f % 43.0,
16374 width: 12.0,
16375 height: 10.0,
16376 },
16377 brush,
16378 radii: CornerRadii::uniform(2.0 + (i % 4) as f32),
16379 stroke,
16380 },
16381 _ => {
16382 let center = Point::new(60.0 + f % 71.0, 60.0 + f % 67.0);
16383 let radius = 5.0 + (i % 13) as f32;
16384 let sweep_angle = if i == 302 { 0.0 } else { 0.4 + (i % 6) as f32 };
16386 let half = radius + 4.0;
16387 DrawPrimitive::Arc {
16388 rect: Rect {
16389 x: center.x - half,
16390 y: center.y - half,
16391 width: half * 2.0,
16392 height: half * 2.0,
16393 },
16394 brush,
16395 center,
16396 radius,
16397 start_angle: f * 0.11,
16398 sweep_angle,
16399 stroke: (i % 2 == 0).then(|| Stroke::new(3.0)),
16400 inner_radius: if i % 4 == 2 { radius * 0.5 } else { 0.0 },
16401 }
16402 }
16403 };
16404 let primitive = if i == 300 {
16405 DrawPrimitive::Blend {
16409 primitive: Box::new(DrawPrimitive::Blend {
16410 primitive: Box::new(primitive),
16411 blend_mode: BlendMode::SrcOver,
16412 }),
16413 blend_mode: BlendMode::DstOut,
16414 }
16415 } else if i % 7 == 3 {
16416 DrawPrimitive::Blend {
16417 primitive: Box::new(primitive),
16418 blend_mode: BlendMode::DstOut,
16419 }
16420 } else {
16421 primitive
16422 };
16423 let clip = (i % 31 == 7).then_some(Rect {
16424 x: 0.0,
16425 y: 0.0,
16426 width: 30.0,
16427 height: 30.0,
16428 });
16429 nodes.push(DrawPrimitiveNode { primitive, clip });
16430 }
16431
16432 let children: Vec<RenderNode> = nodes
16433 .iter()
16434 .map(|node| {
16435 RenderNode::Primitive(PrimitiveEntry {
16436 phase: PrimitivePhase::BeforeChildren,
16437 node: PrimitiveNode::Draw(node.clone()),
16438 })
16439 })
16440 .collect();
16441 let layer = crate::test_support::layer_node(
16442 bounds,
16443 ProjectiveTransform::identity(),
16444 graphics_layer,
16445 children,
16446 );
16447
16448 let mut rect_cache = HashMap::new();
16449 let mut requirements_cache = HashMap::new();
16450 let collected =
16451 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
16452
16453 let local_layer = local_content_layer_for(&layer.graphics_layer);
16456 let mut expected = CompositorScene::new();
16457 for node in &nodes {
16458 let clip = resolve_primitive_clip(
16459 node.clip,
16460 bounds,
16461 &local_layer,
16462 None,
16463 PrimitiveClipSpace::Local,
16464 );
16465 if node.clip.is_some() && clip.is_none() {
16466 continue;
16467 }
16468 crate::pipeline::push_draw_primitive(
16469 &node.primitive,
16470 bounds,
16471 &local_layer,
16472 clip,
16473 &mut expected,
16474 None,
16475 false,
16476 );
16477 }
16478
16479 assert!(
16480 collected.scene.shapes.len() >= 590,
16481 "the runs should engage the parallel branch: got {} shapes",
16482 collected.scene.shapes.len()
16483 );
16484 assert_eq!(collected.scene.shapes.len(), expected.shapes.len());
16485 assert_eq!(collected.scene.draw_ops, expected.draw_ops);
16486 assert_eq!(collected.scene.next_z, expected.next_z);
16487 assert!(
16488 collected
16489 .scene
16490 .shapes
16491 .iter()
16492 .all(|s| s.snap_anchor.is_none()),
16493 "a rotated layer must not rigid-snap; the reference scene assumes it"
16494 );
16495 for (index, (got, want)) in collected
16496 .scene
16497 .shapes
16498 .iter()
16499 .zip(&expected.shapes)
16500 .enumerate()
16501 {
16502 assert_eq!(got.rect, want.rect, "shape {index} rect");
16503 assert_eq!(got.local_rect, want.local_rect, "shape {index} local_rect");
16504 assert_eq!(got.quad, want.quad, "shape {index} quad");
16505 assert_eq!(got.snap_anchor, want.snap_anchor, "shape {index} snap");
16506 assert_eq!(got.brush, want.brush, "shape {index} brush");
16507 assert_eq!(got.shape, want.shape, "shape {index} shape");
16508 assert_eq!(got.stroke, want.stroke, "shape {index} stroke");
16509 assert_eq!(got.arc, want.arc, "shape {index} arc");
16510 assert_eq!(got.z_index, want.z_index, "shape {index} z");
16511 assert_eq!(got.clip, want.clip, "shape {index} clip");
16512 assert_eq!(got.blend_mode, want.blend_mode, "shape {index} blend");
16513 assert_eq!(
16514 got.motion_context_animated, want.motion_context_animated,
16515 "shape {index} motion flag"
16516 );
16517 }
16518 }
16519
16520 #[test]
16525 fn shape_run_collect_matches_per_primitive_emission_exactly() {
16526 assert_shape_run_collect_matches_per_primitive_emission();
16527 crate::normalized_scene::force_shape_run_parallel_for_tests(true);
16528 let outcome =
16529 std::panic::catch_unwind(assert_shape_run_collect_matches_per_primitive_emission);
16530 crate::normalized_scene::force_shape_run_parallel_for_tests(false);
16531 if let Err(payload) = outcome {
16532 std::panic::resume_unwind(payload);
16533 }
16534 }
16535
16536 #[test]
16537 fn non_translated_text_local_surface_keeps_linear_composite_resolve() {
16538 let layer = text_layer_with_style(
16539 AnnotatedString::from("gradient"),
16540 TextStyle::from_span_style(SpanStyle {
16541 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16542 ..SpanStyle::default()
16543 }),
16544 );
16545 let requirements = layer_surface_requirements(&layer);
16546
16547 assert!(requirements
16548 .surface_requirements
16549 .contains(SurfaceRequirement::TextMaterialMask));
16550 assert_eq!(
16551 composite_sample_mode_for_requirements(false, false, requirements),
16552 CompositeSampleMode::Linear
16553 );
16554 }
16555
16556 #[test]
16557 fn inherited_translated_text_local_surface_uses_box4_layer_surface() {
16558 let layer = text_layer_with_style(
16559 AnnotatedString::from("shadow"),
16560 TextStyle::from_span_style(SpanStyle {
16561 shadow: Some(Shadow {
16562 color: Color::BLACK,
16563 offset: Point::new(1.0, 2.0),
16564 blur_radius: 3.0,
16565 }),
16566 ..SpanStyle::default()
16567 }),
16568 );
16569 let requirements = layer_surface_requirements(&layer);
16570
16571 assert!(requirements
16572 .surface_requirements
16573 .contains(SurfaceRequirement::TextMaterialMask));
16574 assert_eq!(
16575 composite_sample_mode_for_requirements(true, false, requirements),
16576 CompositeSampleMode::Box4
16577 );
16578 assert_eq!(
16579 layer_surface_target_scale(
16580 true,
16581 false,
16582 requirements,
16583 1.25,
16584 layer_surface_scale(&layer)
16585 ),
16586 SurfaceRequirementSet::default()
16587 .with(SurfaceRequirement::TextMaterialMask)
16588 .with(SurfaceRequirement::MotionStableCapture)
16589 .target_scale(1.25, 1.0)
16590 );
16591 }
16592
16593 #[test]
16594 fn translated_text_local_surface_inside_capture_keeps_parent_scale() {
16595 let layer = text_layer_with_style(
16596 AnnotatedString::from("shadow"),
16597 TextStyle::from_span_style(SpanStyle {
16598 shadow: Some(Shadow {
16599 color: Color::BLACK,
16600 offset: Point::new(1.0, 2.0),
16601 blur_radius: 3.0,
16602 }),
16603 ..SpanStyle::default()
16604 }),
16605 );
16606 let requirements = layer_surface_requirements(&layer);
16607
16608 assert_eq!(
16609 composite_sample_mode_for_requirements(true, true, requirements),
16610 CompositeSampleMode::Linear
16611 );
16612 assert_eq!(
16613 layer_surface_target_scale(true, true, requirements, 10.0, layer_surface_scale(&layer)),
16614 SurfaceRequirementSet::default()
16615 .with(SurfaceRequirement::TextMaterialMask)
16616 .target_scale(10.0, 1.0)
16617 );
16618 }
16619
16620 #[test]
16621 fn layer_surface_requirements_use_local_surface_for_gradient_and_stroke_text() {
16622 let cases = [
16623 (
16624 "draw_style",
16625 AnnotatedString::from("draw_style"),
16626 TextStyle::from_span_style(SpanStyle {
16627 draw_style: Some(TextDrawStyle::Stroke { width: 2.0 }),
16628 ..SpanStyle::default()
16629 }),
16630 ),
16631 (
16632 "gradient_brush",
16633 AnnotatedString::from("gradient"),
16634 TextStyle::from_span_style(SpanStyle {
16635 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16636 ..SpanStyle::default()
16637 }),
16638 ),
16639 ];
16640
16641 for (label, text, text_style) in cases {
16642 let layer = text_layer_with_style(text, text_style);
16643 let requirements = layer_surface_requirements(&layer);
16644 assert!(
16645 requirements
16646 .surface_requirements
16647 .contains(SurfaceRequirement::TextMaterialMask),
16648 "{label} text should use a bounded local surface: {requirements:?}"
16649 );
16650 }
16651 }
16652
16653 #[test]
16654 fn layer_surface_requirements_use_local_surface_for_complex_text_effects() {
16655 let cases = [
16656 (
16657 "shadow",
16658 AnnotatedString::from("shadow"),
16659 TextStyle::from_span_style(SpanStyle {
16660 shadow: Some(Shadow {
16661 color: Color::BLACK,
16662 offset: Point::new(1.0, 2.0),
16663 blur_radius: 3.0,
16664 }),
16665 ..SpanStyle::default()
16666 }),
16667 ),
16668 (
16669 "background",
16670 AnnotatedString::from("background"),
16671 TextStyle::from_span_style(SpanStyle {
16672 background: Some(Color::BLACK),
16673 ..SpanStyle::default()
16674 }),
16675 ),
16676 (
16677 "baseline_shift",
16678 AnnotatedString::from("baseline_shift"),
16679 TextStyle::from_span_style(SpanStyle {
16680 baseline_shift: Some(BaselineShift::SUPERSCRIPT),
16681 ..SpanStyle::default()
16682 }),
16683 ),
16684 (
16685 "geometric_transform",
16686 AnnotatedString::from("geometric_transform"),
16687 TextStyle::from_span_style(SpanStyle {
16688 text_geometric_transform: Some(TextGeometricTransform {
16689 scale_x: 1.2,
16690 skew_x: 0.15,
16691 }),
16692 ..SpanStyle::default()
16693 }),
16694 ),
16695 (
16696 "letter_spacing",
16697 AnnotatedString::from("letter_spacing"),
16698 TextStyle::from_span_style(SpanStyle {
16699 letter_spacing: TextUnit::Em(0.2),
16700 ..SpanStyle::default()
16701 }),
16702 ),
16703 ];
16704
16705 for (label, text, text_style) in cases {
16706 let layer = text_layer_with_style(text, text_style);
16707 let requirements = layer_surface_requirements(&layer);
16708 assert!(
16709 requirements
16710 .surface_requirements
16711 .contains(SurfaceRequirement::TextMaterialMask),
16712 "{label} text should use a bounded local surface: {requirements:?}"
16713 );
16714 assert_eq!(
16715 requirements.direct_translation,
16716 Some(Point::default()),
16717 "{label} text should still classify as a direct translation"
16718 );
16719 }
16720 }
16721
16722 #[test]
16723 fn layer_surface_requirements_color_only_span_styles_use_direct_path() {
16724 let layer = text_layer_with_style(
16725 AnnotatedString {
16726 text: "styled".to_string(),
16727 span_styles: vec![RangeStyle {
16728 item: SpanStyle {
16729 color: Some(Color::BLACK),
16730 ..SpanStyle::default()
16731 },
16732 range: 0..3,
16733 }],
16734 ..AnnotatedString::default()
16735 },
16736 TextStyle::default(),
16737 );
16738 let requirements = layer_surface_requirements(&layer);
16739 assert!(
16740 !requirements
16741 .surface_requirements
16742 .contains(SurfaceRequirement::TextMaterialMask),
16743 "color-only span styles should render directly via software text raster colors"
16744 );
16745 }
16746
16747 #[test]
16748 fn layer_surface_requirements_keep_decoration_only_text_on_direct_path() {
16749 let layer = text_layer_with_style(
16750 AnnotatedString::from("decoration"),
16751 TextStyle::from_span_style(SpanStyle {
16752 text_decoration: Some(TextDecoration::UNDERLINE),
16753 ..SpanStyle::default()
16754 }),
16755 );
16756
16757 let requirements = layer_surface_requirements(&layer);
16758
16759 assert_eq!(requirements.direct_translation, Some(Point::default()));
16760 assert!(
16761 requirements
16762 .surface_requirements
16763 .contains(SurfaceRequirement::PixelStableComposite)
16764 && !requirements
16765 .surface_requirements
16766 .has_isolating_requirement(),
16767 "decoration-only text should not force an isolating layer surface: {requirements:?}"
16768 );
16769 }
16770
16771 #[test]
16772 fn direct_text_leaf_snaps_modifier_background_and_text_with_one_anchor() {
16773 let root = snapped_text_leaf_root(false, false);
16774 let mut rect_cache = HashMap::new();
16775 let mut requirements_cache = HashMap::new();
16776
16777 let collected =
16778 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16779
16780 assert_eq!(collected.scene.shapes.len(), 1);
16781 assert_eq!(collected.scene.images.len(), 1);
16782 assert_eq!(collected.scene.texts.len(), 1);
16783 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
16784 assert_eq!(collected.scene.shapes[0].snap_anchor, expected_anchor);
16785 assert_eq!(collected.scene.images[0].snap_anchor, expected_anchor);
16786 assert_eq!(collected.scene.texts[0].snap_anchor, expected_anchor);
16787 }
16788
16789 #[test]
16790 fn animated_translated_content_text_leaf_uses_bounded_content_snap() {
16791 let root = snapped_text_leaf_root(true, true);
16792 let mut rect_cache = HashMap::new();
16793 let mut requirements_cache = HashMap::new();
16794
16795 let collected =
16796 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16797
16798 assert_eq!(collected.child_layers.len(), 1);
16799 assert!(collected.scene.shapes.is_empty());
16800 assert!(collected.scene.images.is_empty());
16801 assert!(collected.scene.texts.is_empty());
16802 assert!(collected.scene.effect_layers.is_empty());
16803 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
16804 assert_eq!(
16805 collected.child_layers[0].snap_anchor, expected_anchor,
16806 "active translated leaf surface should keep the content-origin snap phase"
16807 );
16808 }
16809
16810 #[test]
16811 fn translated_content_assigns_motion_anchor_to_rotated_child_surface() {
16812 let mut child = snapped_text_leaf(false, false);
16813 child.graphics_layer.rotation_z = 5.0;
16814 child.transform_to_parent =
16815 cranpose_render_common::layer_transform::layer_transform_to_parent(
16816 child.local_bounds,
16817 Point::new(108.0, 3.0),
16818 &child.graphics_layer,
16819 );
16820 child.recompute_raster_cache_hashes();
16821 let mut root = test_layer(
16822 Rect {
16823 x: 0.0,
16824 y: 0.0,
16825 width: 320.0,
16826 height: 180.0,
16827 },
16828 vec![RenderNode::Layer(Box::new(child))],
16829 );
16830 root.translated_content_context = true;
16831 root.translated_content_offset = Point::new(0.0, -80.8);
16832 root.recompute_raster_cache_hashes();
16833 let mut rect_cache = HashMap::new();
16834 let mut requirements_cache = HashMap::new();
16835
16836 let collected =
16837 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16838
16839 assert_eq!(collected.child_layers.len(), 1);
16840 assert!(
16841 collected.child_layers[0].snap_anchor.is_some(),
16842 "a projective child still translates rigidly with its scrolling parent"
16843 );
16844 }
16845
16846 #[test]
16847 fn rested_translated_content_context_text_leaf_snaps_for_crisp_scroll_rest() {
16848 let root = snapped_text_leaf_root(false, true);
16849 let mut rect_cache = HashMap::new();
16850 let mut requirements_cache = HashMap::new();
16851
16852 let collected =
16853 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16854
16855 assert_eq!(collected.child_layers.len(), 0);
16856 assert_eq!(collected.scene.shapes.len(), 1);
16857 assert_eq!(collected.scene.images.len(), 1);
16858 assert_eq!(collected.scene.texts.len(), 1);
16859 assert_eq!(collected.scene.effect_layers.len(), 0);
16860 let expected_anchor = Some(SnapAnchor::rigid(Point::new(14.25, 16.5)));
16861 assert_eq!(
16862 collected.scene.shapes[0].snap_anchor, expected_anchor,
16863 "rested scroll content should snap back to device pixels"
16864 );
16865 assert_eq!(
16866 collected.scene.images[0].snap_anchor, expected_anchor,
16867 "rested scroll images should snap back to device pixels"
16868 );
16869 assert_eq!(
16870 collected.scene.texts[0].snap_anchor, expected_anchor,
16871 "rested scroll text should snap back to device pixels"
16872 );
16873 }
16874
16875 #[test]
16876 fn complex_text_uses_local_surface() {
16877 let root = translated_content_local_surface_root();
16878 let mut rect_cache = HashMap::new();
16879 let mut requirements_cache = HashMap::new();
16880
16881 let collected =
16882 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16883
16884 assert!(
16885 !collected.child_layers.is_empty(),
16886 "translated-content effectful text should render through a bounded local surface"
16887 );
16888 assert!(collected.scene.texts.is_empty());
16889 assert!(collected.scene.shadow_draws.is_empty());
16890 }
16891
16892 #[test]
16893 fn translated_content_surface_composite_uses_scroll_content_snap_anchor() {
16894 let mut root = translated_content_local_surface_root();
16895 let scroll_offset = Point::new(0.0, -18.5);
16896 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
16897 panic!("expected translated content layer");
16898 };
16899 translated_content.translated_content_offset = scroll_offset;
16900 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
16901 panic!("expected effectful text layer");
16902 };
16903 effectful_text.transform_to_parent =
16904 effectful_text
16905 .transform_to_parent
16906 .then(ProjectiveTransform::translation(
16907 scroll_offset.x,
16908 scroll_offset.y,
16909 ));
16910
16911 let mut rect_cache = HashMap::new();
16912 let mut requirements_cache = HashMap::new();
16913 let collected =
16914 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16915
16916 assert_eq!(collected.child_layers.len(), 1);
16917 assert_eq!(
16918 collected.child_layers[0].snap_anchor,
16919 Some(SnapAnchor::rigid(Point::new(14.25, -2.0))),
16920 "isolated scrolled descendants must composite with the same content-origin snap phase"
16921 );
16922 }
16923
16924 #[test]
16925 fn animated_translated_content_surface_composite_uses_scroll_content_snap_anchor() {
16926 let mut root = translated_content_local_surface_root();
16927 let scroll_offset = Point::new(0.0, -18.5);
16928 let Some(RenderNode::Layer(translated_content)) = root.children.get_mut(0) else {
16929 panic!("expected translated content layer");
16930 };
16931 translated_content.motion_context_animated = true;
16932 translated_content.translated_content_offset = scroll_offset;
16933 let Some(RenderNode::Layer(effectful_text)) = translated_content.children.get_mut(0) else {
16934 panic!("expected effectful text layer");
16935 };
16936 effectful_text.transform_to_parent =
16937 effectful_text
16938 .transform_to_parent
16939 .then(ProjectiveTransform::translation(
16940 scroll_offset.x,
16941 scroll_offset.y,
16942 ));
16943
16944 let mut rect_cache = HashMap::new();
16945 let mut requirements_cache = HashMap::new();
16946 let collected =
16947 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
16948
16949 assert_eq!(collected.child_layers.len(), 1);
16950 assert_eq!(
16951 collected.child_layers[0].snap_anchor,
16952 Some(SnapAnchor::rigid(Point::new(14.25, 16.5))),
16953 "animated translated content should composite the stable local surface at the viewport-origin snap phase"
16954 );
16955 }
16956
16957 #[test]
16958 fn translated_text_material_effect_layer_uses_scroll_content_snap_anchor() {
16959 let mut layer = text_layer_with_style(
16960 AnnotatedString::from("gradient"),
16961 TextStyle::from_span_style(SpanStyle {
16962 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
16963 ..SpanStyle::default()
16964 }),
16965 );
16966 layer.translated_content_context = true;
16967 layer.translated_content_offset = Point::new(0.0, -18.5);
16968 let mut rect_cache = HashMap::new();
16969 let mut requirements_cache = HashMap::new();
16970
16971 let collected =
16972 collect_layer_contents(&layer, None, None, &mut rect_cache, &mut requirements_cache);
16973
16974 assert_eq!(collected.scene.effect_layers.len(), 1);
16975 assert_eq!(
16976 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
16977 CompositeSampleMode::Box4
16978 );
16979 assert_eq!(
16980 collected.scene.effect_layers[0].snap_anchor,
16981 Some(SnapAnchor::rigid(Point::new(0.0, -18.5))),
16982 "text material surfaces must composite with the scroll content-origin snap phase"
16983 );
16984 }
16985
16986 #[test]
16987 fn translated_layer_surface_capture_does_not_restart_local_picture_for_shadow_text() {
16988 let mut layer = text_layer_with_style(
16989 AnnotatedString::from("shadow"),
16990 TextStyle::from_span_style(SpanStyle {
16991 shadow: Some(Shadow {
16992 color: Color::BLACK,
16993 offset: Point::new(1.0, 2.0),
16994 blur_radius: 3.0,
16995 }),
16996 ..SpanStyle::default()
16997 }),
16998 );
16999 layer.translated_content_context = true;
17000 let mut rect_cache = HashMap::new();
17001 let mut requirements_cache = HashMap::new();
17002
17003 let collected = collect_layer_contents_with_translation_context(
17004 &layer,
17005 None,
17006 None,
17007 TranslationRenderContext {
17008 inherited_content_translation: false,
17009 surface_capture_active: true,
17010 local_picture_capture_active: true,
17011 ..TranslationRenderContext::default()
17012 },
17013 &mut rect_cache,
17014 &mut requirements_cache,
17015 );
17016
17017 assert!(
17018 collected.scene.effect_layers.is_empty(),
17019 "a translated layer surface already provides the stable local capture"
17020 );
17021 assert_eq!(collected.scene.shadow_draws.len(), 1);
17022 assert_eq!(collected.scene.texts.len(), 1);
17023 assert!(
17024 !collected.scene.texts[0].translated_content_context,
17025 "text inside an active motion-stable capture must raster in capture-local coordinates"
17026 );
17027 }
17028
17029 #[test]
17030 fn translated_layer_surface_capture_keeps_only_material_effect_layers() {
17031 let mut layer = text_layer_with_style(
17032 AnnotatedString::from("gradient"),
17033 TextStyle::from_span_style(SpanStyle {
17034 brush: Some(Brush::linear_gradient(vec![Color::WHITE, Color::BLACK])),
17035 ..SpanStyle::default()
17036 }),
17037 );
17038 layer.translated_content_context = true;
17039 let mut rect_cache = HashMap::new();
17040 let mut requirements_cache = HashMap::new();
17041
17042 let collected = collect_layer_contents_with_translation_context(
17043 &layer,
17044 None,
17045 None,
17046 TranslationRenderContext {
17047 inherited_content_translation: false,
17048 surface_capture_active: true,
17049 local_picture_capture_active: true,
17050 ..TranslationRenderContext::default()
17051 },
17052 &mut rect_cache,
17053 &mut requirements_cache,
17054 );
17055
17056 assert_eq!(collected.scene.effect_layers.len(), 1);
17057 assert!(
17058 collected.scene.effect_layers[0]
17059 .requirements
17060 .contains(SurfaceRequirement::MotionStableCapture),
17061 "translated text materials still need motion-stable resolve semantics inside a stable capture"
17062 );
17063 assert_eq!(
17064 composite_sample_mode_for_effect_layer(&collected.scene.effect_layers[0]),
17065 CompositeSampleMode::Box4
17066 );
17067 assert_eq!(
17068 effect_layer_target_scale(&collected.scene.effect_layers[0], 10.0),
17069 10.0
17070 );
17071 assert!(collected.scene.effect_layers[0].effect.is_some());
17072 }
17073
17074 #[test]
17075 fn translated_viewport_surface_does_not_add_plain_local_picture_capture() {
17076 let mut layer = text_layer_with_style(
17077 AnnotatedString::from("shadow"),
17078 TextStyle::from_span_style(SpanStyle {
17079 shadow: Some(Shadow {
17080 color: Color::BLACK,
17081 offset: Point::new(1.0, 2.0),
17082 blur_radius: 3.0,
17083 }),
17084 ..SpanStyle::default()
17085 }),
17086 );
17087 layer.translated_content_context = true;
17088 layer.motion_context_animated = true;
17089 let mut rect_cache = HashMap::new();
17090 let mut requirements_cache = HashMap::new();
17091
17092 let collected = collect_layer_contents_with_translation_context(
17093 &layer,
17094 None,
17095 None,
17096 TranslationRenderContext {
17097 surface_capture_active: true,
17098 ..TranslationRenderContext::default()
17099 },
17100 &mut rect_cache,
17101 &mut requirements_cache,
17102 );
17103
17104 assert_eq!(
17105 collected.scene.effect_layers.len(),
17106 0,
17107 "plain translated content inside a viewport surface should not be captured again"
17108 );
17109 assert_eq!(collected.scene.shadow_draws.len(), 1);
17110 assert_eq!(collected.scene.texts.len(), 1);
17111 }
17112
17113 #[test]
17114 fn static_pure_text_leaf_snaps_without_sibling_draw_primitives() {
17115 let root = pure_text_leaf_root(false, false);
17116 let mut rect_cache = HashMap::new();
17117 let mut requirements_cache = HashMap::new();
17118
17119 let collected =
17120 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17121
17122 assert_eq!(collected.scene.texts.len(), 1);
17123 assert!(
17124 collected.scene.texts[0].snap_anchor.is_some(),
17125 "idle pure text leaves should participate in rigid snap anchoring"
17126 );
17127 }
17128
17129 #[test]
17130 fn animated_pure_text_leaf_stays_unsnapped() {
17131 let root = pure_text_leaf_root(true, false);
17132 let mut rect_cache = HashMap::new();
17133 let mut requirements_cache = HashMap::new();
17134
17135 let collected =
17136 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17137
17138 assert_eq!(collected.scene.texts.len(), 1);
17139 assert_eq!(collected.scene.texts[0].snap_anchor, None);
17140 }
17141
17142 #[test]
17143 fn animated_translated_pure_text_uses_bounded_content_snap() {
17144 let root = pure_text_leaf_root(true, true);
17145 let mut rect_cache = HashMap::new();
17146 let mut requirements_cache = HashMap::new();
17147
17148 let collected =
17149 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17150
17151 assert_eq!(collected.child_layers.len(), 1);
17152 assert!(collected.scene.texts.is_empty());
17153 assert!(collected.scene.effect_layers.is_empty());
17154 assert_snap_anchor_close(
17155 collected.child_layers[0].snap_anchor,
17156 Point::new(11.4, 23.6),
17157 "animated translated pure text should use the bounded content snap phase",
17158 );
17159 }
17160
17161 #[test]
17162 fn rested_translated_pure_text_leaf_snaps_for_crisp_scroll_rest() {
17163 let root = pure_text_leaf_root(false, true);
17164 let mut rect_cache = HashMap::new();
17165 let mut requirements_cache = HashMap::new();
17166
17167 let collected =
17168 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17169
17170 assert_eq!(collected.child_layers.len(), 0);
17171 assert_eq!(collected.scene.texts.len(), 1);
17172 assert_eq!(collected.scene.effect_layers.len(), 0);
17173 assert_snap_anchor_close(
17174 collected.scene.texts[0].snap_anchor,
17175 Point::new(11.4, 23.6),
17176 "rested translated text should snap to device pixels",
17177 );
17178 }
17179
17180 #[test]
17181 fn static_gpu_effect_text_leaf_stays_unsnapped() {
17182 let root = text_layer_with_style(
17183 AnnotatedString::from("Gradient"),
17184 TextStyle::from_span_style(SpanStyle {
17185 brush: Some(Brush::linear_gradient(vec![
17186 Color(0.2, 0.8, 1.0, 1.0),
17187 Color(1.0, 0.7, 0.4, 1.0),
17188 ])),
17189 draw_style: Some(TextDrawStyle::Stroke { width: 2.5 }),
17190 ..SpanStyle::default()
17191 }),
17192 );
17193 let mut rect_cache = HashMap::new();
17194 let mut requirements_cache = HashMap::new();
17195
17196 let collected =
17197 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
17198
17199 assert_eq!(collected.scene.texts.len(), 1);
17200 assert_eq!(
17201 collected.scene.texts[0].snap_anchor, None,
17202 "gpu text-effect leaves must not take the rigid text snap path"
17203 );
17204 assert_eq!(
17205 collected.scene.effect_layers.len(),
17206 1,
17207 "gradient stroke text should still emit a runtime shader effect layer"
17208 );
17209 }
17210
17211 #[test]
17212 fn layer_surface_requirements_keep_shape_plus_direct_child_on_direct_path() {
17213 let mut child = test_layer(
17214 Rect {
17215 x: 0.0,
17216 y: 0.0,
17217 width: 40.0,
17218 height: 20.0,
17219 },
17220 vec![RenderNode::Primitive(PrimitiveEntry {
17221 phase: PrimitivePhase::BeforeChildren,
17222 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17223 primitive: DrawPrimitive::Rect {
17224 rect: Rect {
17225 x: 0.0,
17226 y: 0.0,
17227 width: 40.0,
17228 height: 20.0,
17229 },
17230 brush: Brush::solid(Color::WHITE),
17231 stroke: None,
17232 },
17233 clip: None,
17234 }),
17235 })],
17236 );
17237 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
17238
17239 let layer = test_layer(
17240 Rect {
17241 x: 0.0,
17242 y: 0.0,
17243 width: 64.0,
17244 height: 32.0,
17245 },
17246 vec![
17247 RenderNode::Primitive(PrimitiveEntry {
17248 phase: PrimitivePhase::BeforeChildren,
17249 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17250 primitive: DrawPrimitive::Rect {
17251 rect: Rect {
17252 x: 0.0,
17253 y: 0.0,
17254 width: 64.0,
17255 height: 32.0,
17256 },
17257 brush: Brush::solid(Color::BLACK),
17258 stroke: None,
17259 },
17260 clip: None,
17261 }),
17262 }),
17263 RenderNode::Layer(Box::new(child)),
17264 ],
17265 );
17266
17267 let requirements = layer_surface_requirements(&layer);
17268
17269 assert_eq!(requirements.direct_translation, Some(Point::default()));
17270 assert!(!requirements
17271 .surface_requirements
17272 .contains(SurfaceRequirement::MixedDirectContent));
17273 assert!(!requirements
17274 .surface_requirements
17275 .has_isolating_requirement());
17276 }
17277
17278 #[test]
17279 fn collect_layer_contents_translates_direct_text_rects_into_parent_space() {
17280 let mut child = text_layer_with_style(
17281 AnnotatedString::from("direct"),
17282 TextStyle::from_span_style(SpanStyle {
17283 text_decoration: Some(TextDecoration::UNDERLINE),
17284 ..SpanStyle::default()
17285 }),
17286 );
17287 child.transform_to_parent = ProjectiveTransform::translation(9.0, 7.0);
17288
17289 let parent = test_layer(
17290 Rect {
17291 x: 0.0,
17292 y: 0.0,
17293 width: 64.0,
17294 height: 32.0,
17295 },
17296 vec![RenderNode::Layer(Box::new(child))],
17297 );
17298
17299 let mut rect_cache = HashMap::new();
17300 let mut requirements_cache = HashMap::new();
17301 let collected = with_test_app_context(|| {
17302 collect_layer_contents(
17303 &parent,
17304 None,
17305 None,
17306 &mut rect_cache,
17307 &mut requirements_cache,
17308 )
17309 });
17310
17311 assert!(
17312 collected.child_layers.is_empty(),
17313 "decoration-only text child should collapse directly into the parent scene"
17314 );
17315 assert_eq!(collected.scene.texts.len(), 1, "expected one text draw");
17316 let text = &collected.scene.texts[0];
17317 assert!(
17318 text.rect.x >= 9.0 && text.rect.y >= 7.0,
17319 "collapsed text rect should be translated into parent space, got {:?}",
17320 text.rect
17321 );
17322 assert!(
17323 collected
17324 .scene
17325 .shapes
17326 .iter()
17327 .any(|shape| shape.rect.y >= 7.0),
17328 "collapsed underline geometry should also be translated into parent space"
17329 );
17330 }
17331
17332 #[test]
17333 fn normalized_scene_keeps_lazy_after_bound_text_for_prewarm() {
17334 use std::cell::RefCell;
17335
17336 fn collect_graph_text_labels(layer: &LayerNode, labels: &mut Vec<String>) {
17337 for child in &layer.children {
17338 match child {
17339 RenderNode::Primitive(PrimitiveEntry {
17340 node: PrimitiveNode::Text(text),
17341 ..
17342 }) => labels.push(text.text.text.clone()),
17343 RenderNode::Layer(child_layer) => {
17344 collect_graph_text_labels(child_layer, labels)
17345 }
17346 RenderNode::Primitive(_) | RenderNode::DrawRun(_) => {}
17347 }
17348 }
17349 }
17350
17351 let state_holder: Rc<RefCell<Option<LazyListState>>> = Rc::new(RefCell::new(None));
17352 let state_holder_for_comp = state_holder.clone();
17353 let mut composition = cranpose_ui::run_test_composition(move || {
17354 let list_state = remember_lazy_list_state();
17355 *state_holder_for_comp.borrow_mut() = Some(list_state);
17356 let mut spec = LazyColumnSpec::new()
17357 .vertical_arrangement(cranpose_ui::LinearArrangement::SpacedBy(6.0));
17358 spec.beyond_bounds_item_count = 0;
17359 LazyColumn(Modifier::empty().height(96.0), list_state, spec, |scope| {
17360 scope.items(
17361 12,
17362 None::<fn(usize) -> u64>,
17363 None::<fn(usize) -> u64>,
17364 |index| {
17365 Text(
17366 format!("WarmRow {index}"),
17367 Modifier::empty().height(32.0),
17368 TextStyle::default(),
17369 );
17370 },
17371 );
17372 });
17373 });
17374
17375 let list_state = (*state_holder.borrow()).expect("lazy list state should be captured");
17376 list_state.scroll_to_item(4, 0.0);
17377
17378 let root = composition.root().expect("lazy column root");
17379 let handle = composition.runtime_handle();
17380 let mut applier = composition.applier_mut();
17381 applier.set_runtime_handle(handle);
17382 let _ = applier
17383 .compute_layout(
17384 root,
17385 Size {
17386 width: 240.0,
17387 height: 240.0,
17388 },
17389 )
17390 .expect("lazy column layout");
17391 let graph = build_graph_from_applier(&mut applier, root, 1.0).expect("lazy column graph");
17392 applier.clear_runtime_handle();
17393 let mut graph_labels = Vec::new();
17394 collect_graph_text_labels(&graph.root, &mut graph_labels);
17395
17396 let visible_indices: Vec<_> = list_state
17397 .layout_info()
17398 .visible_items_info
17399 .iter()
17400 .map(|item| item.index)
17401 .collect();
17402 assert_eq!(
17403 visible_indices,
17404 vec![4, 5, 6],
17405 "test setup expects exactly three viewport-visible rows"
17406 );
17407
17408 let mut rect_cache = HashMap::new();
17409 let mut requirements_cache = HashMap::new();
17410 let collected = with_test_app_context(|| {
17411 collect_layer_contents(
17412 &graph.root,
17413 None,
17414 None,
17415 &mut rect_cache,
17416 &mut requirements_cache,
17417 )
17418 });
17419 let root_text_labels: Vec<_> = collected
17420 .scene
17421 .texts
17422 .iter()
17423 .map(|text| text.text.text.clone())
17424 .collect();
17425 let child_layer_count = collected.child_layers.len();
17426 let warm_text = collected
17427 .scene
17428 .texts
17429 .iter()
17430 .find(|text| text.text.text == "WarmRow 7")
17431 .unwrap_or_else(|| {
17432 panic!(
17433 "after-bound lazy text should reach WGPU scene collection; graph_texts={graph_labels:?} root_texts={root_text_labels:?} child_layers={child_layer_count}"
17434 )
17435 });
17436
17437 assert!(
17438 warm_text.rect.y >= 96.0,
17439 "after-bound text should be below the viewport, got {:?}",
17440 warm_text.rect
17441 );
17442 assert_eq!(
17443 visible_draw_rect(warm_text.rect, warm_text.clip),
17444 None,
17445 "after-bound text should remain clipped away for drawing while staying available for glyph prewarm"
17446 );
17447 assert!(
17448 text_draw_should_prewarm_in_viewport(
17449 warm_text.rect,
17450 warm_text.clip,
17451 ViewportUniformParams {
17452 width: 240,
17453 height: 96,
17454 offset: [0.0, 0.0],
17455 },
17456 1.0,
17457 ),
17458 "after-bound text inside the warm window must be selected by WGPU prewarm"
17459 );
17460 }
17461
17462 #[test]
17463 fn direct_translation_accepts_nearly_identity_axis_scale_noise() {
17464 let local_bounds = Rect {
17465 x: 0.0,
17466 y: 0.0,
17467 width: 393.3,
17468 height: 16.8,
17469 };
17470 let quad = [
17471 [10.0, 78.399_994],
17472 [403.3, 78.399_994],
17473 [10.0, 95.2],
17474 [403.3, 95.2],
17475 ];
17476 let transform = ProjectiveTransform::from_rect_to_quad(local_bounds, quad);
17477
17478 assert_eq!(
17479 direct_translation(transform),
17480 Some(Point::new(10.0, 78.399_994)),
17481 );
17482 }
17483
17484 #[test]
17485 fn layer_surface_requirements_keep_shape_plus_isolating_child_as_mixed_content() {
17486 let mut child = test_layer(
17487 Rect {
17488 x: 0.0,
17489 y: 0.0,
17490 width: 24.0,
17491 height: 18.0,
17492 },
17493 vec![RenderNode::Primitive(PrimitiveEntry {
17494 phase: PrimitivePhase::BeforeChildren,
17495 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17496 primitive: DrawPrimitive::Rect {
17497 rect: Rect {
17498 x: 0.0,
17499 y: 0.0,
17500 width: 24.0,
17501 height: 18.0,
17502 },
17503 brush: Brush::solid(Color::WHITE),
17504 stroke: None,
17505 },
17506 clip: None,
17507 }),
17508 })],
17509 );
17510 child.transform_to_parent = ProjectiveTransform::translation(8.0, 6.0);
17511 child.graphics_layer.render_effect = Some(RenderEffect::blur(2.0));
17512
17513 let layer = test_layer(
17514 Rect {
17515 x: 0.0,
17516 y: 0.0,
17517 width: 64.0,
17518 height: 32.0,
17519 },
17520 vec![
17521 RenderNode::Primitive(PrimitiveEntry {
17522 phase: PrimitivePhase::BeforeChildren,
17523 node: PrimitiveNode::Draw(DrawPrimitiveNode {
17524 primitive: DrawPrimitive::Rect {
17525 rect: Rect {
17526 x: 0.0,
17527 y: 0.0,
17528 width: 64.0,
17529 height: 32.0,
17530 },
17531 brush: Brush::solid(Color::BLACK),
17532 stroke: None,
17533 },
17534 clip: None,
17535 }),
17536 }),
17537 RenderNode::Layer(Box::new(child)),
17538 ],
17539 );
17540
17541 let requirements = layer_surface_requirements(&layer);
17542
17543 assert!(requirements
17544 .surface_requirements
17545 .contains(SurfaceRequirement::MixedDirectContent));
17546 assert!(!requirements
17547 .surface_requirements
17548 .has_isolating_requirement());
17549 }
17550
17551 #[test]
17552 fn build_scene_window_filters_and_translates_items() {
17553 let mut shape = test_shape(6, BlendMode::SrcOver);
17554 shape.rect.x = 12.0;
17555 shape.rect.y = 25.0;
17556 shape.local_rect.x = 12.0;
17557 shape.local_rect.y = 25.0;
17558 shape.quad = [[12.0, 25.0], [20.0, 25.0], [12.0, 33.0], [20.0, 33.0]];
17559 shape.clip = Some(Rect {
17560 x: 11.0,
17561 y: 24.0,
17562 width: 10.0,
17563 height: 10.0,
17564 });
17565
17566 let mut image = test_image(8, BlendMode::SrcOver);
17567 image.rect.x = 18.0;
17568 image.rect.y = 27.0;
17569 image.local_rect.x = 18.0;
17570 image.local_rect.y = 27.0;
17571 image.quad = [[18.0, 27.0], [26.0, 27.0], [18.0, 35.0], [26.0, 35.0]];
17572
17573 let mut text = test_text(9);
17574 text.rect.x = 16.0;
17575 text.rect.y = 29.0;
17576 text.clip = Some(Rect {
17577 x: 15.0,
17578 y: 28.0,
17579 width: 9.0,
17580 height: 6.0,
17581 });
17582
17583 let mut shadow_shape = test_shape(7, BlendMode::SrcOver);
17584 shadow_shape.rect.x = 14.0;
17585 shadow_shape.rect.y = 26.0;
17586 shadow_shape.local_rect.x = 14.0;
17587 shadow_shape.local_rect.y = 26.0;
17588 shadow_shape.quad = [[14.0, 26.0], [22.0, 26.0], [14.0, 34.0], [22.0, 34.0]];
17589 let mut shadow = test_shadow_draw(vec![(shadow_shape, BlendMode::SrcOver)]);
17590 shadow.z_index = 7;
17591
17592 let mut nested_effect = effect_layer(6, 10);
17593 nested_effect.rect.x = 13.0;
17594 nested_effect.rect.y = 24.0;
17595 nested_effect.clip = Some(Rect {
17596 x: 15.0,
17597 y: 25.0,
17598 width: 4.0,
17599 height: 5.0,
17600 });
17601
17602 let mut nested_backdrop = backdrop_layer(8);
17603 nested_backdrop.rect.x = 17.0;
17604 nested_backdrop.rect.y = 26.0;
17605 nested_backdrop.clip = Some(Rect {
17606 x: 18.0,
17607 y: 27.0,
17608 width: 3.0,
17609 height: 4.0,
17610 });
17611
17612 let window = build_scene_window(
17613 SceneWindowSource {
17614 shapes: &[test_shape(4, BlendMode::SrcOver), shape],
17615 images: &[image],
17616 texts: &[text],
17617 shadow_draws: &[shadow],
17618 draw_ops: &[],
17619 effect_layers: &[effect_layer(2, 4), nested_effect.clone()],
17620 backdrop_layers: &[backdrop_layer(4), nested_backdrop.clone()],
17621 },
17622 5,
17623 10,
17624 Rect {
17625 x: 10.0,
17626 y: 20.0,
17627 width: 20.0,
17628 height: 20.0,
17629 },
17630 );
17631
17632 assert_eq!(window.shapes.len(), 1);
17633 assert_eq!(
17634 window.shapes[0].rect,
17635 Rect {
17636 x: 2.0,
17637 y: 5.0,
17638 width: 8.0,
17639 height: 8.0,
17640 }
17641 );
17642 assert_eq!(
17643 window.shapes[0].clip,
17644 Some(Rect {
17645 x: 1.0,
17646 y: 4.0,
17647 width: 10.0,
17648 height: 10.0,
17649 })
17650 );
17651 assert_eq!(window.images.len(), 1);
17652 assert_eq!(window.images[0].rect.x, 8.0);
17653 assert_eq!(window.images[0].rect.y, 7.0);
17654 assert_eq!(window.texts.len(), 1);
17655 assert_eq!(window.texts[0].rect.x, 6.0);
17656 assert_eq!(window.texts[0].rect.y, 9.0);
17657 assert_eq!(
17658 window.texts[0].clip,
17659 Some(Rect {
17660 x: 5.0,
17661 y: 8.0,
17662 width: 9.0,
17663 height: 6.0,
17664 })
17665 );
17666 assert_eq!(window.shadow_draws.len(), 1);
17667 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.x, 4.0);
17668 assert_eq!(window.shadow_draws[0].shapes[0].0.rect.y, 6.0);
17669 assert_eq!(window.effect_layers.len(), 1);
17670 assert_eq!(
17671 window.effect_layers[0].rect,
17672 Rect {
17673 x: 3.0,
17674 y: 4.0,
17675 width: 10.0,
17676 height: 10.0,
17677 }
17678 );
17679 assert_eq!(
17680 window.effect_layers[0].clip,
17681 Some(Rect {
17682 x: 5.0,
17683 y: 5.0,
17684 width: 4.0,
17685 height: 5.0,
17686 })
17687 );
17688 assert_eq!(window.backdrop_layers.len(), 1);
17689 assert_eq!(
17690 window.backdrop_layers[0].rect,
17691 Rect {
17692 x: 7.0,
17693 y: 6.0,
17694 width: 10.0,
17695 height: 10.0,
17696 }
17697 );
17698 assert_eq!(
17699 window.backdrop_layers[0].clip,
17700 Some(Rect {
17701 x: 8.0,
17702 y: 7.0,
17703 width: 3.0,
17704 height: 4.0,
17705 })
17706 );
17707 }
17708
17709 #[test]
17710 fn filtered_effect_layer_index_counts_only_window_members() {
17711 let effects = vec![
17712 effect_layer(0, 2),
17713 effect_layer(5, 12),
17714 effect_layer(6, 10),
17715 effect_layer(14, 20),
17716 ];
17717
17718 assert_eq!(filtered_effect_layer_index(&effects, 1, 5, 12), Some(0));
17719 assert_eq!(filtered_effect_layer_index(&effects, 2, 5, 12), Some(1));
17720 assert_eq!(filtered_effect_layer_index(&effects, 3, 5, 12), None);
17721 }
17722
17723 #[test]
17724 fn blend_mode_support_matrix_is_explicit() {
17725 assert!(is_blend_mode_supported(BlendMode::SrcOver));
17726 assert!(is_blend_mode_supported(BlendMode::DstOut));
17727 assert!(!is_blend_mode_supported(BlendMode::Clear));
17728 assert!(!is_blend_mode_supported(BlendMode::Multiply));
17729 }
17730
17731 #[test]
17732 fn collect_non_effect_segment_items_preserves_global_z_order() {
17733 let shapes = vec![
17734 test_shape(3, BlendMode::SrcOver),
17735 test_shape(1, BlendMode::DstOut),
17736 ];
17737 let images = vec![test_image(2, BlendMode::SrcOver)];
17738 let texts = vec![test_text(0)];
17739 let shadows: Vec<ShadowDraw> = Vec::new();
17740 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
17741
17742 let mut scratch = Vec::new();
17743 collect_non_effect_segment_items(
17744 &shapes,
17745 &images,
17746 &texts,
17747 &shadows,
17748 &draw_ops,
17749 0,
17750 4,
17751 &[],
17752 100,
17753 100,
17754 1.0,
17755 &mut scratch,
17756 );
17757 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
17758 assert_eq!(
17759 items,
17760 vec![
17761 SegmentDrawItem::Text(0),
17762 SegmentDrawItem::Shape(1),
17763 SegmentDrawItem::Image(0),
17764 SegmentDrawItem::Shape(0),
17765 ]
17766 );
17767 }
17768
17769 #[test]
17770 fn collect_non_effect_segment_items_filters_effect_ranges() {
17771 let shapes = vec![
17772 test_shape(1, BlendMode::SrcOver),
17773 test_shape(3, BlendMode::DstOut),
17774 ];
17775 let images = vec![test_image(2, BlendMode::SrcOver)];
17776 let texts = vec![test_text(4)];
17777 let shadows: Vec<ShadowDraw> = Vec::new();
17778 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
17779 let effect_ranges = [std::ops::Range { start: 2, end: 4 }];
17780
17781 let mut scratch = Vec::new();
17782 collect_non_effect_segment_items(
17783 &shapes,
17784 &images,
17785 &texts,
17786 &shadows,
17787 &draw_ops,
17788 0,
17789 5,
17790 &effect_ranges,
17791 100,
17792 100,
17793 1.0,
17794 &mut scratch,
17795 );
17796 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
17797 assert_eq!(
17798 items,
17799 vec![SegmentDrawItem::Shape(0), SegmentDrawItem::Text(0)]
17800 );
17801 }
17802
17803 #[test]
17804 fn collect_non_effect_segment_items_culls_offscreen_shapes_but_keeps_text_prewarm() {
17805 let mut shape = test_shape(0, BlendMode::SrcOver);
17806 shape.rect.y = 160.0;
17807 shape.local_rect.y = 160.0;
17808 shape.quad = [[0.0, 160.0], [8.0, 160.0], [0.0, 168.0], [8.0, 168.0]];
17809
17810 let shapes = vec![shape];
17811 let images = Vec::new();
17812 let mut text = test_text(1);
17813 text.rect.y = 160.0;
17814 let texts = vec![text];
17815 let shadows: Vec<ShadowDraw> = Vec::new();
17816 let draw_ops = test_draw_ops(&shapes, &images, &texts, &shadows);
17817
17818 let mut scratch = Vec::new();
17819 collect_non_effect_segment_items(
17820 &shapes,
17821 &images,
17822 &texts,
17823 &shadows,
17824 &draw_ops,
17825 0,
17826 2,
17827 &[],
17828 100,
17829 100,
17830 1.0,
17831 &mut scratch,
17832 );
17833
17834 let items: Vec<_> = scratch.iter().map(|(_, item)| *item).collect();
17835 assert_eq!(items, vec![SegmentDrawItem::Text(0)]);
17836 }
17837
17838 #[test]
17839 fn segment_command_iter_merges_non_conflicting_batches_into_one_chunk() {
17840 let ordered_items = vec![
17841 (0, SegmentDrawItem::Shape(0)),
17842 (1, SegmentDrawItem::Image(0)),
17843 (2, SegmentDrawItem::Text(0)),
17844 ];
17845 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
17846 let images = vec![test_image(1, BlendMode::DstOut)];
17847
17848 let commands: Vec<_> = SegmentCommandIter::new(
17849 &ordered_items,
17850 &shapes,
17851 &images,
17852 ShapeBatchLimits::desktop(),
17853 )
17854 .collect();
17855
17856 assert_eq!(
17857 commands,
17858 vec![SegmentRenderCommand::DrawChunk(chunk(&[
17859 SegmentBatchPlan::Shape {
17860 start: 0,
17861 end: 1,
17862 blend_mode: BlendMode::SrcOver,
17863 },
17864 SegmentBatchPlan::Image {
17865 start: 1,
17866 end: 2,
17867 blend_mode: BlendMode::DstOut,
17868 },
17869 SegmentBatchPlan::Text { start: 2, end: 3 },
17870 ]))]
17871 );
17872 }
17873
17874 #[test]
17875 fn segment_command_iter_keeps_layer_composites_in_ordered_draw_chunk() {
17876 let ordered_items = vec![
17877 (0, SegmentDrawItem::Shape(0)),
17878 (1, SegmentDrawItem::Composite(0)),
17879 (2, SegmentDrawItem::Image(0)),
17880 (3, SegmentDrawItem::Composite(1)),
17881 (4, SegmentDrawItem::Text(0)),
17882 ];
17883 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
17884 let images = vec![test_image(2, BlendMode::SrcOver)];
17885
17886 let commands: Vec<_> = SegmentCommandIter::new(
17887 &ordered_items,
17888 &shapes,
17889 &images,
17890 ShapeBatchLimits::desktop(),
17891 )
17892 .collect();
17893
17894 assert_eq!(
17895 commands,
17896 vec![SegmentRenderCommand::DrawChunk(chunk(&[
17897 SegmentBatchPlan::Shape {
17898 start: 0,
17899 end: 1,
17900 blend_mode: BlendMode::SrcOver,
17901 },
17902 SegmentBatchPlan::Composite { start: 1, end: 2 },
17903 SegmentBatchPlan::Image {
17904 start: 2,
17905 end: 3,
17906 blend_mode: BlendMode::SrcOver,
17907 },
17908 SegmentBatchPlan::Composite { start: 3, end: 4 },
17909 SegmentBatchPlan::Text { start: 4, end: 5 },
17910 ]))]
17911 );
17912 }
17913
17914 #[test]
17915 fn retain_renderable_shadow_items_culls_invisible_shadow_boundaries() {
17916 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
17917 let images = vec![test_image(2, BlendMode::SrcOver)];
17918 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
17919 shadow_shape.rect = Rect {
17920 x: 500.0,
17921 y: 500.0,
17922 width: 12.0,
17923 height: 12.0,
17924 };
17925 let shadow_draws = vec![ShadowDraw {
17926 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
17927 texts: Vec::new(),
17928 blur_radius: 8.0,
17929 clip: None,
17930 z_index: 1,
17931 }];
17932 let mut ordered_items = vec![
17933 (0, SegmentDrawItem::Shape(0)),
17934 (1, SegmentDrawItem::Shadow(0)),
17935 (2, SegmentDrawItem::Image(0)),
17936 ];
17937
17938 let culled =
17939 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
17940 let commands: Vec<_> = SegmentCommandIter::new(
17941 &ordered_items,
17942 &shapes,
17943 &images,
17944 ShapeBatchLimits::desktop(),
17945 )
17946 .collect();
17947
17948 assert_eq!(culled, 1);
17949 assert_eq!(
17950 commands,
17951 vec![SegmentRenderCommand::DrawChunk(chunk(&[
17952 SegmentBatchPlan::Shape {
17953 start: 0,
17954 end: 1,
17955 blend_mode: BlendMode::SrcOver,
17956 },
17957 SegmentBatchPlan::Image {
17958 start: 1,
17959 end: 2,
17960 blend_mode: BlendMode::SrcOver,
17961 },
17962 ]))]
17963 );
17964 }
17965
17966 #[test]
17967 fn retain_renderable_shadow_items_keeps_visible_shadow_boundaries() {
17968 let mut shadow_shape = test_shape(1, BlendMode::SrcOver);
17969 shadow_shape.rect = Rect {
17970 x: 20.0,
17971 y: 20.0,
17972 width: 12.0,
17973 height: 12.0,
17974 };
17975 let shadow_draws = vec![ShadowDraw {
17976 shapes: vec![(shadow_shape, BlendMode::SrcOver)],
17977 texts: Vec::new(),
17978 blur_radius: 8.0,
17979 clip: None,
17980 z_index: 1,
17981 }];
17982 let mut ordered_items = vec![(1, SegmentDrawItem::Shadow(0))];
17983
17984 let culled =
17985 retain_renderable_shadow_items(&mut ordered_items, &shadow_draws, 100, 100, 1.0, 4096);
17986
17987 assert_eq!(culled, 0);
17988 assert_eq!(ordered_items, vec![(1, SegmentDrawItem::Shadow(0))]);
17989 }
17990
17991 #[test]
17992 fn shape_data_layout_matches_the_wgsl_mirror() {
17993 assert_eq!(std::mem::size_of::<ShapeData>(), 160);
17997 assert_eq!(std::mem::size_of::<ShapeData>() % 16, 0);
17998 assert_eq!(std::mem::size_of::<GradientStop>(), 32);
17999 }
18000
18001 #[test]
18002 fn shape_flags_pack_kind_cap_and_join_without_collision() {
18003 assert_eq!(
18004 pack_shape_flags(SHAPE_KIND_FILL, StrokeCap::Butt, StrokeJoin::Miter),
18005 0.0
18006 );
18007 assert_eq!(
18008 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Miter),
18009 1.0
18010 );
18011 assert_eq!(
18012 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Butt, StrokeJoin::Miter),
18013 2.0
18014 );
18015 assert_eq!(
18017 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Round, StrokeJoin::Miter),
18018 2.0 + 4.0
18019 );
18020 assert_eq!(
18021 pack_shape_flags(SHAPE_KIND_ARC, StrokeCap::Square, StrokeJoin::Miter),
18022 2.0 + 8.0
18023 );
18024 assert_eq!(
18025 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Round),
18026 1.0 + 16.0
18027 );
18028 assert_eq!(
18029 pack_shape_flags(SHAPE_KIND_STROKE, StrokeCap::Butt, StrokeJoin::Bevel),
18030 1.0 + 32.0
18031 );
18032 for kind in [SHAPE_KIND_FILL, SHAPE_KIND_STROKE, SHAPE_KIND_ARC] {
18034 for cap in [StrokeCap::Butt, StrokeCap::Round, StrokeCap::Square] {
18035 for join in [StrokeJoin::Miter, StrokeJoin::Round, StrokeJoin::Bevel] {
18036 let packed = pack_shape_flags(kind, cap, join);
18037 let bits = packed as u32;
18038 assert_eq!(bits & 3, kind);
18039 assert_eq!((bits >> 2) & 3, stroke_cap_code(cap));
18040 assert_eq!((bits >> 4) & 3, stroke_join_code(join));
18041 assert_eq!(packed, bits as f32, "flags must be exact in f32");
18042 }
18043 }
18044 }
18045 }
18046
18047 #[cfg(not(target_arch = "wasm32"))]
18048 #[test]
18049 fn mesh_vertex_layout_matches_the_wgsl_input() {
18050 assert_eq!(std::mem::size_of::<MeshVertex>(), 20);
18053 }
18054
18055 #[cfg(not(target_arch = "wasm32"))]
18059 #[allow(clippy::too_many_arguments)]
18060 fn sdf_arc_band_reference(
18061 p: [f32; 2],
18062 center: [f32; 2],
18063 inner: f32,
18064 outer: f32,
18065 mid_sin_cos: [f32; 2],
18066 half_sin_cos: [f32; 2],
18067 cap: u32,
18068 ) -> f32 {
18069 let ra = (outer + inner) * 0.5;
18070 let rb = ((outer - inner) * 0.5).max(0.0);
18071 let sm = mid_sin_cos[0];
18072 let cm = mid_sin_cos[1];
18073 let d = [p[0] - center[0], p[1] - center[1]];
18074 let mut q = [-sm * d[0] + cm * d[1], cm * d[0] + sm * d[1]];
18075 q[0] = q[0].abs();
18076 let sc = half_sin_cos;
18077 let mut dist = if sc[1] * q[0] > sc[0] * q[1] {
18078 let dx = q[0] - sc[0] * ra;
18079 let dy = q[1] - sc[1] * ra;
18080 (dx * dx + dy * dy).sqrt() - rb
18081 } else {
18082 ((q[0] * q[0] + q[1] * q[1]).sqrt() - ra).abs() - rb
18083 };
18084 let plane = sc[1] * q[0] - sc[0] * q[1];
18085 if cap == 0 {
18087 dist = dist.max(plane);
18088 } else if cap == 2 {
18089 dist = dist.max(plane - rb);
18090 }
18091 dist
18092 }
18093
18094 #[cfg(not(target_arch = "wasm32"))]
18095 fn point_in_triangle(p: [f64; 2], tri: &[[f64; 2]; 3]) -> bool {
18096 let side = |a: [f64; 2], b: [f64; 2]| {
18097 (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0])
18098 };
18099 let d0 = side(tri[0], tri[1]);
18100 let d1 = side(tri[1], tri[2]);
18101 let d2 = side(tri[2], tri[0]);
18102 let has_neg = d0 < 0.0 || d1 < 0.0 || d2 < 0.0;
18103 let has_pos = d0 > 0.0 || d1 > 0.0 || d2 > 0.0;
18104 !(has_neg && has_pos)
18105 }
18106
18107 #[cfg(not(target_arch = "wasm32"))]
18108 fn converted_arc_shape(arc: cranpose_ui_graphics::ArcGeometry, root_scale: f32) -> ShapeData {
18109 let bounds = arc.bounds();
18110 let mut shape = test_shape(0, BlendMode::SrcOver);
18111 shape.rect = bounds;
18112 shape.local_rect = bounds;
18113 shape.quad = [
18114 [bounds.x, bounds.y],
18115 [bounds.x + bounds.width, bounds.y],
18116 [bounds.x, bounds.y + bounds.height],
18117 [bounds.x + bounds.width, bounds.y + bounds.height],
18118 ];
18119 shape.arc = Some(arc);
18120 let mut converted = ShapeData::zeroed();
18121 convert_shape_into_slots(&shape, root_scale, 0, &mut converted, &mut []);
18122 converted
18123 }
18124
18125 #[cfg(not(target_arch = "wasm32"))]
18130 #[test]
18131 fn arc_mesh_contains_every_band_pixel() {
18132 use cranpose_ui_graphics::ArcGeometry;
18133 let tau = cranpose_ui_graphics::TAU;
18134 let center = Point::new(250.0, 250.0);
18135 let cases: &[(f32, f32, f32, f32, StrokeCap)] = &[
18136 (90.0, 100.0, 0.0, tau, StrokeCap::Round),
18138 (80.0, 100.0, 1.0, 10.0, StrokeCap::Butt),
18140 (0.0, 40.0, 0.0, tau, StrokeCap::Round),
18142 (30.0, 80.0, 0.7, 2.5, StrokeCap::Butt),
18144 (30.0, 80.0, 0.7, 2.5, StrokeCap::Round),
18145 (30.0, 80.0, 0.7, 2.5, StrokeCap::Square),
18146 (99.0, 101.0, 3.0, 4.0, StrokeCap::Round),
18148 (0.6, 2.0, 0.3, 1.2, StrokeCap::Butt),
18150 (1900.0, 1904.0, 0.1, 0.35, StrokeCap::Square),
18152 (40.0, 60.0, 5.0, 1e-3, StrokeCap::Round),
18154 (40.0, 60.0, 0.2, tau - 1e-3, StrokeCap::Butt),
18156 (0.0, 3.0, 1.0, 2.0, StrokeCap::Round),
18158 (20.0, 60.0, 4.5, 1.9, StrokeCap::Butt),
18160 ];
18161 for (case, &(inner, outer, start, sweep, cap)) in cases.iter().enumerate() {
18162 for root_scale in [1.0f32, 2.0, 2.75] {
18167 let arc = ArcGeometry::new(center, inner, outer, start, sweep, cap);
18168 assert!(!arc.is_degenerate(), "case {case} must be drawable");
18169 let converted = converted_arc_shape(arc, root_scale);
18170 let band = arc_mesh_band(&converted)
18171 .unwrap_or_else(|| panic!("case {case} must qualify for meshing"));
18172 let mut vertices = Vec::new();
18173 let mut indices = Vec::new();
18174 let segments =
18175 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
18176 .unwrap_or_else(|| panic!("case {case} must produce a mesh"));
18177 assert!(segments >= ARC_MESH_MIN_SEGMENTS);
18178 let position = |index: u32| {
18181 let p = vertices[index as usize].position;
18182 [p[0] as f64, p[1] as f64]
18183 };
18184 let triangles: Vec<[[f64; 2]; 3]> = indices
18185 .chunks_exact(3)
18186 .map(|tri| [position(tri[0]), position(tri[1]), position(tri[2])])
18187 .collect();
18188
18189 let [qx, qy, ..] = converted.quad01;
18193 let [_, _, qr, qb] = converted.quad23;
18194 let (rw, rh) = (qr - qx, qb - qy);
18195 let cap_bits = (converted.stroke_params[1].max(0.0) as u32 >> 2) & 3;
18196 let step = (rw.max(rh) / 400.0).clamp(0.25, 2.0);
18197 let mut band_points = 0usize;
18198 let mut y = qy;
18199 while y <= qb {
18200 let mut x = qx;
18201 while x <= qr {
18202 let dist = sdf_arc_band_reference(
18203 [x, y],
18204 [converted.arc_params[0], converted.arc_params[1]],
18205 converted.stroke_params[3],
18206 converted.stroke_params[2],
18207 [converted.radii[0], converted.radii[1]],
18208 [converted.radii[2], converted.radii[3]],
18209 cap_bits,
18210 );
18211 if dist <= 0.5 {
18212 band_points += 1;
18213 let p = [x as f64, y as f64];
18214 assert!(
18215 triangles.iter().any(|tri| point_in_triangle(p, tri)),
18216 "case {case} scale {root_scale}: band point ({x}, {y}) \
18217 dist {dist} escapes the mesh"
18218 );
18219 }
18220 x += step;
18221 }
18222 y += step;
18223 }
18224 assert!(
18225 band_points > 0,
18226 "case {case} scale {root_scale}: the sampling grid never hit the band"
18227 );
18228 }
18229 }
18230 }
18231
18232 #[cfg(not(target_arch = "wasm32"))]
18233 #[test]
18234 fn arc_mesh_passthrough_replicates_the_quad_expansion() {
18235 let shape = test_shape(0, BlendMode::SrcOver);
18236 let mut converted = ShapeData::zeroed();
18237 convert_shape_into_slots(&shape, 1.0, 0, &mut converted, &mut []);
18238 let build =
18239 build_arc_mesh_vertices(std::slice::from_ref(&converted)).expect("within budget");
18240 assert_eq!(build.meshed_arcs, 0);
18241 assert_eq!(build.passthrough, 1);
18242 assert_eq!(build.vertices.len(), 4);
18244 assert_eq!(build.index_prefix, vec![0, 6]);
18245 assert_eq!(build.indices, vec![0, 1, 2, 2, 1, 3]);
18246 let corners = [
18247 ([converted.quad01[0], converted.quad01[1]], [0.0f32, 0.0]),
18248 ([converted.quad01[2], converted.quad01[3]], [1.0, 0.0]),
18249 ([converted.quad23[0], converted.quad23[1]], [0.0, 1.0]),
18250 ([converted.quad23[2], converted.quad23[3]], [1.0, 1.0]),
18251 ];
18252 for (vertex, corner) in build.vertices.iter().zip(corners) {
18253 assert_eq!(vertex.position, corner.0);
18254 assert_eq!(vertex.uv, corner.1);
18255 assert_eq!(vertex.shape_idx, 0);
18256 }
18257 for (index, corner) in build.indices.iter().zip([0usize, 1, 2, 2, 1, 3]) {
18260 assert_eq!(build.vertices[*index as usize].position, corners[corner].0);
18261 assert_eq!(build.vertices[*index as usize].uv, corners[corner].1);
18262 }
18263 }
18264
18265 #[cfg(not(target_arch = "wasm32"))]
18271 #[test]
18272 fn arc_mesh_indices_share_boundary_vertices_and_wrap_closed_rings() {
18273 use cranpose_ui_graphics::ArcGeometry;
18274 let tau = cranpose_ui_graphics::TAU;
18275 for (sweep, closed) in [(tau, true), (1.9f32, false)] {
18278 let arc = ArcGeometry::new(
18279 Point::new(250.0, 250.0),
18280 80.0,
18281 100.0,
18282 0.7,
18283 sweep,
18284 StrokeCap::Round,
18285 );
18286 let mut converted = converted_arc_shape(arc, 1.0);
18287 converted.rect = [0.0, 0.0, 500.0, 500.0];
18291 converted.quad01 = [0.0, 0.0, 500.0, 0.0];
18292 converted.quad23 = [0.0, 500.0, 500.0, 500.0];
18293 let band = arc_mesh_band(&converted).expect("arc must qualify");
18294 let mut vertices = Vec::new();
18295 let mut indices = Vec::new();
18296 let segments = emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
18297 .expect("arc must mesh");
18298 let boundary_count = if closed { segments } else { segments + 1 };
18299 assert_eq!(
18300 vertices.len(),
18301 2 * boundary_count,
18302 "closed={closed}: every boundary owns exactly one (inner, outer) pair"
18303 );
18304 assert_eq!(indices.len(), 6 * segments);
18305 for j in 0..segments {
18309 let jb = (j + 1) % boundary_count;
18310 let (in_a, out_a) = (2 * j as u32, 2 * j as u32 + 1);
18311 let (in_b, out_b) = (2 * jb as u32, 2 * jb as u32 + 1);
18312 assert_eq!(
18313 indices[6 * j..6 * j + 6],
18314 [in_a, out_a, out_b, in_a, out_b, in_b],
18315 "closed={closed}: segment {j} must share its boundary pairs"
18316 );
18317 }
18318 if closed {
18319 assert_eq!(indices[6 * segments - 1], 0);
18322 }
18323 for pair in vertices.chunks_exact(2) {
18327 let radius = |v: &MeshVertex| {
18328 let dx = v.position[0] - 250.0;
18329 let dy = v.position[1] - 250.0;
18330 (dx * dx + dy * dy).sqrt()
18331 };
18332 assert!(radius(&pair[0]) < radius(&pair[1]));
18333 }
18334 }
18335 }
18336
18337 #[cfg(not(target_arch = "wasm32"))]
18343 #[test]
18344 fn arc_mesh_clipped_segments_fan_over_private_vertices() {
18345 use cranpose_ui_graphics::ArcGeometry;
18346 let arc = ArcGeometry::new(
18347 Point::new(250.0, 250.0),
18348 80.0,
18349 100.0,
18350 0.0,
18351 cranpose_ui_graphics::TAU,
18352 StrokeCap::Round,
18353 );
18354 let converted = converted_arc_shape(arc, 1.0);
18355 let band = arc_mesh_band(&converted).expect("ring must qualify");
18356 let mut vertices = Vec::new();
18357 let mut indices = Vec::new();
18358 emit_arc_band_mesh(&converted, 0, &band, &mut vertices, &mut indices)
18359 .expect("ring must mesh");
18360 let mut uses = vec![0usize; vertices.len()];
18363 for &index in &indices {
18364 uses[index as usize] += 1;
18365 }
18366 assert!(
18367 uses.iter().any(|&count| count >= 3),
18368 "some boundary vertices must be shared across trapezoids"
18369 );
18370 let [left, top, ..] = converted.quad01;
18376 let [.., right, bottom] = converted.quad23;
18377 let clipped: Vec<&MeshVertex> = vertices
18378 .iter()
18379 .filter(|vertex| {
18380 let [x, y] = vertex.position;
18381 x == left || x == right || y == top || y == bottom
18382 })
18383 .collect();
18384 assert!(
18385 !clipped.is_empty(),
18386 "the tight box must clip the pushed-out chord vertices"
18387 );
18388 assert!(
18391 vertices.len() < indices.len(),
18392 "{} unique vertices should undercut {} triangle corners",
18393 vertices.len(),
18394 indices.len()
18395 );
18396 }
18397
18398 #[cfg(not(target_arch = "wasm32"))]
18399 #[test]
18400 fn arc_mesh_budget_overflow_falls_back_to_whole_slot_passthrough() {
18401 use cranpose_ui_graphics::ArcGeometry;
18402 let arc = ArcGeometry::new(
18407 Point::new(2000.0, 2000.0),
18408 1690.0,
18409 1710.0,
18410 0.0,
18411 cranpose_ui_graphics::TAU,
18412 StrokeCap::Round,
18413 );
18414 let converted = converted_arc_shape(arc, 1.0);
18415 let shapes = vec![converted; 100];
18416 assert!(build_arc_mesh_vertices(&shapes).is_none());
18417 }
18418
18419 #[cfg(not(target_arch = "wasm32"))]
18420 #[test]
18421 fn shape_batch_limits_follow_uniform_binding_size() {
18422 let desktop_shapes = 65536 / std::mem::size_of::<ShapeData>();
18425 assert_eq!(desktop_shapes, 409);
18426 assert_eq!(
18427 ShapeBatchLimits::desktop(),
18428 ShapeBatchLimits {
18429 max_shapes_per_batch: desktop_shapes.min(MAX_SHAPES_PER_BATCH),
18430 max_gradient_stops: MAX_GRADIENT_STOPS,
18431 storage: false,
18432 }
18433 );
18434
18435 let downlevel = ShapeBatchLimits::for_uniform_binding_size(16384);
18438 assert_eq!(downlevel.max_shapes_per_batch, 16384 / 160);
18439 assert_eq!(downlevel.max_shapes_per_batch, 102);
18440 assert_eq!(downlevel.max_gradient_stops, 512.min(MAX_GRADIENT_STOPS));
18441 assert!(downlevel.max_shapes_per_batch * std::mem::size_of::<ShapeData>() <= 16384);
18442 assert!(downlevel.max_gradient_stops * std::mem::size_of::<GradientStop>() <= 16384);
18443
18444 let tiny = ShapeBatchLimits::for_uniform_binding_size(1);
18446 assert_eq!(tiny.max_shapes_per_batch, 1);
18447 assert_eq!(tiny.max_gradient_stops, 1);
18448 }
18449
18450 #[test]
18451 fn storage_shape_batch_limits_uncap_the_batch_and_start_small() {
18452 let storage = ShapeBatchLimits::for_storage_binding_size(128 << 20);
18455 assert!(storage.storage);
18456 assert_eq!(storage.max_shapes_per_batch, MAX_SHAPES_PER_STORAGE_BATCH);
18457 assert_eq!(
18458 storage.max_gradient_stops,
18459 MAX_GRADIENT_STOPS_PER_STORAGE_BATCH
18460 );
18461
18462 assert_eq!(
18465 storage.initial_shape_capacity(),
18466 INITIAL_STORAGE_BATCH_CAPACITY
18467 );
18468 assert_eq!(
18469 storage.initial_gradient_capacity(),
18470 INITIAL_STORAGE_BATCH_CAPACITY
18471 );
18472 assert_eq!(
18473 storage.data_binding_type(),
18474 wgpu::BufferBindingType::Storage { read_only: true }
18475 );
18476 assert!(storage
18477 .data_buffer_usage()
18478 .contains(wgpu::BufferUsages::STORAGE));
18479
18480 let uniform = ShapeBatchLimits::desktop();
18483 assert_eq!(
18484 uniform.initial_shape_capacity(),
18485 uniform.max_shapes_per_batch
18486 );
18487 assert_eq!(
18488 uniform.initial_gradient_capacity(),
18489 uniform.max_gradient_stops
18490 );
18491 assert_eq!(
18492 uniform.data_binding_type(),
18493 wgpu::BufferBindingType::Uniform
18494 );
18495 assert!(uniform
18496 .data_buffer_usage()
18497 .contains(wgpu::BufferUsages::UNIFORM));
18498 }
18499
18500 #[test]
18501 fn storage_shape_shader_swaps_the_arrays_to_runtime_sized_storage() {
18502 let source = shape_shader_source(ShapeBatchLimits::for_storage_binding_size(128 << 20));
18503 assert!(
18504 source.contains("var<storage, read> shape_data: array<ShapeData>;"),
18505 "storage-mode shader must declare a runtime-sized shape array"
18506 );
18507 assert!(
18508 source.contains("var<storage, read> gradient_stops: array<GradientStop>;"),
18509 "storage-mode shader must declare a runtime-sized gradient array"
18510 );
18511 assert!(
18512 !source.contains("var<uniform> shape_data"),
18513 "the uniform shape declaration must be fully replaced"
18514 );
18515 assert!(
18516 !source.contains("var<uniform> gradient_stops"),
18517 "the uniform gradient declaration must be fully replaced"
18518 );
18519 assert!(
18520 source.contains("var<storage, read> paint: array<vec4<f32>>;"),
18521 "storage-mode shader must declare the retained paint array"
18522 );
18523 assert!(
18524 source.contains("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)"),
18525 "storage-mode shader must read paint under the paint_select flag"
18526 );
18527 assert!(
18528 source.contains("fn vs_mesh("),
18529 "the storage rewrite must leave the retained-mesh vertex entry intact"
18530 );
18531 assert!(
18532 source.contains("fn vs_shape_instanced("),
18533 "the storage rewrite must leave the instanced-quad vertex entry intact"
18534 );
18535 assert_eq!(
18536 source
18537 .matches("select(shape.color, paint[shape_idx], similarity.paint_select > 0.5)")
18538 .count(),
18539 3,
18540 "vs_main, vs_shape_instanced and vs_mesh must all read paint under \
18541 the paint_select flag (meshless retained draws ride the instanced \
18542 entry when the selection is latched on)"
18543 );
18544
18545 let module = naga::front::wgsl::parse_str(&source)
18548 .expect("storage-mode shape shader must parse as WGSL");
18549 naga::valid::Validator::new(
18550 naga::valid::ValidationFlags::all(),
18551 naga::valid::Capabilities::all(),
18552 )
18553 .validate(&module)
18554 .expect("storage-mode shape shader must validate for WebGPU");
18555 }
18556
18557 #[test]
18558 fn uniform_shape_shader_keeps_the_in_record_color_and_no_paint_binding() {
18559 for source in [
18563 Cow::Borrowed(shaders::SHADER),
18564 shape_shader_source(ShapeBatchLimits::desktop()),
18565 ] {
18566 assert!(
18567 !source.contains("paint: array"),
18568 "the uniform variant must not declare a paint array"
18569 );
18570 assert!(
18571 source.contains("output.color = shape.color;"),
18572 "the uniform variant must read the color from ShapeData \
18573 (this literal is also what `shape_shader_source` rewrites)"
18574 );
18575 assert!(
18576 source.contains("paint_select: f32"),
18577 "SimilarityTransform must name the flag field in both \
18578 variants; the Rust mirror is Pod and uploads raw bytes"
18579 );
18580 }
18581 }
18582
18583 #[test]
18584 fn shipped_shape_shader_array_length_fits_the_downlevel_uniform_floor() {
18585 assert!(
18589 shaders::SHADER.contains("array<ShapeData, 102>"),
18590 "shape.wgsl array length must stay in sync with \
18591 `shape_shader_source`'s replace string and MAX_SHAPES_PER_BATCH"
18592 );
18593 assert!(102 * std::mem::size_of::<ShapeData>() <= 16384);
18594 assert!(103 * std::mem::size_of::<ShapeData>() > 16384);
18595 }
18596
18597 #[test]
18598 fn glyph_atlas_doubles_on_overflow_and_stops_at_the_device_ceiling() {
18599 assert_eq!(
18602 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MIN_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
18603 1024
18604 );
18605 assert_eq!(
18606 next_glyph_atlas_size(2048, TEXT_GLYPH_ATLAS_MAX_SIZE),
18607 TEXT_GLYPH_ATLAS_MAX_SIZE
18608 );
18609 assert_eq!(
18610 next_glyph_atlas_size(TEXT_GLYPH_ATLAS_MAX_SIZE, TEXT_GLYPH_ATLAS_MAX_SIZE),
18611 TEXT_GLYPH_ATLAS_MAX_SIZE
18612 );
18613
18614 assert_eq!(next_glyph_atlas_size(1024, 2048), 2048);
18617 assert_eq!(next_glyph_atlas_size(2048, 2048), 2048);
18618
18619 assert_eq!(next_glyph_atlas_size(u32::MAX, 4096), 4096);
18621 assert_eq!(next_glyph_atlas_size(0, 0), 1);
18622 }
18623
18624 #[test]
18625 fn glyph_atlas_uv_rect_normalizes_against_the_atlas_it_was_placed_in() {
18626 let entry = GlyphAtlasEntry {
18630 x: 128,
18631 y: 256,
18632 width: 16,
18633 height: 32,
18634 };
18635
18636 let small = glyph_atlas_uv_rect(entry, 512);
18637 let large = glyph_atlas_uv_rect(entry, 4096);
18638
18639 assert_eq!(small.min, [128.0 / 512.0, 256.0 / 512.0]);
18640 assert_eq!(large.min, [128.0 / 4096.0, 256.0 / 4096.0]);
18641 assert_eq!(small.max, [144.0 / 512.0, 288.0 / 512.0]);
18642 assert_eq!(large.max, [144.0 / 4096.0, 288.0 / 4096.0]);
18643 }
18644
18645 #[test]
18646 fn native_shape_shader_source_uses_native_batch_limits() {
18647 let limits = ShapeBatchLimits::desktop();
18648 let source = shape_shader_source(limits);
18649
18650 assert!(source.contains(&format!(
18651 "array<ShapeData, {}>",
18652 limits.max_shapes_per_batch
18653 )));
18654 assert!(source.contains(&format!(
18655 "array<GradientStop, {}>",
18656 limits.max_gradient_stops
18657 )));
18658 assert!(!source.contains("array<ShapeData, 146>"));
18661 }
18662
18663 #[test]
18664 fn stroked_and_arc_shapes_batch_together_with_fills() {
18665 let fill = test_shape(0, BlendMode::SrcOver);
18671 let mut stroked = test_shape(1, BlendMode::SrcOver);
18672 stroked.stroke = Some(
18673 cranpose_ui_graphics::Stroke::new(3.0)
18674 .with_cap(StrokeCap::Round)
18675 .with_join(StrokeJoin::Bevel),
18676 );
18677 let mut arc = test_shape(2, BlendMode::SrcOver);
18678 arc.arc = Some(cranpose_ui_graphics::ArcGeometry::new(
18679 Point::new(4.0, 4.0),
18680 2.0,
18681 4.0,
18682 0.0,
18683 1.0,
18684 StrokeCap::Round,
18685 ));
18686 let trailing_fill = test_shape(3, BlendMode::SrcOver);
18687
18688 assert!(!fill.has_stroke_or_arc());
18689 assert!(stroked.has_stroke_or_arc());
18690 assert!(arc.has_stroke_or_arc());
18691 assert!(!trailing_fill.has_stroke_or_arc());
18692
18693 let shapes = vec![fill, stroked, arc, trailing_fill];
18694 let ordered_items: Vec<_> = (0..shapes.len())
18695 .map(|index| (index, SegmentDrawItem::Shape(index)))
18696 .collect();
18697 let images = Vec::new();
18698
18699 let commands: Vec<_> = SegmentCommandIter::new(
18700 &ordered_items,
18701 &shapes,
18702 &images,
18703 ShapeBatchLimits::desktop(),
18704 )
18705 .collect();
18706
18707 assert_eq!(
18708 commands,
18709 vec![SegmentRenderCommand::DrawChunk(chunk(&[
18710 SegmentBatchPlan::Shape {
18711 start: 0,
18712 end: 4,
18713 blend_mode: BlendMode::SrcOver,
18714 }
18715 ]))],
18716 "mixed fill/stroke/arc runs must stay one batch"
18717 );
18718 }
18719
18720 #[cfg(not(target_arch = "wasm32"))]
18721 #[test]
18722 fn native_segment_fusion_budget_allows_small_interleaved_chunks() {
18723 let ordered_items = vec![
18724 (0, SegmentDrawItem::Shape(0)),
18725 (1, SegmentDrawItem::Image(0)),
18726 (2, SegmentDrawItem::Text(0)),
18727 (3, SegmentDrawItem::Shape(1)),
18728 ];
18729 let shapes = vec![
18730 test_shape(0, BlendMode::SrcOver),
18731 test_shape(3, BlendMode::DstOut),
18732 ];
18733 let segment = chunk(&[
18734 SegmentBatchPlan::Shape {
18735 start: 0,
18736 end: 1,
18737 blend_mode: BlendMode::SrcOver,
18738 },
18739 SegmentBatchPlan::Image {
18740 start: 1,
18741 end: 2,
18742 blend_mode: BlendMode::SrcOver,
18743 },
18744 SegmentBatchPlan::Text { start: 2, end: 3 },
18745 SegmentBatchPlan::Shape {
18746 start: 3,
18747 end: 4,
18748 blend_mode: BlendMode::DstOut,
18749 },
18750 ]);
18751
18752 let budget = native_segment_fusion_budget(
18753 &ordered_items,
18754 &shapes,
18755 &segment,
18756 ShapeBatchLimits::desktop(),
18757 )
18758 .expect("budget should be valid")
18759 .expect("chunk should fit native fusion budget");
18760
18761 assert_eq!(
18762 budget,
18763 NativeSegmentFusionBudget {
18764 shape_count: 2,
18765 gradient_stop_count: 0,
18766 }
18767 );
18768 }
18769
18770 #[cfg(not(target_arch = "wasm32"))]
18771 #[test]
18772 fn native_segment_fusion_budget_rejects_shape_uniform_overflow() {
18773 let ordered_items: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
18774 .map(|index| (index, SegmentDrawItem::Shape(index)))
18775 .collect();
18776 let shapes: Vec<_> = (0..=MAX_SHAPES_PER_BATCH)
18777 .map(|index| test_shape(index, BlendMode::SrcOver))
18778 .collect();
18779 let segment = chunk(&[
18780 SegmentBatchPlan::Shape {
18781 start: 0,
18782 end: MAX_SHAPES_PER_BATCH,
18783 blend_mode: BlendMode::SrcOver,
18784 },
18785 SegmentBatchPlan::Shape {
18786 start: MAX_SHAPES_PER_BATCH,
18787 end: MAX_SHAPES_PER_BATCH + 1,
18788 blend_mode: BlendMode::SrcOver,
18789 },
18790 ]);
18791
18792 let budget = native_segment_fusion_budget(
18793 &ordered_items,
18794 &shapes,
18795 &segment,
18796 ShapeBatchLimits::desktop(),
18797 )
18798 .expect("valid plan");
18799
18800 assert_eq!(budget, None);
18801 }
18802
18803 #[cfg(not(target_arch = "wasm32"))]
18804 #[test]
18805 fn native_segment_fusion_budget_rejects_gradient_uniform_overflow() {
18806 let ordered_items = vec![(0, SegmentDrawItem::Shape(0))];
18807 let mut shape = test_shape(0, BlendMode::SrcOver);
18808 shape.brush = Brush::linear_gradient(vec![Color::BLACK; MAX_GRADIENT_STOPS + 1]);
18809 let shapes = vec![shape];
18810 let segment = chunk(&[SegmentBatchPlan::Shape {
18811 start: 0,
18812 end: 1,
18813 blend_mode: BlendMode::SrcOver,
18814 }]);
18815
18816 let budget = native_segment_fusion_budget(
18817 &ordered_items,
18818 &shapes,
18819 &segment,
18820 ShapeBatchLimits::desktop(),
18821 )
18822 .expect("valid plan");
18823
18824 assert_eq!(budget, None);
18825 }
18826
18827 #[cfg(not(target_arch = "wasm32"))]
18828 #[test]
18829 fn native_segment_fusion_partitions_shape_uniform_overflow() {
18830 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
18833 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
18834 .map(|index| (index, SegmentDrawItem::Shape(index)))
18835 .collect();
18836 let shapes: Vec<_> = (0..=desktop_batch_cap)
18837 .map(|index| test_shape(index, BlendMode::SrcOver))
18838 .collect();
18839 let segment = chunk(&[
18840 SegmentBatchPlan::Shape {
18841 start: 0,
18842 end: desktop_batch_cap,
18843 blend_mode: BlendMode::SrcOver,
18844 },
18845 SegmentBatchPlan::Shape {
18846 start: desktop_batch_cap,
18847 end: desktop_batch_cap + 1,
18848 blend_mode: BlendMode::SrcOver,
18849 },
18850 ]);
18851
18852 let partitions = native_segment_fusion_partitions(
18853 &ordered_items,
18854 &shapes,
18855 &segment,
18856 ShapeBatchLimits::desktop(),
18857 )
18858 .expect("valid plan")
18859 .expect("overflowing segment should be partitionable");
18860
18861 assert_eq!(partitions.len(), 2);
18862 assert_eq!(
18863 partitions[0],
18864 NativeSegmentFusionPartition {
18865 chunk: chunk(&[SegmentBatchPlan::Shape {
18866 start: 0,
18867 end: desktop_batch_cap,
18868 blend_mode: BlendMode::SrcOver,
18869 }]),
18870 budget: NativeSegmentFusionBudget {
18871 shape_count: desktop_batch_cap,
18872 gradient_stop_count: 0,
18873 },
18874 }
18875 );
18876 assert_eq!(
18877 partitions[1],
18878 NativeSegmentFusionPartition {
18879 chunk: chunk(&[SegmentBatchPlan::Shape {
18880 start: desktop_batch_cap,
18881 end: desktop_batch_cap + 1,
18882 blend_mode: BlendMode::SrcOver,
18883 }]),
18884 budget: NativeSegmentFusionBudget {
18885 shape_count: 1,
18886 gradient_stop_count: 0,
18887 },
18888 }
18889 );
18890 }
18891
18892 #[cfg(not(target_arch = "wasm32"))]
18893 #[test]
18894 fn native_segment_fusion_partitions_gradient_uniform_overflow() {
18895 const STOPS_PER_SHAPE: usize = MAX_GRADIENT_STOPS / 2;
18896 let ordered_items = vec![
18897 (0, SegmentDrawItem::Shape(0)),
18898 (1, SegmentDrawItem::Shape(1)),
18899 (2, SegmentDrawItem::Shape(2)),
18900 ];
18901 let mut shapes = Vec::new();
18902 for index in 0..3 {
18903 let mut shape = test_shape(index, BlendMode::SrcOver);
18904 shape.brush = Brush::linear_gradient(vec![Color::BLACK; STOPS_PER_SHAPE]);
18905 shapes.push(shape);
18906 }
18907 let segment = chunk(&[SegmentBatchPlan::Shape {
18908 start: 0,
18909 end: 3,
18910 blend_mode: BlendMode::SrcOver,
18911 }]);
18912
18913 let partitions = native_segment_fusion_partitions(
18914 &ordered_items,
18915 &shapes,
18916 &segment,
18917 ShapeBatchLimits::desktop(),
18918 )
18919 .expect("valid plan")
18920 .expect("overflowing gradient segment should be partitionable");
18921
18922 assert_eq!(partitions.len(), 2);
18923 assert_eq!(
18924 partitions[0],
18925 NativeSegmentFusionPartition {
18926 chunk: chunk(&[SegmentBatchPlan::Shape {
18927 start: 0,
18928 end: 2,
18929 blend_mode: BlendMode::SrcOver,
18930 }]),
18931 budget: NativeSegmentFusionBudget {
18932 shape_count: 2,
18933 gradient_stop_count: MAX_GRADIENT_STOPS,
18934 },
18935 }
18936 );
18937 assert_eq!(
18938 partitions[1],
18939 NativeSegmentFusionPartition {
18940 chunk: chunk(&[SegmentBatchPlan::Shape {
18941 start: 2,
18942 end: 3,
18943 blend_mode: BlendMode::SrcOver,
18944 }]),
18945 budget: NativeSegmentFusionBudget {
18946 shape_count: 1,
18947 gradient_stop_count: STOPS_PER_SHAPE,
18948 },
18949 }
18950 );
18951 }
18952
18953 #[cfg(not(target_arch = "wasm32"))]
18954 #[test]
18955 fn native_segment_fusion_accepts_layer_composite_chunks() {
18956 let ordered_items = vec![
18957 (0, SegmentDrawItem::Shape(0)),
18958 (1, SegmentDrawItem::Composite(0)),
18959 (2, SegmentDrawItem::ShaderComposite(0)),
18960 (3, SegmentDrawItem::Shape(1)),
18961 ];
18962 let shapes = vec![
18963 test_shape(0, BlendMode::SrcOver),
18964 test_shape(1, BlendMode::SrcOver),
18965 ];
18966 let segment = chunk(&[
18967 SegmentBatchPlan::Shape {
18968 start: 0,
18969 end: 1,
18970 blend_mode: BlendMode::SrcOver,
18971 },
18972 SegmentBatchPlan::Composite { start: 1, end: 2 },
18973 SegmentBatchPlan::ShaderComposite { start: 2, end: 3 },
18974 SegmentBatchPlan::Shape {
18975 start: 3,
18976 end: 4,
18977 blend_mode: BlendMode::SrcOver,
18978 },
18979 ]);
18980
18981 let partitions = native_segment_fusion_partitions(
18982 &ordered_items,
18983 &shapes,
18984 &segment,
18985 ShapeBatchLimits::desktop(),
18986 )
18987 .expect("valid plan")
18988 .expect("composites are drawable inside the native fused pass");
18989
18990 assert_eq!(
18991 partitions,
18992 vec![NativeSegmentFusionPartition {
18993 chunk: segment,
18994 budget: NativeSegmentFusionBudget {
18995 shape_count: 2,
18996 gradient_stop_count: 0,
18997 },
18998 }],
18999 "layer composites and shader composites must preserve order without forcing separate render passes"
19000 );
19001 }
19002
19003 #[cfg(not(target_arch = "wasm32"))]
19004 #[test]
19005 fn native_segment_fusion_partitions_preserve_non_shape_order_at_budget_boundary() {
19006 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
19009 let ordered_items: Vec<_> = (0..desktop_batch_cap)
19010 .map(|index| (index, SegmentDrawItem::Shape(index)))
19011 .chain([
19012 (desktop_batch_cap, SegmentDrawItem::Image(0)),
19013 (
19014 desktop_batch_cap + 1,
19015 SegmentDrawItem::Shape(desktop_batch_cap),
19016 ),
19017 ])
19018 .collect();
19019 let shapes: Vec<_> = (0..=desktop_batch_cap)
19020 .map(|index| test_shape(index, BlendMode::SrcOver))
19021 .collect();
19022 let segment = chunk(&[
19023 SegmentBatchPlan::Shape {
19024 start: 0,
19025 end: desktop_batch_cap,
19026 blend_mode: BlendMode::SrcOver,
19027 },
19028 SegmentBatchPlan::Image {
19029 start: desktop_batch_cap,
19030 end: desktop_batch_cap + 1,
19031 blend_mode: BlendMode::SrcOver,
19032 },
19033 SegmentBatchPlan::Shape {
19034 start: desktop_batch_cap + 1,
19035 end: desktop_batch_cap + 2,
19036 blend_mode: BlendMode::SrcOver,
19037 },
19038 ]);
19039
19040 let partitions = native_segment_fusion_partitions(
19041 &ordered_items,
19042 &shapes,
19043 &segment,
19044 ShapeBatchLimits::desktop(),
19045 )
19046 .expect("valid plan")
19047 .expect("overflowing segment should be partitionable");
19048
19049 assert_eq!(partitions.len(), 2);
19050 assert_eq!(
19051 partitions[0].chunk,
19052 chunk(&[
19053 SegmentBatchPlan::Shape {
19054 start: 0,
19055 end: desktop_batch_cap,
19056 blend_mode: BlendMode::SrcOver,
19057 },
19058 SegmentBatchPlan::Image {
19059 start: desktop_batch_cap,
19060 end: desktop_batch_cap + 1,
19061 blend_mode: BlendMode::SrcOver,
19062 },
19063 ])
19064 );
19065 assert_eq!(
19066 partitions[1].chunk,
19067 chunk(&[SegmentBatchPlan::Shape {
19068 start: desktop_batch_cap + 1,
19069 end: desktop_batch_cap + 2,
19070 blend_mode: BlendMode::SrcOver,
19071 }])
19072 );
19073 }
19074
19075 #[test]
19076 fn segment_command_iter_keeps_repeated_batch_kinds_in_one_chunk() {
19077 let ordered_items = vec![
19078 (0, SegmentDrawItem::Shape(0)),
19079 (1, SegmentDrawItem::Image(0)),
19080 (2, SegmentDrawItem::Shape(1)),
19081 ];
19082 let shapes = vec![
19083 test_shape(0, BlendMode::SrcOver),
19084 test_shape(2, BlendMode::DstOut),
19085 ];
19086 let images = vec![test_image(1, BlendMode::SrcOver)];
19087
19088 let commands: Vec<_> = SegmentCommandIter::new(
19089 &ordered_items,
19090 &shapes,
19091 &images,
19092 ShapeBatchLimits::desktop(),
19093 )
19094 .collect();
19095
19096 assert_eq!(
19097 commands,
19098 vec![SegmentRenderCommand::DrawChunk(chunk(&[
19099 SegmentBatchPlan::Shape {
19100 start: 0,
19101 end: 1,
19102 blend_mode: BlendMode::SrcOver,
19103 },
19104 SegmentBatchPlan::Image {
19105 start: 1,
19106 end: 2,
19107 blend_mode: BlendMode::SrcOver,
19108 },
19109 SegmentBatchPlan::Shape {
19110 start: 2,
19111 end: 3,
19112 blend_mode: BlendMode::DstOut,
19113 },
19114 ]))]
19115 );
19116 }
19117
19118 #[test]
19119 fn segment_command_iter_splits_contiguous_shape_runs_at_uniform_batch_limit() {
19120 let desktop_batch_cap = ShapeBatchLimits::desktop().max_shapes_per_batch;
19123 let ordered_items: Vec<_> = (0..=desktop_batch_cap)
19124 .map(|index| (index, SegmentDrawItem::Shape(index)))
19125 .collect();
19126 let shapes: Vec<_> = (0..=desktop_batch_cap)
19127 .map(|index| test_shape(index, BlendMode::SrcOver))
19128 .collect();
19129 let images = Vec::new();
19130
19131 let commands: Vec<_> = SegmentCommandIter::new(
19132 &ordered_items,
19133 &shapes,
19134 &images,
19135 ShapeBatchLimits::desktop(),
19136 )
19137 .collect();
19138
19139 assert_eq!(
19140 commands,
19141 vec![SegmentRenderCommand::DrawChunk(chunk(&[
19142 SegmentBatchPlan::Shape {
19143 start: 0,
19144 end: desktop_batch_cap,
19145 blend_mode: BlendMode::SrcOver,
19146 },
19147 SegmentBatchPlan::Shape {
19148 start: desktop_batch_cap,
19149 end: desktop_batch_cap + 1,
19150 blend_mode: BlendMode::SrcOver,
19151 },
19152 ]))]
19153 );
19154 }
19155
19156 #[test]
19157 fn segment_command_iter_keeps_shadows_as_explicit_boundaries() {
19158 let ordered_items = vec![
19159 (0, SegmentDrawItem::Shape(0)),
19160 (1, SegmentDrawItem::Shadow(0)),
19161 (2, SegmentDrawItem::Image(0)),
19162 (3, SegmentDrawItem::Text(0)),
19163 ];
19164 let shapes = vec![test_shape(0, BlendMode::SrcOver)];
19165 let images = vec![test_image(2, BlendMode::SrcOver)];
19166
19167 let commands: Vec<_> = SegmentCommandIter::new(
19168 &ordered_items,
19169 &shapes,
19170 &images,
19171 ShapeBatchLimits::desktop(),
19172 )
19173 .collect();
19174
19175 assert_eq!(
19176 commands,
19177 vec![
19178 SegmentRenderCommand::DrawChunk(chunk(&[SegmentBatchPlan::Shape {
19179 start: 0,
19180 end: 1,
19181 blend_mode: BlendMode::SrcOver,
19182 }])),
19183 SegmentRenderCommand::Shadow(0),
19184 SegmentRenderCommand::DrawChunk(chunk(&[
19185 SegmentBatchPlan::Image {
19186 start: 2,
19187 end: 3,
19188 blend_mode: BlendMode::SrcOver,
19189 },
19190 SegmentBatchPlan::Text { start: 3, end: 4 },
19191 ])),
19192 ]
19193 );
19194 }
19195
19196 #[test]
19197 fn staged_buffer_uploads_align_new_copies_to_copy_buffer_alignment() {
19198 let mut uploads = StagedBufferUploads::default();
19199 uploads.bytes.extend_from_slice(&[1, 2]);
19200
19201 uploads.stage(UploadTarget::ImageIndex, &[3, 4, 5, 6]);
19202
19203 assert_eq!(uploads.bytes, vec![1, 2, 0, 0, 3, 4, 5, 6]);
19204 assert_eq!(
19205 uploads.copies,
19206 vec![PendingBufferCopy {
19207 source_offset: 4,
19208 target_offset: 0,
19209 size: 4,
19210 target: UploadTarget::ImageIndex,
19211 }]
19212 );
19213 }
19214
19215 #[test]
19216 fn staged_buffer_uploads_ignore_empty_payloads() {
19217 let mut uploads = StagedBufferUploads::default();
19218
19219 uploads.stage(UploadTarget::Uniform, &[]);
19220
19221 assert!(uploads.is_empty());
19222 assert!(uploads.bytes.is_empty());
19223 }
19224
19225 #[test]
19226 fn staged_buffer_uploads_return_exact_payload_slice_for_copy() {
19227 let mut uploads = StagedBufferUploads::default();
19228 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
19229 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
19230
19231 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
19232 assert_eq!(uploads.payload_for_copy(uploads.copies[1]), &[5, 6, 7, 8]);
19233 }
19234
19235 #[test]
19236 fn staged_buffer_uploads_record_destination_offsets() {
19237 let mut uploads = StagedBufferUploads::default();
19238
19239 uploads.stage_at(UploadTarget::ImageIndex, 256, &[1, 2, 3, 4]);
19240
19241 assert_eq!(uploads.copies[0].target_offset, 256);
19242 assert_eq!(uploads.payload_for_copy(uploads.copies[0]), &[1, 2, 3, 4]);
19243 }
19244
19245 #[test]
19246 fn staged_buffer_uploads_truncate_restores_previous_state() {
19247 let mut uploads = StagedBufferUploads::default();
19248 uploads.stage(UploadTarget::Uniform, &[1, 2, 3, 4]);
19249 let bytes_len = uploads.bytes.len();
19250 let copies_len = uploads.copies.len();
19251 uploads.stage(UploadTarget::ImageIndex, &[5, 6, 7, 8]);
19252
19253 uploads.truncate(bytes_len, copies_len);
19254
19255 assert_eq!(uploads.bytes, vec![1, 2, 3, 4]);
19256 assert_eq!(uploads.copies.len(), 1);
19257 }
19258
19259 #[test]
19260 fn inner_shadow_composite_mask_uses_fill_shape_and_scale() {
19261 let mut fill = test_shape(0, BlendMode::SrcOver);
19262 fill.local_rect = Rect {
19263 x: 10.0,
19264 y: 12.0,
19265 width: 40.0,
19266 height: 20.0,
19267 };
19268 fill.shape = Some(RoundedCornerShape::uniform(6.0));
19269
19270 let cutout = test_shape(1, BlendMode::DstOut);
19271 let shadow = test_shadow_draw(vec![
19272 (fill, BlendMode::SrcOver),
19273 (cutout, BlendMode::DstOut),
19274 ]);
19275
19276 let mask = inner_shadow_composite_mask(&shadow, 1.5).expect("inner mask expected");
19277 assert_eq!(mask.rect, [15.0, 18.0, 60.0, 30.0]);
19278 assert_eq!(mask.radii, [9.0, 9.0, 9.0, 9.0]);
19279 }
19280
19281 #[test]
19282 fn inner_shadow_composite_mask_is_none_without_dst_out() {
19283 let fill = test_shape(0, BlendMode::SrcOver);
19284 let shadow = test_shadow_draw(vec![(fill, BlendMode::SrcOver)]);
19285 assert!(inner_shadow_composite_mask(&shadow, 1.0).is_none());
19286 }
19287
19288 #[test]
19289 fn render_effect_support_matrix_covers_all_variants() {
19290 let blur = RenderEffect::blur(4.0);
19291 let offset = RenderEffect::offset(2.0, 3.0);
19292 let shader = RenderEffect::runtime_shader(cranpose_ui_graphics::RuntimeShader::new(
19293 r#"
19294 @group(0) @binding(0) var input_texture: texture_2d<f32>;
19295 @group(0) @binding(1) var input_sampler: sampler;
19296 @group(1) @binding(0) var<uniform> u: array<vec4<f32>, 64>;
19297 struct VertexOutput {
19298 @builtin(position) position: vec4<f32>,
19299 @location(0) uv: vec2<f32>,
19300 }
19301 @vertex
19302 fn fullscreen_vs(@builtin(vertex_index) vertex_index: u32) -> VertexOutput {
19303 var output: VertexOutput;
19304 let x = f32(i32(vertex_index & 1u) * 2 - 1);
19305 let y = f32(i32(vertex_index >> 1u) * 2 - 1);
19306 output.uv = vec2<f32>(x * 0.5 + 0.5, 1.0 - (y * 0.5 + 0.5));
19307 output.position = vec4<f32>(x, y, 0.0, 1.0);
19308 return output;
19309 }
19310 @fragment
19311 fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
19312 return textureSample(input_texture, input_sampler, input.uv);
19313 }
19314 "#,
19315 ));
19316 let chain = blur.clone().then(offset.clone());
19317
19318 assert!(is_render_effect_supported(&blur));
19319 assert!(is_render_effect_supported(&offset));
19320 assert!(is_render_effect_supported(&shader));
19321 assert!(is_render_effect_supported(&chain));
19322 }
19323
19324 #[test]
19325 fn clip_to_bounds_propagates_visual_clip_to_all_descendant_shapes() {
19326 let container_local_bounds = Rect {
19329 x: 0.0,
19330 y: 0.0,
19331 width: 800.0,
19332 height: 500.0,
19333 };
19334 let container_clip_in_parent = Rect {
19336 x: 0.0,
19337 y: 50.0,
19338 width: 800.0,
19339 height: 500.0,
19340 };
19341
19342 let shape_above = RenderNode::Primitive(PrimitiveEntry {
19344 phase: PrimitivePhase::BeforeChildren,
19345 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19346 primitive: DrawPrimitive::Rect {
19347 rect: Rect {
19348 x: 10.0,
19349 y: -30.0,
19350 width: 100.0,
19351 height: 40.0,
19352 },
19353 brush: Brush::solid(Color::WHITE),
19354 stroke: None,
19355 },
19356 clip: None,
19357 }),
19358 });
19359
19360 let shape_inside = RenderNode::Primitive(PrimitiveEntry {
19362 phase: PrimitivePhase::BeforeChildren,
19363 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19364 primitive: DrawPrimitive::Rect {
19365 rect: Rect {
19366 x: 10.0,
19367 y: 100.0,
19368 width: 100.0,
19369 height: 40.0,
19370 },
19371 brush: Brush::solid(Color::WHITE),
19372 stroke: None,
19373 },
19374 clip: None,
19375 }),
19376 });
19377
19378 let shape_below = RenderNode::Primitive(PrimitiveEntry {
19380 phase: PrimitivePhase::BeforeChildren,
19381 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19382 primitive: DrawPrimitive::Rect {
19383 rect: Rect {
19384 x: 10.0,
19385 y: 600.0,
19386 width: 100.0,
19387 height: 40.0,
19388 },
19389 brush: Brush::solid(Color::WHITE),
19390 stroke: None,
19391 },
19392 clip: None,
19393 }),
19394 });
19395
19396 let mut content_layer = test_layer(
19398 Rect {
19399 x: 0.0,
19400 y: 0.0,
19401 width: 800.0,
19402 height: 1000.0,
19403 },
19404 vec![shape_above, shape_inside, shape_below],
19405 );
19406 content_layer.transform_to_parent = ProjectiveTransform::translation(0.0, -30.0);
19407 content_layer.translated_content_context = true;
19408
19409 let mut clip_container = test_layer(
19411 container_local_bounds,
19412 vec![RenderNode::Layer(Box::new(content_layer))],
19413 );
19414 clip_container.clip_to_bounds = true;
19415 clip_container.transform_to_parent = ProjectiveTransform::translation(0.0, 50.0);
19416
19417 let root = test_layer(
19419 Rect {
19420 x: 0.0,
19421 y: 0.0,
19422 width: 800.0,
19423 height: 600.0,
19424 },
19425 vec![RenderNode::Layer(Box::new(clip_container))],
19426 );
19427
19428 let mut rect_cache = HashMap::new();
19429 let mut requirements_cache = HashMap::new();
19430 let collected =
19431 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19432
19433 assert_eq!(
19434 collected.scene.shapes.len(),
19435 3,
19436 "all three shapes should be flattened into the scene"
19437 );
19438
19439 for (i, shape) in collected.scene.shapes.iter().enumerate() {
19440 assert!(
19441 shape.clip.is_some(),
19442 "shape {} at rect {:?} must have a clip from clip_to_bounds container, but clip is None",
19443 i,
19444 shape.rect
19445 );
19446 let clip = shape.clip.unwrap();
19447 assert_eq!(
19448 clip, container_clip_in_parent,
19449 "shape {} clip should match the clip_to_bounds container bounds in parent space",
19450 i
19451 );
19452 }
19453 }
19454
19455 #[test]
19456 fn clip_to_bounds_culls_child_layers_outside_boundary() {
19457 let clip_container_bounds = Rect {
19465 x: 0.0,
19466 y: 0.0,
19467 width: 800.0,
19468 height: 500.0,
19469 };
19470
19471 let shape_in_card = RenderNode::Primitive(PrimitiveEntry {
19472 phase: PrimitivePhase::BeforeChildren,
19473 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19474 primitive: DrawPrimitive::Rect {
19475 rect: Rect {
19476 x: 0.0,
19477 y: 0.0,
19478 width: 300.0,
19479 height: 80.0,
19480 },
19481 brush: Brush::solid(Color::WHITE),
19482 stroke: None,
19483 },
19484 clip: None,
19485 }),
19486 });
19487
19488 let mut card_outside = crate::test_support::layer_node(
19490 Rect {
19491 x: 0.0,
19492 y: 0.0,
19493 width: 300.0,
19494 height: 80.0,
19495 },
19496 ProjectiveTransform::identity(),
19497 GraphicsLayer {
19498 clip: true,
19499 ..GraphicsLayer::default()
19500 },
19501 vec![shape_in_card.clone()],
19502 );
19503 card_outside.transform_to_parent = ProjectiveTransform::translation(10.0, 600.0);
19504
19505 let mut card_inside = crate::test_support::layer_node(
19507 Rect {
19508 x: 0.0,
19509 y: 0.0,
19510 width: 300.0,
19511 height: 80.0,
19512 },
19513 ProjectiveTransform::identity(),
19514 GraphicsLayer {
19515 clip: true,
19516 ..GraphicsLayer::default()
19517 },
19518 vec![shape_in_card],
19519 );
19520 card_inside.transform_to_parent = ProjectiveTransform::translation(10.0, 100.0);
19521
19522 let content = test_layer(
19524 Rect {
19525 x: 0.0,
19526 y: 0.0,
19527 width: 800.0,
19528 height: 1000.0,
19529 },
19530 vec![
19531 RenderNode::Layer(Box::new(card_inside)),
19532 RenderNode::Layer(Box::new(card_outside)),
19533 ],
19534 );
19535
19536 let mut clip_container = test_layer(
19538 clip_container_bounds,
19539 vec![RenderNode::Layer(Box::new(content))],
19540 );
19541 clip_container.clip_to_bounds = true;
19542
19543 let root = test_layer(
19545 Rect {
19546 x: 0.0,
19547 y: 0.0,
19548 width: 800.0,
19549 height: 600.0,
19550 },
19551 vec![RenderNode::Layer(Box::new(clip_container))],
19552 );
19553
19554 let mut rect_cache = HashMap::new();
19555 let mut requirements_cache = HashMap::new();
19556 let collected =
19557 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19558
19559 assert_eq!(
19560 collected.scene.shapes.len(),
19561 1,
19562 "only the card inside the clip boundary should produce shapes; \
19563 the card outside must be culled entirely"
19564 );
19565
19566 let shape = &collected.scene.shapes[0];
19567 assert!(
19568 shape.clip.is_some(),
19569 "the visible card's shape must have a clip from clip_to_bounds"
19570 );
19571 }
19572
19573 #[test]
19574 fn flattened_layer_shadow_z_index_is_below_content() {
19575 let shape = RenderNode::Primitive(PrimitiveEntry {
19579 phase: PrimitivePhase::BeforeChildren,
19580 node: PrimitiveNode::Draw(DrawPrimitiveNode {
19581 primitive: DrawPrimitive::Rect {
19582 rect: Rect {
19583 x: 0.0,
19584 y: 0.0,
19585 width: 100.0,
19586 height: 100.0,
19587 },
19588 brush: Brush::solid(Color::WHITE),
19589 stroke: None,
19590 },
19591 clip: None,
19592 }),
19593 });
19594
19595 let child_bounds = Rect {
19596 x: 0.0,
19597 y: 0.0,
19598 width: 100.0,
19599 height: 100.0,
19600 };
19601
19602 let child = crate::test_support::layer_node(
19603 child_bounds,
19604 ProjectiveTransform::translation(50.0, 50.0),
19605 GraphicsLayer {
19606 shadow_elevation: 20.0,
19607 ..GraphicsLayer::default()
19608 },
19609 vec![shape],
19610 );
19611
19612 let root = test_layer(
19613 Rect {
19614 x: 0.0,
19615 y: 0.0,
19616 width: 800.0,
19617 height: 600.0,
19618 },
19619 vec![RenderNode::Layer(Box::new(child))],
19620 );
19621
19622 let mut rect_cache = HashMap::new();
19623 let mut requirements_cache = HashMap::new();
19624 let collected =
19625 collect_layer_contents(&root, None, None, &mut rect_cache, &mut requirements_cache);
19626
19627 assert!(
19628 !collected.scene.shadow_draws.is_empty(),
19629 "shadow_elevation > 0 must produce shadow draws"
19630 );
19631 let max_shadow_z = collected
19632 .scene
19633 .shadow_draws
19634 .iter()
19635 .map(|s| s.z_index)
19636 .max()
19637 .unwrap();
19638 let min_content_z = collected
19639 .scene
19640 .shapes
19641 .iter()
19642 .map(|s| s.z_index)
19643 .min()
19644 .unwrap();
19645 assert!(
19646 max_shadow_z < min_content_z,
19647 "shadow z-index ({}) must be less than content z-index ({}); \
19648 shadows must render behind their content",
19649 max_shadow_z,
19650 min_content_z
19651 );
19652 }
19653
19654 #[cfg(not(target_arch = "wasm32"))]
19657 fn bundle_op(slot: u32, epoch: Option<u64>, first: u32, last: u32) -> RetainedBundleOpKey {
19658 RetainedBundleOpKey {
19659 slot,
19660 capture_epoch: epoch,
19661 first,
19662 last,
19663 retained_index: slot,
19664 has_mesh: false,
19665 }
19666 }
19667
19668 #[cfg(not(target_arch = "wasm32"))]
19669 fn bundle_key(ops: &[RetainedBundleOpKey]) -> RetainedBundleKey {
19670 RetainedBundleKey { ops: ops.to_vec() }
19671 }
19672
19673 #[cfg(not(target_arch = "wasm32"))]
19676 #[test]
19677 fn retained_bundle_cache_reuses_stable_keys() {
19678 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
19679 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
19680 let key = bundle_key(&ops);
19681
19682 assert!(!cache.hit(&key), "empty cache must miss");
19683 cache.insert(key.clone(), 111);
19684 assert_eq!(cache.get(&key), Some(&111));
19685 cache.end_frame();
19686
19687 for _ in 0..3 {
19688 assert!(cache.hit(&bundle_key(&ops)), "stable key must stay cached");
19689 cache.end_frame();
19690 }
19691 assert_eq!(cache.stats(), (1, 3), "one rebuild, three cached executes");
19692 }
19693
19694 #[cfg(not(target_arch = "wasm32"))]
19698 #[test]
19699 fn retained_bundle_cache_invalidates_on_any_op_change() {
19700 let ops = [bundle_op(3, Some(7), 0, 40), bundle_op(5, Some(9), 4, 12)];
19701 let variants: [Vec<RetainedBundleOpKey>; 5] = [
19702 vec![bundle_op(3, Some(8), 0, 40), bundle_op(5, Some(9), 4, 12)],
19704 vec![bundle_op(5, Some(9), 4, 12), bundle_op(3, Some(7), 0, 40)],
19706 vec![bundle_op(3, Some(7), 0, 40)],
19708 vec![bundle_op(3, Some(7), 0, 41), bundle_op(5, Some(9), 4, 12)],
19710 vec![bundle_op(3, Some(7), 0, 40), bundle_op(5, None, 4, 12)],
19712 ];
19713 for changed in variants {
19714 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
19715 cache.insert(bundle_key(&ops), 111);
19716 cache.end_frame();
19717 assert!(
19718 !cache.hit(&RetainedBundleKey {
19719 ops: changed.clone()
19720 }),
19721 "changed key {changed:?} must not reuse the stale bundle"
19722 );
19723 }
19724 }
19725
19726 #[cfg(not(target_arch = "wasm32"))]
19730 #[test]
19731 fn retained_bundle_cache_evicts_unused_entries() {
19732 let mut cache: RetainedBundleCacheImpl<u32> = RetainedBundleCacheImpl::new();
19733 let stale = bundle_key(&[bundle_op(1, Some(1), 0, 6)]);
19734 let live = bundle_key(&[bundle_op(2, Some(2), 0, 6)]);
19735 cache.insert(stale.clone(), 1);
19736 cache.insert(live.clone(), 2);
19737 cache.end_frame();
19738
19739 assert!(cache.hit(&live));
19740 cache.end_frame();
19741
19742 assert!(
19743 !cache.hit(&stale),
19744 "entry unused for a frame must have been evicted"
19745 );
19746 assert!(cache.hit(&live), "used entry must survive eviction");
19747
19748 cache.clear();
19749 assert!(!cache.hit(&live), "clear must drop every entry");
19750 }
19751}